Add NAS dashboard MVP: Docker mgmt, Gitea, Files, Terminal

This commit is contained in:
Gan, Jimmy
2026-02-19 01:59:50 +08:00
parent d85b290c33
commit 70aa421d7f
29 changed files with 2665 additions and 0 deletions
@@ -0,0 +1,63 @@
<script>
import { get, del } from "../lib/api.js";
let entries = $state([]);
let currentPath = $state("");
async function browse(path) {
currentPath = path;
const data = await get(`/files/browse?path=${encodeURIComponent(path)}`);
entries = data.entries || [];
}
function goUp() {
const parts = currentPath.split("/").filter(Boolean);
parts.pop();
browse(parts.join("/"));
}
async function remove(name) {
if (!confirm(`Delete ${name}?`)) return;
const path = currentPath ? `${currentPath}/${name}` : name;
await del(`/files/delete?path=${encodeURIComponent(path)}`);
browse(currentPath);
}
function downloadUrl(name) {
const path = currentPath ? `${currentPath}/${name}` : name;
return `/api/files/download?path=${encodeURIComponent(path)}`;
}
function formatSize(bytes) {
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1048576) return `${(bytes / 1024).toFixed(1)} KB`;
if (bytes < 1073741824) return `${(bytes / 1048576).toFixed(1)} MB`;
return `${(bytes / 1073741824).toFixed(1)} GB`;
}
browse("");
</script>
<h2 class="text-2xl font-bold mb-4">Files</h2>
<div class="flex items-center gap-2 mb-4 text-sm text-gray-500">
<button class="hover:text-blue-600" onclick={() => browse("")}>/volume1</button>
{#each currentPath.split("/").filter(Boolean) as part, i}
<span>/</span>
<button class="hover:text-blue-600" onclick={() => browse(currentPath.split("/").slice(0, i + 1).join("/"))}>{part}</button>
{/each}
{#if currentPath}
<button class="ml-4 text-xs px-2 py-1 bg-gray-100 rounded hover:bg-gray-200" onclick={goUp}>Up</button>
{/if}
</div>
<div class="border rounded-lg divide-y">
{#each entries as e}
<div class="flex items-center justify-between px-4 py-2 hover:bg-gray-50">
{#if e.is_dir}
<button class="text-blue-600 hover:underline text-sm" onclick={() => browse(currentPath ? `${currentPath}/${e.name}` : e.name)}>{e.name}/</button>
{:else}
<span class="text-sm">{e.name}</span>
{/if}
<div class="flex items-center gap-3">
<span class="text-xs text-gray-400">{e.is_dir ? "" : formatSize(e.size)}</span>
{#if !e.is_dir}
<a href={downloadUrl(e.name)} class="text-xs text-blue-500 hover:underline">Download</a>
{/if}
<button class="text-xs text-red-400 hover:text-red-600" onclick={() => remove(e.name)}>Delete</button>
</div>
</div>
{/each}
</div>