站点背景功能优化、页脚上边距调整

This commit is contained in:
2026-09-18 23:11:07 +08:00
parent d784b0ea7a
commit a8efb9ecef
6 changed files with 151 additions and 54 deletions

View File

@@ -24,8 +24,8 @@ import {
AdminSettingRow,
AdminStepper,
AdminSwitch,
AdminSegmented,
} from "@/components/admin";
import BgModePicker from "./BgModePicker";
import {
apiUpdateSiteSettings,
apiUploadSiteBackground,
@@ -41,10 +41,9 @@ import {
normalizeHex,
} from "@/lib/theme";
import {
BG_MODE_OPTIONS,
BG_PREVIEW_ASPECT,
applySiteBackground,
bgConfigFromSettings,
normalizeBgMode,
previewBoxStyle,
type BgMode,
type BgSurface,
@@ -239,7 +238,7 @@ export default function AppearanceAdmin({ initial }: { initial: PublicSettings }
try {
await saveBgPatch(
{ [key]: mode },
surface === "site" ? "前台铺放模式已保存" : "后台铺放模式已保存"
surface === "site" ? "前台铺放方式已保存" : "后台铺放方式已保存"
);
} catch (e) {
toast(e instanceof Error ? e.message : "保存失败", "error");
@@ -285,11 +284,15 @@ export default function AppearanceAdmin({ initial }: { initial: PublicSettings }
</div>
</div>
<div
className="relative h-36 rounded-[var(--r-sm)] border overflow-hidden"
className="relative w-full max-w-[480px] rounded-[var(--r-sm)] border overflow-hidden"
style={{
aspectRatio: BG_PREVIEW_ASPECT,
borderColor: "var(--line)",
...previewBoxStyle(url, mode),
}}
role="img"
aria-label={`${title}背景预览,按 16:9 常见显示器比例`}
title="按 16:9(1920×1080)显示器比例预览"
>
{!url && (
<span
@@ -299,6 +302,17 @@ export default function AppearanceAdmin({ initial }: { initial: PublicSettings }
默认纹饰画布
</span>
)}
<span
className="absolute bottom-1.5 right-1.5 text-[10.5px] leading-none px-1.5 py-1 rounded-[4px] pointer-events-none"
aria-hidden
style={{
background: "color-mix(in srgb, var(--panel) 88%, transparent)",
color: "var(--ink-3)",
border: "1px solid var(--line)",
}}
>
16:9
</span>
</div>
<input
ref={fileRef}
@@ -333,22 +347,13 @@ export default function AppearanceAdmin({ initial }: { initial: PublicSettings }
) : null}
</div>
<div>
<p className="field-label mb-2">铺放模式</p>
<AdminSegmented
size="sm"
role="group"
ariaLabel={`${title}铺放模式`}
<p className="field-label mb-2">铺放方式</p>
<BgModePicker
value={mode}
onChange={(key) => void onChangeBgMode(surface, normalizeBgMode(key))}
options={BG_MODE_OPTIONS.map((o) => ({
key: o.key,
label: o.label,
disabled: busy,
}))}
disabled={busy}
ariaLabel={`${title}铺放方式`}
onChange={(next) => void onChangeBgMode(surface, next)}
/>
<p className="meta text-[12px] mt-1.5">
{BG_MODE_OPTIONS.find((o) => o.key === mode)?.hint}
</p>
</div>
</div>
);
@@ -675,7 +680,7 @@ export default function AppearanceAdmin({ initial }: { initial: PublicSettings }
<ImageIcon size={15} /> 站点背景图
</span>
}
description="前台与后台可分别上传 JPEG / PNG / WebP(≤8MB)。未配置时使用默认纹饰画布。"
description="前台与后台可分别上传 JPEG / PNG / WebP(≤8MB)。预览按 16:9 常见显示器比例。未配置时使用默认纹饰画布。"
>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{renderBgRail(

View File

@@ -0,0 +1,107 @@
"use client";
import { BG_MODE_OPTIONS, type BgMode } from "@/lib/bg";
/** 铺放示意:16:9 小窗里用色块表达裁切 / 完整 / 平铺 / 拉伸。 */
function ModeGlyph({ mode }: { mode: BgMode }) {
const photo = "var(--accent-soft)";
const ink = "var(--accent)";
return (
<span
className="relative block w-16 aspect-video overflow-hidden rounded-[3px] shrink-0"
style={{ background: "var(--bg)", border: "1px solid var(--line-2)" }}
aria-hidden
>
{mode === "cover" && (
<span
className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2"
style={{ width: "72%", height: "140%", background: photo }}
>
<span
className="absolute left-1/2 top-[18%] -translate-x-1/2 w-[30%] aspect-square rounded-full"
style={{ background: ink, opacity: 0.45 }}
/>
</span>
)}
{mode === "contain" && (
<span
className="absolute inset-y-0 left-1/2 -translate-x-1/2"
style={{ width: "58%", background: photo }}
>
<span
className="absolute left-1/2 top-[18%] -translate-x-1/2 w-[36%] aspect-square rounded-full"
style={{ background: ink, opacity: 0.45 }}
/>
</span>
)}
{mode === "repeat" && (
<span className="absolute inset-[2px] grid grid-cols-3 grid-rows-2 gap-[2px]">
{Array.from({ length: 6 }, (_, i) => (
<span key={i} style={{ background: photo }} />
))}
</span>
)}
{mode === "stretch" && (
<span className="absolute inset-0" style={{ background: photo }}>
<span
className="absolute left-1/2 top-[22%] -translate-x-1/2 h-[38%]"
style={{
width: "70%",
borderRadius: "50%",
background: ink,
opacity: 0.45,
}}
/>
</span>
)}
</span>
);
}
export default function BgModePicker({
value,
onChange,
disabled,
ariaLabel,
}: {
value: BgMode;
onChange: (mode: BgMode) => void;
disabled?: boolean;
ariaLabel: string;
}) {
return (
<div className="grid grid-cols-2 gap-2" role="group" aria-label={ariaLabel}>
{BG_MODE_OPTIONS.map((o) => {
const active = value === o.key;
return (
<button
key={o.key}
type="button"
disabled={disabled}
aria-pressed={active}
onClick={() => {
if (!active) onChange(o.key);
}}
className="flex items-center gap-2.5 min-w-0 px-2.5 py-2 rounded-[var(--r-sm)] border text-left transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
style={{
borderColor: active ? "var(--accent)" : "var(--line)",
background: active ? "var(--accent-soft)" : "var(--panel)",
cursor: disabled ? "not-allowed" : "pointer",
}}
>
<ModeGlyph mode={o.key} />
<span className="min-w-0">
<span
className="block text-[13px] font-medium leading-tight"
style={{ color: active ? "var(--accent)" : "var(--ink)" }}
>
{o.label}
</span>
<span className="block meta text-[11.5px] mt-0.5 leading-snug">{o.hint}</span>
</span>
</button>
);
})}
</div>
);
}

View File

@@ -16,7 +16,6 @@ export default function Footer({ siteName = "姜十三社区" }: { siteName?: st
return (
<footer
className="mt-14"
style={{
borderTop: "1px solid var(--line)",
background: "var(--panel)",

View File

@@ -6,25 +6,18 @@ import type { CSSProperties } from "react";
export type BgSurface = "site" | "admin";
export type BgMode =
| "cover"
| "contain"
| "repeat"
| "repeat-x"
| "repeat-y"
| "stretch"
| "center";
export type BgMode = "cover" | "contain" | "repeat" | "stretch";
export const DEFAULT_BG_MODE: BgMode = "cover";
/** 预览框按常见 PC 显示器 16:9(1920×1080)等比例缩放 */
export const BG_PREVIEW_ASPECT = "16 / 9";
export const BG_MODE_OPTIONS: { key: BgMode; label: string; hint: string }[] = [
{ key: "cover", label: "覆盖铺满", hint: "裁切以铺满视口" },
{ key: "contain", label: "完整显示", hint: "完整放入,两侧留底色" },
{ key: "stretch", label: "拉伸填满", hint: "变形拉伸至视口" },
{ key: "repeat", label: "平铺", hint: "按原尺寸重复铺满" },
{ key: "repeat-x", label: "横向平铺", hint: "只沿水平方向重复" },
{ key: "repeat-y", label: "纵向平铺", hint: "只沿垂直方向重复" },
{ key: "center", label: "居中原图", hint: "不缩放,居中显示" },
{ key: "cover", label: "填充", hint: "裁切铺满,适合照片" },
{ key: "contain", label: "适应", hint: "整图可见,可能留边" },
{ key: "repeat", label: "平铺", hint: "原尺寸重复,适合纹理" },
{ key: "stretch", label: "拉伸", hint: "拉满屏幕,可能变形" },
];
export interface SiteBgConfig {
@@ -72,14 +65,8 @@ function modeCSS(mode: BgMode): { size: string; repeat: string; position: string
return { size: "contain", repeat: "no-repeat", position: "center" };
case "repeat":
return { size: "auto", repeat: "repeat", position: "0 0" };
case "repeat-x":
return { size: "auto", repeat: "repeat-x", position: "center top" };
case "repeat-y":
return { size: "auto", repeat: "repeat-y", position: "center top" };
case "stretch":
return { size: "100% 100%", repeat: "no-repeat", position: "center" };
case "center":
return { size: "auto", repeat: "no-repeat", position: "center" };
default:
return { size: "cover", repeat: "no-repeat", position: "center" };
}