Commit 7d534b67 authored by xuchentao's avatar xuchentao

fix: keep article categories in sync

parent 27f939e3
Pipeline #413 passed with stages
in 17 seconds
...@@ -223,10 +223,10 @@ async function createCategory() { ...@@ -223,10 +223,10 @@ async function createCategory() {
const name = input.value.trim(); const name = input.value.trim();
$("#category-error").textContent = ""; $("#category-error").textContent = "";
try { try {
const result = await api("/categories", { method: "POST", body: { name } }); await triggerBuild("/categories", { action: "添加分类", body: { name } });
categories = result.categories; await loadCategories();
input.value = ""; input.value = "";
fillCategories(result.category); fillCategories(name);
dirty = true; dirty = true;
closeCategoryMenu(); closeCategoryMenu();
if (!$("#category-modal").classList.contains("hidden")) await loadCategoryManager(); if (!$("#category-modal").classList.contains("hidden")) await loadCategoryManager();
...@@ -383,9 +383,10 @@ $("#manage-categories").addEventListener("click", openCategoryManager); ...@@ -383,9 +383,10 @@ $("#manage-categories").addEventListener("click", openCategoryManager);
$("#close-category-modal").addEventListener("click", () => hide("#category-modal")); $("#close-category-modal").addEventListener("click", () => hide("#category-modal"));
$("#manager-add-category").addEventListener("click", async () => { $("#manager-add-category").addEventListener("click", async () => {
const input = $("#manager-new-category"); const input = $("#manager-new-category");
const name = input.value.trim();
$("#manager-category-error").textContent = ""; $("#manager-category-error").textContent = "";
try { try {
await api("/categories", { method: "POST", body: { name: input.value.trim() } }); await triggerBuild("/categories", { action: "添加分类", body: { name } });
input.value = ""; input.value = "";
await loadCategoryManager(); await loadCategoryManager();
} catch (error) { $("#manager-category-error").textContent = error.message; } } catch (error) { $("#manager-category-error").textContent = error.message; }
......
--- ---
import logo from "../../横向-绿0013.svg"; import logo from "../../横向-绿0013.svg";
import { site } from "../data/site"; import { site } from "../data/site";
import { categoryToSlug, getCategoryNames } from "../lib/articles";
const path = Astro.url.pathname; const path = Astro.url.pathname;
const isActive = (href: string) => href === "/" ? path === "/" : path.startsWith(href); const isActive = (href: string) => href === "/" ? path === "/" : path.startsWith(href);
const categoryChildren = (await getCategoryNames()).map((label) => ({
href: `/articles/topic/${categoryToSlug(label)}/`,
label,
}));
const nav = site.nav.map((item) => item.href === "/articles/"
? { ...item, children: [{ href: "/articles/", label: "全部资讯" }, ...categoryChildren] }
: item);
--- ---
<div class="top-note"> <div class="top-note">
<div class="container top-note__inner"> <div class="container top-note__inner">
...@@ -20,7 +28,7 @@ const isActive = (href: string) => href === "/" ? path === "/" : path.startsWith ...@@ -20,7 +28,7 @@ const isActive = (href: string) => href === "/" ? path === "/" : path.startsWith
<nav class="desktop-nav" aria-label="主导航"> <nav class="desktop-nav" aria-label="主导航">
<ul> <ul>
{site.nav.map((item) => ( {nav.map((item) => (
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href={item.href} aria-current={isActive(item.href) ? "page" : undefined} aria-haspopup="true"> <a class="nav-link" href={item.href} aria-current={isActive(item.href) ? "page" : undefined} aria-haspopup="true">
{item.label}<span class="nav-chevron" aria-hidden="true">⌄</span> {item.label}<span class="nav-chevron" aria-hidden="true">⌄</span>
...@@ -44,7 +52,7 @@ const isActive = (href: string) => href === "/" ? path === "/" : path.startsWith ...@@ -44,7 +52,7 @@ const isActive = (href: string) => href === "/" ? path === "/" : path.startsWith
<span></span><span></span><span></span> <span></span><span></span><span></span>
</summary> </summary>
<nav data-mobile-nav aria-label="移动端导航"> <nav data-mobile-nav aria-label="移动端导航">
{site.nav.map((item) => ( {nav.map((item) => (
<div class="mobile-nav__group"> <div class="mobile-nav__group">
<a class="mobile-nav__primary" href={item.href} aria-current={isActive(item.href) ? "page" : undefined}>{item.label}</a> <a class="mobile-nav__primary" href={item.href} aria-current={isActive(item.href) ? "page" : undefined}>{item.label}</a>
<div class="mobile-subnav"> <div class="mobile-subnav">
......
...@@ -169,10 +169,6 @@ export const site = { ...@@ -169,10 +169,6 @@ export const site = {
label: "健康资讯", label: "健康资讯",
children: [ children: [
{ href: "/articles/", label: "全部资讯" }, { href: "/articles/", label: "全部资讯" },
{ href: "/articles/topic/body-age/", label: "体龄管理" },
{ href: "/articles/topic/meridian/", label: "经络调理" },
{ href: "/articles/topic/wellness/", label: "健康科普" },
{ href: "/articles/topic/store-growth/", label: "门店经营" },
], ],
}, },
{ {
......
import { getPublishedArticles as readPublishedArticles, type Article } from "./article-store"; import { getCategoryNames as readCategoryNames, getPublishedArticles as readPublishedArticles, type Article } from "./article-store";
export type { Article }; export type { Article };
export const PAGE_SIZE = 9; export const PAGE_SIZE = 9;
...@@ -28,14 +28,18 @@ export async function getPublishedArticles(): Promise<Article[]> { ...@@ -28,14 +28,18 @@ export async function getPublishedArticles(): Promise<Article[]> {
return readPublishedArticles(); return readPublishedArticles();
} }
export async function getCategoryNames(): Promise<string[]> {
return readCategoryNames();
}
export interface CategoryInfo { export interface CategoryInfo {
name: string; name: string;
slug: string; slug: string;
count: number; count: number;
} }
export function getCategories(items: Article[]): CategoryInfo[] { export function getCategories(items: Article[], categoryNames: string[] = []): CategoryInfo[] {
const counts = new Map<string, number>(); const counts = new Map<string, number>(categoryNames.map((name) => [name, 0]));
for (const item of items) counts.set(item.data.category, (counts.get(item.data.category) ?? 0) + 1); for (const item of items) counts.set(item.data.category, (counts.get(item.data.category) ?? 0) + 1);
return [...counts.entries()] return [...counts.entries()]
.map(([name, count]) => ({ name, slug: categoryToSlug(name), count })) .map(([name, count]) => ({ name, slug: categoryToSlug(name), count }))
......
...@@ -162,8 +162,7 @@ export async function handleCmsApi(request: Request, routeValue: string, clientA ...@@ -162,8 +162,7 @@ export async function handleCmsApi(request: Request, routeValue: string, clientA
if (method === "GET") return json({ categories: await getCategoryNames(), stats: await getCategoryStats() }); if (method === "GET") return json({ categories: await getCategoryNames(), stats: await getCategoryStats() });
const body = await bodyOf(request); const body = await bodyOf(request);
if (method === "POST") { if (method === "POST") {
const categories = await withSiteBuildLock(() => addCategory(body.name)); return startOrConflict(async () => { await addCategory(body.name); });
return json({ ok: true, category: String(body.name || "").trim(), categories, stats: await getCategoryStats() }, 201);
} }
if (method === "PATCH") { if (method === "PATCH") {
return startOrConflict(async () => { await renameCategory(body.current, body.name); }); return startOrConflict(async () => { await renameCategory(body.current, body.name); });
......
--- ---
import ArticlesView from "../../components/ArticlesView.astro"; import ArticlesView from "../../components/ArticlesView.astro";
import { getPublishedArticles, getCategories, pageSlice, lastPageOf } from "../../lib/articles"; import { getPublishedArticles, getCategoryNames, getCategories, pageSlice, lastPageOf } from "../../lib/articles";
const all = await getPublishedArticles(); const all = await getPublishedArticles();
const categories = getCategories(all); const categories = getCategories(all, await getCategoryNames());
const items = pageSlice(all, 1); const items = pageSlice(all, 1);
--- ---
<ArticlesView items={items} categories={categories} totalCount={all.length} activeSlug={null} activeName={null} currentPage={1} lastPage={lastPageOf(all.length)} basePath="/articles" /> <ArticlesView items={items} categories={categories} totalCount={all.length} activeSlug={null} activeName={null} currentPage={1} lastPage={lastPageOf(all.length)} basePath="/articles" />
--- ---
import ArticlesView from "../../../components/ArticlesView.astro"; import ArticlesView from "../../../components/ArticlesView.astro";
import { getPublishedArticles, getCategories, pageSlice, lastPageOf } from "../../../lib/articles"; import { getPublishedArticles, getCategoryNames, getCategories, pageSlice, lastPageOf } from "../../../lib/articles";
export async function getStaticPaths() { export async function getStaticPaths() {
const articles = await getPublishedArticles(); const articles = await getPublishedArticles();
...@@ -12,4 +12,4 @@ export async function getStaticPaths() { ...@@ -12,4 +12,4 @@ export async function getStaticPaths() {
const all = await getPublishedArticles(); const all = await getPublishedArticles();
const page = Number(Astro.params.page); const page = Number(Astro.params.page);
--- ---
<ArticlesView items={pageSlice(all, page)} categories={getCategories(all)} totalCount={all.length} activeSlug={null} activeName={null} currentPage={page} lastPage={lastPageOf(all.length)} basePath="/articles" /> <ArticlesView items={pageSlice(all, page)} categories={getCategories(all, await getCategoryNames())} totalCount={all.length} activeSlug={null} activeName={null} currentPage={page} lastPage={lastPageOf(all.length)} basePath="/articles" />
--- ---
import ArticlesView from "../../../../components/ArticlesView.astro"; import ArticlesView from "../../../../components/ArticlesView.astro";
import { getPublishedArticles, getCategories, slugToCategory, pageSlice, lastPageOf } from "../../../../lib/articles"; import { categoryToSlug, getPublishedArticles, getCategoryNames, getCategories, slugToCategory, pageSlice, lastPageOf } from "../../../../lib/articles";
export async function getStaticPaths() { export async function getStaticPaths() {
const articles = await getPublishedArticles(); return (await getCategoryNames()).map((categoryName) => ({
return getCategories(articles).map((category) => ({ params: { slug: categoryToSlug(categoryName) },
params: { slug: category.slug }, props: { categoryName },
props: { categoryName: category.name },
})); }));
} }
const all = await getPublishedArticles(); const all = await getPublishedArticles();
const categories = getCategories(all); const categories = getCategories(all, await getCategoryNames());
const categoryName = String(Astro.props.categoryName || slugToCategory(Astro.params.slug!, categories.map((item) => item.name)) || ""); const categoryName = String(Astro.props.categoryName || slugToCategory(Astro.params.slug!, categories.map((item) => item.name)) || "");
const filtered = all.filter((item) => item.data.category === categoryName); const filtered = all.filter((item) => item.data.category === categoryName);
--- ---
......
--- ---
import ArticlesView from "../../../../../components/ArticlesView.astro"; import ArticlesView from "../../../../../components/ArticlesView.astro";
import { getPublishedArticles, getCategories, slugToCategory, pageSlice, lastPageOf } from "../../../../../lib/articles"; import { getPublishedArticles, getCategoryNames, getCategories, slugToCategory, pageSlice, lastPageOf } from "../../../../../lib/articles";
export async function getStaticPaths() { export async function getStaticPaths() {
const articles = await getPublishedArticles(); const articles = await getPublishedArticles();
const categories = getCategories(articles, await getCategoryNames());
const paths: Array<{ params: { slug: string; page: string }; props: { categoryName: string } }> = []; const paths: Array<{ params: { slug: string; page: string }; props: { categoryName: string } }> = [];
for (const category of getCategories(articles)) { for (const category of categories) {
for (let page = 2; page <= lastPageOf(category.count); page += 1) { for (let page = 2; page <= lastPageOf(category.count); page += 1) {
paths.push({ params: { slug: category.slug, page: String(page) }, props: { categoryName: category.name } }); paths.push({ params: { slug: category.slug, page: String(page) }, props: { categoryName: category.name } });
} }
...@@ -14,7 +15,7 @@ export async function getStaticPaths() { ...@@ -14,7 +15,7 @@ export async function getStaticPaths() {
} }
const all = await getPublishedArticles(); const all = await getPublishedArticles();
const categories = getCategories(all); const categories = getCategories(all, await getCategoryNames());
const categoryName = String(Astro.props.categoryName || slugToCategory(Astro.params.slug!, categories.map((item) => item.name)) || ""); const categoryName = String(Astro.props.categoryName || slugToCategory(Astro.params.slug!, categories.map((item) => item.name)) || "");
const filtered = all.filter((item) => item.data.category === categoryName); const filtered = all.filter((item) => item.data.category === categoryName);
const page = Number(Astro.params.page); const page = Number(Astro.params.page);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment