From 9de094ea5432ba7faffdb729627a127c13517686 Mon Sep 17 00:00:00 2001 From: qinglong Date: Fri, 12 Jun 2026 19:00:19 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E7=A7=BB=E9=99=A4=E9=81=97=E7=95=99fmfu?= =?UTF-8?q?ncs=E7=9B=AE=E5=BD=95=E5=88=9B=E5=BB=BA=20+=20=E6=B8=85?= =?UTF-8?q?=E7=90=86init=5Fservice=E6=97=A0=E7=94=A8=E5=AF=BC=E5=85=A5=20+?= =?UTF-8?q?=20=E6=96=87=E4=BB=B6=E7=AE=A1=E7=90=86=E5=99=A8SVG=E5=9B=BE?= =?UTF-8?q?=E6=A0=87=20+=20=E7=A4=BA=E4=BE=8B=E6=8F=92=E4=BB=B6MD3?= =?UTF-8?q?=E6=94=B9=E9=80=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- config/plugins/commands.yaml | 2 +- config/services/network_routes.yaml | 2 +- docs/SenSu 插件开发详细指南.md | 79 +++++++---- plugins/example_plugin/dashboard.html | 40 ++++-- services/init_service.py | 46 +------ static/web_panel/css/style.css | 9 +- static/web_panel/home.html | 2 +- static/web_panel/js/app.js | 104 ++++++++++---- .../pages/config/framework/base_config.yaml | 21 --- .../config/framework/permission_rules.yaml | 12 -- .../permissions/granted_permissions.json | 1 - .../config/permissions/pending_requests.json | 1 - .../config/permissions/plugin_status.json | 1 - .../pages/config/plugins/commands.yaml | 89 ------------ static/web_panel/pages/files.html | 54 ++++---- static/web_panel/pages/files.js | 128 +++++++++++------- 16 files changed, 283 insertions(+), 308 deletions(-) delete mode 100644 static/web_panel/pages/config/framework/base_config.yaml delete mode 100644 static/web_panel/pages/config/framework/permission_rules.yaml delete mode 100644 static/web_panel/pages/config/permissions/granted_permissions.json delete mode 100644 static/web_panel/pages/config/permissions/pending_requests.json delete mode 100644 static/web_panel/pages/config/permissions/plugin_status.json delete mode 100644 static/web_panel/pages/config/plugins/commands.yaml diff --git a/config/plugins/commands.yaml b/config/plugins/commands.yaml index d27f62a..af90530 100644 --- a/config/plugins/commands.yaml +++ b/config/plugins/commands.yaml @@ -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 diff --git a/config/services/network_routes.yaml b/config/services/network_routes.yaml index 731fcc8..85887ef 100644 --- a/config/services/network_routes.yaml +++ b/config/services/network_routes.yaml @@ -1,5 +1,5 @@ http_port: 4200 -last_updated: 297792.741311191 +last_updated: 316707.287225746 plugin_routes: example_plugin: - methods: diff --git a/docs/SenSu 插件开发详细指南.md b/docs/SenSu 插件开发详细指南.md index 7458e46..e275b29 100644 --- a/docs/SenSu 插件开发详细指南.md +++ b/docs/SenSu 插件开发详细指南.md @@ -3432,41 +3432,72 @@ class MyPlugin(PluginWebMixin): - 提取 ` +function doAction(){ /* ... */ } + ``` +**常用 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` 函数 | 函数需在 ` \ No newline at end of file +
+

Counter

+
0
+
+ + +
+
+
+

Events (SSE)

