fix: 移除遗留fmfuncs目录创建 + 清理init_service无用导入 + 文件管理器SVG图标 + 示例插件MD3改造
- init_service: 删除_load_fmfuncs方法和fmfuncs目录创建(已迁移到sdk/) - init_service: 清理未使用的importlib.util和sys导入 - 文件管理器: 全部emoji替换为MD3 SVG矢量图标(文件夹/文件类型/右键菜单) - 示例插件: dashboard.html改为MD3风格 CSS变量自动适配日夜主题 - 面包屑: 加底色+模糊+左右外边距 - CSS: .btn::after加pointer-events:none - 插件开发指南: 更新2.6节MD3主题同步和完善的CSS变量/组件类参考表 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -92,7 +92,7 @@ commands:
|
||||
permissions:
|
||||
- framework.command.test
|
||||
source: internal
|
||||
last_updated: 297792.734060618
|
||||
last_updated: 316707.28385033
|
||||
plugin_commands:
|
||||
example_plugin:
|
||||
echo: *id001
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
http_port: 4200
|
||||
last_updated: 297792.741311191
|
||||
last_updated: 316707.287225746
|
||||
plugin_routes:
|
||||
example_plugin:
|
||||
- methods:
|
||||
|
||||
+55
-24
@@ -3432,41 +3432,72 @@ class MyPlugin(PluginWebMixin):
|
||||
- 提取 `<script>` 标签,通过动态 script 元素执行
|
||||
- 页面 content 区 padding 归零,插件内容边到边铺满
|
||||
|
||||
#### 2.6.3 HTML 编写建议
|
||||
#### 2.6.3 MD3 主题同步与 HTML 编写建议
|
||||
|
||||
插件页面会自动注入到 SenSu 主面板的 `<div class=\"plugin-page-root\">` 容器中。**CSS 中的所有 `body` 选择器会被自动替换为 `.plugin-page-root`**,不会污染主页面。
|
||||
|
||||
**主题同步无需任何代码** — CSS 中使用 `var(--xxx)` 引用 MD3 token,日夜切换自动生效。
|
||||
|
||||
```html
|
||||
<!-- 推荐:使用内联样式 + 容器类,不依赖 body 选择器 -->
|
||||
<!DOCTYPE html><html lang="zh"><head><meta charset="UTF-8"><title>My Plugin</title>
|
||||
<style>
|
||||
/* body 选择器不会生效(body 标签已被剥离) */
|
||||
/* 改用类选择器或直接用容器 div */
|
||||
.plugin-root {
|
||||
background: var(--bg); /* 继承 SenSu 主题背景 */
|
||||
color: var(--text); /* 继承 SenSu 主题文字 */
|
||||
font-family: system-ui, sans-serif;
|
||||
padding: 16px;
|
||||
min-height: 100%;
|
||||
}
|
||||
.plugin-root h2 { color: var(--primary); }
|
||||
</style>
|
||||
<div class="plugin-root">
|
||||
<h2>插件面板标题</h2>
|
||||
<button class="btn btn-sm btn-tonal" onclick="...">操作</button>
|
||||
/* ✅ 正确: 使用 MD3 CSS 变量,自动适配日夜主题 */
|
||||
.plugin-page-root{font-family:system-ui,sans-serif;padding:20px;min-height:100%}
|
||||
h2{color:var(--primary);font-weight:500}
|
||||
/* ✅ 使用 MD3 组件类 */
|
||||
</style></head><body>
|
||||
<h2>插件面板</h2>
|
||||
<!-- ✅ 复用 MD3 卡片样式 -->
|
||||
<div class="card" style="margin-bottom:16px">
|
||||
<h3>数据面板</h3>
|
||||
<div id="value">0</div>
|
||||
<button class="btn btn-sm btn-filled" onclick="doAction()">操作</button>
|
||||
</div>
|
||||
<script>
|
||||
// 脚本会被动态 script 元素执行,声明的函数可被 onclick 调用
|
||||
function handleClick() { ... }
|
||||
</script>
|
||||
function doAction(){ /* ... */ }
|
||||
</script></body></html>
|
||||
```
|
||||
|
||||
**常用 MD3 CSS 变量 (自动跟随日夜主题):**
|
||||
|
||||
| 变量 | 用途 |
|
||||
|------|------|
|
||||
| `--bg` | 页面背景色 |
|
||||
| `--bg-card` | 卡片/容器背景 |
|
||||
| `--text` | 主文字色 |
|
||||
| `--text-dim` | 次要文字色 |
|
||||
| `--primary` | 主题色 |
|
||||
| `--primary-container` | 主题色容器背景 |
|
||||
| `--outline` | 边框色 |
|
||||
| `--error` | 错误/危险色 |
|
||||
| `--shape-xs` / `--shape-sm` / `--shape-md` | 圆角 (8/12/16px) |
|
||||
| `--md-sys-elevation-1` ~ `--md-sys-elevation-5` | 阴影 |
|
||||
|
||||
**常用 MD3 组件类:**
|
||||
|
||||
| 类 | 用途 |
|
||||
|------|------|
|
||||
| `.card` | MD3 卡片容器 |
|
||||
| `.card.outlined` | 带边框的卡片 |
|
||||
| `.btn` + `.btn-filled` | 实心按钮 |
|
||||
| `.btn` + `.btn-tonal` | 半透明按钮 |
|
||||
| `.btn` + `.btn-outlined` | 轮廓按钮 |
|
||||
| `.btn-sm` / `.btn-lg` | 按钮尺寸 (需配合 .btn) |
|
||||
| `.input` | MD3 输入框 |
|
||||
| `.input-group` | 输入框容器 (含 label) |
|
||||
| `.badge` / `.badge-run` / `.badge-stop` | 状态徽章 |
|
||||
| `.chip` / `.chip.active` | 标签/筛选 |
|
||||
|
||||
#### 2.6.4 注意事项
|
||||
|
||||
| 事项 | 说明 |
|
||||
|------|------|
|
||||
| `body` 选择器 | CSS 中 `body { ... }` 不会生效,改用容器类 |
|
||||
| 硬编码背景色 | 避免 `background: #000`,用 `var(--bg)` 自适应主题 |
|
||||
| `onclick` 函数 | 函数需在 `<script>` 中声明,浏览器 innerHTML 不执行 script |
|
||||
| `<html>/<head>` | 可省略,直接写 body 内容 |
|
||||
| 文件管理器调用 | 可通过 postMessage API 唤出文件选择器 (见第八章) |
|
||||
| `body` 选择器 | v0.6.0 起自动替换为 `.plugin-page-root`,可放心使用 |
|
||||
| 硬编码颜色 | ❌ `background:#000` / `color:#fff` → ✅ `var(--bg)` / `var(--text)` |
|
||||
| 主题同步 | 无需额外代码,CSS 变量自动跟随 `data-theme` 属性切换 |
|
||||
| `onclick` 函数 | 函数需在 `<script>` 中声明,框架通过动态 script 元素执行 |
|
||||
| `<html>/<head>` | 可完整书写,框架自动剥离包装标签 |
|
||||
| 文件管理器调用 | 通过 `window.pickPath('dir', callback)` 唤出选择器 |
|
||||
|
||||
## 三、插件生命周期管理
|
||||
|
||||
|
||||
@@ -1,12 +1,34 @@
|
||||
<!DOCTYPE html><html lang="zh"><head><meta charset="UTF-8"><title>Example</title>
|
||||
<style>body{font-family:monospace;background:#0e1416;color:#e0e3e4;padding:16px}
|
||||
h2{color:#00bcd4}.card{background:#1a1f21;border-radius:8px;padding:12px;margin:8px 0}
|
||||
button{background:#00bcd4;color:#000;padding:8px 16px;border:none;border-radius:4px;cursor:pointer}
|
||||
.counter{font-size:48px;color:#4caf50;text-align:center;padding:20px}
|
||||
.log{background:#000;color:#0f0;padding:8px;border-radius:4px;max-height:200px;overflow-y:auto;font-size:12px}
|
||||
<style>
|
||||
.plugin-page-root{font-family:system-ui,sans-serif;padding:20px;min-height:100%}
|
||||
h2{color:var(--primary);font-weight:500;margin-bottom:16px}
|
||||
.counter{font-size:48px;color:var(--primary);text-align:center;padding:20px}
|
||||
.log{background:var(--md-sys-color-surface-container-lowest);color:#4caf50;padding:8px 12px;border-radius:var(--shape-xs);max-height:200px;overflow-y:auto;font-size:12px;font-family:monospace;line-height:1.6}
|
||||
</style></head><body>
|
||||
<h2>Example Plugin Panel</h2>
|
||||
<div class="card"><h3>Counter</h3><div class="counter" id="count">0</div>
|
||||
<button onclick="inc()">+1</button> <button onclick="reset()">Reset</button></div>
|
||||
<div class="card"><h3>Events (SSE)</h3><div class="log" id="log">Waiting...</div></div>
|
||||
<script>let n=0;function inc(){n++;document.getElementById("count").textContent=n;fetch("/SenSu/plugin/example_plugin/event",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({type:"counter",value:n})})}function reset(){n=0;document.getElementById("count").textContent=0}const es=new EventSource("/SenSu/plugin/example_plugin/sse");es.addEventListener("counter",e=>{const d=JSON.parse(e.data);document.getElementById("log").innerHTML+=new Date().toLocaleTimeString()+" counter="+d.value+"<br>"})</script></body></html>
|
||||
<div class="card" style="margin-bottom:16px">
|
||||
<h3 style="margin-bottom:8px">Counter</h3>
|
||||
<div class="counter" id="count">0</div>
|
||||
<div style="display:flex;gap:8px;justify-content:center">
|
||||
<button class="btn btn-sm btn-filled" onclick="inc()">+1</button>
|
||||
<button class="btn btn-sm btn-outlined" onclick="reset()">Reset</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3 style="margin-bottom:8px">Events (SSE)</h3>
|
||||
<div class="log" id="log">Waiting...</div>
|
||||
</div>
|
||||
<script>
|
||||
var n=0;
|
||||
function inc(){
|
||||
n++;
|
||||
document.getElementById("count").textContent=n;
|
||||
fetch("/SenSu/plugin/example_plugin/event",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({type:"counter",value:n})});
|
||||
}
|
||||
function reset(){n=0;document.getElementById("count").textContent=0;}
|
||||
var es=new EventSource("/SenSu/plugin/example_plugin/sse");
|
||||
es.addEventListener("counter",function(e){
|
||||
var d=JSON.parse(e.data);
|
||||
document.getElementById("log").innerHTML+=new Date().toLocaleTimeString()+" counter="+d.value+"<br>";
|
||||
});
|
||||
</script></body></html>
|
||||
@@ -6,8 +6,6 @@ import asyncio
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any
|
||||
import yaml
|
||||
import importlib.util
|
||||
import sys
|
||||
import os
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -18,7 +16,6 @@ class InitService:
|
||||
def __init__(self, config_path: str = "config/framework"):
|
||||
self.config_path = Path(config_path)
|
||||
self.configs: Dict[str, Any] = {}
|
||||
self.fmfuncs_loaded = False
|
||||
logger.debug("InitService初始化开始")
|
||||
|
||||
async def initialize_framework(self):
|
||||
@@ -32,10 +29,7 @@ class InitService:
|
||||
# 2. 创建必要目录
|
||||
await self._create_directories()
|
||||
|
||||
# 3. 加载框架功能集
|
||||
await self._load_fmfuncs()
|
||||
|
||||
# 4. 验证初始化状态
|
||||
# 3. 验证初始化状态
|
||||
await self._validate_init()
|
||||
|
||||
logger.info("框架初始化完成")
|
||||
@@ -94,8 +88,7 @@ class InitService:
|
||||
"logs/runtime",
|
||||
"logs/debug",
|
||||
"plugins",
|
||||
"utils",
|
||||
"fmfuncs"
|
||||
"utils"
|
||||
]
|
||||
|
||||
for dir_path in directories:
|
||||
@@ -109,41 +102,6 @@ class InitService:
|
||||
logger.error(f"创建目录时出错: {str(e)}", exc_info=True)
|
||||
raise
|
||||
|
||||
async def _load_fmfuncs(self):
|
||||
"""加载框架功能集"""
|
||||
try:
|
||||
logger.debug("开始加载框架功能集")
|
||||
|
||||
fmfuncs_path = Path(os.getenv("SENSU_CODE_DIR", ".")) / "fmfuncs"
|
||||
if not fmfuncs_path.exists():
|
||||
logger.warning("fmfuncs目录不存在,跳过加载")
|
||||
return
|
||||
|
||||
# 动态加载所有Python文件
|
||||
for py_file in fmfuncs_path.glob("*.py"):
|
||||
if py_file.name == "__init__.py":
|
||||
continue
|
||||
|
||||
try:
|
||||
module_name = f"fmfuncs.{py_file.stem}"
|
||||
spec = importlib.util.spec_from_file_location(module_name, py_file)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
sys.modules[module_name] = module
|
||||
spec.loader.exec_module(module)
|
||||
|
||||
logger.debug(f"加载框架功能: {module_name}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"加载框架功能 {py_file} 时出错: {str(e)}", exc_info=True)
|
||||
continue
|
||||
|
||||
self.fmfuncs_loaded = True
|
||||
logger.debug("框架功能集加载完成")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"加载框架功能集时出错: {str(e)}", exc_info=True)
|
||||
raise
|
||||
|
||||
async def _validate_init(self):
|
||||
"""验证初始化状态"""
|
||||
try:
|
||||
|
||||
@@ -275,6 +275,9 @@ h3{font-size:1rem;font-weight:500;letter-spacing:.15px}
|
||||
.tab.active{background:var(--primary-container);color:var(--md-sys-color-on-primary-container)}
|
||||
.tab:hover:not(.active){color:var(--text)}
|
||||
|
||||
/* ── Plugin page container ── */
|
||||
.plugin-page-root{font-family:inherit;color:inherit}
|
||||
|
||||
/* ── Responsive ── */
|
||||
@media(max-width:1100px){.dash-layout{flex-direction:column;height:auto;overflow:visible}.dash-sidebar{width:100%}}
|
||||
@media(max-width:768px){.app-frame{grid-template-columns:var(--sidebar-collapsed) 1fr}.nav-label{display:none}.toggle-sidebar{display:none}}
|
||||
@@ -353,6 +356,7 @@ h3{font-size:1rem;font-weight:500;letter-spacing:.15px}
|
||||
.btn::after{
|
||||
content:"";position:absolute;inset:0;background:radial-gradient(circle at center,currentColor 10%,transparent 10%);
|
||||
background-size:0 0;background-repeat:no-repeat;opacity:0;transition:background-size .4s,opacity .3s;
|
||||
pointer-events:none;
|
||||
}
|
||||
.btn:active::after{background-size:300% 300%;opacity:.12;transition:0s}
|
||||
|
||||
@@ -443,10 +447,9 @@ h3{font-size:1rem;font-weight:500;letter-spacing:.15px}
|
||||
═══════════════════════════════════════════ */
|
||||
.fm-container{display:flex;flex-direction:column;height:calc(100vh - var(--topbar-h) - 48px);gap:12px}
|
||||
.fm-toolbar{display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:8px}
|
||||
.fm-path-bar{flex:1;min-width:0;display:flex;flex-direction:column;gap:4px}
|
||||
.fm-path-input{width:100%;padding:8px 12px;background:var(--md-sys-color-surface-container-lowest);border:1px solid var(--outline);border-radius:var(--shape-xs);color:var(--text);font-family:"JetBrains Mono",monospace;font-size:12px;outline:none;transition:border-color .2s}
|
||||
.fm-path-input{flex:1;min-width:140px;padding:8px 12px;background:var(--md-sys-color-surface-container-lowest);border:1px solid var(--outline);border-radius:var(--shape-xs);color:var(--text);font-family:"JetBrains Mono",monospace;font-size:12px;outline:none;transition:border-color .2s}
|
||||
.fm-path-input:focus{border-color:var(--primary);box-shadow:0 0 0 2px rgba(208,188,255,.15)}
|
||||
.fm-breadcrumb{display:flex;align-items:center;flex-wrap:wrap;gap:4px;overflow-x:auto;scrollbar-width:none;flex:1;min-width:0}
|
||||
.fm-breadcrumb{display:flex;align-items:center;flex-wrap:wrap;gap:4px;padding:6px 10px;margin:4px 8px 0 8px;overflow-x:auto;scrollbar-width:none;flex-shrink:0;background:var(--md-sys-color-surface-container);border-radius:var(--shape-xs);backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px)}
|
||||
.fm-breadcrumb::-webkit-scrollbar{display:none}
|
||||
.fm-crumb{white-space:nowrap;padding:4px 10px;border-radius:var(--shape-full);cursor:pointer;font-size:.82rem;color:var(--text-dim);transition:.15s}
|
||||
.fm-crumb:hover{background:rgba(208,188,255,.1);color:var(--primary)}
|
||||
|
||||
@@ -72,6 +72,6 @@
|
||||
<script src="./static/js/chart.js?v=0603"></script>
|
||||
|
||||
<!-- 🟢 2. 再加载主逻辑 -->
|
||||
<script src="./static/js/app.js?v=0702"></script>
|
||||
<script src="./static/js/app.js?v=0704"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+77
-25
@@ -154,8 +154,12 @@ async function loadPluginPage(pluginName) {
|
||||
var scripts = [];
|
||||
clean = clean.replace(/<script\b[^>]*>([\s\S]*?)<\/script>/gi, function(m,code){ scripts.push(code.trim()); return ''; });
|
||||
|
||||
// Scope plugin styles: replace body selector with .plugin-page-root to avoid
|
||||
// styling the main page's <body> element
|
||||
styles = styles.replace(/body\s*\{/gi, '.plugin-page-root{').replace(/body\b/gi, '.plugin-page-root');
|
||||
|
||||
content.style.padding = '0';
|
||||
content.innerHTML = '<style>'+styles+'</style>'+clean;
|
||||
content.innerHTML = '<div class="plugin-page-root"><style>'+styles+'</style>'+clean+'</div>';
|
||||
|
||||
scripts.forEach(function(code){
|
||||
try { var s=document.createElement('script'); s.textContent=code; document.head.appendChild(s); document.head.removeChild(s); }
|
||||
@@ -172,50 +176,98 @@ async function loadPluginPage(pluginName) {
|
||||
}
|
||||
|
||||
|
||||
// ═══ Global file/dir picker (in-page iframe modal) ═══
|
||||
// ═══ Global file/dir picker (in-page overlay, no iframe) ═══
|
||||
window.pickPath = function(mode, callback) {
|
||||
var id = 'picker_' + Date.now();
|
||||
var url = './static/pages/files.html?picker=1&mode=' + (mode || 'dir') + '&cb=' + id;
|
||||
|
||||
// Create modal overlay
|
||||
var overlay = document.createElement('div');
|
||||
overlay.id = 'fm-picker-overlay';
|
||||
overlay.style.cssText = 'position:fixed;inset:0;z-index:500;background:rgba(0,0,0,.5);display:flex;align-items:center;justify-content:center';
|
||||
overlay.onclick = function(e) { if (e.target === overlay) closePicker(); };
|
||||
|
||||
var box = document.createElement('div');
|
||||
box.style.cssText = 'background:var(--md-sys-color-surface-container-high);border-radius:var(--shape-lg);width:720px;height:520px;max-width:95vw;max-height:85vh;display:flex;flex-direction:column;overflow:hidden;box-shadow:var(--md-sys-elevation-5)';
|
||||
|
||||
// Header
|
||||
// Header with close button
|
||||
var header = document.createElement('div');
|
||||
header.style.cssText = 'display:flex;justify-content:space-between;align-items:center;padding:12px 16px;border-bottom:1px solid var(--outline);flex-shrink:0';
|
||||
header.innerHTML = '<span style="font-weight:500;color:var(--primary)">📂 选择' + (mode==='file'?'文件':'目录') + '</span>' +
|
||||
'<button class="btn btn-sm btn-outlined" onclick="closePicker()" style="font-size:16px;line-height:1">✕</button>';
|
||||
var headerTitle = document.createElement('span');
|
||||
headerTitle.style.cssText = 'font-weight:500;color:var(--primary)';
|
||||
headerTitle.textContent = '📂 选择' + (mode==='file'?'文件':'目录');
|
||||
header.appendChild(headerTitle);
|
||||
var closeBtn = document.createElement('span');
|
||||
closeBtn.style.cssText = 'cursor:pointer;font-size:20px;line-height:1;padding:4px 8px;border-radius:4px;color:var(--text-dim)';
|
||||
closeBtn.textContent = '✕';
|
||||
closeBtn.onclick = function(){ closePicker(); };
|
||||
header.appendChild(closeBtn);
|
||||
box.appendChild(header);
|
||||
|
||||
// Iframe
|
||||
var iframe = document.createElement('iframe');
|
||||
iframe.src = url;
|
||||
iframe.style.cssText = 'flex:1;border:none;width:100%';
|
||||
box.appendChild(iframe);
|
||||
// Content area — load file manager via fetch
|
||||
var contentArea = document.createElement('div');
|
||||
contentArea.style.cssText = 'flex:1;overflow:hidden;display:flex;flex-direction:column';
|
||||
box.appendChild(contentArea);
|
||||
overlay.appendChild(box);
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
function closePicker() {
|
||||
window.removeEventListener('message', handler);
|
||||
if (overlay.parentNode) overlay.parentNode.removeChild(overlay);
|
||||
}
|
||||
window._closePicker = closePicker;
|
||||
// Close on backdrop click
|
||||
overlay.addEventListener('click', function(e){ if(e.target===overlay) closePicker(); });
|
||||
|
||||
// Listen for pick result from iframe
|
||||
window.addEventListener('message', function handler(e) {
|
||||
try {
|
||||
var d = JSON.parse(e.data);
|
||||
if (d.action === 'fm-picked' && d.callback_id === id) {
|
||||
closePicker();
|
||||
if (d.path && callback) callback(d.path);
|
||||
function closePicker() {
|
||||
if(overlay.parentNode) overlay.parentNode.removeChild(overlay);
|
||||
}
|
||||
} catch(ex) {}
|
||||
|
||||
// Load file manager HTML into content area
|
||||
fetch('./static/pages/files.html?t=' + Date.now())
|
||||
.then(function(r){ return r.text(); })
|
||||
.then(function(html){
|
||||
// Strip meta and CSS link (already available in main page)
|
||||
html = html.replace(/<meta[^>]*>/gi, '').replace(/<link[^>]*>/gi, '');
|
||||
// Extract inline scripts
|
||||
var scripts = [];
|
||||
var clean = html.replace(/<script\b[^>]*>([\s\S]*?)<\/script>/gi, function(m,code){ scripts.push(code.trim()); return ''; });
|
||||
contentArea.innerHTML = clean;
|
||||
// Execute scripts
|
||||
scripts.forEach(function(code){
|
||||
try { var s=document.createElement('script'); s.textContent=code; document.head.appendChild(s); document.head.removeChild(s); }
|
||||
catch(ex){}
|
||||
});
|
||||
// Load files.js if not already loaded
|
||||
// Fix container height for overlay
|
||||
var fmContainer = contentArea.querySelector('.fm-container');
|
||||
if(fmContainer) fmContainer.style.height = '100%';
|
||||
function doInit(){
|
||||
window.FilesModule._apiBase = '';
|
||||
window.FilesModule.currentPath = '';
|
||||
window.FilesModule.pickerMode = mode || 'dir';
|
||||
window.FilesModule.pickerCallback = id;
|
||||
window.FilesModule._scope = contentArea;
|
||||
window.FilesModule._getEl = function(sel){ return contentArea.querySelector(sel); };
|
||||
// Override _pickResult to close overlay instead of alert
|
||||
window.FilesModule._pickResult = function(path){
|
||||
closePicker();
|
||||
if(callback) callback(path || window.FilesModule.currentPath || '/');
|
||||
};
|
||||
if(window.FilesModule.destroy) window.FilesModule.destroy();
|
||||
if(window.FilesModule.init) window.FilesModule.init();
|
||||
}
|
||||
// Override picker actions to close overlay and call callback
|
||||
window._fmPick = function(){
|
||||
var p = window.FilesModule && window.FilesModule.currentPath || '/';
|
||||
closePicker();
|
||||
if(callback) callback(p);
|
||||
};
|
||||
window._fmCancel = function(){ closePicker(); };
|
||||
|
||||
if(window.FilesModule){ doInit(); }
|
||||
else {
|
||||
var s=document.createElement('script');
|
||||
s.src='./static/pages/files.js?v=0603';
|
||||
s.onload=doInit;
|
||||
document.head.appendChild(s);
|
||||
}
|
||||
})
|
||||
.catch(function(e){
|
||||
contentArea.innerHTML = '<div style="color:var(--error);padding:40px;text-align:center">加载失败: '+e.message+'</div>';
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
framework:
|
||||
debug: true
|
||||
name: SenSu
|
||||
version: Alpha_0.2.0
|
||||
logging:
|
||||
debug_level_file: true
|
||||
level: INFO
|
||||
max_file_size: 10MB
|
||||
max_log_files: 20
|
||||
plugins:
|
||||
auto_load: true
|
||||
hot_reload: true
|
||||
max_retry_count: 3
|
||||
services:
|
||||
internet:
|
||||
api_port: 8000
|
||||
enable_reverse_proxy: false
|
||||
ws_port: 8765
|
||||
tui:
|
||||
layout:
|
||||
grid-rows: 4fr 5fr 1fr
|
||||
@@ -1,12 +0,0 @@
|
||||
admin_permissions:
|
||||
- framework.*
|
||||
- plugin.*
|
||||
- service.*
|
||||
default_permissions:
|
||||
- framework.status.read
|
||||
- plugin.self.info.read
|
||||
permission_levels:
|
||||
- read
|
||||
- write
|
||||
- execute
|
||||
- admin
|
||||
@@ -1 +0,0 @@
|
||||
{}
|
||||
@@ -1 +0,0 @@
|
||||
{}
|
||||
@@ -1 +0,0 @@
|
||||
{}
|
||||
@@ -1,89 +0,0 @@
|
||||
commands:
|
||||
autoscroll:
|
||||
description: '滚动控制: 切换自动滚动'
|
||||
permissions:
|
||||
- framework.tui.control
|
||||
source: internal
|
||||
create-plugin:
|
||||
description: 创建新插件脚手架
|
||||
permissions:
|
||||
- framework.scaffold.plugin
|
||||
source: internal
|
||||
help:
|
||||
description: 显示帮助信息
|
||||
permissions:
|
||||
- framework.command.help.read
|
||||
source: internal
|
||||
history:
|
||||
description: 显示命令历史
|
||||
permissions:
|
||||
- framework.command.history.read
|
||||
source: internal
|
||||
netdiag:
|
||||
description: 网络服务诊断
|
||||
permissions:
|
||||
- framework.network.diagnose
|
||||
source: internal
|
||||
permissions:
|
||||
description: '权限管理: 显示权限状态'
|
||||
permissions:
|
||||
- framework.permission.read
|
||||
source: internal
|
||||
pm_plugin_status:
|
||||
description: '权限管理: 查看插件权限状态'
|
||||
permissions:
|
||||
- framework.permission.read
|
||||
source: internal
|
||||
pmallow:
|
||||
description: '权限管理: 同意权限请求'
|
||||
permissions:
|
||||
- framework.permission.read
|
||||
source: internal
|
||||
pmdeny:
|
||||
description: '权限管理: 拒绝权限请求'
|
||||
permissions:
|
||||
- framework.permission.read
|
||||
source: internal
|
||||
pmhelp:
|
||||
description: '权限管理: 显示权限命令帮助'
|
||||
permissions:
|
||||
- framework.permission.read
|
||||
source: internal
|
||||
pmignore:
|
||||
description: '权限管理: 暂时忽略权限请求'
|
||||
permissions:
|
||||
- framework.permission.read
|
||||
source: internal
|
||||
pmpending:
|
||||
description: '权限管理: 查看待授权请求列表'
|
||||
permissions:
|
||||
- framework.permission.read
|
||||
source: internal
|
||||
pmrequests:
|
||||
description: '权限管理: 查看待授权请求列表(别名)'
|
||||
permissions:
|
||||
- framework.permission.read
|
||||
source: internal
|
||||
pmtest:
|
||||
description: '权限管理: 测试权限配置文件'
|
||||
permissions:
|
||||
- framework.permission.read
|
||||
source: internal
|
||||
scroll:
|
||||
description: '滚动控制: 手动滚动到底部'
|
||||
permissions:
|
||||
- framework.tui.control
|
||||
source: internal
|
||||
status:
|
||||
description: 显示框架状态
|
||||
permissions:
|
||||
- framework.status.read
|
||||
source: internal
|
||||
testlog:
|
||||
description: 生成测试日志
|
||||
permissions:
|
||||
- framework.command.test
|
||||
source: internal
|
||||
last_updated: 283025.170846772
|
||||
plugin_commands: {}
|
||||
total_commands: 17
|
||||
@@ -4,10 +4,7 @@
|
||||
<div class="fm-container">
|
||||
<!-- Top bar: editable path + actions -->
|
||||
<div class="fm-toolbar">
|
||||
<div class="fm-path-bar">
|
||||
<input class="fm-path-input" id="fm-path-input" type="text" spellcheck="false" title="输入路径后回车跳转">
|
||||
<div class="fm-breadcrumb" id="fm-breadcrumb"></div>
|
||||
</div>
|
||||
<div class="fm-actions">
|
||||
<label class="fm-toggle">
|
||||
<input type="checkbox" id="fm-hidden" onchange="FilesModule.refresh()">
|
||||
@@ -29,6 +26,9 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Breadcrumb row (own line) -->
|
||||
<div class="fm-breadcrumb" id="fm-breadcrumb"></div>
|
||||
|
||||
<!-- File list -->
|
||||
<div class="fm-list" id="fm-list">
|
||||
<div style="text-align:center;color:var(--text-dim);padding:40px">加载中...</div>
|
||||
@@ -59,36 +59,40 @@
|
||||
|
||||
<!-- Context menu -->
|
||||
<div class="fm-context" id="fm-context" style="display:none">
|
||||
<div class="fm-context-item" onclick="FilesModule.ctxDownload()">⬇ 下载</div>
|
||||
<div class="fm-context-item" onclick="FilesModule.ctxRename()">✏ 重命名</div>
|
||||
<div class="fm-context-item" onclick="FilesModule.ctxEdit()">📝 编辑</div>
|
||||
<div class="fm-context-item" onclick="FilesModule.ctxDownload()"><svg width="14" height="14" viewBox="0 0 24 24"><path fill="currentColor" d="M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z"/></svg> 下载</div>
|
||||
<div class="fm-context-item" onclick="FilesModule.ctxRename()"><svg width="14" height="14" viewBox="0 0 24 24"><path fill="currentColor" d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"/></svg> 重命名</div>
|
||||
<div class="fm-context-item" onclick="FilesModule.ctxEdit()"><svg width="14" height="14" viewBox="0 0 24 24"><path fill="currentColor" d="M14 2H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V8l-6-6zm-1 2l5 5h-5V4zM8 14h8v2H8v-2zm0-4h8v2H8v-2z"/></svg> 编辑</div>
|
||||
<div class="fm-context-sep"></div>
|
||||
<div class="fm-context-item danger" onclick="FilesModule.ctxDelete()">🗑 删除</div>
|
||||
<div class="fm-context-item danger" onclick="FilesModule.ctxDelete()"><svg width="14" height="14" viewBox="0 0 24 24"><path fill="currentColor" d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/></svg> 删除</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Load files.js for standalone mode (iframe picker)
|
||||
(function(){
|
||||
if(window.parent !== window && !window.FilesModule){
|
||||
// Sync theme from parent window
|
||||
try {
|
||||
var parentTheme = window.parent.document.documentElement.getAttribute('data-theme');
|
||||
if(parentTheme) document.documentElement.setAttribute('data-theme', parentTheme);
|
||||
} catch(e){}
|
||||
|
||||
// Reset body/container for iframe — no topbar, no sidebar
|
||||
if(window.parent !== window){
|
||||
var s=document.createElement('style');
|
||||
s.textContent='html,body{height:100%;margin:0;overflow:hidden}.fm-container{height:100%;gap:8px}.fm-list{padding-bottom:0}';
|
||||
document.head.appendChild(s);
|
||||
}
|
||||
var cssHref = document.querySelector('link[href*=\"style.css\"]')?.href || '';
|
||||
// e.g. http://host:4200/SenSu/static/css/style.css → staticBase = .../SenSu/static
|
||||
var staticBase = cssHref.replace(/\/css\/style\.css.*$/, '');
|
||||
// apiBase = .../SenSu (strip /static)
|
||||
var apiBase = cssHref.replace(/\/static\/css\/style\.css.*$/, '');
|
||||
var fmStyle=document.createElement('style');
|
||||
fmStyle.textContent='html,body{height:100%;margin:0;overflow:hidden}.fm-container{height:100%;gap:8px}.fm-list{padding-bottom:0}';
|
||||
document.head.appendChild(fmStyle);
|
||||
|
||||
// Derive paths from our own URL: /SenSu/static/pages/files.html → /SenSu
|
||||
var apiBase = location.pathname.replace(/\/static\/pages\/files\.html.*$/, '');
|
||||
var script = document.createElement('script');
|
||||
script.src = staticBase + '/pages/files.js?v=0602';
|
||||
script.src = apiBase + '/static/pages/files.js?v=0603';
|
||||
script.onload = function(){
|
||||
if(window.FilesModule){
|
||||
if(!window.FilesModule) return;
|
||||
window.FilesModule._apiBase = apiBase;
|
||||
if(window.FilesModule.init) window.FilesModule.init();
|
||||
}
|
||||
window.FilesModule.init();
|
||||
};
|
||||
script.onerror = function(){
|
||||
document.body.innerHTML = '<div style=\"color:red;padding:20px\">Failed to load files.js</div>';
|
||||
};
|
||||
document.head.appendChild(script);
|
||||
})();
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -27,7 +27,7 @@ window.FilesModule = {
|
||||
});
|
||||
|
||||
// Path input: Enter to jump
|
||||
var input = document.getElementById('fm-path-input');
|
||||
var input = FilesModule._$('fm-path-input');
|
||||
input.onkeydown = function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
var p = input.value.trim();
|
||||
@@ -40,6 +40,8 @@ window.FilesModule = {
|
||||
|
||||
/* ── API helpers ── */
|
||||
_apiBase: '',
|
||||
_scope: null,
|
||||
_$: function(id){ return this._scope ? this._scope.querySelector('#'+id) : document.getElementById(id); },
|
||||
_apiUrl: function(path) {
|
||||
// Use explicit base if set (standalone/iframe mode), otherwise relative
|
||||
if (this._apiBase) return this._apiBase + '/api/files' + path;
|
||||
@@ -77,11 +79,11 @@ window.FilesModule = {
|
||||
},
|
||||
|
||||
_pathError: function(msg) {
|
||||
var list = document.getElementById('fm-list');
|
||||
var list = FilesModule._$('fm-list');
|
||||
list.innerHTML = '<div style="text-align:center;color:var(--error);padding:40px">' +
|
||||
'⚠ ' + this._esc(msg) + '</div>';
|
||||
// Still update the input and breadcrumb to show what was attempted
|
||||
document.getElementById('fm-breadcrumb').innerHTML =
|
||||
FilesModule._$('fm-breadcrumb').innerHTML =
|
||||
'<span class="fm-crumb" style="color:var(--error)">' + this._esc(msg) + '</span>';
|
||||
},
|
||||
|
||||
@@ -92,12 +94,12 @@ window.FilesModule = {
|
||||
|
||||
refresh: function() {
|
||||
var self = this;
|
||||
var showHidden = document.getElementById('fm-hidden')?.checked ? '1' : '0';
|
||||
var showHidden = FilesModule._$('fm-hidden')?.checked ? '1' : '0';
|
||||
self._get('/list?path=' + encodeURIComponent(self.currentPath) + '&show_hidden=' + showHidden)
|
||||
.then(function(d) {
|
||||
self._render(d);
|
||||
}).catch(function(e) {
|
||||
document.getElementById('fm-list').innerHTML =
|
||||
FilesModule._$('fm-list').innerHTML =
|
||||
'<div style="text-align:center;color:var(--error);padding:40px">加载失败: ' + self._esc(e.message) + '</div>';
|
||||
});
|
||||
},
|
||||
@@ -105,13 +107,14 @@ window.FilesModule = {
|
||||
/* ── Render ── */
|
||||
_render: function(d) {
|
||||
var self = this;
|
||||
self.currentPath = d.current || '';
|
||||
|
||||
// Update editable path input
|
||||
var input = document.getElementById('fm-path-input');
|
||||
input.value = d.current || '';
|
||||
var input = FilesModule._$('fm-path-input');
|
||||
input.value = self.currentPath;
|
||||
|
||||
// Breadcrumbs (logical path)
|
||||
var bc = document.getElementById('fm-breadcrumb');
|
||||
var bc = FilesModule._$('fm-breadcrumb');
|
||||
var html = self._buildBreadcrumbHTML(d.breadcrumbs);
|
||||
// Resolved breadcrumbs — show as second row when symlink redirect
|
||||
if (d.resolved_breadcrumbs) {
|
||||
@@ -125,13 +128,13 @@ window.FilesModule = {
|
||||
});
|
||||
|
||||
// File list
|
||||
var list = document.getElementById('fm-list');
|
||||
var list = FilesModule._$('fm-list');
|
||||
var rows = '';
|
||||
|
||||
// ".." row for parent (unless at filesystem root)
|
||||
if (d.parent && d.parent !== d.current) {
|
||||
rows += '<div class="fm-row fm-dir fm-parent" data-path="' + self._escAttr(d.parent) + '" data-is-dir="1">' +
|
||||
'<span class="fm-icon">📂</span>' +
|
||||
'<span class="fm-icon"><svg width="18" height="18" viewBox="0 0 24 24"><path fill="var(--text-dim)" d="M20 6h-8l-2-2H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2zm-1 4H5V8h14v2z"/></svg></span>' +
|
||||
'<span class="fm-name" style="font-weight:600">..</span>' +
|
||||
'<span class="fm-size"></span>' +
|
||||
'<span class="fm-date"></span>' +
|
||||
@@ -143,13 +146,13 @@ window.FilesModule = {
|
||||
if (item.broken) {
|
||||
// Broken symlink or unreadable entry — show as disabled
|
||||
rows += '<div class="fm-row fm-broken" data-path="">' +
|
||||
'<span class="fm-icon">❓</span>' +
|
||||
'<span class="fm-icon"><svg width="18" height="18" viewBox="0 0 24 24"><path fill="var(--error)" d="M11 18h2v-2h-2v2zm1-16C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm0-14c-2.21 0-4 1.79-4 4h2c0-1.1.9-2 2-2s2 .9 2 2c0 2-3 1.75-3 5h2c0-2.25 3-2.5 3-5 0-2.21-1.79-4-4-4z"/></svg></span>' +
|
||||
'<span class="fm-name" style="color:var(--error)" title="损坏的链接或无法访问">' + self._esc(item.name) + '</span>' +
|
||||
'<span class="fm-size">—</span>' +
|
||||
'<span class="fm-date">—</span></div>';
|
||||
return;
|
||||
}
|
||||
var icon = item.is_dir ? '📁' : FilesModule._fileIcon(item.ext);
|
||||
var icon = item.is_dir ? FilesModule._dirIcon() : FilesModule._fileIcon(item.ext);
|
||||
var sizeStr = item.is_dir ? '—' : FilesModule._fmtSize(item.size);
|
||||
rows += '<div class="fm-row' + (item.is_dir ? ' fm-dir' : '') +
|
||||
'" data-path="' + self._escAttr(item.path) +
|
||||
@@ -190,19 +193,19 @@ window.FilesModule = {
|
||||
|
||||
// Close context on outside click
|
||||
document.onclick = function() {
|
||||
document.getElementById('fm-context').style.display = 'none';
|
||||
FilesModule._$('fm-context').style.display = 'none';
|
||||
};
|
||||
|
||||
// Picker banner — at bottom
|
||||
if (self.pickerMode) {
|
||||
var banner = document.getElementById('fm-picker-banner');
|
||||
var banner = FilesModule._$('fm-picker-banner');
|
||||
if (!banner) {
|
||||
banner = document.createElement('div');
|
||||
banner.id = 'fm-picker-banner';
|
||||
banner.innerHTML = '<div style="display:flex;align-items:center;gap:8px;padding:6px 12px;background:var(--primary-container);color:var(--md-sys-color-on-primary-container);border-radius:var(--shape-xs);font-size:.82rem">' +
|
||||
'<span>📂 选择' + (self.pickerMode === 'file' ? '文件' : '目录') + '模式</span>' +
|
||||
'<button class="btn btn-sm btn-filled" onclick="FilesModule._pickResult(FilesModule.currentPath)" style="margin-left:auto">选择当前目录</button>' +
|
||||
'<button class="btn btn-sm btn-outlined" onclick="FilesModule._cancelPicker()">取消</button>' +
|
||||
banner.innerHTML = '<div style="display:flex;align-items:center;gap:8px;padding:8px 12px;background:var(--primary-container);color:var(--md-sys-color-on-primary-container);border-radius:var(--shape-xs);font-size:.82rem">' +
|
||||
'<span><svg width="16" height="16" viewBox="0 0 24 24" style="vertical-align:-3px"><path fill="currentColor" d="M20 6h-8l-2-2H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2z"/></svg> 选择' + (self.pickerMode === 'file' ? '文件' : '目录') + '模式</span>' +
|
||||
'<button type="button" style="margin-left:auto;padding:6px 18px;border:none;border-radius:20px;background:var(--primary);color:var(--on-primary);font-weight:500;cursor:pointer;font-size:.82rem" onclick="_fmPick()">选择当前目录</button>' +
|
||||
'<button type="button" style="padding:6px 18px;border:1px solid;border-radius:20px;background:transparent;color:var(--md-sys-color-on-primary-container);font-weight:500;cursor:pointer;font-size:.82rem" onclick="_fmCancel()">取消</button>' +
|
||||
'</div>';
|
||||
list.parentNode.appendChild(banner);
|
||||
list.style.flex = '';
|
||||
@@ -212,7 +215,7 @@ window.FilesModule = {
|
||||
banner.style.display = 'block';
|
||||
}
|
||||
} else {
|
||||
var oldBanner = document.getElementById('fm-picker-banner');
|
||||
var oldBanner = FilesModule._$('fm-picker-banner');
|
||||
if (oldBanner) oldBanner.style.display = 'none';
|
||||
}
|
||||
},
|
||||
@@ -221,7 +224,9 @@ window.FilesModule = {
|
||||
_pickResult: function(path) {
|
||||
// In iframe (picker mode): post to parent, parent closes overlay
|
||||
if (window.parent !== window) {
|
||||
window.parent.postMessage(JSON.stringify({action:'fm-picked', path: path, callback_id: this.pickerCallback}), '*');
|
||||
// Ensure path is never empty — use current path as fallback
|
||||
var p = path || this.currentPath || '/';
|
||||
window.parent.postMessage(JSON.stringify({action:'fm-picked', path: p, callback_id: this.pickerCallback}), '*');
|
||||
return;
|
||||
}
|
||||
// Standalone clipboard fallback
|
||||
@@ -274,18 +279,18 @@ window.FilesModule = {
|
||||
var self = this;
|
||||
this._get('/read?path=' + encodeURIComponent(path))
|
||||
.then(function(d) {
|
||||
document.getElementById('fm-editor-title').textContent = '📝 ' + (name || d.name);
|
||||
document.getElementById('fm-editor-textarea').value = d.content;
|
||||
document.getElementById('fm-editor-textarea').dataset.path = path;
|
||||
document.getElementById('fm-editor').style.display = 'flex';
|
||||
FilesModule._$('fm-editor-title').innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" style="vertical-align:-3px;margin-right:4px"><path fill="var(--primary)" d="M14 2H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V8l-6-6zm-1 2l5 5h-5V4zM8 14h8v2H8v-2zm0-4h8v2H8v-2z"/></svg>' + (name || d.name);
|
||||
FilesModule._$('fm-editor-textarea').value = d.content;
|
||||
FilesModule._$('fm-editor-textarea').dataset.path = path;
|
||||
FilesModule._$('fm-editor').style.display = 'flex';
|
||||
}).catch(function(e) { alert('无法读取: ' + e.message); });
|
||||
},
|
||||
closeEditor: function() {
|
||||
document.getElementById('fm-editor').style.display = 'none';
|
||||
FilesModule._$('fm-editor').style.display = 'none';
|
||||
},
|
||||
saveFile: function() {
|
||||
var self = this;
|
||||
var ta = document.getElementById('fm-editor-textarea');
|
||||
var ta = FilesModule._$('fm-editor-textarea');
|
||||
this._post('/write', {path: ta.dataset.path, content: ta.value})
|
||||
.then(function() { self.closeEditor(); self.refresh(); })
|
||||
.catch(function(e) { alert('保存失败: ' + e.message); });
|
||||
@@ -294,7 +299,7 @@ window.FilesModule = {
|
||||
/* ── Context menu ── */
|
||||
_showContext: function(e, item) {
|
||||
this.contextTarget = item;
|
||||
var ctx = document.getElementById('fm-context');
|
||||
var ctx = FilesModule._$('fm-context');
|
||||
ctx.style.display = 'block';
|
||||
ctx.style.left = e.pageX + 'px';
|
||||
ctx.style.top = e.pageY + 'px';
|
||||
@@ -304,7 +309,7 @@ window.FilesModule = {
|
||||
ctxDownload: function() {
|
||||
var t = this.contextTarget;
|
||||
if (t) window.open(FilesModule._apiUrl('/download?path=') + encodeURIComponent(t.path), '_blank');
|
||||
document.getElementById('fm-context').style.display = 'none';
|
||||
FilesModule._$('fm-context').style.display = 'none';
|
||||
},
|
||||
ctxRename: function() {
|
||||
var self = this;
|
||||
@@ -315,20 +320,20 @@ window.FilesModule = {
|
||||
this._post('/rename', {path: t.path, new_name: nn})
|
||||
.then(function() { self.refresh(); })
|
||||
.catch(function(e) { alert('重命名失败: ' + e.message); });
|
||||
document.getElementById('fm-context').style.display = 'none';
|
||||
FilesModule._$('fm-context').style.display = 'none';
|
||||
},
|
||||
ctxEdit: function() {
|
||||
var t = this.contextTarget;
|
||||
if (t && !t.isDir) this.openEditor(t.path, t.name);
|
||||
document.getElementById('fm-context').style.display = 'none';
|
||||
FilesModule._$('fm-context').style.display = 'none';
|
||||
},
|
||||
ctxDelete: function() {
|
||||
var t = this.contextTarget;
|
||||
if (!t) return;
|
||||
this.toDelete = t;
|
||||
document.getElementById('fm-dialog-msg').textContent = '确认删除 "' + t.name + '"?此操作不可撤销。';
|
||||
document.getElementById('fm-dialog').style.display = 'flex';
|
||||
document.getElementById('fm-context').style.display = 'none';
|
||||
FilesModule._$('fm-dialog-msg').textContent = '确认删除 "' + t.name + '"?此操作不可撤销。';
|
||||
FilesModule._$('fm-dialog').style.display = 'flex';
|
||||
FilesModule._$('fm-context').style.display = 'none';
|
||||
},
|
||||
confirmDelete: function() {
|
||||
var self = this;
|
||||
@@ -338,7 +343,7 @@ window.FilesModule = {
|
||||
.catch(function(e) { alert('删除失败: ' + e.message); });
|
||||
},
|
||||
closeDialog: function() {
|
||||
document.getElementById('fm-dialog').style.display = 'none';
|
||||
FilesModule._$('fm-dialog').style.display = 'none';
|
||||
this.toDelete = null;
|
||||
},
|
||||
|
||||
@@ -366,24 +371,49 @@ window.FilesModule = {
|
||||
if (bytes < 1073741824) return (bytes / 1048576).toFixed(1) + ' MB';
|
||||
return (bytes / 1073741824).toFixed(2) + ' GB';
|
||||
},
|
||||
_dirIcon: function() {
|
||||
return '<svg width="18" height="18" viewBox="0 0 24 24"><path fill="var(--primary)" d="M10 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z"/></svg>';
|
||||
},
|
||||
_fileIcon: function(ext) {
|
||||
var map = {
|
||||
'.py':'🐍','.js':'📜','.ts':'📘','.html':'🌐','.css':'🎨',
|
||||
'.json':'📋','.yaml':'⚙','.yml':'⚙','.md':'📝','.txt':'📄',
|
||||
'.log':'📊','.sh':'💻','.bat':'💻','.xml':'📰','.toml':'⚙',
|
||||
'.cfg':'⚙','.ini':'⚙','.conf':'⚙','.sql':'🗄','.csv':'📊',
|
||||
'.zip':'📦','.tar':'📦','.gz':'📦','.7z':'📦',
|
||||
'.png':'🖼','.jpg':'🖼','.jpeg':'🖼','.gif':'🖼','.svg':'🖼','.ico':'🖼',
|
||||
'.mp3':'🎵','.wav':'🎵','.ogg':'🎵','.mp4':'🎬','.avi':'🎬',
|
||||
'.pdf':'📕','.doc':'📃','.docx':'📃','.xls':'📊','.xlsx':'📊',
|
||||
'.c':'⚡','.h':'⚡','.cpp':'⚡','.java':'☕','.rs':'🦀','.go':'🔵',
|
||||
};
|
||||
return map[ext] || '📄';
|
||||
// Group by category → single SVG per category
|
||||
var code = '<svg width="18" height="18" viewBox="0 0 24 24"><path fill="var(--text-dim)" d="M14 2H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V8l-6-6zm-1 2l5 5h-5V4zM8 14h4v-2H8v2zm0 3h2v-2H8v2z"/></svg>';
|
||||
var img = '<svg width="18" height="18" viewBox="0 0 24 24"><path fill="var(--text-dim)" d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"/></svg>';
|
||||
var arch = '<svg width="18" height="18" viewBox="0 0 24 24"><path fill="var(--text-dim)" d="M20 6h-8l-2-2H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2zm-2 10H6v-2h12v2zm0-4H6V8h12v4z"/></svg>';
|
||||
var media = '<svg width="18" height="18" viewBox="0 0 24 24"><path fill="var(--text-dim)" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/></svg>';
|
||||
|
||||
var codeExts = ['.py','.js','.ts','.jsx','.tsx','.html','.css','.scss','.less','.json','.yaml','.yml','.xml','.toml','.ini','.cfg','.conf','.sql','.sh','.bat','.c','.h','.cpp','.hpp','.java','.kt','.rs','.go','.rb','.php','.swift','.r','.lua','.pl','.scala','.dart','.Makefile','.gitignore','.dockerfile','.env'];
|
||||
var imgExts = ['.png','.jpg','.jpeg','.gif','.svg','.ico','.bmp','.webp','.tiff'];
|
||||
var mediaExts = ['.mp3','.wav','.ogg','.flac','.aac','.mp4','.avi','.mkv','.mov','.webm'];
|
||||
var archExts = ['.zip','.tar','.gz','.7z','.rar','.bz2','.xz'];
|
||||
|
||||
if (codeExts.indexOf(ext) >= 0) return code;
|
||||
if (imgExts.indexOf(ext) >= 0) return img;
|
||||
if (mediaExts.indexOf(ext) >= 0) return media;
|
||||
if (archExts.indexOf(ext) >= 0) return arch;
|
||||
return code; // default: document icon
|
||||
},
|
||||
|
||||
destroy: function() {
|
||||
document.getElementById('fm-context').style.display = 'none';
|
||||
document.getElementById('fm-dialog').style.display = 'none';
|
||||
document.getElementById('fm-editor').style.display = 'none';
|
||||
FilesModule._$('fm-context').style.display = 'none';
|
||||
FilesModule._$('fm-dialog').style.display = 'none';
|
||||
FilesModule._$('fm-editor').style.display = 'none';
|
||||
}
|
||||
};
|
||||
|
||||
/* ── Global picker functions (used by onclick in banner HTML) ── */
|
||||
window._fmPick = function() {
|
||||
var p = window.FilesModule.currentPath || '/';
|
||||
// Visual feedback to confirm execution
|
||||
var banner = FilesModule._$('fm-picker-banner');
|
||||
if(banner) banner.style.background = '#4caf50';
|
||||
if (window.parent !== window) {
|
||||
window.parent.postMessage(JSON.stringify({action:'fm-picked', path:p, callback_id:window.FilesModule.pickerCallback}), '*');
|
||||
}
|
||||
};
|
||||
window._fmCancel = function() {
|
||||
var banner = FilesModule._$('fm-picker-banner');
|
||||
if(banner) banner.style.background = '#f44336';
|
||||
if (window.parent !== window) {
|
||||
window.parent.postMessage(JSON.stringify({action:'fm-picked', path:null, callback_id:window.FilesModule.pickerCallback}), '*');
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user