feat: animated expansion of md-link

This commit is contained in:
hypercross 2026-02-26 17:44:58 +08:00
parent 2794b21a41
commit 31b6b57ec8
2 changed files with 34 additions and 9 deletions

View File

@ -7,6 +7,7 @@ export interface ArticleProps {
section?: string; // 指定要显示的标题(不含 # section?: string; // 指定要显示的标题(不含 #
onLoaded?: () => void; onLoaded?: () => void;
onError?: (error: Error) => void; onError?: (error: Error) => void;
class?: string; // 额外的 class 用于样式控制
} }
async function fetchArticleContent(params: { src: string; section?: string }): Promise<string> { async function fetchArticleContent(params: { src: string; section?: string }): Promise<string> {
@ -39,7 +40,7 @@ export const Article: Component<ArticleProps> = (props) => {
}); });
return ( return (
<article ref={articleRef} class="prose" data-src={props.src}> <article ref={articleRef} class={`prose ${props.class || ''}`} data-src={props.src}>
<Show when={content.loading}> <Show when={content.loading}>
<div class="text-gray-500">...</div> <div class="text-gray-500">...</div>
</Show> </Show>

View File

@ -8,8 +8,10 @@ customElement("md-link", {}, (props, { element }) => {
noShadowDOM(); noShadowDOM();
const [showArticle, setShowArticle] = createSignal(false); const [showArticle, setShowArticle] = createSignal(false);
const [expanded, setExpanded] = createSignal(false);
let articleContainer: HTMLDivElement | undefined; let articleContainer: HTMLDivElement | undefined;
let disposeArticle: (() => void) | null = null; let disposeArticle: (() => void) | null = null;
let articleElement: HTMLElement | undefined;
// 从 element 的 textContent 获取链接目标(支持 path#section 语法) // 从 element 的 textContent 获取链接目标(支持 path#section 语法)
const rawLinkSrc = element?.textContent?.trim() || ""; const rawLinkSrc = element?.textContent?.trim() || "";
@ -38,14 +40,20 @@ customElement("md-link", {}, (props, { element }) => {
if (!parentP) return; if (!parentP) return;
if (showArticle()) { if (showArticle()) {
// 隐藏文章 // 隐藏文章 - 先折叠再移除
setExpanded(false);
if (articleContainer) { if (articleContainer) {
articleContainer.style.height = '0';
articleContainer.style.opacity = '0';
setTimeout(() => {
if (disposeArticle) { if (disposeArticle) {
disposeArticle(); disposeArticle();
disposeArticle = null; disposeArticle = null;
} }
articleContainer.remove(); articleContainer?.remove();
articleContainer = undefined; articleContainer = undefined;
articleElement = undefined;
}, 300);
} }
setShowArticle(false); setShowArticle(false);
} else { } else {
@ -53,6 +61,10 @@ customElement("md-link", {}, (props, { element }) => {
articleContainer = document.createElement("div"); articleContainer = document.createElement("div");
articleContainer.classList.add("md-link-article"); articleContainer.classList.add("md-link-article");
articleContainer.classList.add("ml-4", "border-l-2", "border-gray-200", "pl-4"); articleContainer.classList.add("ml-4", "border-l-2", "border-gray-200", "pl-4");
articleContainer.style.height = '0';
articleContainer.style.opacity = '0';
articleContainer.style.overflow = 'hidden';
articleContainer.style.transition = 'height 0.3s ease, opacity 0.3s ease';
parentP.after(articleContainer); parentP.after(articleContainer);
// 渲染 Article 组件 // 渲染 Article 组件
@ -60,7 +72,19 @@ customElement("md-link", {}, (props, { element }) => {
<Article <Article
src={linkSrc} src={linkSrc}
section={section} section={section}
onLoaded={() => console.log("Article loaded:", linkSrc)} class="article-animate"
onLoaded={() => {
// 内容加载完成后,获取实际高度并展开
requestAnimationFrame(() => {
articleElement = articleContainer?.querySelector('article[data-src]');
if (articleElement) {
const height = articleElement.scrollHeight;
articleContainer!.style.height = `${height}px`;
articleContainer!.style.opacity = '1';
setExpanded(true);
}
});
}}
onError={(err) => console.error("Article error:", err)} onError={(err) => console.error("Article error:", err)}
/> />
), articleContainer); ), articleContainer);