Add image upload to site settings with old-file cleanup

This commit is contained in:
2026-09-12 10:04:23 +08:00
parent 74792c595a
commit 6e550c0d5b
2 changed files with 41 additions and 8 deletions
+3 -3
View File
@@ -1,4 +1,4 @@
export type FieldType = "text" | "textarea" | "url"; export type FieldType = "text" | "textarea" | "url" | "image";
export type SettingsField = { export type SettingsField = {
key: string; key: string;
@@ -39,7 +39,7 @@ export const SETTINGS_GROUPS: SettingsGroup[] = [
{ key: "hero_subtitle", label: "副標題", type: "textarea" }, { key: "hero_subtitle", label: "副標題", type: "textarea" },
{ key: "hero_primary_cta", label: "主要按鈕文字" }, { key: "hero_primary_cta", label: "主要按鈕文字" },
{ key: "hero_secondary_cta", label: "次要按鈕文字" }, { key: "hero_secondary_cta", label: "次要按鈕文字" },
{ key: "hero_image", label: "主視覺圖片 URL", type: "url", placeholder: "https://..." }, { key: "hero_image", label: "主視覺圖片", type: "image" },
], ],
}, },
{ {
@@ -47,7 +47,7 @@ export const SETTINGS_GROUPS: SettingsGroup[] = [
fields: [ fields: [
{ key: "seo_default_title", label: "預設標題" }, { key: "seo_default_title", label: "預設標題" },
{ key: "seo_default_description", label: "預設描述", type: "textarea" }, { key: "seo_default_description", label: "預設描述", type: "textarea" },
{ key: "og_image", label: "分享圖片 URL (OG image)", type: "url", placeholder: "https://..." }, { key: "og_image", label: "分享圖片 (OG image)", type: "image" },
], ],
}, },
]; ];
+36 -3
View File
@@ -1,16 +1,25 @@
--- ---
import AdminLayout from "../../layouts/AdminLayout.astro"; import AdminLayout from "../../layouts/AdminLayout.astro";
import ImageField from "../../components/admin/ImageField.astro";
import { ALL_SETTING_KEYS, SETTINGS_GROUPS } from "../../data/settings-fields"; import { ALL_SETTING_KEYS, SETTINGS_GROUPS } from "../../data/settings-fields";
import { siteSettings } from "../../db/schema"; import { siteSettings } from "../../db/schema";
import { getSettings } from "../../data/content"; import { getSettings } from "../../data/content";
import { getDb } from "../../lib/db"; import { getDb } from "../../lib/db";
import { getEnv } from "../../lib/env"; import { getEnv } from "../../lib/env";
import { parseForm } from "../../lib/form"; import { parseForm } from "../../lib/form";
import { deleteMedia } from "../../lib/media";
import { settingsInput } from "../../schemas"; import { settingsInput } from "../../schemas";
export const prerender = false; export const prerender = false;
const db = getDb(getEnv().DB); const env = getEnv();
const db = getDb(env.DB);
const IMAGE_SETTING_KEYS = new Set(
SETTINGS_GROUPS.flatMap((g) =>
g.fields.filter((f) => f.type === "image").map((f) => f.key),
),
);
let saved = false; let saved = false;
let error = ""; let error = "";
@@ -21,6 +30,7 @@ if (Astro.request.method === "POST") {
if (!parsed.ok) { if (!parsed.ok) {
error = Object.values(parsed.errors)[0] ?? "設定有誤。"; error = Object.values(parsed.errors)[0] ?? "設定有誤。";
} else { } else {
const before = await getSettings(db);
const now = new Date(); const now = new Date();
const submitted = new Set( const submitted = new Set(
[...form.keys()].filter((k) => ALL_SETTING_KEYS.includes(k)), [...form.keys()].filter((k) => ALL_SETTING_KEYS.includes(k)),
@@ -28,6 +38,9 @@ if (Astro.request.method === "POST") {
for (const key of ALL_SETTING_KEYS) { for (const key of ALL_SETTING_KEYS) {
if (!submitted.has(key)) continue; if (!submitted.has(key)) continue;
const value = String((parsed.data as Record<string, unknown>)[key] ?? ""); const value = String((parsed.data as Record<string, unknown>)[key] ?? "");
if (IMAGE_SETTING_KEYS.has(key) && before[key] !== value) {
await deleteMedia(env, before[key]);
}
await db await db
.insert(siteSettings) .insert(siteSettings)
.values({ key, value, updatedAt: now }) .values({ key, value, updatedAt: now })
@@ -56,11 +69,31 @@ const settings = await getSettings(db);
<div class="grid2"> <div class="grid2">
{group.fields.map((f) => ( {group.fields.map((f) => (
<div style={f.type === "textarea" ? "grid-column:1/-1" : ""}> <div style={f.type === "textarea" ? "grid-column:1/-1" : ""}>
{f.type === "image" ? (
<ImageField
name={f.key}
label={f.label}
value={settings[f.key] ?? ""}
scope="settings"
id={f.key}
/>
) : (
<>
<label for={f.key}>{f.label}</label> <label for={f.key}>{f.label}</label>
{f.type === "textarea" ? ( {f.type === "textarea" ? (
<textarea id={f.key} name={f.key} placeholder={f.placeholder}>{settings[f.key] ?? ""}</textarea> <textarea id={f.key} name={f.key} placeholder={f.placeholder}>
{settings[f.key] ?? ""}
</textarea>
) : ( ) : (
<input id={f.key} name={f.key} type="text" value={settings[f.key] ?? ""} placeholder={f.placeholder} /> <input
id={f.key}
name={f.key}
type="text"
value={settings[f.key] ?? ""}
placeholder={f.placeholder}
/>
)}
</>
)} )}
</div> </div>
))} ))}