+
Waiting...
+
+ \ No newline at end of file diff --git a/services/init_service.py b/services/init_service.py index bf418b5..2736caa 100644 --- a/services/init_service.py +++ b/services/init_service.py @@ -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: diff --git a/static/web_panel/css/style.css b/static/web_panel/css/style.css index 490e895..07d8513 100644 --- a/static/web_panel/css/style.css +++ b/static/web_panel/css/style.css @@ -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)} diff --git a/static/web_panel/home.html b/static/web_panel/home.html index 505e2f0..18dfcdf 100644 --- a/static/web_panel/home.html +++ b/static/web_panel/home.html @@ -72,6 +72,6 @@ - + diff --git a/static/web_panel/js/app.js b/static/web_panel/js/app.js index b21b71a..7dac60a 100644 --- a/static/web_panel/js/app.js +++ b/static/web_panel/js/app.js @@ -154,8 +154,12 @@ async function loadPluginPage(pluginName) { var scripts = []; clean = clean.replace(/]*>([\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 element + styles = styles.replace(/body\s*\{/gi, '.plugin-page-root{').replace(/body\b/gi, '.plugin-page-root'); + content.style.padding = '0'; - content.innerHTML = ''+clean; + content.innerHTML = '
'+clean+'
'; scripts.forEach(function(code){ try { var s=document.createElement('script'); s.textContent=code; document.head.appendChild(s); document.head.removeChild(s); } @@ -172,51 +176,99 @@ 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 = '📂 选择' + (mode==='file'?'文件':'目录') + '' + - ''; + 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); + } + + // 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(/]*>/gi, '').replace(/]*>/gi, ''); + // Extract inline scripts + var scripts = []; + var clean = html.replace(/]*>([\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(); } - } catch(ex) {} - }); + // 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 = '
加载失败: '+e.message+'
'; + }); }; // ═══ Sidebar toggle ═══ diff --git a/static/web_panel/pages/config/framework/base_config.yaml b/static/web_panel/pages/config/framework/base_config.yaml deleted file mode 100644 index b202411..0000000 --- a/static/web_panel/pages/config/framework/base_config.yaml +++ /dev/null @@ -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 diff --git a/static/web_panel/pages/config/framework/permission_rules.yaml b/static/web_panel/pages/config/framework/permission_rules.yaml deleted file mode 100644 index 4977aeb..0000000 --- a/static/web_panel/pages/config/framework/permission_rules.yaml +++ /dev/null @@ -1,12 +0,0 @@ -admin_permissions: -- framework.* -- plugin.* -- service.* -default_permissions: -- framework.status.read -- plugin.self.info.read -permission_levels: -- read -- write -- execute -- admin diff --git a/static/web_panel/pages/config/permissions/granted_permissions.json b/static/web_panel/pages/config/permissions/granted_permissions.json deleted file mode 100644 index 9e26dfe..0000000 --- a/static/web_panel/pages/config/permissions/granted_permissions.json +++ /dev/null @@ -1 +0,0 @@ -{} \ No newline at end of file diff --git a/static/web_panel/pages/config/permissions/pending_requests.json b/static/web_panel/pages/config/permissions/pending_requests.json deleted file mode 100644 index 9e26dfe..0000000 --- a/static/web_panel/pages/config/permissions/pending_requests.json +++ /dev/null @@ -1 +0,0 @@ -{} \ No newline at end of file diff --git a/static/web_panel/pages/config/permissions/plugin_status.json b/static/web_panel/pages/config/permissions/plugin_status.json deleted file mode 100644 index 9e26dfe..0000000 --- a/static/web_panel/pages/config/permissions/plugin_status.json +++ /dev/null @@ -1 +0,0 @@ -{} \ No newline at end of file diff --git a/static/web_panel/pages/config/plugins/commands.yaml b/static/web_panel/pages/config/plugins/commands.yaml deleted file mode 100644 index 27c5cdb..0000000 --- a/static/web_panel/pages/config/plugins/commands.yaml +++ /dev/null @@ -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 diff --git a/static/web_panel/pages/files.html b/static/web_panel/pages/files.html index e5f821e..97d9f0f 100644 --- a/static/web_panel/pages/files.html +++ b/static/web_panel/pages/files.html @@ -4,10 +4,7 @@
-
- -
-
+
+ +
+
加载中...
@@ -59,36 +59,40 @@
diff --git a/static/web_panel/pages/files.js b/static/web_panel/pages/files.js index 65fc9b9..f749d4b 100644 --- a/static/web_panel/pages/files.js +++ b/static/web_panel/pages/files.js @@ -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 = '
' + '⚠ ' + this._esc(msg) + '
'; // Still update the input and breadcrumb to show what was attempted - document.getElementById('fm-breadcrumb').innerHTML = + FilesModule._$('fm-breadcrumb').innerHTML = '' + this._esc(msg) + ''; }, @@ -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 = '
加载失败: ' + self._esc(e.message) + '
'; }); }, @@ -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 += '
' + - '📂' + + '' + '..' + '' + '' + @@ -143,13 +146,13 @@ window.FilesModule = { if (item.broken) { // Broken symlink or unreadable entry — show as disabled rows += '
' + - '' + + '' + '' + self._esc(item.name) + '' + '' + '
'; 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 += '
' + - '📂 选择' + (self.pickerMode === 'file' ? '文件' : '目录') + '模式' + - '' + - '' + + banner.innerHTML = '
' + + ' 选择' + (self.pickerMode === 'file' ? '文件' : '目录') + '模式' + + '' + + '' + '
'; 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 = '' + (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 ''; + }, _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 = ''; + var img = ''; + var arch = ''; + var media = ''; + + 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}), '*'); } };