726 lines
22 KiB
JavaScript
726 lines
22 KiB
JavaScript
// GoTunnel File Manager Plugin v2.0.0
|
||
// 提供完整的 Web 文件管理界面
|
||
|
||
function metadata() {
|
||
return {
|
||
name: "file-manager",
|
||
version: "2.0.0",
|
||
description: "Web 文件管理器,提供远程文件浏览、上传、下载和管理功能",
|
||
author: "GoTunnel Official",
|
||
type: "app",
|
||
run_at: "client"
|
||
};
|
||
}
|
||
|
||
var rootPath = ".";
|
||
|
||
function start() {
|
||
rootPath = config("root_path") || ".";
|
||
log("File Manager started, root: " + rootPath);
|
||
}
|
||
|
||
function stop() {
|
||
log("File Manager stopped");
|
||
}
|
||
|
||
function handleConn(conn) {
|
||
http.serve(conn, handleRequest);
|
||
}
|
||
|
||
// ============================================================================
|
||
// 请求路由
|
||
// ============================================================================
|
||
|
||
function handleRequest(req) {
|
||
var path = req.path;
|
||
var method = req.method;
|
||
|
||
// 静态页面
|
||
if (path === "/" || path === "/index.html") {
|
||
return { status: 200, contentType: "text/html", body: getIndexHTML() };
|
||
}
|
||
|
||
// API 路由
|
||
if (path === "/api/list" && method === "GET") {
|
||
return apiList(req);
|
||
}
|
||
if (path === "/api/stat" && method === "GET") {
|
||
return apiStat(req);
|
||
}
|
||
if (path === "/api/read" && method === "POST") {
|
||
return apiRead(req);
|
||
}
|
||
if (path === "/api/write" && method === "POST") {
|
||
return apiWrite(req);
|
||
}
|
||
if (path === "/api/mkdir" && method === "POST") {
|
||
return apiMkdir(req);
|
||
}
|
||
if (path === "/api/delete" && method === "POST") {
|
||
return apiDelete(req);
|
||
}
|
||
if (path === "/api/rename" && method === "POST") {
|
||
return apiRename(req);
|
||
}
|
||
if (path === "/api/download" && method === "GET") {
|
||
return apiDownload(req);
|
||
}
|
||
|
||
return jsonError(404, "Not Found");
|
||
}
|
||
|
||
// ============================================================================
|
||
// API 处理函数
|
||
// ============================================================================
|
||
|
||
function apiList(req) {
|
||
var dir = getQueryParam(req.path, "path") || "/";
|
||
var fullPath = resolvePath(dir);
|
||
|
||
if (!fullPath) {
|
||
return jsonError(403, "Access denied");
|
||
}
|
||
|
||
var result = fs.readDir(fullPath);
|
||
if (result.error) {
|
||
return jsonError(500, result.error);
|
||
}
|
||
|
||
var entries = [];
|
||
for (var i = 0; i < result.entries.length; i++) {
|
||
var e = result.entries[i];
|
||
entries.push({
|
||
name: e.name,
|
||
isDir: e.isDir,
|
||
size: e.size || 0
|
||
});
|
||
}
|
||
|
||
// 排序:文件夹在前,然后按名称
|
||
entries.sort(function (a, b) {
|
||
if (a.isDir !== b.isDir) return a.isDir ? -1 : 1;
|
||
return a.name.localeCompare(b.name);
|
||
});
|
||
|
||
return jsonSuccess({ path: dir, entries: entries });
|
||
}
|
||
|
||
function apiStat(req) {
|
||
var path = getQueryParam(req.path, "path") || "/";
|
||
var fullPath = resolvePath(path);
|
||
|
||
if (!fullPath) {
|
||
return jsonError(403, "Access denied");
|
||
}
|
||
|
||
var result = fs.stat(fullPath);
|
||
if (result.error) {
|
||
return jsonError(404, result.error);
|
||
}
|
||
|
||
return jsonSuccess({
|
||
name: result.name,
|
||
size: result.size,
|
||
isDir: result.isDir,
|
||
modTime: result.modTime
|
||
});
|
||
}
|
||
|
||
function apiRead(req) {
|
||
var body = parseBody(req.body);
|
||
var path = body.path;
|
||
|
||
if (!path) {
|
||
return jsonError(400, "path required");
|
||
}
|
||
|
||
var fullPath = resolvePath(path);
|
||
if (!fullPath) {
|
||
return jsonError(403, "Access denied");
|
||
}
|
||
|
||
var result = fs.readFile(fullPath);
|
||
if (result.error) {
|
||
return jsonError(500, result.error);
|
||
}
|
||
|
||
return jsonSuccess({ content: result.data });
|
||
}
|
||
|
||
function apiWrite(req) {
|
||
var body = parseBody(req.body);
|
||
var path = body.path;
|
||
var content = body.content;
|
||
|
||
if (!path) {
|
||
return jsonError(400, "path required");
|
||
}
|
||
|
||
var fullPath = resolvePath(path);
|
||
if (!fullPath) {
|
||
return jsonError(403, "Access denied");
|
||
}
|
||
|
||
var result = fs.writeFile(fullPath, content || "");
|
||
if (!result.ok) {
|
||
return jsonError(500, result.error);
|
||
}
|
||
|
||
return jsonSuccess({ message: "File saved" });
|
||
}
|
||
|
||
function apiMkdir(req) {
|
||
var body = parseBody(req.body);
|
||
var path = body.path;
|
||
|
||
if (!path) {
|
||
return jsonError(400, "path required");
|
||
}
|
||
|
||
var fullPath = resolvePath(path);
|
||
if (!fullPath) {
|
||
return jsonError(403, "Access denied");
|
||
}
|
||
|
||
var result = fs.mkdir(fullPath);
|
||
if (!result.ok) {
|
||
return jsonError(500, result.error);
|
||
}
|
||
|
||
return jsonSuccess({ message: "Directory created" });
|
||
}
|
||
|
||
function apiDelete(req) {
|
||
var body = parseBody(req.body);
|
||
var path = body.path;
|
||
|
||
if (!path) {
|
||
return jsonError(400, "path required");
|
||
}
|
||
|
||
var fullPath = resolvePath(path);
|
||
if (!fullPath) {
|
||
return jsonError(403, "Access denied");
|
||
}
|
||
|
||
var result = fs.remove(fullPath);
|
||
if (!result.ok) {
|
||
return jsonError(500, result.error);
|
||
}
|
||
|
||
return jsonSuccess({ message: "Deleted" });
|
||
}
|
||
|
||
function apiRename(req) {
|
||
var body = parseBody(req.body);
|
||
var oldPath = body.path;
|
||
var newPath = body.newPath;
|
||
|
||
if (!oldPath || !newPath) {
|
||
return jsonError(400, "path and newPath required");
|
||
}
|
||
|
||
var fullOldPath = resolvePath(oldPath);
|
||
var fullNewPath = resolvePath(newPath);
|
||
|
||
if (!fullOldPath || !fullNewPath) {
|
||
return jsonError(403, "Access denied");
|
||
}
|
||
|
||
// 读取旧文件
|
||
var stat = fs.stat(fullOldPath);
|
||
if (stat.error) {
|
||
return jsonError(404, stat.error);
|
||
}
|
||
|
||
if (stat.isDir) {
|
||
return jsonError(400, "Cannot rename directories");
|
||
}
|
||
|
||
var content = fs.readFile(fullOldPath);
|
||
if (content.error) {
|
||
return jsonError(500, content.error);
|
||
}
|
||
|
||
// 写入新位置
|
||
var writeResult = fs.writeFile(fullNewPath, content.data);
|
||
if (!writeResult.ok) {
|
||
return jsonError(500, writeResult.error);
|
||
}
|
||
|
||
// 删除旧文件
|
||
fs.remove(fullOldPath);
|
||
|
||
return jsonSuccess({ message: "Renamed" });
|
||
}
|
||
|
||
function apiDownload(req) {
|
||
var path = getQueryParam(req.path, "path");
|
||
|
||
if (!path) {
|
||
return jsonError(400, "path required");
|
||
}
|
||
|
||
var fullPath = resolvePath(path);
|
||
if (!fullPath) {
|
||
return jsonError(403, "Access denied");
|
||
}
|
||
|
||
var result = fs.readFile(fullPath);
|
||
if (result.error) {
|
||
return jsonError(404, result.error);
|
||
}
|
||
|
||
var filename = path.split("/").pop() || "download";
|
||
|
||
return {
|
||
status: 200,
|
||
contentType: "application/octet-stream",
|
||
body: result.data
|
||
};
|
||
}
|
||
|
||
// ============================================================================
|
||
// 工具函数
|
||
// ============================================================================
|
||
|
||
function resolvePath(path) {
|
||
// 规范化路径
|
||
path = path || "/";
|
||
|
||
// 移除开头的斜杠
|
||
while (path.charAt(0) === "/") {
|
||
path = path.substring(1);
|
||
}
|
||
|
||
// 检查路径穿越攻击
|
||
if (path.indexOf("..") !== -1) {
|
||
return null;
|
||
}
|
||
|
||
// 拼接根路径
|
||
if (path === "") {
|
||
return rootPath;
|
||
}
|
||
|
||
return rootPath + "/" + path;
|
||
}
|
||
|
||
function getQueryParam(path, key) {
|
||
var idx = path.indexOf("?");
|
||
if (idx === -1) return "";
|
||
|
||
var query = path.substring(idx + 1);
|
||
var pairs = query.split("&");
|
||
|
||
for (var i = 0; i < pairs.length; i++) {
|
||
var pair = pairs[i].split("=");
|
||
if (pair[0] === key) {
|
||
return decodeURIComponent(pair[1] || "");
|
||
}
|
||
}
|
||
return "";
|
||
}
|
||
|
||
function parseBody(body) {
|
||
try {
|
||
return JSON.parse(body || "{}");
|
||
} catch (e) {
|
||
return {};
|
||
}
|
||
}
|
||
|
||
function jsonSuccess(data) {
|
||
return {
|
||
status: 200,
|
||
contentType: "application/json",
|
||
body: http.json({ success: true, data: data })
|
||
};
|
||
}
|
||
|
||
function jsonError(status, message) {
|
||
return {
|
||
status: status,
|
||
contentType: "application/json",
|
||
body: http.json({ success: false, error: message })
|
||
};
|
||
}
|
||
|
||
// ============================================================================
|
||
// Web UI - HTML/CSS/JavaScript
|
||
// ============================================================================
|
||
|
||
function getIndexHTML() {
|
||
return `<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>File Manager</title>
|
||
<style>
|
||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||
:root {
|
||
--bg-primary: #0f0f23;
|
||
--bg-secondary: #1a1a2e;
|
||
--bg-tertiary: #252542;
|
||
--text-primary: #e4e4e7;
|
||
--text-secondary: #a1a1aa;
|
||
--accent: #6366f1;
|
||
--accent-hover: #818cf8;
|
||
--danger: #ef4444;
|
||
--success: #22c55e;
|
||
--border: #2e2e4a;
|
||
}
|
||
body {
|
||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||
background: var(--bg-primary);
|
||
color: var(--text-primary);
|
||
min-height: 100vh;
|
||
}
|
||
.container { max-width: 1200px; margin: 0 auto; padding: 20px; }
|
||
header {
|
||
background: linear-gradient(135deg, var(--bg-secondary), var(--bg-tertiary));
|
||
padding: 20px;
|
||
border-radius: 12px;
|
||
margin-bottom: 20px;
|
||
border: 1px solid var(--border);
|
||
}
|
||
header h1 { font-size: 1.5rem; display: flex; align-items: center; gap: 10px; }
|
||
header h1::before { content: "📁"; }
|
||
.breadcrumb {
|
||
display: flex; align-items: center; gap: 8px; margin-top: 15px;
|
||
padding: 10px 15px; background: var(--bg-primary); border-radius: 8px;
|
||
overflow-x: auto;
|
||
}
|
||
.breadcrumb a {
|
||
color: var(--accent); text-decoration: none;
|
||
white-space: nowrap;
|
||
}
|
||
.breadcrumb a:hover { color: var(--accent-hover); }
|
||
.breadcrumb span { color: var(--text-secondary); }
|
||
.toolbar {
|
||
display: flex; gap: 10px; margin-bottom: 20px; flex-wrap: wrap;
|
||
}
|
||
.btn {
|
||
padding: 10px 20px; border: none; border-radius: 8px;
|
||
cursor: pointer; font-size: 14px; font-weight: 500;
|
||
transition: all 0.2s; display: flex; align-items: center; gap: 6px;
|
||
}
|
||
.btn-primary { background: var(--accent); color: white; }
|
||
.btn-primary:hover { background: var(--accent-hover); transform: translateY(-1px); }
|
||
.btn-danger { background: var(--danger); color: white; }
|
||
.btn-danger:hover { opacity: 0.9; }
|
||
.btn-secondary { background: var(--bg-tertiary); color: var(--text-primary); border: 1px solid var(--border); }
|
||
.btn-secondary:hover { background: var(--border); }
|
||
.file-list {
|
||
background: var(--bg-secondary); border-radius: 12px;
|
||
border: 1px solid var(--border); overflow: hidden;
|
||
}
|
||
.file-item {
|
||
display: flex; align-items: center; padding: 15px 20px;
|
||
border-bottom: 1px solid var(--border); cursor: pointer;
|
||
transition: background 0.2s;
|
||
}
|
||
.file-item:last-child { border-bottom: none; }
|
||
.file-item:hover { background: var(--bg-tertiary); }
|
||
.file-item.selected { background: rgba(99, 102, 241, 0.2); }
|
||
.file-icon { font-size: 24px; margin-right: 15px; }
|
||
.file-info { flex: 1; }
|
||
.file-name { font-weight: 500; margin-bottom: 4px; }
|
||
.file-meta { font-size: 12px; color: var(--text-secondary); }
|
||
.file-actions { display: flex; gap: 8px; opacity: 0; transition: opacity 0.2s; }
|
||
.file-item:hover .file-actions { opacity: 1; }
|
||
.action-btn {
|
||
width: 32px; height: 32px; border: none; border-radius: 6px;
|
||
cursor: pointer; display: flex; align-items: center; justify-content: center;
|
||
background: var(--bg-primary); color: var(--text-primary);
|
||
transition: all 0.2s;
|
||
}
|
||
.action-btn:hover { background: var(--accent); }
|
||
.action-btn.delete:hover { background: var(--danger); }
|
||
.modal-overlay {
|
||
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
|
||
background: rgba(0,0,0,0.7); display: flex;
|
||
align-items: center; justify-content: center; z-index: 1000;
|
||
}
|
||
.modal {
|
||
background: var(--bg-secondary); border-radius: 12px;
|
||
padding: 25px; width: 90%; max-width: 500px;
|
||
border: 1px solid var(--border);
|
||
}
|
||
.modal h2 { margin-bottom: 20px; font-size: 1.2rem; }
|
||
.modal input, .modal textarea {
|
||
width: 100%; padding: 12px; border-radius: 8px;
|
||
border: 1px solid var(--border); background: var(--bg-primary);
|
||
color: var(--text-primary); margin-bottom: 15px; font-size: 14px;
|
||
}
|
||
.modal textarea { min-height: 200px; resize: vertical; font-family: monospace; }
|
||
.modal-actions { display: flex; gap: 10px; justify-content: flex-end; }
|
||
.empty-state {
|
||
text-align: center; padding: 60px 20px; color: var(--text-secondary);
|
||
}
|
||
.empty-state::before { content: "📂"; font-size: 48px; display: block; margin-bottom: 15px; }
|
||
.loading { text-align: center; padding: 40px; color: var(--text-secondary); }
|
||
.toast {
|
||
position: fixed; bottom: 20px; right: 20px; padding: 15px 25px;
|
||
background: var(--success); color: white; border-radius: 8px;
|
||
animation: slideIn 0.3s ease;
|
||
}
|
||
.toast.error { background: var(--danger); }
|
||
@keyframes slideIn { from { transform: translateX(100%); opacity: 0; } }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<header>
|
||
<h1>File Manager</h1>
|
||
<div class="breadcrumb" id="breadcrumb"></div>
|
||
</header>
|
||
<div class="toolbar">
|
||
<button class="btn btn-primary" onclick="showNewFolderModal()">📁 新建文件夹</button>
|
||
<button class="btn btn-primary" onclick="showNewFileModal()">📄 新建文件</button>
|
||
<button class="btn btn-secondary" onclick="refresh()">🔄 刷新</button>
|
||
</div>
|
||
<div class="file-list" id="fileList">
|
||
<div class="loading">加载中...</div>
|
||
</div>
|
||
</div>
|
||
<div id="modalContainer"></div>
|
||
<script>
|
||
var currentPath = "/";
|
||
|
||
function init() { loadDirectory("/"); }
|
||
|
||
function loadDirectory(path) {
|
||
currentPath = path;
|
||
updateBreadcrumb();
|
||
document.getElementById("fileList").innerHTML = "<div class=\\"loading\\">加载中...</div>";
|
||
|
||
fetch("/api/list?path=" + encodeURIComponent(path))
|
||
.then(function(r) { return r.json(); })
|
||
.then(function(data) {
|
||
if (!data.success) { showToast(data.error, true); return; }
|
||
renderFileList(data.data.entries);
|
||
})
|
||
.catch(function(e) { showToast("加载失败: " + e.message, true); });
|
||
}
|
||
|
||
function renderFileList(entries) {
|
||
var list = document.getElementById("fileList");
|
||
if (!entries || entries.length === 0) {
|
||
list.innerHTML = "<div class=\\"empty-state\\">空文件夹</div>";
|
||
return;
|
||
}
|
||
|
||
var html = "";
|
||
if (currentPath !== "/") {
|
||
html += "<div class=\\"file-item\\" onclick=\\"goUp()\\"><span class=\\"file-icon\\">⬆️</span><div class=\\"file-info\\"><div class=\\"file-name\\">..</div><div class=\\"file-meta\\">返回上级</div></div></div>";
|
||
}
|
||
|
||
for (var i = 0; i < entries.length; i++) {
|
||
var e = entries[i];
|
||
var icon = e.isDir ? "📁" : getFileIcon(e.name);
|
||
var size = e.isDir ? "" : formatSize(e.size);
|
||
var onclick = e.isDir ? "navigateTo(\\"" + e.name + "\\")" : "viewFile(\\"" + e.name + "\\")";
|
||
|
||
html += "<div class=\\"file-item\\" onclick=\\""+ onclick +"\\">";
|
||
html += "<span class=\\"file-icon\\">" + icon + "</span>";
|
||
html += "<div class=\\"file-info\\"><div class=\\"file-name\\">" + escapeHtml(e.name) + "</div>";
|
||
html += "<div class=\\"file-meta\\">" + size + "</div></div>";
|
||
html += "<div class=\\"file-actions\\">";
|
||
if (!e.isDir) {
|
||
html += "<button class=\\"action-btn\\" onclick=\\"event.stopPropagation();downloadFile('" + e.name + "')\\">⬇️</button>";
|
||
}
|
||
html += "<button class=\\"action-btn\\" onclick=\\"event.stopPropagation();renameItem('" + e.name + "')\\">✏️</button>";
|
||
html += "<button class=\\"action-btn delete\\" onclick=\\"event.stopPropagation();deleteItem('" + e.name + "')\\">🗑️</button>";
|
||
html += "</div></div>";
|
||
}
|
||
list.innerHTML = html;
|
||
}
|
||
|
||
function updateBreadcrumb() {
|
||
var parts = currentPath.split("/").filter(function(p) { return p; });
|
||
var html = "<a href=\\"#\\" onclick=\\"loadDirectory('/');return false;\\">🏠 根目录</a>";
|
||
var path = "";
|
||
for (var i = 0; i < parts.length; i++) {
|
||
path += "/" + parts[i];
|
||
html += "<span>/</span><a href=\\"#\\" onclick=\\"loadDirectory('"+path+"');return false;\\">" + escapeHtml(parts[i]) + "</a>";
|
||
}
|
||
document.getElementById("breadcrumb").innerHTML = html;
|
||
}
|
||
|
||
function navigateTo(name) { loadDirectory(joinPath(currentPath, name)); }
|
||
function goUp() {
|
||
var parts = currentPath.split("/").filter(function(p) { return p; });
|
||
parts.pop();
|
||
loadDirectory("/" + parts.join("/"));
|
||
}
|
||
function refresh() { loadDirectory(currentPath); }
|
||
|
||
function joinPath(base, name) {
|
||
if (base === "/") return "/" + name;
|
||
return base + "/" + name;
|
||
}
|
||
|
||
function viewFile(name) {
|
||
var path = joinPath(currentPath, name);
|
||
fetch("/api/read", {
|
||
method: "POST",
|
||
headers: {"Content-Type": "application/json"},
|
||
body: JSON.stringify({path: path})
|
||
})
|
||
.then(function(r) { return r.json(); })
|
||
.then(function(data) {
|
||
if (!data.success) { showToast(data.error, true); return; }
|
||
showEditModal(name, data.data.content);
|
||
});
|
||
}
|
||
|
||
function downloadFile(name) {
|
||
var path = joinPath(currentPath, name);
|
||
window.open("/api/download?path=" + encodeURIComponent(path));
|
||
}
|
||
|
||
function deleteItem(name) {
|
||
if (!confirm("确定删除 " + name + "?")) return;
|
||
var path = joinPath(currentPath, name);
|
||
fetch("/api/delete", {
|
||
method: "POST",
|
||
headers: {"Content-Type": "application/json"},
|
||
body: JSON.stringify({path: path})
|
||
})
|
||
.then(function(r) { return r.json(); })
|
||
.then(function(data) {
|
||
if (data.success) { showToast("已删除"); refresh(); }
|
||
else { showToast(data.error, true); }
|
||
});
|
||
}
|
||
|
||
function renameItem(name) {
|
||
var newName = prompt("输入新名称:", name);
|
||
if (!newName || newName === name) return;
|
||
var oldPath = joinPath(currentPath, name);
|
||
var newPath = joinPath(currentPath, newName);
|
||
fetch("/api/rename", {
|
||
method: "POST",
|
||
headers: {"Content-Type": "application/json"},
|
||
body: JSON.stringify({path: oldPath, newPath: newPath})
|
||
})
|
||
.then(function(r) { return r.json(); })
|
||
.then(function(data) {
|
||
if (data.success) { showToast("已重命名"); refresh(); }
|
||
else { showToast(data.error, true); }
|
||
});
|
||
}
|
||
|
||
function showNewFolderModal() {
|
||
var name = prompt("文件夹名称:");
|
||
if (!name) return;
|
||
fetch("/api/mkdir", {
|
||
method: "POST",
|
||
headers: {"Content-Type": "application/json"},
|
||
body: JSON.stringify({path: joinPath(currentPath, name)})
|
||
})
|
||
.then(function(r) { return r.json(); })
|
||
.then(function(data) {
|
||
if (data.success) { showToast("已创建"); refresh(); }
|
||
else { showToast(data.error, true); }
|
||
});
|
||
}
|
||
|
||
function showNewFileModal() {
|
||
var html = "<div class=\\"modal-overlay\\" onclick=\\"closeModal()\\">";
|
||
html += "<div class=\\"modal\\" onclick=\\"event.stopPropagation()\\">";
|
||
html += "<h2>📄 新建文件</h2>";
|
||
html += "<input type=\\"text\\" id=\\"newFileName\\" placeholder=\\"文件名\\">";
|
||
html += "<textarea id=\\"newFileContent\\" placeholder=\\"文件内容\\"></textarea>";
|
||
html += "<div class=\\"modal-actions\\">";
|
||
html += "<button class=\\"btn btn-secondary\\" onclick=\\"closeModal()\\">取消</button>";
|
||
html += "<button class=\\"btn btn-primary\\" onclick=\\"createFile()\\">创建</button>";
|
||
html += "</div></div></div>";
|
||
document.getElementById("modalContainer").innerHTML = html;
|
||
}
|
||
|
||
function showEditModal(name, content) {
|
||
var html = "<div class=\\"modal-overlay\\" onclick=\\"closeModal()\\">";
|
||
html += "<div class=\\"modal\\" onclick=\\"event.stopPropagation()\\">";
|
||
html += "<h2>📝 编辑: " + escapeHtml(name) + "</h2>";
|
||
html += "<input type=\\"hidden\\" id=\\"editFileName\\" value=\\"" + escapeHtml(name) + "\\">";
|
||
html += "<textarea id=\\"editFileContent\\">" + escapeHtml(content) + "</textarea>";
|
||
html += "<div class=\\"modal-actions\\">";
|
||
html += "<button class=\\"btn btn-secondary\\" onclick=\\"closeModal()\\">取消</button>";
|
||
html += "<button class=\\"btn btn-primary\\" onclick=\\"saveFile()\\">保存</button>";
|
||
html += "</div></div></div>";
|
||
document.getElementById("modalContainer").innerHTML = html;
|
||
}
|
||
|
||
function createFile() {
|
||
var name = document.getElementById("newFileName").value;
|
||
var content = document.getElementById("newFileContent").value;
|
||
if (!name) { showToast("请输入文件名", true); return; }
|
||
fetch("/api/write", {
|
||
method: "POST",
|
||
headers: {"Content-Type": "application/json"},
|
||
body: JSON.stringify({path: joinPath(currentPath, name), content: content})
|
||
})
|
||
.then(function(r) { return r.json(); })
|
||
.then(function(data) {
|
||
if (data.success) { closeModal(); showToast("已创建"); refresh(); }
|
||
else { showToast(data.error, true); }
|
||
});
|
||
}
|
||
|
||
function saveFile() {
|
||
var name = document.getElementById("editFileName").value;
|
||
var content = document.getElementById("editFileContent").value;
|
||
fetch("/api/write", {
|
||
method: "POST",
|
||
headers: {"Content-Type": "application/json"},
|
||
body: JSON.stringify({path: joinPath(currentPath, name), content: content})
|
||
})
|
||
.then(function(r) { return r.json(); })
|
||
.then(function(data) {
|
||
if (data.success) { closeModal(); showToast("已保存"); }
|
||
else { showToast(data.error, true); }
|
||
});
|
||
}
|
||
|
||
function closeModal() { document.getElementById("modalContainer").innerHTML = ""; }
|
||
|
||
function showToast(msg, isError) {
|
||
var toast = document.createElement("div");
|
||
toast.className = "toast" + (isError ? " error" : "");
|
||
toast.textContent = msg;
|
||
document.body.appendChild(toast);
|
||
setTimeout(function() { toast.remove(); }, 3000);
|
||
}
|
||
|
||
function formatSize(bytes) {
|
||
if (bytes === 0) return "0 B";
|
||
var k = 1024;
|
||
var sizes = ["B", "KB", "MB", "GB"];
|
||
var i = Math.floor(Math.log(bytes) / Math.log(k));
|
||
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + " " + sizes[i];
|
||
}
|
||
|
||
function getFileIcon(name) {
|
||
var ext = name.split(".").pop().toLowerCase();
|
||
var icons = {
|
||
js: "📜", json: "📋", html: "🌐", css: "🎨", md: "📝",
|
||
txt: "📄", log: "📃", py: "🐍", go: "🔵", java: "☕",
|
||
png: "🖼️", jpg: "🖼️", gif: "🖼️", svg: "🖼️",
|
||
mp3: "🎵", mp4: "🎬", pdf: "📕", zip: "📦", tar: "📦"
|
||
};
|
||
return icons[ext] || "📄";
|
||
}
|
||
|
||
function escapeHtml(str) {
|
||
return String(str).replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""");
|
||
}
|
||
|
||
init();
|
||
</script>
|
||
</body>
|
||
</html>`;
|
||
}
|