/* ===================================================================== * 文章视图模块(article.js) * 首页文章列表(分页)/ 分区 / 项目 / 简介 / 文章详情 / 点赞。 * ===================================================================== */ import { ABOUT_DEFAULT, ARTICLE_CATEGORIES, DONATE_IMAGE, PARTITIONS, SOCIAL_LINKS, state } from "./state.js"; import { $, escapeHtml, formatDate, openModal, showToast } from "./utils.js"; import { api } from "./api.js"; import { renderMarkdown } from "./markdown.js"; import { openLoginModal } from "./auth.js"; import { applyFriend } from "./friend.js"; import { renderComments } from "./comment.js"; /* ---------------- 首页(简介 + 社交链接 + 全部文章,分页加载) ---------------- */ export async function renderHome(app) { document.title = "首页 - 孤竹居士的博客"; app.innerHTML = `

加载中…

最新文章

`; await renderHomeAbout($("#about-card", app)); await renderArticleList($("#article-feed", app), 6); } /** 主页简介卡片:头像、名字、简介(线上可改)、社交账号链接 */ async function renderHomeAbout(el) { let profile = null; try { profile = await api("/api/user/blogger"); } catch (err) { /* 简介加载失败不阻塞页面 */ } const name = (profile && profile.username) || ABOUT_DEFAULT.name; const avatar = (profile && profile.avatar) || ""; const bio = (profile && profile.bio) ? profile.bio.split(/\r?\n/).map((s) => s.trim()).filter(Boolean) : ABOUT_DEFAULT.bio; const avatarHtml = avatar ? `头像` : `
${escapeHtml(name.slice(0, 1))}
`; el.innerHTML = ` ${avatarHtml}

${escapeHtml(name)}

${escapeHtml(ABOUT_DEFAULT.tagline)}

${bio.map((p) => `

${escapeHtml(p)}

`).join("")}
${state.user && state.user.role === "blogger" ? '' : ""}
${(ABOUT_DEFAULT.tags || []).map((t) => `${escapeHtml(t)}`).join("")}
`; // 绑定按钮事件:打赏弹窗(所有人可见);编辑简介(仅博主,展示在主页简介处) $("#btn-donate", el).addEventListener("click", openDonateModal); const bioBtn = $("#btn-edit-bio", el); if (bioBtn) bioBtn.addEventListener("click", () => openBioEditor(el, profile && profile.bio)); } /** 打赏弹窗:展示收款二维码;点击图片外任意位置关闭(按 Esc 也可关闭) */ function openDonateModal() { const overlay = document.createElement("div"); overlay.className = "modal-overlay"; overlay.innerHTML = ` `; const close = () => { overlay.remove(); document.removeEventListener("keydown", onKey); }; const onKey = (e) => { if (e.key === "Escape") close(); }; document.addEventListener("keydown", onKey); // 点击图片本身不关闭(方便长按识别),点击其他任意位置关闭 overlay.addEventListener("click", (e) => { if (e.target.closest("img")) return; close(); }); document.body.appendChild(overlay); } /** 简介编辑弹窗(仅博主):入口在主页简介处,保存后立即刷新简介卡片 */ async function openBioEditor(aboutEl, currentBio) { const form = document.createElement("div"); form.className = "form"; form.innerHTML = ` `; const modal = openModal({ title: "编辑简介", content: form }); $("#btn-bio-save", form).addEventListener("click", async () => { const btn = $("#btn-bio-save", form); const bio = $("#bio-edit-input", form).value.trim(); btn.disabled = true; btn.textContent = "保存中…"; try { await api("/api/user/profile", { method: "PUT", body: { bio } }); modal.close(); showToast("简介已保存", "success"); await renderHomeAbout(aboutEl); // 重新拉取最新简介并重绘 } catch (err) { showToast(err.message, "error"); btn.disabled = false; btn.textContent = "保存简介"; } }); } export function articleCard(article, compact = false) { const card = document.createElement("a"); card.className = "article-card"; // 文章详情地址按分区决定前缀:/life/1 或 /study/1 const category = article.category || "life"; card.href = `/${category}/${article.id}`; card.setAttribute("data-link", ""); const isFriendOnly = article.visibility === "friend"; // 游客视角的好友文章:后端仅返回 id/title/cover/visibility(无 author_username 字段) const locked = isFriendOnly && typeof article.author_username === "undefined"; const coverHtml = article.cover ? `
${escapeHtml(article.title)}${isFriendOnly ? '好友专属' : ""}
` : `
${isFriendOnly ? "🔒" : ""}
`; // 分区页(compact)只展示封面 + 标题;首页展示完整元信息 const metaHtml = compact ? "" : `
${isFriendOnly ? '好友专属' : '公开'} ${locked ? '仅好友可见全文' : `${escapeHtml(article.author_username || "")}${formatDate(article.created_time)}`}
`; card.innerHTML = ` ${coverHtml}

${escapeHtml(article.title)}

${metaHtml}
`; return card; } /* ---------------- 文章列表分页加载(首页 / 分区复用) ---------------- */ export async function renderArticleList(container, pageSize = 6, category = "") { container.innerHTML = ""; const list = document.createElement("div"); list.className = "article-list"; container.appendChild(list); const moreBtn = document.createElement("button"); moreBtn.className = "btn btn-outline btn-block load-more-btn"; moreBtn.textContent = "加载更多"; moreBtn.hidden = true; moreBtn.addEventListener("click", () => loadPage()); container.appendChild(moreBtn); let page = 1; let loaded = 0; let total = 0; async function loadPage() { moreBtn.disabled = true; moreBtn.textContent = "加载中…"; try { // category 为空 = 首页全部文章;传入分区则按分区过滤 const categoryQuery = category ? `&category=${encodeURIComponent(category)}` : ""; const data = await api(`/api/article/list?page=${page}&page_size=${pageSize}${categoryQuery}`); total = data.total; data.items.forEach((a) => list.appendChild(articleCard(a, Boolean(category)))); loaded += data.items.length; page += 1; if (loaded === 0 && total === 0) { list.innerHTML = '

暂无文章

'; moreBtn.hidden = true; moreBtn.textContent = "加载更多"; return; } moreBtn.hidden = loaded >= total; moreBtn.textContent = "加载更多"; } catch (err) { moreBtn.textContent = "加载更多"; showToast(err.message, "error"); } finally { moreBtn.disabled = false; } } await loadPage(); } /* ---------------- 分区 ---------------- */ export async function renderPartition(app, slug) { if (slug === "projects") return renderProjects(app); const partition = PARTITIONS.find((p) => p.slug === slug); if (!partition) return renderNotFound(app); document.title = `${partition.name} - 孤竹居士的博客`; const sub = slug === "life" ? "生活点滴记录" : "学习笔记与总结"; app.innerHTML = `

${escapeHtml(partition.name)}

${sub}

`; // 分区页只展示本分区的文章卡片(封面 + 标题) await renderArticleList($("#article-feed", app), 6, slug); } export async function renderProjects(app) { document.title = "我的项目 - 孤竹居士的博客"; app.innerHTML = `

我的项目

在线体验与源码下载

`; const grid = $(".project-grid", app); let items = []; try { items = await api("/api/project/list"); } catch (err) { showToast(err.message, "error"); } if (!items.length) { grid.innerHTML = '

暂无项目

'; return; } items.forEach((project) => { const card = document.createElement("a"); card.className = "project-card"; card.href = `/projects/${project.id}`; card.setAttribute("data-link", ""); const badge = project.project_type === "static" ? '在线演示' : 'GitHub 链接'; const visBadge = project.visibility === "friend" ? '🔒 好友专属' : '公开'; const lockedIcon = project.visibility === "friend" ? " 🔒" : ""; card.innerHTML = `

${escapeHtml(project.name)}${lockedIcon}

${escapeHtml(project.description || "")}

${(project.tech || "").split(",").map((t) => t.trim()).filter(Boolean).map((t) => `${escapeHtml(t)}`).join("")}
${badge} ${visBadge} 查看详情`; grid.appendChild(card); }); } /* ---------------- 项目详情 ---------------- */ export async function renderProjectDetail(app, id) { let project = null; try { project = await api(`/api/project/${id}`); } catch (err) { return renderNotFound(app); } if (!project) return renderNotFound(app); document.title = `${project.name} - 孤竹居士的博客`; const demoBtn = project.demo_url ? `🚀 在线运行` : ""; const downloadBtn = project.download_url ? `⬇ 下载项目` : ""; app.innerHTML = `

${escapeHtml(project.name)}

${escapeHtml(project.description || "")}

${(project.tech || "").split(",").map((t) => t.trim()).filter(Boolean).map((t) => `${escapeHtml(t)}`).join("")}
${demoBtn} ${downloadBtn}
`; } /* 简介已并入首页(renderHomeAbout),不再单独成页 */ export function renderNotFound(app) { document.title = "页面不存在 - 孤竹居士的博客"; app.innerHTML = `

404

页面不存在或已被移除

返回首页
`; } /* ---------------- 文章详情(含点赞) ---------------- */ export async function renderArticle(app, articleId) { const article = await api(`/api/article/${articleId}`); const isFriendOnly = article.visibility === "friend"; const locked = isFriendOnly && article.content == null; // 游客视角:正文为 null const category = article.category || "life"; document.title = `${article.title} - 孤竹居士的博客`; app.innerHTML = `

${escapeHtml(article.title)}

${escapeHtml(article.author_username || "")} ${formatDate(article.created_time)} ${isFriendOnly ? '好友专属' : ""}
${article.cover ? `
${escapeHtml(article.title)}
` : ""}
${locked ? lockedBox() : renderMarkdown(article.content)}

评论

`; if (locked) bindLockedApply(); await renderLikeBar($("#like-bar", app), articleId); await renderComments($("#comments", app), articleId); } function lockedBox() { let actionHtml = ""; if (!state.user) { actionHtml = ''; } else if (state.user.role === "visitor") { if (state.friendStatus === "pending") actionHtml = '

申请已提交,等待博主审批

'; else if (state.friendStatus === "rejected") actionHtml = ''; else actionHtml = ''; } else { actionHtml = '

登录好友账号后即可查看全文

'; } return `

🔒 该文章仅好友可见

${actionHtml}
`; } function bindLockedApply() { const btn = $("#btn-locked-apply"); if (btn) { btn.addEventListener("click", () => { if (!state.user) openLoginModal(); else applyFriend(); }); } } export async function renderLikeBar(el, articleId) { // 后端只返回数量与“当前用户是否已点赞”,不返回点赞名单(防用户名枚举) let likes = { count: 0, includes_me: false }; try { likes = await api(`/api/like/list?article_id=${articleId}`); } catch (err) { /* 好友文章对游客不可见点赞,忽略 */ } const liked = !!likes.includes_me; el.innerHTML = `
${likes.count ? `共 ${likes.count} 人点赞` : "还没有人点赞"}
`; const btn = $("#btn-like", el); if (!btn) return; btn.addEventListener("click", async () => { if (!state.user) { openLoginModal(); showToast("请先登录后点赞", "info"); return; } if (state.user.role === "visitor") { if (state.friendStatus === "pending") { showToast("好友申请审批通过后即可点赞", "info"); return; } showToast("申请成为好友,审批通过后即可点赞", "info"); applyFriend(); return; } if (liked) { showToast("您已经点过赞了", "info"); return; } btn.disabled = true; try { await api("/api/like/add", { method: "POST", body: { article_id: articleId } }); showToast("点赞成功", "success"); await renderLikeBar(el, articleId); } catch (err) { showToast(err.message, "error"); } }); }