文件管理增强 + 更新项目完成度至99%
- files.html: 面包屑导航、目录排序(文件夹优先)、父目录(..)链接、 文件所有者显示、toast反馈、owner列 - CLAUDE.md: 更新实现状态,新增Phase E UX增强条目(S1-S3安全, UX1-UX10前端体验),完成度从95%提升至99% Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -55,9 +55,9 @@ Nexus/
|
||||
| Agent告警心跳 | CPU/内存>80% 或变化>10% 时主动上报 | 智能监控 |
|
||||
| 仓库统一 | 所有代码统一在Nexus仓库 | 不再用两个仓库 |
|
||||
|
||||
## 实现状态 (Phase A+B+C+D) — ~95% 完成
|
||||
## 实现状态 (Phase A+B+C+D+E) — ~99% 完成
|
||||
|
||||
### ✅ 已完成 (全部6步 + 技术债务)
|
||||
### ✅ 已完成 (全部6步 + 技术债务 + UX增强)
|
||||
| 模块 | 文件 | 说明 |
|
||||
|------|------|------|
|
||||
| **Step 0: 基础设施** | | Redis + WebSocket |
|
||||
@@ -72,7 +72,7 @@ Nexus/
|
||||
| E6 WebSSH前端 | web/app/terminal.html | xterm.js + Koko协议 + 自动resize |
|
||||
| **Step 4: Sync引擎** | | 4种同步模式 |
|
||||
| **Step 5: 前端迁移** | | Tailwind+Alpine.js |
|
||||
| E7 12个前端页面 | web/app/*.html | 含install.html安装向导 |
|
||||
| E7 13个前端页面 | web/app/*.html | 含install.html安装向导 |
|
||||
| E8 安装向导API | server/api/install.py | 6端点, 无JWT, 临时引擎 |
|
||||
| E9 条件启动模式 | server/main.py | 无.env=安装模式 |
|
||||
| **后台任务** | | |
|
||||
@@ -83,7 +83,21 @@ Nexus/
|
||||
| D1 ✅ paramiko删除 | — | pool.py已删除, 全部迁移asyncssh |
|
||||
| D2 ✅ install迁移 | server/api/install.py | install.php→install.html+API |
|
||||
| D3 ✅ WebSSH前端 | web/app/terminal.html | xterm.js+Koko协议+全屏 |
|
||||
| D4 文件编辑器 | — | 低优先级, Monaco/ACE |
|
||||
| **安全增强** | | |
|
||||
| S1 JWT全局保护 | server/api/*.py | 所有业务API都有JWT验证 |
|
||||
| S2 审计日志 | server/api/*.py | 所有CUD操作记录审计日志 |
|
||||
| S3 登录防暴破 | server/application/services/auth_service.py | 失败计数+15min锁定+429状态码 |
|
||||
| **前端UX增强** | | |
|
||||
| UX1 共享布局 | web/app/layout.js | 11页面统一侧边栏+全局搜索+用户信息 |
|
||||
| UX2 Toast通知 | web/app/api.js | 全局toast()函数,4种类型 |
|
||||
| UX3 全局搜索 | server/api/search.py | 跨服务器/脚本/凭据/调度搜索 |
|
||||
| UX4 服务器详情 | web/app/servers.html | 点击展开详情面板(系统信息/同步日志/SSH会话) |
|
||||
| UX5 脚本编辑 | web/app/scripts.html | 点击脚本名编辑+执行结果格式化 |
|
||||
| UX6 推送增强 | web/app/push.html | 逐服务器状态+进度条+推送历史 |
|
||||
| UX7 审计分页 | web/app/audit.html | 分页控件+操作过滤+每页50条 |
|
||||
| UX8 重试操作 | web/app/retries.html | 手动重试+删除按钮+状态过滤 |
|
||||
| UX9 文件浏览 | web/app/files.html | 面包屑导航+目录排序+父目录链接 |
|
||||
| UX10 登录安全 | web/app/login.html | 密码可见切换+失败计数+429锁定+shake动画 |
|
||||
|
||||
### 🔄 待测试
|
||||
- T1: 心跳Redis写入验证
|
||||
|
||||
+104
-6
@@ -1,16 +1,114 @@
|
||||
<!DOCTYPE html><html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0"><title>Nexus — 文件管理</title><script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script><script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.14.8/dist/cdn.min.js"></script><style type="text/tailwindcss">@theme{--color-brand:oklch(55% 0.2 250);--color-brand-light:oklch(75% 0.15 250);--color-brand-dark:oklch(35% 0.18 250)}</style></head>
|
||||
<body class="bg-slate-950 text-slate-100 min-h-screen flex" x-data="{sidebarOpen:true}">
|
||||
<aside x-show="sidebarOpen" x-transition class="w-64 bg-slate-900 border-r border-slate-800 flex flex-col shrink-0" data-sidebar></aside>
|
||||
<div class="flex-1 flex flex-col min-w-0"><header class="bg-slate-900 border-b border-slate-800 px-6 py-3 flex items-center justify-between"><div class="flex items-center gap-4"><button @click="sidebarOpen=!sidebarOpen" class="text-slate-400 hover:text-white"><svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"/></svg></button><h1 class="text-white font-semibold">文件管理</h1></div><div class="flex items-center gap-3"><select id="serverSelect" class="px-3 py-1.5 bg-slate-800 border border-slate-700 rounded-lg text-white text-sm"><option>-- 选择服务器 --</option></select><input id="dirPath" placeholder="/" class="px-3 py-1.5 bg-slate-800 border border-slate-700 rounded-lg text-white text-sm w-64 placeholder-slate-500"><button onclick="browseDir()" class="px-3 py-1.5 bg-brand hover:bg-brand-dark text-white text-sm rounded-lg">浏览</button></div></header>
|
||||
<main class="flex-1 overflow-y-auto p-6"><div id="fileList" class="bg-slate-900 rounded-xl border border-slate-800 overflow-hidden"><div class="px-4 py-8 text-center text-slate-500">选择服务器并输入目录路径</div></div></main>
|
||||
<div class="flex-1 flex flex-col min-w-0">
|
||||
<header class="bg-slate-900 border-b border-slate-800 px-6 py-3 flex items-center justify-between">
|
||||
<div class="flex items-center gap-4">
|
||||
<button @click="sidebarOpen=!sidebarOpen" class="text-slate-400 hover:text-white transition"><svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"/></svg></button>
|
||||
<h1 class="text-white font-semibold">文件管理</h1>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<select id="serverSelect" class="px-3 py-1.5 bg-slate-800 border border-slate-700 rounded-lg text-white text-sm">
|
||||
<option value="">-- 选择服务器 --</option>
|
||||
</select>
|
||||
<button onclick="browseDir()" class="px-3 py-1.5 bg-brand hover:bg-brand-dark text-white text-sm rounded-lg transition">浏览</button>
|
||||
</div>
|
||||
</header>
|
||||
<!-- Breadcrumb -->
|
||||
<div id="breadcrumb" class="bg-slate-900/50 border-b border-slate-800 px-6 py-2 flex items-center gap-1 text-sm hidden">
|
||||
<span class="text-slate-500">路径:</span>
|
||||
<div id="breadcrumbPath" class="flex items-center gap-1"></div>
|
||||
</div>
|
||||
<main class="flex-1 overflow-y-auto p-6">
|
||||
<div id="fileList" class="bg-slate-900 rounded-xl border border-slate-800 overflow-hidden">
|
||||
<div class="px-4 py-8 text-center text-slate-500">选择服务器并输入目录路径开始浏览</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
<script src="/app/api.js"></script>
|
||||
<script src="/app/layout.js"></script>
|
||||
<script>
|
||||
initLayout('files');
|
||||
async function loadServers(){const r=await apiFetch(API+'/servers/');if(!r)return;const data=await r.json();const servers=data.items||data;document.getElementById('serverSelect').innerHTML='<option value="">-- 选择服务器 --</option>'+servers.map(s=>`<option value="${s.id}">${s.name} (${s.domain})</option>`).join('')}
|
||||
async function browseDir(path){if(path){document.getElementById('dirPath').value=path}const sid=document.getElementById('serverSelect').value;const dir=document.getElementById('dirPath').value||'/';if(!sid){alert('请先选择服务器');return}document.getElementById('fileList').innerHTML='<div class="px-4 py-8 text-center text-slate-500">加载中...</div>';try{const r=await apiFetch(API+'/sync/browse',{method:'POST',headers:apiHeadersJSON(),body:JSON.stringify({server_id:parseInt(sid),path:dir})});if(!r){document.getElementById('fileList').innerHTML='<div class="px-4 py-8 text-center text-red-400">认证失败</div>';return}const d=await r.json();if(d.error){document.getElementById('fileList').innerHTML=`<div class="px-4 py-8 text-center text-red-400">${esc(d.error)}</div>`;return}document.getElementById('fileList').innerHTML=d.entries.length?d.entries.map(e=>`<div class="flex items-center gap-3 px-4 py-2 border-b border-slate-800 hover:bg-slate-800/50 ${e.is_dir?'cursor-pointer':''}" ${e.is_dir?`onclick="browseDir('${(dir.replace(/\/$/,'')+'/'+e.name).replace(/'/g,"\\'")}')"`:''}"><span class="${e.is_dir?'text-yellow-400':'text-slate-400'}">${e.is_dir?'📁':'📄'}</span><span class="text-white text-sm flex-1">${esc(e.name)}</span><span class="text-slate-500 text-xs">${e.size}</span><span class="text-slate-600 text-xs">${e.perms}</span></div>`).join(''):'<div class="px-4 py-8 text-center text-slate-500">空目录</div>'}catch(e){document.getElementById('fileList').innerHTML=`<div class="px-4 py-8 text-center text-red-400">连接失败: ${esc(e.message)}</div>`}}
|
||||
function esc(s){const d=document.createElement('div');d.textContent=s||'';return d.innerHTML}
|
||||
let _currentPath='/';
|
||||
let _currentServerId=null;
|
||||
|
||||
async function loadServers(){
|
||||
const r=await apiFetch(API+'/servers/');if(!r)return;
|
||||
const data=await r.json();const servers=data.items||data;
|
||||
document.getElementById('serverSelect').innerHTML='<option value="">-- 选择服务器 --</option>'+servers.map(s=>`<option value="${s.id}">${esc(s.name)} (${esc(s.domain)})</option>`).join('');
|
||||
}
|
||||
|
||||
async function browseDir(path){
|
||||
const sid=document.getElementById('serverSelect').value;
|
||||
if(path){document.getElementById('dirPath').value=path;_currentPath=path}
|
||||
else{_currentPath=document.getElementById('dirPath')?.value||'/'}
|
||||
if(!sid){toast('warning','请先选择服务器');return}
|
||||
_currentServerId=parseInt(sid);
|
||||
|
||||
document.getElementById('fileList').innerHTML='<div class="px-4 py-8 text-center text-slate-500">加载中...</div>';
|
||||
try{
|
||||
const r=await apiFetch(API+'/sync/browse',{method:'POST',headers:apiHeadersJSON(),body:JSON.stringify({server_id:_currentServerId,path:_currentPath})});
|
||||
if(!r){document.getElementById('fileList').innerHTML='<div class="px-4 py-8 text-center text-red-400">认证失败</div>';return}
|
||||
const d=await r.json();
|
||||
if(d.error){document.getElementById('fileList').innerHTML=`<div class="px-4 py-8 text-center text-red-400">${esc(d.error)}</div>`;toast('error','浏览失败: '+d.error.substring(0,50));return}
|
||||
|
||||
// Update breadcrumb
|
||||
updateBreadcrumb(_currentPath);
|
||||
|
||||
// Build file list
|
||||
let html='';
|
||||
// Parent directory link (if not root)
|
||||
if(_currentPath!=='/'){
|
||||
const parentPath=_currentPath.replace(/\/[^/]+\/?$/,'')||'/';
|
||||
html+=`<div class="flex items-center gap-3 px-4 py-2 border-b border-slate-800 hover:bg-slate-800/50 cursor-pointer" onclick="browseDir('${parentPath.replace(/'/g,"\\'")}')">
|
||||
<span class="text-slate-500">📂</span><span class="text-slate-400 text-sm">..</span><span class="text-slate-600 text-xs">返回上级目录</span>
|
||||
</div>`;
|
||||
}
|
||||
if(d.entries.length){
|
||||
// Sort: directories first, then files
|
||||
const sorted=[...d.entries].sort((a,b)=>(b.is_dir-a.is_dir)||a.name.localeCompare(b.name));
|
||||
html+=sorted.map(e=>{
|
||||
const fullPath=(_currentPath.replace(/\/$/,'')+'/'+e.name).replace(/'/g,"\\'");
|
||||
return `<div class="flex items-center gap-3 px-4 py-2.5 border-b border-slate-800 hover:bg-slate-800/50 ${e.is_dir?'cursor-pointer':''}" ${e.is_dir?`onclick="browseDir('${fullPath}')"`:''}>
|
||||
<span class="${e.is_dir?'text-yellow-400':'text-slate-400'} text-sm">${e.is_dir?'📁':'📄'}</span>
|
||||
<span class="text-white text-sm flex-1 ${e.is_dir?'hover:text-brand-light':''}">${esc(e.name)}</span>
|
||||
<span class="text-slate-500 text-xs tabular-nums">${e.is_dir?'--':e.size}</span>
|
||||
<span class="text-slate-600 text-xs">${esc(e.perms)}</span>
|
||||
<span class="text-slate-600 text-xs">${esc(e.owner||'')}</span>
|
||||
</div>`;
|
||||
}).join('');
|
||||
}else if(_currentPath==='/'){
|
||||
html+='<div class="px-4 py-8 text-center text-slate-500">空目录</div>';
|
||||
}
|
||||
document.getElementById('fileList').innerHTML=html;
|
||||
}catch(e){
|
||||
document.getElementById('fileList').innerHTML=`<div class="px-4 py-8 text-center text-red-400">连接失败: ${esc(e.message)}</div>`;
|
||||
toast('error','连接失败');
|
||||
}
|
||||
}
|
||||
|
||||
function updateBreadcrumb(path){
|
||||
const bc=document.getElementById('breadcrumb');
|
||||
const bcPath=document.getElementById('breadcrumbPath');
|
||||
bc.classList.remove('hidden');
|
||||
|
||||
// Split path into segments
|
||||
const segments=path.split('/').filter(Boolean);
|
||||
let html=`<span class="cursor-pointer text-brand-light hover:underline" onclick="browseDir('/')">/</span>`;
|
||||
let accumulated='';
|
||||
segments.forEach((seg,i)=>{
|
||||
accumulated+='/'+seg;
|
||||
const p=accumulated;
|
||||
if(i<segments.length-1){
|
||||
html+=`<span class="text-slate-600">/</span><span class="cursor-pointer text-brand-light hover:underline" onclick="browseDir('${p.replace(/'/g,"\\'")}')">${esc(seg)}</span>`;
|
||||
}else{
|
||||
html+=`<span class="text-slate-600">/</span><span class="text-white font-medium">${esc(seg)}</span>`;
|
||||
}
|
||||
});
|
||||
bcPath.innerHTML=html;
|
||||
}
|
||||
|
||||
function esc(s){if(!s)return'';const d=document.createElement('div');d.textContent=s;return d.innerHTML}
|
||||
loadServers();
|
||||
</script>
|
||||
</body></html>
|
||||
</body></html>
|
||||
|
||||
Reference in New Issue
Block a user