Skip to content

Commit 871fc0b

Browse files
committed
timeline 组件组件增强,新增 externalLink 组件展示 icon
1 parent c4847f8 commit 871fc0b

15 files changed

Lines changed: 866 additions & 106 deletions

File tree

packages/pure/components/basic/Footer.astro

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import config from 'virtual:config'
44
import { getPlatformLabel, hasIcon } from '../../libs/social'
55
import { Icon } from '../user'
66
import type { IconName } from '../../libs/icons'
7+
import ExternalLink from '@/components/ExternalLink.astro'
78
89
const footerConf = config.footer
910
const footerLink1 = footerConf.links?.filter(({ pos }) => pos === 1) || []
@@ -98,13 +99,12 @@ const socialLinks = normalizeSocialLinks(footerConf.social)
9899
{
99100
footerConf.credits && (
100101
<span class='inline-flex items-center gap-x-2'>
101-
<a
102+
<ExternalLink
102103
href='https://github.com/cworld1/astro-theme-pure'
103-
target='_blank'
104-
class='hover:text-primary external-link'
104+
class='hover:text-primary'
105105
>
106106
Astro & Pure theme
107-
</a>
107+
</ExternalLink>
108108
<span class='inline-flex items-center gap-x-1'>powered</span>
109109
</span>
110110
)
Lines changed: 43 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,53 @@
11
---
2-
import type { TimelineEvent } from '../../types'
3-
42
interface Props {
53
class?: string
6-
events: TimelineEvent[]
74
}
85
9-
const { class: className, events, ...props } = Astro.props
6+
const { class: className, ...props } = Astro.props
107
---
118

129
<div class={className} {...props}>
13-
<ul class='ps-0 sm:ps-2'>
14-
{
15-
events.map((event, index) => (
16-
<li class='group relative flex list-none gap-x-3 rounded-full ps-0 sm:gap-x-2'>
17-
{/* circle */}
18-
<span class='z-10 my-2 ms-2 h-3 w-3 min-w-3 rounded-full border-2 border-muted-foreground transition-transform group-hover:scale-125' />
19-
{/* line */}
20-
{index !== events.length - 1 && (
21-
<span
22-
class='absolute start-[12px] top-[20px] w-1 bg-border'
23-
style={{ height: 'calc(100% - 4px)' }}
24-
/>
25-
)}
26-
<div class='flex gap-2 max-sm:flex-col'>
27-
<samp class='w-fit grow-0 rounded-md py-1 text-sm max-sm:bg-primary-foreground max-sm:px-2 sm:min-w-[82px] sm:text-right'>
28-
{event.date}
29-
</samp>
30-
<div>
31-
<Fragment set:html={event.content} />
32-
</div>
33-
</div>
34-
</li>
35-
))
36-
}
10+
<ul class='ps-0 sm:ps-2 timeline-list m-0 p-0'>
11+
<slot />
3712
</ul>
3813
</div>
14+
15+
<style>
16+
/* 重置 ul 和 li 的默认样式 */
17+
:global(.timeline-list) {
18+
margin: 0;
19+
padding: 0;
20+
}
21+
22+
:global(.timeline-list > li) {
23+
margin-top: 0 !important;
24+
margin-bottom: 0 !important;
25+
padding-inline-start: 0 !important;
26+
}
27+
28+
/* 为 TimelineItem 添加连接线(除了最后一个) */
29+
/* 竖线对齐圆点:从当前圆点中心到下一个圆点中心 */
30+
:global(.timeline-list > li[data-timeline-item]:not(:last-child)::after) {
31+
content: '';
32+
position: absolute;
33+
left: 14px; /* ms-2 (8px) + 圆圈半径 (6px) = 14px,使竖线在圆圈中心 */
34+
top: 20px; /* 从当前圆点中心开始(pt-0.5 + 圆点中心约20px) */
35+
width: 1px;
36+
height: calc(100% - 20px); /* 延伸到下一个圆点中心 */
37+
background-color: hsl(var(--border));
38+
z-index: 0;
39+
}
40+
41+
/* 里程碑节点的背景高亮 */
42+
:global(.timeline-list > li[data-milestone="true"]) {
43+
background: linear-gradient(
44+
to right,
45+
hsl(var(--primary) / 0.05),
46+
transparent 20%
47+
);
48+
border-radius: 0.375rem;
49+
padding: 0.25rem 0;
50+
margin: 0.125rem 0;
51+
}
52+
</style>
53+
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
---
2+
/**
3+
* Timeline 单个事件项组件
4+
* 使用 slot 支持嵌套任意组件
5+
*
6+
* 使用示例:
7+
* <Timeline>
8+
* <TimelineItem date="2025-01-01">
9+
* 内容可以使用任意组件,如 <ExternalLink href="...">链接</ExternalLink>
10+
* </TimelineItem>
11+
* <TimelineItem date="2025-01-01" milestone>
12+
* 里程碑节点,会有特殊样式
13+
* </TimelineItem>
14+
* </Timeline>
15+
*/
16+
17+
interface Props {
18+
date: string
19+
class?: string
20+
milestone?: boolean
21+
}
22+
23+
const { date, class: className, milestone = false } = Astro.props
24+
---
25+
26+
<li class:list={['group relative flex list-none gap-x-3 rounded-full ps-0 sm:gap-x-2', className]} data-timeline-item data-milestone={milestone}>
27+
{/* circle container */}
28+
<div class='relative flex items-start pt-0.5'>
29+
{/* circle */}
30+
<span class:list={[
31+
'z-10 ms-2 h-3 w-3 min-w-3 rounded-full border-2 transition-all duration-300',
32+
milestone
33+
? 'border-primary bg-primary'
34+
: 'border-muted-foreground bg-background group-hover:border-primary group-hover:scale-110'
35+
]} />
36+
</div>
37+
<div class='flex flex-col gap-0 flex-1 timeline-content py-0.5'>
38+
<samp class='w-fit rounded-md py-0 text-sm max-sm:bg-primary-foreground max-sm:px-2 sm:min-w-[82px] leading-tight'>
39+
{date}
40+
</samp>
41+
<div class='leading-tight timeline-text-content'>
42+
<slot />
43+
</div>
44+
</div>
45+
</li>

packages/pure/components/user/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export { default as MdxRepl } from './MdxRepl.astro'
99
// List
1010
export { default as CardList } from './CardList.astro'
1111
export { default as Timeline } from './Timeline.astro'
12+
export { default as TimelineItem } from './TimelineItem.astro'
1213
export { default as Steps } from './Steps.astro'
1314

1415
// Simple text rerender

packages/pure/plugins/rehype-external-links.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@ export default function rehypeExternalLinks(options: ExternalLinkOptions = {}) {
2626
return function transformer(tree: Root): void {
2727
visit(tree, 'element', (node: Element) => {
2828
if (node.tagName === 'a' && typeof node.properties?.href === 'string') {
29+
// 跳过已经处理过的链接(由 ExternalLink 组件处理的)
30+
if (node.properties['data-external-link-processed']) {
31+
return
32+
}
33+
2934
const href = node.properties.href
3035
const protocolRelative = href.startsWith('//')
3136
const protocol = protocolRelative ? 'http' : href.slice(0, href.indexOf(':'))

packages/pure/types/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export type CardList = {
2424

2525
export type TimelineEvent = {
2626
date: string
27-
content: string
27+
content: string | any // 支持 string 或组件/JSX
2828
}
2929

3030
export type iconsType = keyof typeof Icons

src/assets/styles/external-link.css

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
background-color: transparent;
1212
border-bottom-color: hsla(200, 29%, 45%, 0.3);
1313
margin: 0.1em;
14+
white-space: nowrap;
15+
vertical-align: baseline;
1416
}
1517

1618
.external-link:hover {
@@ -24,6 +26,13 @@
2426
transition: transform 0.2s ease-in-out;
2527
}
2628

29+
.external-link-icon-emoji {
30+
display: inline-block;
31+
vertical-align: text-bottom;
32+
font-size: 1em;
33+
line-height: 1;
34+
}
35+
2736
.external-link:hover .external-link-icon {
2837
transform: scale(1.1);
2938
}

src/components/ExternalLink.astro

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
---
2+
/**
3+
* 外部链接组件,自动添加图标和属性
4+
*
5+
* 使用示例:
6+
* <ExternalLink href="https://google.com">Google</ExternalLink>
7+
*/
8+
9+
import { Icons } from 'astro-pure/libs'
10+
import config from 'virtual:config'
11+
12+
interface Props {
13+
href: string
14+
target?: string
15+
rel?: string | string[]
16+
class?: string
17+
customIcons?: Record<string, string>
18+
iconSize?: number
19+
}
20+
21+
const {
22+
href,
23+
target = '_blank',
24+
rel = ['nofollow', 'noopener', 'noreferrer'],
25+
class: className = '',
26+
customIcons,
27+
iconSize = 16
28+
} = Astro.props
29+
30+
// 获取全局配置
31+
const globalCustomIcons = config.content?.externalLinks?.customIcons || {}
32+
const mergedCustomIcons = { ...globalCustomIcons, ...customIcons }
33+
const relString = Array.isArray(rel) ? rel.join(' ') : rel
34+
35+
// 检查是否为绝对 URL
36+
function isAbsoluteUrl(url: string): boolean {
37+
if (typeof url !== 'string') return false
38+
if (/^[a-zA-Z]:\\/.test(url)) return false
39+
return /^[a-zA-Z][a-zA-Z\d+\-.]*?:/.test(url)
40+
}
41+
42+
// 获取 hostname
43+
function getHostname(href: string): string | null {
44+
try {
45+
const url = href.startsWith('//') ? `http:${href}` : href
46+
return new URL(url).hostname
47+
} catch {
48+
return null
49+
}
50+
}
51+
52+
// 判断是否为外部链接
53+
const protocolRelative = href.startsWith('//')
54+
const isAbsolute = isAbsoluteUrl(href)
55+
let hostname: string | null = null
56+
let iconSrc: string | null = null
57+
58+
if (protocolRelative || isAbsolute) {
59+
hostname = getHostname(href)
60+
if (hostname) {
61+
// 处理自定义图标
62+
const customIconKey = mergedCustomIcons[hostname]
63+
if (customIconKey) {
64+
const iconSvg = Icons[customIconKey as keyof typeof Icons]
65+
if (iconSvg) {
66+
let svgString = iconSvg
67+
if (!svgString.trim().startsWith('<svg')) {
68+
svgString = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">${svgString}</svg>`
69+
}
70+
iconSrc = `data:image/svg+xml;base64,${Buffer.from(svgString).toString('base64')}`
71+
}
72+
}
73+
74+
// 如果没有自定义图标,使用 favicon 服务
75+
if (!iconSrc) {
76+
iconSrc = `https://favicon.im/${hostname}`
77+
}
78+
}
79+
}
80+
81+
// 如果是外部链接,添加图标和属性
82+
const isExternal = !!(protocolRelative || (isAbsolute && hostname))
83+
---
84+
85+
{isExternal ? (
86+
<a
87+
href={href}
88+
target={target}
89+
rel={relString}
90+
class:list={['external-link', 'not-prose', className]}
91+
data-external-link-processed="true"
92+
>
93+
{iconSrc ? (
94+
<img
95+
src={iconSrc}
96+
alt=""
97+
width={iconSize}
98+
height={iconSize}
99+
class="external-link-icon"
100+
onerror="this.style.display='none'; this.nextElementSibling.style.display='inline-block'"
101+
/>
102+
) : null}
103+
<span class="external-link-icon-emoji" style={iconSrc ? 'display:none' : 'display:inline-block'} aria-hidden="true">🌐</span>
104+
<slot />
105+
</a>
106+
) : (
107+
<a href={href} class={className}>
108+
<slot />
109+
</a>
110+
)}

src/content/blog/coding/bloom_filter.mdx

Lines changed: 19 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -16,40 +16,32 @@ draft: true
1616

1717
import Paper from '@/custom/components/pages/Paper.astro'
1818

19-
import { Steps, Timeline } from 'astro-pure/user'
19+
import { Steps } from 'astro-pure/user'
20+
import { Timeline, TimelineItem } from '@/custom/components/user'
2021

2122
> [!quote] Bloom Filter 简介
2223
> 布隆过滤器(Bloom Filter)是一种开创性的概率型数据结构,其核心价值在于提供了一种在空间和时间上都极具效率的近似集合成员关系测试方法。
2324
> 自问世以来,它已成为处理大规模数据集时不可或缺的工具。本文旨在深入探讨布隆过滤器的原理、实现方式以及在实际应用中的优势和局限性。
2425
2526
## 演变历程
2627

27-
<Timeline
28-
events={[
29-
{
30-
date: '1970s',
31-
content: 'Burton Howard Bloom 提出了布隆过滤器的概念。'
32-
},
33-
{
34-
date: '1990s',
35-
content: '布隆过滤器开始被广泛应用于缓存系统和网络协议中。'
36-
},
37-
{
38-
date: '2000s',
39-
content: '布隆过滤器的变种和优化算法陆续被提出,如计数布隆过滤器、分布式布隆过滤器等。'
40-
},
41-
{
42-
date: '2010s',
43-
content:
44-
'布隆过滤器在大数据处理、机器学习等领域得到了更广泛的应用,如 Apache Hadoop、Apache Spark 等大数据框架中都集成了布隆过滤器。'
45-
},
46-
{
47-
date: '2020s',
48-
content:
49-
'随着云计算和边缘计算的发展,布隆过滤器在分布式系统中的应用越来越普遍,成为处理大规模数据集时的重要工具。'
50-
}
51-
]}
52-
/>
28+
<Timeline>
29+
<TimelineItem date="1970s">
30+
Burton Howard Bloom 提出了布隆过滤器的概念。
31+
</TimelineItem>
32+
<TimelineItem date="1990s">
33+
布隆过滤器开始被广泛应用于缓存系统和网络协议中。
34+
</TimelineItem>
35+
<TimelineItem date="2000s">
36+
布隆过滤器的变种和优化算法陆续被提出,如计数布隆过滤器、分布式布隆过滤器等。
37+
</TimelineItem>
38+
<TimelineItem date="2010s">
39+
布隆过滤器在大数据处理、机器学习等领域得到了更广泛的应用,如 Apache Hadoop、Apache Spark 等大数据框架中都集成了布隆过滤器。
40+
</TimelineItem>
41+
<TimelineItem date="2020s">
42+
随着云计算和边缘计算的发展,布隆过滤器在分布式系统中的应用越来越普遍,成为处理大规模数据集时的重要工具。
43+
</TimelineItem>
44+
</Timeline>
5345

5446
## 历史背景
5547

0 commit comments

Comments
 (0)