Add ImageField component and client-side upload script

This commit is contained in:
2026-09-12 10:01:04 +08:00
parent e18c3306d4
commit fbc383decf
2 changed files with 101 additions and 0 deletions
+73
View File
@@ -49,6 +49,73 @@ const isActive = (href: string) =>
<main class="inner">
<slot />
</main>
<script>
const MAX_WIDTH = 1600;
const QUALITY = 0.85;
async function toOptimised(file: File): Promise<Blob> {
if (!("createImageBitmap" in window)) return file;
try {
const bitmap = await createImageBitmap(file);
const scale = Math.min(1, MAX_WIDTH / bitmap.width);
const width = Math.max(1, Math.round(bitmap.width * scale));
const height = Math.max(1, Math.round(bitmap.height * scale));
const canvas = document.createElement("canvas");
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext("2d");
if (!ctx) return file;
ctx.drawImage(bitmap, 0, 0, width, height);
if (typeof bitmap.close === "function") bitmap.close();
const blob = await new Promise<Blob | null>((resolve) =>
canvas.toBlob(resolve, "image/webp", QUALITY),
);
return blob ?? file;
} catch {
return file;
}
}
async function upload(field: HTMLElement, file: File): Promise<void> {
const scope = field.dataset.scope ?? "";
const input = field.querySelector<HTMLInputElement>(".image-url");
const preview = field.querySelector<HTMLImageElement>(".image-preview");
const status = field.querySelector<HTMLElement>(".image-status");
if (!input || !status) return;
status.textContent = "上傳中…";
const blob = await toOptimised(file);
const body = new FormData();
body.append("file", new File([blob], file.name, { type: blob.type }));
body.append("scope", scope);
try {
const res = await fetch("/admin/upload", { method: "POST", body });
const data = (await res.json()) as { ok: boolean; url?: string; error?: string };
if (!res.ok || !data.ok || !data.url) {
status.textContent = data.error ?? "上傳失敗。";
return;
}
input.value = data.url;
if (preview) {
preview.src = data.url;
preview.hidden = false;
}
status.textContent = "✓ 已上傳";
} catch {
status.textContent = "上傳失敗,請檢查網絡。";
}
}
document.querySelectorAll<HTMLElement>("[data-image-field]").forEach((field) => {
const fileInput = field.querySelector<HTMLInputElement>("[data-image-input]");
if (!fileInput) return;
fileInput.addEventListener("change", () => {
const file = fileInput.files?.[0];
if (file) void upload(field, file);
});
});
</script>
</body>
</html>
@@ -114,5 +181,11 @@ const isActive = (href: string) =>
.err { color: #a32d2d; font-size: 13px; font-weight: 600; }
.grid2 { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
@media (max-width: 640px) { .grid2 { grid-template-columns: 1fr; } }
.image-field { display: block; }
.image-actions { display: flex; align-items: center; gap: 10px; margin-top: 8px; flex-wrap: wrap; }
.image-actions input[type="file"] { font: inherit; font-size: 13px; max-width: 100%; }
.image-status { font-size: 12.5px; font-weight: 600; color: #0b8a5e; }
.image-preview { display: block; margin-top: 10px; max-width: 220px; border-radius: 10px; }
.image-preview[hidden] { display: none; }
.row { display: grid; gap: 12px; grid-template-columns: 1fr; }
</style>