fix: 修复博客文章目录Tab生成
This commit is contained in:
parent
52cbef7d65
commit
3d85b91c57
@ -1,3 +1,6 @@
|
||||
packages:
|
||||
- '.'
|
||||
|
||||
overrides:
|
||||
'vue': 'beta'
|
||||
'@vue/compiler-core': 'beta'
|
||||
|
||||
@ -78,6 +78,13 @@ describe('useBlogArticles', () => {
|
||||
cover: 'https://img.demo/cover.jpg',
|
||||
date: '2026-06-05 10:20',
|
||||
excerpt: '摘要',
|
||||
headings: [
|
||||
{
|
||||
id: 'header-id-1',
|
||||
level: 2,
|
||||
text: '标题',
|
||||
},
|
||||
],
|
||||
readTime: '1 分钟',
|
||||
slug: decodedSlug,
|
||||
tags: ['Milkdown'],
|
||||
@ -218,10 +225,54 @@ describe('useBlogArticles', () => {
|
||||
expect(detailUrl.pathname).toBe('/api/blog/article/public/detail');
|
||||
expect(detailUrl.searchParams.get('slug')).toBe(decodedSlug);
|
||||
expect(article?.contentHtml).toBe('<h1>详情标题</h1><p>详情正文</p>');
|
||||
expect(article?.headings).toEqual([
|
||||
{
|
||||
id: 'header-id-1',
|
||||
level: 1,
|
||||
text: '详情标题',
|
||||
},
|
||||
]);
|
||||
expect(blogArticles.getArticleBySlug(decodedSlug)?.content).toEqual([
|
||||
'详情标题 详情正文',
|
||||
]);
|
||||
});
|
||||
|
||||
it('derives article catalog headings from public API html when heading metadata is absent', async () => {
|
||||
mockFetch([
|
||||
{
|
||||
body: {
|
||||
code: 200,
|
||||
data: {
|
||||
list: [
|
||||
{
|
||||
...publicArticle,
|
||||
contentHtml: '<h2 id="intro">介绍</h2><p>正文</p><h3>细节</h3>',
|
||||
},
|
||||
],
|
||||
total: 1,
|
||||
},
|
||||
},
|
||||
status: 200,
|
||||
},
|
||||
]);
|
||||
const { useBlogArticles } = await import('@/hooks/useBlogArticles');
|
||||
const blogArticles = useBlogArticles();
|
||||
|
||||
await blogArticles.loadArticles();
|
||||
|
||||
expect(blogArticles.articles.value[0]?.headings).toEqual([
|
||||
{
|
||||
id: 'intro',
|
||||
level: 2,
|
||||
text: '介绍',
|
||||
},
|
||||
{
|
||||
id: 'header-id-2',
|
||||
level: 3,
|
||||
text: '细节',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
function mockFetch(
|
||||
|
||||
@ -11,6 +11,12 @@ export type WordpressResolvedTerm = {
|
||||
slug?: string;
|
||||
};
|
||||
|
||||
export type WordpressArticleHeading = {
|
||||
id?: string;
|
||||
level?: number;
|
||||
text?: string;
|
||||
};
|
||||
|
||||
export type WordpressPublicArticle = {
|
||||
authorName?: string;
|
||||
categoriesResolved?: WordpressResolvedTerm[];
|
||||
@ -21,6 +27,7 @@ export type WordpressPublicArticle = {
|
||||
date?: string;
|
||||
excerpt?: string | { rendered?: string };
|
||||
excerptText?: string;
|
||||
headings?: WordpressArticleHeading[];
|
||||
id: number;
|
||||
link?: string;
|
||||
modified?: string;
|
||||
|
||||
@ -13,10 +13,12 @@ import {
|
||||
isArticleInCategory,
|
||||
tags as fallbackTags,
|
||||
type BlogArticle,
|
||||
type BlogArticleHeading,
|
||||
type BlogCategory,
|
||||
type BlogTag,
|
||||
} from '@/data/blog';
|
||||
import { PREVIOUS_BLOG_BACKGROUND_IMAGE, resolveBlogStaticAsset } from '@/data/blogStaticAssets';
|
||||
import { blogGeneratedHeadingId } from '@/factories/blogDomFactory';
|
||||
|
||||
const defaultCover = PREVIOUS_BLOG_BACKGROUND_IMAGE;
|
||||
const colorPool = ['blue', 'purple', 'green', 'orange', 'geekblue', 'cyan', 'volcano', 'magenta'];
|
||||
@ -220,6 +222,7 @@ function normalizeWordpressArticle(article: WordpressPublicArticle): BlogArticle
|
||||
cover: resolveBlogStaticAsset(article.cover, defaultCover),
|
||||
date: formatDate(article.date || article.modified),
|
||||
excerpt,
|
||||
headings: normalizeArticleHeadings(article.headings, contentHtml),
|
||||
id: article.id,
|
||||
readTime: `${Math.max(1, Math.ceil(words / 500))} 分钟`,
|
||||
slug: decodeSlug(article.slug),
|
||||
@ -230,6 +233,56 @@ function normalizeWordpressArticle(article: WordpressPublicArticle): BlogArticle
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param headings Optional heading outline supplied directly by the public API.
|
||||
* @param contentHtml Rendered WordPress HTML used as the source of truth when the API omits headings.
|
||||
* @returns Heading outline used only to decide whether Argon's article/sidebar catalog tab should be visible.
|
||||
*/
|
||||
function normalizeArticleHeadings(
|
||||
headings: WordpressPublicArticle['headings'],
|
||||
contentHtml: string,
|
||||
): BlogArticleHeading[] {
|
||||
if (headings?.length) {
|
||||
return headings.map((heading, index) => ({
|
||||
id: heading.id || blogGeneratedHeadingId(index + 1),
|
||||
level: normalizeHeadingLevel(heading.level),
|
||||
text: stripHtml(heading.text),
|
||||
})).filter((heading) => heading.text);
|
||||
}
|
||||
|
||||
return extractArticleHeadingsFromHtml(contentHtml);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param contentHtml Rendered WordPress article HTML returned by the public article API.
|
||||
* @returns Ordered h1-h6 headings parsed from the article body so post pages can expose the catalog tab.
|
||||
*/
|
||||
function extractArticleHeadingsFromHtml(contentHtml: string): BlogArticleHeading[] {
|
||||
const parser = typeof DOMParser === 'undefined' ? null : new DOMParser();
|
||||
if (!parser) return [];
|
||||
|
||||
const document = parser.parseFromString(contentHtml, 'text/html');
|
||||
|
||||
return Array.from(document.querySelectorAll<HTMLHeadingElement>('h1, h2, h3, h4, h5, h6'))
|
||||
.map((heading, index) => ({
|
||||
id: heading.id || blogGeneratedHeadingId(index + 1),
|
||||
level: normalizeHeadingLevel(Number(heading.tagName.slice(1))),
|
||||
text: stripHtml(heading.textContent),
|
||||
}))
|
||||
.filter((heading) => heading.text);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param value Heading level from API metadata or a parsed HTML tag name.
|
||||
* @returns Safe heading level constrained to the h1-h6 range accepted by the catalog model.
|
||||
*/
|
||||
function normalizeHeadingLevel(value: unknown): BlogArticleHeading['level'] {
|
||||
const level = Number(value);
|
||||
if (level >= 1 && level <= 6) return level as BlogArticleHeading['level'];
|
||||
|
||||
return 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param articles Local article list whose WordPress category memberships should be counted.
|
||||
* @returns Category list with counts based on all article category terms, not only the primary term.
|
||||
|
||||
Loading…
Reference in New Issue
Block a user