fix: 文件选择器改为页内iframe弹窗 + 乱码编码修复 + API路径修复
- pickPath()从window.open改为页内iframe模态弹窗(遮罩+关闭按钮) - files.html添加meta charset+CSS link,独立模式自动加载files.js - API路径统一通过_apiUrl()构建,适配主面板和iframe两种上下文 - 独立iframe模式注入body/container覆写样式(无topbar) - 选择模式栏移至底部margin-top:auto紧凑排列 - MD3风格toggle开关(显示隐藏复选框) - CSS版本号缓存破坏 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -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-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-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-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{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{margin:0}
|
.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-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{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)}
|
.fm-row:hover{background:rgba(208,188,255,.06)}
|
||||||
|
|||||||
@@ -72,6 +72,6 @@
|
|||||||
<script src="./static/js/chart.js?v=0603"></script>
|
<script src="./static/js/chart.js?v=0603"></script>
|
||||||
|
|
||||||
<!-- 🟢 2. 再加载主逻辑 -->
|
<!-- 🟢 2. 再加载主逻辑 -->
|
||||||
<script src="./static/js/app.js?v=0701"></script>
|
<script src="./static/js/app.js?v=0702"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -172,20 +172,47 @@ async function loadPluginPage(pluginName) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// ═══ Global file/dir picker (reusable by any page) ═══
|
// ═══ Global file/dir picker (in-page iframe modal) ═══
|
||||||
// Usage: pickPath('dir', function(path) { document.getElementById('my-input').value = path; })
|
|
||||||
window.pickPath = function(mode, callback) {
|
window.pickPath = function(mode, callback) {
|
||||||
var id = 'picker_' + Date.now();
|
var id = 'picker_' + Date.now();
|
||||||
window['_pickCallback_' + id] = callback;
|
|
||||||
var url = './static/pages/files.html?picker=1&mode=' + (mode || 'dir') + '&cb=' + id;
|
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; }
|
// Create modal overlay
|
||||||
// Listen for pick result
|
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 = '<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>';
|
||||||
|
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) {
|
window.addEventListener('message', function handler(e) {
|
||||||
try {
|
try {
|
||||||
var d = JSON.parse(e.data);
|
var d = JSON.parse(e.data);
|
||||||
if (d.action === 'fm-picked' && d.callback_id === id) {
|
if (d.action === 'fm-picked' && d.callback_id === id) {
|
||||||
window.removeEventListener('message', handler);
|
closePicker();
|
||||||
if (d.path && callback) callback(d.path);
|
if (d.path && callback) callback(d.path);
|
||||||
}
|
}
|
||||||
} catch(ex) {}
|
} catch(ex) {}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
<!-- File Manager Page -->
|
<!-- File Manager Page -->
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<link rel="stylesheet" href="../css/style.css?v=0601">
|
||||||
<div class="fm-container">
|
<div class="fm-container">
|
||||||
<!-- Top bar: editable path + actions -->
|
<!-- Top bar: editable path + actions -->
|
||||||
<div class="fm-toolbar">
|
<div class="fm-toolbar">
|
||||||
@@ -64,3 +66,29 @@
|
|||||||
<div class="fm-context-item danger" onclick="FilesModule.ctxDelete()">🗑 删除</div>
|
<div class="fm-context-item danger" onclick="FilesModule.ctxDelete()">🗑 删除</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Load files.js for standalone mode (iframe picker)
|
||||||
|
(function(){
|
||||||
|
// 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 script = document.createElement('script');
|
||||||
|
script.src = staticBase + '/pages/files.js?v=0602';
|
||||||
|
script.onload = function(){
|
||||||
|
if(window.FilesModule){
|
||||||
|
window.FilesModule._apiBase = apiBase;
|
||||||
|
if(window.FilesModule.init) window.FilesModule.init();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.head.appendChild(script);
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -39,6 +39,12 @@ window.FilesModule = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
/* ── API helpers ── */
|
/* ── 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) {
|
_api: function(url, opts) {
|
||||||
opts = opts || {};
|
opts = opts || {};
|
||||||
opts.credentials = 'include';
|
opts.credentials = 'include';
|
||||||
@@ -47,16 +53,16 @@ window.FilesModule = {
|
|||||||
return r.json();
|
return r.json();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
_get: function(url) { return this._api(url); },
|
_get: function(url) { return this._api(this._apiUrl(url)); },
|
||||||
_post: function(url, body) {
|
_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) ── */
|
/* ── Jump to path (with existence check) ── */
|
||||||
_jumpTo: function(path) {
|
_jumpTo: function(path) {
|
||||||
var self = this;
|
var self = this;
|
||||||
// Probe: try to list the target path
|
// Probe: try to list the target path
|
||||||
this._get('./api/files/info?path=' + encodeURIComponent(path))
|
this._get('/info?path=' + encodeURIComponent(path))
|
||||||
.then(function(info) {
|
.then(function(info) {
|
||||||
if (info.error) { self._pathError(info.error); return; }
|
if (info.error) { self._pathError(info.error); return; }
|
||||||
if (info.is_dir) {
|
if (info.is_dir) {
|
||||||
@@ -87,7 +93,7 @@ window.FilesModule = {
|
|||||||
refresh: function() {
|
refresh: function() {
|
||||||
var self = this;
|
var self = this;
|
||||||
var showHidden = document.getElementById('fm-hidden')?.checked ? '1' : '0';
|
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) {
|
.then(function(d) {
|
||||||
self._render(d);
|
self._render(d);
|
||||||
}).catch(function(e) {
|
}).catch(function(e) {
|
||||||
@@ -187,37 +193,48 @@ window.FilesModule = {
|
|||||||
document.getElementById('fm-context').style.display = 'none';
|
document.getElementById('fm-context').style.display = 'none';
|
||||||
};
|
};
|
||||||
|
|
||||||
// Picker banner
|
// Picker banner — at bottom
|
||||||
if (self.pickerMode) {
|
if (self.pickerMode) {
|
||||||
var banner = document.getElementById('fm-picker-banner');
|
var banner = document.getElementById('fm-picker-banner');
|
||||||
if (!banner) {
|
if (!banner) {
|
||||||
banner = document.createElement('div');
|
banner = document.createElement('div');
|
||||||
banner.id = 'fm-picker-banner';
|
banner.id = 'fm-picker-banner';
|
||||||
banner.innerHTML = '<div style="display:flex;align-items:center;gap:8px;padding:8px 16px;background:var(--primary-container);color:var(--md-sys-color-on-primary-container);border-radius:var(--shape-xs);margin-bottom:8px;font-size:.85rem">' +
|
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>' +
|
'<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-filled" onclick="FilesModule._pickResult(FilesModule.currentPath)" style="margin-left:auto">选择当前目录</button>' +
|
||||||
'<button class="btn btn-sm btn-outlined" onclick="FilesModule._cancelPicker()">取消</button>' +
|
'<button class="btn btn-sm btn-outlined" onclick="FilesModule._cancelPicker()">取消</button>' +
|
||||||
'</div>';
|
'</div>';
|
||||||
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 ── */
|
/* ── Picker ── */
|
||||||
_pickResult: function(path) {
|
_pickResult: function(path) {
|
||||||
if (window.opener) {
|
// In iframe (picker mode): post to parent, parent closes overlay
|
||||||
window.opener.postMessage(JSON.stringify({action:'fm-picked', path: path, callback_id: this.pickerCallback}), '*');
|
if (window.parent !== window) {
|
||||||
window.close();
|
|
||||||
} else if (window.parent !== window) {
|
|
||||||
window.parent.postMessage(JSON.stringify({action:'fm-picked', path: path, callback_id: this.pickerCallback}), '*');
|
window.parent.postMessage(JSON.stringify({action:'fm-picked', path: path, callback_id: this.pickerCallback}), '*');
|
||||||
} else {
|
return;
|
||||||
|
}
|
||||||
|
// Standalone clipboard fallback
|
||||||
navigator.clipboard?.writeText(path);
|
navigator.clipboard?.writeText(path);
|
||||||
alert('已选择: ' + path + '\n(路径已复制到剪贴板)');
|
alert('已选择: ' + path);
|
||||||
this.pickerMode = false;
|
this.pickerMode = false;
|
||||||
this.refresh();
|
this.refresh();
|
||||||
}
|
|
||||||
},
|
},
|
||||||
_cancelPicker: function() {
|
_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.pickerMode = false;
|
||||||
this.refresh();
|
this.refresh();
|
||||||
},
|
},
|
||||||
@@ -227,7 +244,7 @@ window.FilesModule = {
|
|||||||
var self = this;
|
var self = this;
|
||||||
var name = prompt('新建文件名:');
|
var name = prompt('新建文件名:');
|
||||||
if (!name) return;
|
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(); })
|
.then(function() { self.refresh(); })
|
||||||
.catch(function(e) { alert('创建失败: ' + e.message); });
|
.catch(function(e) { alert('创建失败: ' + e.message); });
|
||||||
},
|
},
|
||||||
@@ -235,7 +252,7 @@ window.FilesModule = {
|
|||||||
var self = this;
|
var self = this;
|
||||||
var name = prompt('新建文件夹名:');
|
var name = prompt('新建文件夹名:');
|
||||||
if (!name) return;
|
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(); })
|
.then(function() { self.refresh(); })
|
||||||
.catch(function(e) { alert('创建失败: ' + e.message); });
|
.catch(function(e) { alert('创建失败: ' + e.message); });
|
||||||
},
|
},
|
||||||
@@ -244,7 +261,7 @@ window.FilesModule = {
|
|||||||
if (!files || !files.length) return;
|
if (!files || !files.length) return;
|
||||||
var form = new FormData();
|
var form = new FormData();
|
||||||
for (var i = 0; i < files.length; i++) form.append('file', files[i]);
|
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(r) { return r.json(); })
|
||||||
.then(function(d) {
|
.then(function(d) {
|
||||||
if (d.ok) self.refresh();
|
if (d.ok) self.refresh();
|
||||||
@@ -255,7 +272,7 @@ window.FilesModule = {
|
|||||||
/* ── Editor ── */
|
/* ── Editor ── */
|
||||||
openEditor: function(path, name) {
|
openEditor: function(path, name) {
|
||||||
var self = this;
|
var self = this;
|
||||||
this._get('./api/files/read?path=' + encodeURIComponent(path))
|
this._get('/read?path=' + encodeURIComponent(path))
|
||||||
.then(function(d) {
|
.then(function(d) {
|
||||||
document.getElementById('fm-editor-title').textContent = '📝 ' + (name || d.name);
|
document.getElementById('fm-editor-title').textContent = '📝 ' + (name || d.name);
|
||||||
document.getElementById('fm-editor-textarea').value = d.content;
|
document.getElementById('fm-editor-textarea').value = d.content;
|
||||||
@@ -269,7 +286,7 @@ window.FilesModule = {
|
|||||||
saveFile: function() {
|
saveFile: function() {
|
||||||
var self = this;
|
var self = this;
|
||||||
var ta = document.getElementById('fm-editor-textarea');
|
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(); })
|
.then(function() { self.closeEditor(); self.refresh(); })
|
||||||
.catch(function(e) { alert('保存失败: ' + e.message); });
|
.catch(function(e) { alert('保存失败: ' + e.message); });
|
||||||
},
|
},
|
||||||
@@ -286,7 +303,7 @@ window.FilesModule = {
|
|||||||
},
|
},
|
||||||
ctxDownload: function() {
|
ctxDownload: function() {
|
||||||
var t = this.contextTarget;
|
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';
|
document.getElementById('fm-context').style.display = 'none';
|
||||||
},
|
},
|
||||||
ctxRename: function() {
|
ctxRename: function() {
|
||||||
@@ -295,7 +312,7 @@ window.FilesModule = {
|
|||||||
if (!t) return;
|
if (!t) return;
|
||||||
var nn = prompt('新名称:', t.name);
|
var nn = prompt('新名称:', t.name);
|
||||||
if (!nn || nn === t.name) return;
|
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(); })
|
.then(function() { self.refresh(); })
|
||||||
.catch(function(e) { alert('重命名失败: ' + e.message); });
|
.catch(function(e) { alert('重命名失败: ' + e.message); });
|
||||||
document.getElementById('fm-context').style.display = 'none';
|
document.getElementById('fm-context').style.display = 'none';
|
||||||
@@ -316,7 +333,7 @@ window.FilesModule = {
|
|||||||
confirmDelete: function() {
|
confirmDelete: function() {
|
||||||
var self = this;
|
var self = this;
|
||||||
if (!this.toDelete) return;
|
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(); })
|
.then(function() { self.toDelete = null; self.closeDialog(); self.refresh(); })
|
||||||
.catch(function(e) { alert('删除失败: ' + e.message); });
|
.catch(function(e) { alert('删除失败: ' + e.message); });
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user