feat: 友链/书库/移动端导航/排行榜等多模块功能
新增模块: - 友链(friendlink)后端处理 + 前端管理页与友链板 - 书库(library):导入、章节、封面、阅读页、横竖版自适应 AdaptiveCoverSlot - 顶栏导航(header_nav)配置与 MobileTabBar/MobileRailDrawers 移动端抽屉 - 排行榜 service 测试、站点页面测试、时间线发布(timeline_release) 其它改动: - 后端 handlers/services 全量小幅调整 - 前端组件、库函数、URL/品牌/站点 URL 工具更新 - Lightbox 图片、MdEntries 条目卡、coverColor 派生色相等前端能力
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { ImagePlus, Images, X } from "lucide-react";
|
||||
import { Check, ImagePlus, Images, Link2, X } from "lucide-react";
|
||||
import { useEffect, useRef, useState, type Dispatch, type SetStateAction } from "react";
|
||||
import {
|
||||
AdminSegmented,
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
BRAND_LOGO_FITS,
|
||||
BRAND_LOGO_SIZES,
|
||||
brandAccept,
|
||||
brandAliasPath,
|
||||
brandFileOk,
|
||||
type BrandLogoFit,
|
||||
type BrandLogoSize,
|
||||
@@ -75,6 +76,7 @@ export default function BrandSeoPanel({
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [saveError, setSaveError] = useState("");
|
||||
const [picker, setPicker] = useState<BrandSlot | null>(null);
|
||||
const [copiedSlot, setCopiedSlot] = useState<BrandSlot | null>(null);
|
||||
const blobs = useRef<string[]>([]);
|
||||
const saveGen = useRef(0);
|
||||
|
||||
@@ -203,6 +205,21 @@ export default function BrandSeoPanel({
|
||||
const slotDraft = (id: BrandSlot) =>
|
||||
id === "logo_dark" ? dark : id === "favicon" ? favicon : light;
|
||||
|
||||
// 已保存的槽位 URL:存在才展示固定地址(未保存过无图可引用)
|
||||
const savedOf = (slot: BrandSlot) =>
|
||||
slot === "logo_dark" ? saved.dark : slot === "favicon" ? saved.favicon : saved.light;
|
||||
|
||||
const copyAlias = async (slot: BrandSlot) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(`${window.location.origin}${brandAliasPath(slot)}`);
|
||||
setCopiedSlot(slot);
|
||||
toast("已复制固定地址", "ok");
|
||||
window.setTimeout(() => setCopiedSlot((cur) => (cur === slot ? null : cur)), 1500);
|
||||
} catch {
|
||||
toast("复制失败,请手动复制");
|
||||
}
|
||||
};
|
||||
|
||||
const renderSlot = (slot: BrandSlot, label: string, swatch: "light" | "dark" | "neutral", compact?: boolean) => {
|
||||
const draft = slotDraft(slot);
|
||||
const preview = previewOf(draft);
|
||||
@@ -224,6 +241,18 @@ export default function BrandSeoPanel({
|
||||
</div>
|
||||
<div className="admin-brand-asset-meta">
|
||||
<span className="admin-brand-asset-label">{label}</span>
|
||||
{savedOf(slot) ? (
|
||||
<button
|
||||
type="button"
|
||||
className="admin-brand-alias"
|
||||
title="点击复制固定地址(换图不变链,友链引用请用此地址)"
|
||||
onClick={() => void copyAlias(slot)}
|
||||
>
|
||||
<Link2 size={12} aria-hidden />
|
||||
<span className="admin-brand-alias-url">{brandAliasPath(slot)}</span>
|
||||
{copiedSlot === slot ? <Check size={12} aria-hidden /> : null}
|
||||
</button>
|
||||
) : null}
|
||||
<div className="admin-brand-asset-actions">
|
||||
<label className="btn btn-line btn-sm cursor-pointer">
|
||||
<ImagePlus size={14} /> 上传
|
||||
|
||||
Reference in New Issue
Block a user