diff --git a/static/web_panel/css/style.css b/static/web_panel/css/style.css index 1c2757d..490e895 100644 --- a/static/web_panel/css/style.css +++ b/static/web_panel/css/style.css @@ -453,8 +453,11 @@ h3{font-size:1rem;font-weight:500;letter-spacing:.15px} .fm-crumb-sep{color:var(--outline);font-size:.7rem;flex-shrink:0} .fm-resolved-crumbs{display:flex;align-items:center;gap:4px;font-size:.75rem;opacity:.8;flex-basis:100%} .fm-actions{display:flex;gap:6px;flex-shrink:0;align-items:center} -.fm-toggle{display:flex;align-items:center;gap:4px;font-size:.75rem;color:var(--text-dim);cursor:pointer} -.fm-toggle input{margin:0} +.fm-toggle{display:flex;align-items:center;gap:8px;font-size:.8rem;font-weight:500;color:var(--text-dim);cursor:pointer;user-select:none} +.fm-toggle input[type="checkbox"]{-webkit-appearance:none;appearance:none;width:36px;height:20px;background:var(--md-sys-color-surface-container-highest);border:2px solid var(--outline);border-radius:10px;cursor:pointer;position:relative;transition:.2s;margin:0;flex-shrink:0} +.fm-toggle input[type="checkbox"]::after{content:"";position:absolute;top:2px;left:2px;width:12px;height:12px;background:var(--outline);border-radius:50%;transition:.2s} +.fm-toggle input[type="checkbox"]:checked{background:var(--primary);border-color:var(--primary)} +.fm-toggle input[type="checkbox"]:checked::after{left:18px;background:var(--on-primary)} .fm-list{flex:1;overflow-y:auto;scrollbar-width:thin;scrollbar-color:var(--outline) transparent} .fm-row{display:grid;grid-template-columns:32px 1fr 100px 140px;align-items:center;padding:10px 12px;cursor:pointer;border-radius:var(--shape-xs);transition:background .12s;gap:8px;font-size:.88rem} .fm-row:hover{background:rgba(208,188,255,.06)} diff --git a/static/web_panel/home.html b/static/web_panel/home.html index 9b89b90..505e2f0 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 4af5209..b21b71a 100644 --- a/static/web_panel/js/app.js +++ b/static/web_panel/js/app.js @@ -172,20 +172,47 @@ async function loadPluginPage(pluginName) { } -// ═══ Global file/dir picker (reusable by any page) ═══ -// Usage: pickPath('dir', function(path) { document.getElementById('my-input').value = path; }) +// ═══ Global file/dir picker (in-page iframe modal) ═══ window.pickPath = function(mode, callback) { var id = 'picker_' + Date.now(); - window['_pickCallback_' + id] = callback; var url = './static/pages/files.html?picker=1&mode=' + (mode || 'dir') + '&cb=' + id; - var w = window.open(url, 'fm-picker', 'width=680,height=520'); - if (!w) { alert('请允许弹窗以使用文件选择器'); return; } - // Listen for pick result + + // 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 + 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'?'文件':'目录') + '' + + ''; + box.appendChild(header); + + // Iframe + var iframe = document.createElement('iframe'); + iframe.src = url; + iframe.style.cssText = 'flex:1;border:none;width:100%'; + box.appendChild(iframe); + overlay.appendChild(box); + document.body.appendChild(overlay); + + function closePicker() { + window.removeEventListener('message', handler); + if (overlay.parentNode) overlay.parentNode.removeChild(overlay); + } + window._closePicker = 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) { - window.removeEventListener('message', handler); + closePicker(); if (d.path && callback) callback(d.path); } } catch(ex) {} diff --git a/static/web_panel/pages/files.html b/static/web_panel/pages/files.html index 7860fd2..e5f821e 100644 --- a/static/web_panel/pages/files.html +++ b/static/web_panel/pages/files.html @@ -1,4 +1,6 @@ + +
@@ -64,3 +66,29 @@
🗑 删除
+ + diff --git a/static/web_panel/pages/files.js b/static/web_panel/pages/files.js index c07544c..65fc9b9 100644 --- a/static/web_panel/pages/files.js +++ b/static/web_panel/pages/files.js @@ -39,6 +39,12 @@ window.FilesModule = { }, /* ── API helpers ── */ + _apiBase: '', + _apiUrl: function(path) { + // Use explicit base if set (standalone/iframe mode), otherwise relative + if (this._apiBase) return this._apiBase + '/api/files' + path; + return './api/files' + path; + }, _api: function(url, opts) { opts = opts || {}; opts.credentials = 'include'; @@ -47,16 +53,16 @@ window.FilesModule = { return r.json(); }); }, - _get: function(url) { return this._api(url); }, + _get: function(url) { return this._api(this._apiUrl(url)); }, _post: function(url, body) { - return this._api(url, {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(body)}); + return this._api(this._apiUrl(url), {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(body)}); }, /* ── Jump to path (with existence check) ── */ _jumpTo: function(path) { var self = this; // Probe: try to list the target path - this._get('./api/files/info?path=' + encodeURIComponent(path)) + this._get('/info?path=' + encodeURIComponent(path)) .then(function(info) { if (info.error) { self._pathError(info.error); return; } if (info.is_dir) { @@ -87,7 +93,7 @@ window.FilesModule = { refresh: function() { var self = this; var showHidden = document.getElementById('fm-hidden')?.checked ? '1' : '0'; - self._get('./api/files/list?path=' + encodeURIComponent(self.currentPath) + '&show_hidden=' + showHidden) + self._get('/list?path=' + encodeURIComponent(self.currentPath) + '&show_hidden=' + showHidden) .then(function(d) { self._render(d); }).catch(function(e) { @@ -187,37 +193,48 @@ window.FilesModule = { document.getElementById('fm-context').style.display = 'none'; }; - // Picker banner + // Picker banner — at bottom if (self.pickerMode) { var banner = document.getElementById('fm-picker-banner'); if (!banner) { banner = document.createElement('div'); banner.id = 'fm-picker-banner'; - banner.innerHTML = '
' + + banner.innerHTML = '
' + '📂 选择' + (self.pickerMode === 'file' ? '文件' : '目录') + '模式' + '' + '' + '
'; - list.parentNode.insertBefore(banner, list); + list.parentNode.appendChild(banner); + list.style.flex = ''; + banner.style.marginTop = 'auto'; + banner.style.paddingBottom = '0'; + } else { + banner.style.display = 'block'; } + } else { + var oldBanner = document.getElementById('fm-picker-banner'); + if (oldBanner) oldBanner.style.display = 'none'; } }, /* ── Picker ── */ _pickResult: function(path) { - if (window.opener) { - window.opener.postMessage(JSON.stringify({action:'fm-picked', path: path, callback_id: this.pickerCallback}), '*'); - window.close(); - } else if (window.parent !== window) { + // 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}), '*'); - } else { - navigator.clipboard?.writeText(path); - alert('已选择: ' + path + '\n(路径已复制到剪贴板)'); - this.pickerMode = false; - this.refresh(); + return; } + // Standalone clipboard fallback + navigator.clipboard?.writeText(path); + alert('已选择: ' + path); + this.pickerMode = false; + this.refresh(); }, _cancelPicker: function() { + if (window.parent !== window) { + window.parent.postMessage(JSON.stringify({action:'fm-picked', path: null, callback_id: this.pickerCallback}), '*'); + return; + } this.pickerMode = false; this.refresh(); }, @@ -227,7 +244,7 @@ window.FilesModule = { var self = this; var name = prompt('新建文件名:'); if (!name) return; - this._post('./api/files/touch', {path: this.currentPath, name: name}) + this._post('/touch', {path: this.currentPath, name: name}) .then(function() { self.refresh(); }) .catch(function(e) { alert('创建失败: ' + e.message); }); }, @@ -235,7 +252,7 @@ window.FilesModule = { var self = this; var name = prompt('新建文件夹名:'); if (!name) return; - this._post('./api/files/mkdir', {path: this.currentPath, name: name}) + this._post('/mkdir', {path: this.currentPath, name: name}) .then(function() { self.refresh(); }) .catch(function(e) { alert('创建失败: ' + e.message); }); }, @@ -244,7 +261,7 @@ window.FilesModule = { if (!files || !files.length) return; var form = new FormData(); for (var i = 0; i < files.length; i++) form.append('file', files[i]); - fetch('./api/files/upload?path=' + encodeURIComponent(self.currentPath), {method:'POST', credentials:'include', body:form}) + fetch(self._apiUrl('/upload?path=') + encodeURIComponent(self.currentPath), {method:'POST', credentials:'include', body:form}) .then(function(r) { return r.json(); }) .then(function(d) { if (d.ok) self.refresh(); @@ -255,7 +272,7 @@ window.FilesModule = { /* ── Editor ── */ openEditor: function(path, name) { var self = this; - this._get('./api/files/read?path=' + encodeURIComponent(path)) + 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; @@ -269,7 +286,7 @@ window.FilesModule = { saveFile: function() { var self = this; var ta = document.getElementById('fm-editor-textarea'); - this._post('./api/files/write', {path: ta.dataset.path, content: ta.value}) + this._post('/write', {path: ta.dataset.path, content: ta.value}) .then(function() { self.closeEditor(); self.refresh(); }) .catch(function(e) { alert('保存失败: ' + e.message); }); }, @@ -286,7 +303,7 @@ window.FilesModule = { }, ctxDownload: function() { var t = this.contextTarget; - if (t) window.open('./api/files/download?path=' + encodeURIComponent(t.path), '_blank'); + if (t) window.open(FilesModule._apiUrl('/download?path=') + encodeURIComponent(t.path), '_blank'); document.getElementById('fm-context').style.display = 'none'; }, ctxRename: function() { @@ -295,7 +312,7 @@ window.FilesModule = { if (!t) return; var nn = prompt('新名称:', t.name); if (!nn || nn === t.name) return; - this._post('./api/files/rename', {path: t.path, new_name: nn}) + 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'; @@ -316,7 +333,7 @@ window.FilesModule = { confirmDelete: function() { var self = this; if (!this.toDelete) return; - this._post('./api/files/delete', {path: this.toDelete.path}) + this._post('/delete', {path: this.toDelete.path}) .then(function() { self.toDelete = null; self.closeDialog(); self.refresh(); }) .catch(function(e) { alert('删除失败: ' + e.message); }); },