feat: 友链/书库/移动端导航/排行榜等多模块功能
新增模块: - 友链(friendlink)后端处理 + 前端管理页与友链板 - 书库(library):导入、章节、封面、阅读页、横竖版自适应 AdaptiveCoverSlot - 顶栏导航(header_nav)配置与 MobileTabBar/MobileRailDrawers 移动端抽屉 - 排行榜 service 测试、站点页面测试、时间线发布(timeline_release) 其它改动: - 后端 handlers/services 全量小幅调整 - 前端组件、库函数、URL/品牌/站点 URL 工具更新 - Lightbox 图片、MdEntries 条目卡、coverColor 派生色相等前端能力
This commit is contained in:
@@ -7,6 +7,7 @@ import {
|
||||
GitBranch,
|
||||
Loader2,
|
||||
Plus,
|
||||
Tag,
|
||||
Trash2,
|
||||
} from "lucide-react";
|
||||
import Modal from "@/components/Modal";
|
||||
@@ -19,7 +20,7 @@ import {
|
||||
serializeTimelineBlock,
|
||||
type TimelineItem,
|
||||
} from "@/lib/timelineBlocks";
|
||||
import { apiTimelineFromGit, type TimelineGitItem } from "@/lib/api";
|
||||
import { apiTimelineFromGit, apiTimelineFromReleases, type TimelineGitItem } from "@/lib/api";
|
||||
import { toast } from "@/lib/toast";
|
||||
|
||||
type ImportMode = "replace" | "append";
|
||||
@@ -108,11 +109,34 @@ export default function TimelineEditor({
|
||||
);
|
||||
};
|
||||
|
||||
const runImport = async () => {
|
||||
const urls = urlsText
|
||||
const collectURLs = () =>
|
||||
urlsText
|
||||
.split(/\n/)
|
||||
.map((l) => l.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
const applyImported = (mapped: TimelineItem[], errorHint: string) => {
|
||||
if (mapped.length === 0) {
|
||||
setImportError(errorHint);
|
||||
return false;
|
||||
}
|
||||
setItems((prev) => {
|
||||
if (importMode === "append") {
|
||||
const seen = new Set(
|
||||
prev.map((p) => p.sourceUrl || `${p.date}|${p.title}`).filter(Boolean)
|
||||
);
|
||||
const extra = mapped.filter(
|
||||
(m) => !m.sourceUrl || !seen.has(m.sourceUrl)
|
||||
);
|
||||
return [...prev.filter((p) => p.title.trim() || p.body.trim()), ...extra];
|
||||
}
|
||||
return mapped;
|
||||
});
|
||||
return true;
|
||||
};
|
||||
|
||||
const runImportCommits = async () => {
|
||||
const urls = collectURLs();
|
||||
if (urls.length === 0) {
|
||||
setImportError("请粘贴至少一条提交记录页面 URL");
|
||||
return;
|
||||
@@ -122,23 +146,10 @@ export default function TimelineEditor({
|
||||
try {
|
||||
const res = await apiTimelineFromGit({ urls, follow_pages: followPages });
|
||||
const mapped = (res.items || []).map(fromGitItem);
|
||||
if (mapped.length === 0) {
|
||||
setImportError(res.error || "未能解析出提交;请检查 URL 或改为手填");
|
||||
if (!applyImported(mapped, res.error || "未能解析出提交;请检查 URL 或改为手填")) {
|
||||
if (res.warning) toast(res.warning);
|
||||
return;
|
||||
}
|
||||
setItems((prev) => {
|
||||
if (importMode === "append") {
|
||||
const seen = new Set(
|
||||
prev.map((p) => p.sourceUrl || `${p.date}|${p.title}`).filter(Boolean)
|
||||
);
|
||||
const extra = mapped.filter(
|
||||
(m) => !m.sourceUrl || !seen.has(m.sourceUrl)
|
||||
);
|
||||
return [...prev.filter((p) => p.title.trim() || p.body.trim()), ...extra];
|
||||
}
|
||||
return mapped;
|
||||
});
|
||||
if (res.warning) toast(res.warning, "ok");
|
||||
else toast(`已导入 ${mapped.length} 条,请点「插入/更新时间线」写入正文`, "ok");
|
||||
if (res.error) setImportError(res.error);
|
||||
@@ -149,6 +160,31 @@ export default function TimelineEditor({
|
||||
}
|
||||
};
|
||||
|
||||
const runImportReleases = async () => {
|
||||
const urls = collectURLs();
|
||||
if (urls.length === 0) {
|
||||
setImportError("请粘贴至少一条 Release 页面 URL");
|
||||
return;
|
||||
}
|
||||
setImporting(true);
|
||||
setImportError(null);
|
||||
try {
|
||||
const res = await apiTimelineFromReleases({ urls });
|
||||
const mapped = (res.items || []).map(fromGitItem);
|
||||
if (!applyImported(mapped, res.error || "未能解析出 Release;请检查 URL 或改为手填")) {
|
||||
if (res.warning) toast(res.warning);
|
||||
return;
|
||||
}
|
||||
if (res.warning) toast(res.warning, "ok");
|
||||
else toast(`已从 Release 导入 ${mapped.length} 条,请点「插入/更新时间线」写入正文`, "ok");
|
||||
if (res.error) setImportError(res.error);
|
||||
} catch (e) {
|
||||
setImportError(e instanceof Error ? e.message : "导入失败,请手填时间线");
|
||||
} finally {
|
||||
setImporting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleInsert = () => {
|
||||
const kept = items.filter((it) => it.selected !== false);
|
||||
if (kept.length === 0) {
|
||||
@@ -222,7 +258,7 @@ export default function TimelineEditor({
|
||||
</p>
|
||||
</div>
|
||||
<p className="meta text-[12px] mb-2 leading-relaxed">
|
||||
粘贴 Gitea / GitHub 的 commits 列表页 URL,每行一条。默认自动拉取后续页,也可只贴某几页。
|
||||
粘贴 Gitea / GitHub 的 commits 或 releases 列表页 URL,每行一条。点「导入提交」拉取提交记录,点「导入 Release」拉取发布日志。
|
||||
</p>
|
||||
<textarea
|
||||
value={urlsText}
|
||||
@@ -231,7 +267,7 @@ export default function TimelineEditor({
|
||||
setImportError(null);
|
||||
}}
|
||||
rows={3}
|
||||
placeholder={"https://github.com/org/repo/commits/main\nhttps://git.example.com/user/repo/commits/branch/main"}
|
||||
placeholder={"https://github.com/org/repo/commits/main\nhttps://git.iioio.com/freefire/jiang13-bbs/releases"}
|
||||
className="j13-tl-urls"
|
||||
disabled={importing}
|
||||
/>
|
||||
@@ -260,20 +296,40 @@ export default function TimelineEditor({
|
||||
<option value="append">追加</option>
|
||||
</select>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-line btn-sm ml-auto"
|
||||
disabled={importing}
|
||||
onClick={() => void runImport()}
|
||||
>
|
||||
{importing ? (
|
||||
<>
|
||||
<Loader2 size={14} className="animate-spin" /> 导入中…
|
||||
</>
|
||||
) : (
|
||||
"导入"
|
||||
)}
|
||||
</button>
|
||||
<div className="ml-auto flex items-center gap-1.5">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-line btn-sm"
|
||||
disabled={importing}
|
||||
onClick={() => void runImportCommits()}
|
||||
>
|
||||
{importing ? (
|
||||
<>
|
||||
<Loader2 size={14} className="animate-spin" /> 导入中…
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<GitBranch size={14} /> 导入提交
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-line btn-sm"
|
||||
disabled={importing}
|
||||
onClick={() => void runImportReleases()}
|
||||
>
|
||||
{importing ? (
|
||||
<>
|
||||
<Loader2 size={14} className="animate-spin" /> 导入中…
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Tag size={14} /> 导入 Release
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{showReplaceWarning ? (
|
||||
<p className="j13-tl-banner is-warn">
|
||||
@@ -300,7 +356,7 @@ export default function TimelineEditor({
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-line btn-sm"
|
||||
onClick={() => setItems((prev) => [...prev, emptyItem()])}
|
||||
onClick={() => setItems((prev) => [emptyItem(), ...prev])}
|
||||
>
|
||||
<Plus size={14} /> 添加
|
||||
</button>
|
||||
@@ -408,6 +464,7 @@ export default function TimelineEditor({
|
||||
title="删除条目"
|
||||
message="确定删除这条时间线项目?"
|
||||
danger
|
||||
zIndexClass="z-[1300]"
|
||||
confirmLabel="删除"
|
||||
onCancel={() => setDeleteIdx(null)}
|
||||
onConfirm={() => {
|
||||
|
||||
Reference in New Issue
Block a user