@coffic/cosy-ui 0.1.25 → 0.1.27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -50
- package/index.ts +14 -4
- package/package.json +10 -7
- package/src/components/Alert.astro +78 -0
- package/src/components/Article.astro +11 -0
- package/src/components/Banner.astro +41 -4
- package/src/components/Blog.astro +115 -0
- package/src/components/Button.astro +49 -0
- package/src/components/Card.astro +113 -0
- package/src/components/CodeBlock.astro +186 -0
- package/src/components/Footer.astro +5 -6
- package/src/components/Header.astro +31 -48
- package/src/components/Hero.astro +69 -0
- package/src/components/Link.astro +70 -4
- package/src/components/Modal.astro +67 -0
- package/src/components/TeamMember.astro +68 -0
- package/src/components/TeamMembers.astro +43 -0
- package/src/components/ThemeItem.astro +14 -7
- package/src/components/Component.astro +0 -10
- package/src/types/footer.ts +0 -11
package/README.md
CHANGED
@@ -1,64 +1,26 @@
|
|
1
|
-
#
|
1
|
+
# Cosy UI
|
2
2
|
|
3
3
|
这是一个 Astro 组件库,为同一个组织下的多个项目提供统一的 UI 风格。
|
4
4
|
|
5
5
|
## 安装
|
6
6
|
|
7
7
|
```bash
|
8
|
-
npm install @coffic/
|
8
|
+
npm install @coffic/cosy-ui
|
9
9
|
```
|
10
10
|
|
11
|
-
|
12
|
-
|
13
|
-
本组件库依赖以下包,请确保您的项目中已正确安装和配置:
|
14
|
-
|
15
|
-
### 1. Astro
|
16
|
-
|
17
|
-
确保您的项目是一个 Astro 项目。如果不是,可以按照以下步骤创建:
|
18
|
-
|
19
|
-
```bash
|
20
|
-
npm create astro@latest
|
21
|
-
```
|
22
|
-
|
23
|
-
### 2. TailwindCSS
|
24
|
-
|
25
|
-
如果您的项目还没有配置 TailwindCSS,请按照以下步骤安装:
|
26
|
-
|
27
|
-
```bash
|
28
|
-
npm install -D tailwindcss @astrojs/tailwind
|
29
|
-
```
|
30
|
-
|
31
|
-
然后在您的 `astro.config.mjs` 中添加:
|
32
|
-
|
33
|
-
```javascript
|
34
|
-
import tailwind from '@astrojs/tailwind';
|
11
|
+
在 Tailwind 的 CSS 文件中增加:
|
35
12
|
|
36
|
-
|
37
|
-
|
38
|
-
});
|
39
|
-
```
|
40
|
-
|
41
|
-
### 3. DaisyUI
|
42
|
-
|
43
|
-
安装 DaisyUI:
|
44
|
-
|
45
|
-
```bash
|
46
|
-
npm install -D daisyui
|
13
|
+
```css
|
14
|
+
@source '../node_modules/@coffic/shared-ui';
|
47
15
|
```
|
48
16
|
|
49
|
-
|
50
|
-
|
51
|
-
```javascript
|
52
|
-
export default {
|
53
|
-
plugins: [require('daisyui')],
|
54
|
-
};
|
55
|
-
```
|
17
|
+
## 必要依赖
|
56
18
|
|
57
|
-
|
19
|
+
本组件库依赖以下包,请确保您的项目中已正确安装和配置:
|
58
20
|
|
59
|
-
|
60
|
-
|
61
|
-
|
21
|
+
- Astro
|
22
|
+
- TailwindCSS
|
23
|
+
- DaisyUI
|
62
24
|
|
63
25
|
## 内置组件
|
64
26
|
|
@@ -72,8 +34,6 @@ export default {
|
|
72
34
|
- 组件的样式会受到您项目中 Tailwind 配置的影响
|
73
35
|
- 如果您修改了 Tailwind 的默认主题或 DaisyUI 的主题,可能会影响组件的外观
|
74
36
|
|
75
|
-
2. 建议在您的项目中保持默认的 Tailwind 和 DaisyUI 配置,以确保组件显示正常
|
76
|
-
|
77
37
|
## 贡献
|
78
38
|
|
79
39
|
欢迎提交 Issue 和 Pull Request!
|
package/index.ts
CHANGED
@@ -1,7 +1,17 @@
|
|
1
|
-
|
2
|
-
export { default as
|
1
|
+
// 这里用于导出所有组件,注意按照字母排序
|
2
|
+
export { default as Alert } from './src/components/Alert.astro';
|
3
|
+
export { default as Article } from './src/components/Article.astro';
|
4
|
+
export { default as Blog } from './src/components/Blog.astro';
|
3
5
|
export { default as Banner } from './src/components/Banner.astro';
|
6
|
+
export { default as Button } from './src/components/Button.astro';
|
7
|
+
export { default as Card } from './src/components/Card.astro';
|
8
|
+
export { default as CodeBlock } from './src/components/CodeBlock.astro';
|
9
|
+
export { default as Footer } from './src/components/Footer.astro';
|
10
|
+
export { default as Header } from './src/components/Header.astro';
|
11
|
+
export { default as Hero } from './src/components/Hero.astro';
|
4
12
|
export { default as Link } from './src/components/Link.astro';
|
13
|
+
export { default as Modal } from './src/components/Modal.astro';
|
5
14
|
export { default as SocialIcon } from './src/components/SocialIcon.astro';
|
6
|
-
export { default as
|
7
|
-
export { default as
|
15
|
+
export { default as ThemeItem } from './src/components/ThemeItem.astro';
|
16
|
+
export { default as TeamMember } from './src/components/TeamMember.astro';
|
17
|
+
export { default as TeamMembers } from './src/components/TeamMembers.astro';
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@coffic/cosy-ui",
|
3
|
-
"version": "0.1.
|
3
|
+
"version": "0.1.27",
|
4
4
|
"description": "A astro component library",
|
5
5
|
"repository": {
|
6
6
|
"url": "https://github.com/CofficLab/cosy-ui"
|
@@ -23,16 +23,19 @@
|
|
23
23
|
"scripts": {
|
24
24
|
"format": "prettier -w .",
|
25
25
|
"lint": "eslint . --ext .ts,.js,.astro src",
|
26
|
-
"dev": "
|
26
|
+
"dev": "astro dev --host 0.0.0.0 --root example",
|
27
|
+
"build": "astro build --root example",
|
28
|
+
"preview": "astro preview --root example"
|
27
29
|
},
|
28
30
|
"devDependencies": {
|
31
|
+
"@tailwindcss/typography": "^0.5.16",
|
29
32
|
"@tailwindcss/vite": "^4.0.14",
|
30
33
|
"@types/chai": "^5.2.0",
|
31
34
|
"@types/eslint": "^9.6.1",
|
32
35
|
"@types/mocha": "^10.0.10",
|
33
36
|
"@types/node": "^22.13.10",
|
34
37
|
"@typescript-eslint/parser": "^8.26.1",
|
35
|
-
"astro": "^5.5.
|
38
|
+
"astro": "^5.5.2",
|
36
39
|
"chai": "^4.5.0",
|
37
40
|
"daisyui": "^5.0.3",
|
38
41
|
"eslint": "^8.57.1",
|
@@ -40,18 +43,18 @@
|
|
40
43
|
"mocha": "^10.8.2",
|
41
44
|
"prettier": "^3.5.3",
|
42
45
|
"prettier-plugin-astro": "^0.14.1",
|
46
|
+
"sharp": "^0.33.2",
|
43
47
|
"tailwindcss": "^4.0.14",
|
44
48
|
"typescript": "^5.8.2"
|
45
49
|
},
|
46
50
|
"peerDependencies": {
|
47
|
-
"@remixicon/vue": "^4.6.0",
|
48
|
-
"@astrojs/vue": "^5.0.7",
|
49
51
|
"astro": "^5.0.0",
|
50
|
-
"daisyui": "^5.0.3"
|
51
|
-
"tailwindcss": "^4.0.13"
|
52
|
+
"daisyui": "^5.0.3"
|
52
53
|
},
|
53
54
|
"dependencies": {
|
54
55
|
"@astrojs/vue": "^5.0.7",
|
56
|
+
"@remixicon/vue": "^4.6.0",
|
57
|
+
"prismjs": "^1.29.0",
|
55
58
|
"vue": "^3.5.13"
|
56
59
|
}
|
57
60
|
}
|
@@ -0,0 +1,78 @@
|
|
1
|
+
---
|
2
|
+
interface Props {
|
3
|
+
type?: 'info' | 'success' | 'warning' | 'error';
|
4
|
+
title?: string;
|
5
|
+
closeable?: boolean;
|
6
|
+
class?: string;
|
7
|
+
}
|
8
|
+
|
9
|
+
const {
|
10
|
+
type = 'info',
|
11
|
+
title,
|
12
|
+
closeable = false,
|
13
|
+
class: className = '',
|
14
|
+
} = Astro.props;
|
15
|
+
|
16
|
+
// 根据类型设置样式
|
17
|
+
const alertClass = {
|
18
|
+
info: 'alert-info',
|
19
|
+
success: 'alert-success',
|
20
|
+
warning: 'alert-warning',
|
21
|
+
error: 'alert-error',
|
22
|
+
}[type];
|
23
|
+
|
24
|
+
// 根据类型设置图标
|
25
|
+
const icons = {
|
26
|
+
info: `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="h-6 w-6 stroke-current">
|
27
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
28
|
+
</svg>`,
|
29
|
+
success: `<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 stroke-current" fill="none" viewBox="0 0 24 24">
|
30
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
31
|
+
</svg>`,
|
32
|
+
warning: `<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 stroke-current" fill="none" viewBox="0 0 24 24">
|
33
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
34
|
+
</svg>`,
|
35
|
+
error: `<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 stroke-current" fill="none" viewBox="0 0 24 24">
|
36
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
37
|
+
</svg>`,
|
38
|
+
};
|
39
|
+
---
|
40
|
+
|
41
|
+
<div class={`alert ${alertClass} ${className}`} role="alert">
|
42
|
+
<div class="flex items-center gap-2">
|
43
|
+
<Fragment set:html={icons[type]} />
|
44
|
+
<div class="flex-1">
|
45
|
+
{title && <h3 class="font-bold">{title}</h3>}
|
46
|
+
<div><slot /></div>
|
47
|
+
</div>
|
48
|
+
{closeable && (
|
49
|
+
<button class="btn btn-ghost btn-sm btn-circle close-alert">
|
50
|
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
51
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
52
|
+
</svg>
|
53
|
+
</button>
|
54
|
+
)}
|
55
|
+
</div>
|
56
|
+
</div>
|
57
|
+
|
58
|
+
{closeable && (
|
59
|
+
<script>
|
60
|
+
function initializeCloseButtons() {
|
61
|
+
const closeButtons = document.querySelectorAll('.close-alert');
|
62
|
+
closeButtons.forEach(button => {
|
63
|
+
button.addEventListener('click', () => {
|
64
|
+
const alert = button.closest('.alert');
|
65
|
+
if (alert) {
|
66
|
+
alert.remove();
|
67
|
+
}
|
68
|
+
});
|
69
|
+
});
|
70
|
+
}
|
71
|
+
|
72
|
+
// 初始化关闭按钮
|
73
|
+
initializeCloseButtons();
|
74
|
+
|
75
|
+
// Astro 页面切换时重新初始化
|
76
|
+
document.addEventListener('astro:page-load', initializeCloseButtons);
|
77
|
+
</script>
|
78
|
+
)}
|
@@ -1,12 +1,49 @@
|
|
1
|
-
<div class="w-full px-8 z-50 mx-auto">
|
1
|
+
<div class="w-full max-w-7xl px-4 sm:px-8 z-50 mx-auto">
|
2
2
|
<div
|
3
|
-
class="mt-0 hero p-4 rounded-2xl bg-base-300/50 dark:bg-base-200/50 backdrop-blur-xl
|
3
|
+
class="mt-0 hero p-4 rounded-2xl bg-base-300/50 dark:bg-base-200/50 backdrop-blur-xl
|
4
|
+
hover:shadow-2xl transform duration-300 ease-in-out
|
5
|
+
hover:scale-[1.02] hover:bg-base-300/70 dark:hover:bg-base-200/70
|
6
|
+
motion-safe:animate-fadeIn"
|
7
|
+
>
|
4
8
|
<div class="text-center hero-content">
|
5
|
-
<div class="max-w-
|
6
|
-
<p
|
9
|
+
<div class="w-full max-w-prose flex flex-row justify-center items-center mx-auto">
|
10
|
+
<p
|
11
|
+
class="text-xl sm:text-2xl md:text-3xl font-bold text-base-content
|
12
|
+
motion-safe:animate-slideUp"
|
13
|
+
>
|
7
14
|
<slot />
|
8
15
|
</p>
|
9
16
|
</div>
|
10
17
|
</div>
|
11
18
|
</div>
|
12
19
|
</div>
|
20
|
+
|
21
|
+
<style>
|
22
|
+
@keyframes fadeIn {
|
23
|
+
from {
|
24
|
+
opacity: 0;
|
25
|
+
}
|
26
|
+
to {
|
27
|
+
opacity: 1;
|
28
|
+
}
|
29
|
+
}
|
30
|
+
|
31
|
+
@keyframes slideUp {
|
32
|
+
from {
|
33
|
+
opacity: 0;
|
34
|
+
transform: translateY(1rem);
|
35
|
+
}
|
36
|
+
to {
|
37
|
+
opacity: 1;
|
38
|
+
transform: translateY(0);
|
39
|
+
}
|
40
|
+
}
|
41
|
+
|
42
|
+
.animate-fadeIn {
|
43
|
+
animation: fadeIn 0.5s ease-out;
|
44
|
+
}
|
45
|
+
|
46
|
+
.animate-slideUp {
|
47
|
+
animation: slideUp 0.5s ease-out;
|
48
|
+
}
|
49
|
+
</style>
|
@@ -0,0 +1,115 @@
|
|
1
|
+
---
|
2
|
+
interface Props {
|
3
|
+
title: string;
|
4
|
+
subtitle?: string;
|
5
|
+
author?: string;
|
6
|
+
date?: Date;
|
7
|
+
cover?: {
|
8
|
+
src: string;
|
9
|
+
alt?: string;
|
10
|
+
};
|
11
|
+
tags?: string[];
|
12
|
+
}
|
13
|
+
|
14
|
+
const {
|
15
|
+
title,
|
16
|
+
subtitle,
|
17
|
+
author,
|
18
|
+
date,
|
19
|
+
cover,
|
20
|
+
tags = [],
|
21
|
+
} = Astro.props;
|
22
|
+
|
23
|
+
// 格式化日期
|
24
|
+
const formatDate = (date?: Date) => {
|
25
|
+
if (!date) return '';
|
26
|
+
return new Intl.DateTimeFormat('zh-CN', {
|
27
|
+
year: 'numeric',
|
28
|
+
month: 'long',
|
29
|
+
day: 'numeric'
|
30
|
+
}).format(date);
|
31
|
+
};
|
32
|
+
---
|
33
|
+
|
34
|
+
<article class="article">
|
35
|
+
{/* 文章头部 */}
|
36
|
+
<header class="mb-8">
|
37
|
+
{cover && (
|
38
|
+
<div class="relative w-full h-[300px] md:h-[400px] mb-6 rounded-lg overflow-hidden">
|
39
|
+
<img
|
40
|
+
src={cover.src}
|
41
|
+
alt={cover.alt || title}
|
42
|
+
class="w-full h-full object-cover"
|
43
|
+
/>
|
44
|
+
</div>
|
45
|
+
)}
|
46
|
+
|
47
|
+
<div class="space-y-4">
|
48
|
+
<h1 class="text-4xl md:text-5xl font-bold tracking-tight">{title}</h1>
|
49
|
+
{subtitle && (
|
50
|
+
<p class="text-xl text-base-content/70">{subtitle}</p>
|
51
|
+
)}
|
52
|
+
|
53
|
+
<div class="flex flex-wrap items-center gap-4 text-sm text-base-content/60">
|
54
|
+
{author && (
|
55
|
+
<div class="flex items-center gap-2">
|
56
|
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
57
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
58
|
+
</svg>
|
59
|
+
<span>{author}</span>
|
60
|
+
</div>
|
61
|
+
)}
|
62
|
+
|
63
|
+
{date && (
|
64
|
+
<div class="flex items-center gap-2">
|
65
|
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
66
|
+
<rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect>
|
67
|
+
<line x1="16" y1="2" x2="16" y2="6"></line>
|
68
|
+
<line x1="8" y1="2" x2="8" y2="6"></line>
|
69
|
+
<line x1="3" y1="10" x2="21" y2="10"></line>
|
70
|
+
</svg>
|
71
|
+
<time datetime={date.toISOString()}>{formatDate(date)}</time>
|
72
|
+
</div>
|
73
|
+
)}
|
74
|
+
</div>
|
75
|
+
|
76
|
+
{tags.length > 0 && (
|
77
|
+
<div class="flex flex-wrap gap-2">
|
78
|
+
{tags.map(tag => (
|
79
|
+
<span class="badge badge-outline">{tag}</span>
|
80
|
+
))}
|
81
|
+
</div>
|
82
|
+
)}
|
83
|
+
</div>
|
84
|
+
</header>
|
85
|
+
|
86
|
+
{/* 文章内容 */}
|
87
|
+
<div class="prose prose-lg max-w-none">
|
88
|
+
<slot />
|
89
|
+
</div>
|
90
|
+
</article>
|
91
|
+
|
92
|
+
<style>
|
93
|
+
/* 自定义文章样式 */
|
94
|
+
.article {
|
95
|
+
container-type: inline-size;
|
96
|
+
}
|
97
|
+
|
98
|
+
/* 优化大屏幕阅读体验 */
|
99
|
+
@container (min-width: 768px) {
|
100
|
+
.article {
|
101
|
+
font-size: 1.125rem;
|
102
|
+
line-height: 1.75;
|
103
|
+
}
|
104
|
+
}
|
105
|
+
|
106
|
+
/* 暗色主题适配 */
|
107
|
+
:global([data-theme="dark"]) .article {
|
108
|
+
--tw-prose-body: hsl(var(--bc));
|
109
|
+
--tw-prose-headings: hsl(var(--bc));
|
110
|
+
--tw-prose-links: hsl(var(--p));
|
111
|
+
--tw-prose-bold: hsl(var(--bc));
|
112
|
+
--tw-prose-quotes: hsl(var(--bc));
|
113
|
+
--tw-prose-code: hsl(var(--bc));
|
114
|
+
}
|
115
|
+
</style>
|
@@ -0,0 +1,49 @@
|
|
1
|
+
---
|
2
|
+
interface Props {
|
3
|
+
variant?: 'primary' | 'secondary' | 'accent' | 'info' | 'success' | 'warning' | 'error' | 'ghost' | 'link' | 'outline' | 'neutral';
|
4
|
+
size?: 'lg' | 'md' | 'sm' | 'xs';
|
5
|
+
shape?: 'circle' | 'square';
|
6
|
+
wide?: boolean;
|
7
|
+
block?: boolean;
|
8
|
+
loading?: boolean;
|
9
|
+
disabled?: boolean;
|
10
|
+
type?: 'button' | 'submit' | 'reset';
|
11
|
+
class?: string;
|
12
|
+
onClick?: string;
|
13
|
+
}
|
14
|
+
|
15
|
+
const {
|
16
|
+
variant,
|
17
|
+
size,
|
18
|
+
shape,
|
19
|
+
wide = false,
|
20
|
+
block = false,
|
21
|
+
loading = false,
|
22
|
+
disabled = false,
|
23
|
+
type = 'button',
|
24
|
+
class: className,
|
25
|
+
onClick,
|
26
|
+
} = Astro.props;
|
27
|
+
|
28
|
+
const classes = [
|
29
|
+
'btn',
|
30
|
+
variant && `btn-${variant}`,
|
31
|
+
size && `btn-${size}`,
|
32
|
+
shape && `btn-${shape}`,
|
33
|
+
wide && 'btn-wide',
|
34
|
+
block && 'btn-block',
|
35
|
+
loading && 'loading',
|
36
|
+
className,
|
37
|
+
].filter(Boolean).join(' ');
|
38
|
+
---
|
39
|
+
|
40
|
+
<button
|
41
|
+
type={type}
|
42
|
+
class={classes}
|
43
|
+
disabled={disabled}
|
44
|
+
onclick={onClick}
|
45
|
+
>
|
46
|
+
<slot name="icon-left" />
|
47
|
+
<slot />
|
48
|
+
<slot name="icon-right" />
|
49
|
+
</button>
|
@@ -0,0 +1,113 @@
|
|
1
|
+
---
|
2
|
+
interface Props {
|
3
|
+
title?: string;
|
4
|
+
subtitle?: string;
|
5
|
+
imageUrl?: string;
|
6
|
+
href?: string;
|
7
|
+
}
|
8
|
+
|
9
|
+
const { title, subtitle, imageUrl, href } = Astro.props;
|
10
|
+
---
|
11
|
+
|
12
|
+
<div class="group perspective-1000">
|
13
|
+
<div class:list={[
|
14
|
+
"card w-full bg-base-100/60 dark:bg-base-300/60 backdrop-blur-lg",
|
15
|
+
"transform transition-all duration-300 ease-out",
|
16
|
+
"hover:shadow-2xl hover:scale-[1.02]",
|
17
|
+
"active:scale-[0.98]",
|
18
|
+
"motion-safe:animate-fadeIn",
|
19
|
+
href && "cursor-pointer"
|
20
|
+
]}>
|
21
|
+
{href ? (
|
22
|
+
<a href={href} class="card-body group-hover:translate-y-[-2px] transition-transform duration-300">
|
23
|
+
{imageUrl && (
|
24
|
+
<figure class="relative overflow-hidden rounded-t-2xl">
|
25
|
+
<img
|
26
|
+
src={imageUrl}
|
27
|
+
alt={title}
|
28
|
+
class="w-full h-48 object-cover transform transition-transform duration-300 group-hover:scale-105"
|
29
|
+
/>
|
30
|
+
</figure>
|
31
|
+
)}
|
32
|
+
{title && (
|
33
|
+
<h2 class="card-title text-xl font-bold tracking-tight text-base-content">
|
34
|
+
{title}
|
35
|
+
</h2>
|
36
|
+
)}
|
37
|
+
{subtitle && (
|
38
|
+
<p class="text-base-content/80">
|
39
|
+
{subtitle}
|
40
|
+
</p>
|
41
|
+
)}
|
42
|
+
<slot />
|
43
|
+
</a>
|
44
|
+
) : (
|
45
|
+
<div class="card-body">
|
46
|
+
{imageUrl && (
|
47
|
+
<figure class="relative overflow-hidden rounded-t-2xl">
|
48
|
+
<img
|
49
|
+
src={imageUrl}
|
50
|
+
alt={title}
|
51
|
+
class="w-full h-48 object-cover transform transition-transform duration-300 group-hover:scale-105"
|
52
|
+
/>
|
53
|
+
</figure>
|
54
|
+
)}
|
55
|
+
{title && (
|
56
|
+
<h2 class="card-title text-xl font-bold tracking-tight text-base-content">
|
57
|
+
{title}
|
58
|
+
</h2>
|
59
|
+
)}
|
60
|
+
{subtitle && (
|
61
|
+
<p class="text-base-content/80">
|
62
|
+
{subtitle}
|
63
|
+
</p>
|
64
|
+
)}
|
65
|
+
<slot />
|
66
|
+
</div>
|
67
|
+
)}
|
68
|
+
</div>
|
69
|
+
</div>
|
70
|
+
|
71
|
+
<style>
|
72
|
+
@keyframes fadeIn {
|
73
|
+
from {
|
74
|
+
opacity: 0;
|
75
|
+
transform: translateY(10px);
|
76
|
+
}
|
77
|
+
to {
|
78
|
+
opacity: 1;
|
79
|
+
transform: translateY(0);
|
80
|
+
}
|
81
|
+
}
|
82
|
+
|
83
|
+
.animate-fadeIn {
|
84
|
+
animation: fadeIn 0.5s cubic-bezier(0.4, 0, 0.2, 1);
|
85
|
+
}
|
86
|
+
|
87
|
+
.perspective-1000 {
|
88
|
+
perspective: 1000px;
|
89
|
+
}
|
90
|
+
|
91
|
+
.card {
|
92
|
+
position: relative;
|
93
|
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
94
|
+
}
|
95
|
+
|
96
|
+
.card::before {
|
97
|
+
content: '';
|
98
|
+
position: absolute;
|
99
|
+
inset: 0;
|
100
|
+
z-index: -1;
|
101
|
+
background: radial-gradient(
|
102
|
+
circle at 50% 0%,
|
103
|
+
rgba(255, 255, 255, 0.1),
|
104
|
+
transparent 70%
|
105
|
+
);
|
106
|
+
opacity: 0;
|
107
|
+
transition: opacity 0.3s ease;
|
108
|
+
}
|
109
|
+
|
110
|
+
.card:hover::before {
|
111
|
+
opacity: 1;
|
112
|
+
}
|
113
|
+
</style>
|
@@ -0,0 +1,186 @@
|
|
1
|
+
---
|
2
|
+
interface Props {
|
3
|
+
code: string;
|
4
|
+
lang?: string;
|
5
|
+
title?: string;
|
6
|
+
showLineNumbers?: boolean;
|
7
|
+
}
|
8
|
+
|
9
|
+
const {
|
10
|
+
code,
|
11
|
+
lang = 'plaintext',
|
12
|
+
title,
|
13
|
+
showLineNumbers = true,
|
14
|
+
} = Astro.props;
|
15
|
+
|
16
|
+
// 移除代码字符串开头和结尾的空行
|
17
|
+
const trimmedCode = code.trim();
|
18
|
+
|
19
|
+
// 生成行号
|
20
|
+
const lines = trimmedCode.split('\n');
|
21
|
+
const lineNumbers = Array.from({ length: lines.length }, (_, i) => i + 1);
|
22
|
+
---
|
23
|
+
|
24
|
+
<div class="code-block not-prose rounded-lg border border-base-300 bg-base-200 my-4">
|
25
|
+
{/* 标题栏 */}
|
26
|
+
{title && (
|
27
|
+
<div class="flex items-center justify-between px-4 py-2 border-b border-base-300">
|
28
|
+
<span class="text-sm font-medium">{title}</span>
|
29
|
+
<button
|
30
|
+
class="copy-button btn btn-ghost btn-xs gap-1"
|
31
|
+
data-code={trimmedCode}
|
32
|
+
title="复制代码"
|
33
|
+
>
|
34
|
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
35
|
+
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
|
36
|
+
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
|
37
|
+
</svg>
|
38
|
+
<span class="copy-text">复制</span>
|
39
|
+
</button>
|
40
|
+
</div>
|
41
|
+
)}
|
42
|
+
|
43
|
+
{/* 代码区域 */}
|
44
|
+
<div class="relative">
|
45
|
+
{/* 行号 */}
|
46
|
+
{showLineNumbers && (
|
47
|
+
<div class="hidden md:block absolute left-0 top-0 bottom-0 px-4 py-4 select-none text-base-content/50 bg-base-300 border-r border-base-300">
|
48
|
+
{lineNumbers.map(num => (
|
49
|
+
<div class="text-right leading-6 text-sm">{num}</div>
|
50
|
+
))}
|
51
|
+
</div>
|
52
|
+
)}
|
53
|
+
|
54
|
+
{/* 代码内容 */}
|
55
|
+
<pre class={`language-${lang} ${showLineNumbers ? 'pl-[3.5rem]' : 'px-4'} py-4 m-0 overflow-x-auto`}><code class={`language-${lang}`} set:html={trimmedCode} /></pre>
|
56
|
+
</div>
|
57
|
+
</div>
|
58
|
+
|
59
|
+
<script>
|
60
|
+
function initializeCopyButtons() {
|
61
|
+
const copyButtons = document.querySelectorAll('.copy-button');
|
62
|
+
|
63
|
+
copyButtons.forEach(button => {
|
64
|
+
button.addEventListener('click', async () => {
|
65
|
+
const code = button.getAttribute('data-code') || '';
|
66
|
+
const copyText = button.querySelector('.copy-text');
|
67
|
+
|
68
|
+
try {
|
69
|
+
await navigator.clipboard.writeText(code);
|
70
|
+
if (copyText) {
|
71
|
+
copyText.textContent = '已复制!';
|
72
|
+
setTimeout(() => {
|
73
|
+
copyText.textContent = '复制';
|
74
|
+
}, 2000);
|
75
|
+
}
|
76
|
+
} catch (err) {
|
77
|
+
console.error('复制失败:', err);
|
78
|
+
if (copyText) {
|
79
|
+
copyText.textContent = '复制失败';
|
80
|
+
setTimeout(() => {
|
81
|
+
copyText.textContent = '复制';
|
82
|
+
}, 2000);
|
83
|
+
}
|
84
|
+
}
|
85
|
+
});
|
86
|
+
});
|
87
|
+
}
|
88
|
+
|
89
|
+
// 在页面加载时初始化
|
90
|
+
initializeCopyButtons();
|
91
|
+
|
92
|
+
// 在 Astro 页面切换时重新初始化
|
93
|
+
document.addEventListener('astro:page-load', initializeCopyButtons);
|
94
|
+
</script>
|
95
|
+
|
96
|
+
<style is:global>
|
97
|
+
/* Prism.js 暗色主题自定义 */
|
98
|
+
.code-block pre {
|
99
|
+
background: transparent !important;
|
100
|
+
}
|
101
|
+
|
102
|
+
.code-block code {
|
103
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
104
|
+
font-size: 0.875rem;
|
105
|
+
line-height: 1.5rem;
|
106
|
+
}
|
107
|
+
|
108
|
+
/* 代码高亮颜色 */
|
109
|
+
.token.comment,
|
110
|
+
.token.prolog,
|
111
|
+
.token.doctype,
|
112
|
+
.token.cdata {
|
113
|
+
color: #636f88;
|
114
|
+
}
|
115
|
+
|
116
|
+
.token.punctuation {
|
117
|
+
color: #81a1c1;
|
118
|
+
}
|
119
|
+
|
120
|
+
.namespace {
|
121
|
+
opacity: 0.7;
|
122
|
+
}
|
123
|
+
|
124
|
+
.token.property,
|
125
|
+
.token.tag,
|
126
|
+
.token.constant,
|
127
|
+
.token.symbol,
|
128
|
+
.token.deleted {
|
129
|
+
color: #81a1c1;
|
130
|
+
}
|
131
|
+
|
132
|
+
.token.number {
|
133
|
+
color: #b48ead;
|
134
|
+
}
|
135
|
+
|
136
|
+
.token.boolean {
|
137
|
+
color: #81a1c1;
|
138
|
+
}
|
139
|
+
|
140
|
+
.token.selector,
|
141
|
+
.token.attr-name,
|
142
|
+
.token.string,
|
143
|
+
.token.char,
|
144
|
+
.token.builtin,
|
145
|
+
.token.inserted {
|
146
|
+
color: #a3be8c;
|
147
|
+
}
|
148
|
+
|
149
|
+
.token.operator,
|
150
|
+
.token.entity,
|
151
|
+
.token.url,
|
152
|
+
.language-css .token.string,
|
153
|
+
.style .token.string,
|
154
|
+
.token.variable {
|
155
|
+
color: #81a1c1;
|
156
|
+
}
|
157
|
+
|
158
|
+
.token.atrule,
|
159
|
+
.token.attr-value,
|
160
|
+
.token.function,
|
161
|
+
.token.class-name {
|
162
|
+
color: #88c0d0;
|
163
|
+
}
|
164
|
+
|
165
|
+
.token.keyword {
|
166
|
+
color: #81a1c1;
|
167
|
+
}
|
168
|
+
|
169
|
+
.token.regex,
|
170
|
+
.token.important {
|
171
|
+
color: #ebcb8b;
|
172
|
+
}
|
173
|
+
|
174
|
+
.token.important,
|
175
|
+
.token.bold {
|
176
|
+
font-weight: bold;
|
177
|
+
}
|
178
|
+
|
179
|
+
.token.italic {
|
180
|
+
font-style: italic;
|
181
|
+
}
|
182
|
+
|
183
|
+
.token.entity {
|
184
|
+
cursor: help;
|
185
|
+
}
|
186
|
+
</style>
|
@@ -27,7 +27,7 @@ const currentYear = new Date().getFullYear();
|
|
27
27
|
const processedSocialLinks = socialLinks.map((link) => processSocialLink(link));
|
28
28
|
---
|
29
29
|
|
30
|
-
<footer class="bg-
|
30
|
+
<footer class="bg-base-200/50 dark:bg-base-300/50 z-50 backdrop-blur-md">
|
31
31
|
<div class="footer sm:footer-horizontal p-10 text-base-content">
|
32
32
|
<aside class="flex flex-col items-center sm:items-start gap-2 place-self-center">
|
33
33
|
<Link href={homeLink}>
|
@@ -51,15 +51,14 @@ const processedSocialLinks = socialLinks.map((link) => processSocialLink(link));
|
|
51
51
|
<div class="flex gap-4 mt-4 sm:mt-0">
|
52
52
|
{
|
53
53
|
processedSocialLinks.map((link) => (
|
54
|
-
<
|
54
|
+
<Link
|
55
55
|
href={link.url}
|
56
|
-
|
57
|
-
rel="noopener noreferrer"
|
56
|
+
external={true}
|
58
57
|
class="btn btn-ghost btn-sm p-1 hover:text-primary"
|
59
|
-
|
58
|
+
>
|
60
59
|
<SocialIcon platform={link.platform} />
|
61
60
|
<span class="sr-only">{link.name}</span>
|
62
|
-
</
|
61
|
+
</Link>
|
63
62
|
))
|
64
63
|
}
|
65
64
|
</div>
|
@@ -1,8 +1,9 @@
|
|
1
1
|
---
|
2
2
|
import { Image } from 'astro:assets';
|
3
|
-
import {
|
3
|
+
import { RiSearch2Line, RiMenuLine, RiSunCloudyLine } from '@remixicon/vue';
|
4
4
|
import ThemeItem from './ThemeItem.astro';
|
5
5
|
import Link from './Link.astro';
|
6
|
+
import Button from './Button.astro';
|
6
7
|
|
7
8
|
interface Props {
|
8
9
|
logo: ImageMetadata;
|
@@ -70,7 +71,8 @@ function generateLanguageUrl(langCode: string): string {
|
|
70
71
|
text-neutral-200 dark:text-neutral-300
|
71
72
|
px-4
|
72
73
|
rounded-lg h-12 mt-1 mx-4 fixed top-0 left-0 right-0 z-50
|
73
|
-
border border-white/5 dark:border-black/5"
|
74
|
+
border border-white/5 dark:border-black/5"
|
75
|
+
>
|
74
76
|
<div class="flex flex-row justify-start items-center">
|
75
77
|
<Link href="/" transition:persist>
|
76
78
|
<div class="h-8 w-8 flex flex-col items-center justify-center">
|
@@ -79,68 +81,48 @@ function generateLanguageUrl(langCode: string): string {
|
|
79
81
|
</Link>
|
80
82
|
|
81
83
|
<!-- 移动端菜单按钮 -->
|
82
|
-
<
|
83
|
-
<RiMenuLine class="w-5 h-5" />
|
84
|
-
</
|
84
|
+
<Button variant="ghost" size="sm" class="lg:hidden ml-2" onClick="mobile_menu.showModal()">
|
85
|
+
<RiMenuLine class="w-5 h-5" slot="icon-left" />
|
86
|
+
</Button>
|
85
87
|
|
86
88
|
<!-- 桌面端导航 -->
|
87
89
|
<div class="hidden lg:flex flex-row gap-4 items-center ml-4" transition:animate="fade">
|
88
|
-
{
|
89
|
-
navItems.map((item) => (
|
90
|
-
<Link
|
91
|
-
class:list={[
|
92
|
-
'btn btn-sm',
|
93
|
-
{
|
94
|
-
'btn-ghost': !item.match(currentPath),
|
95
|
-
'btn-primary': item.match(currentPath),
|
96
|
-
},
|
97
|
-
]}
|
98
|
-
href={item.href}>
|
99
|
-
{item.label}
|
100
|
-
</Link>
|
101
|
-
))
|
102
|
-
}
|
90
|
+
{navItems.map((item) => <Link href={item.href}>{item.label}</Link>)}
|
103
91
|
</div>
|
104
92
|
</div>
|
105
93
|
|
106
94
|
<div class="h-12 flex flex-row justify-end gap-2 items-center">
|
107
95
|
<div class="dropdown dropdown-end">
|
108
|
-
<
|
96
|
+
<Button variant="ghost" size="sm" class="p-1">
|
109
97
|
{currentLanguageName}
|
110
|
-
</
|
98
|
+
</Button>
|
111
99
|
<ul
|
112
100
|
tabindex={0}
|
113
|
-
class="dropdown-content menu bg-slate-900 dark:bg-slate-800 rounded-box z-[1] w-40 p-2 shadow-lg"
|
101
|
+
class="dropdown-content menu bg-slate-900 dark:bg-slate-800 rounded-box z-[1] w-40 p-2 shadow-lg"
|
102
|
+
>
|
114
103
|
{
|
115
104
|
languages.map((lang) => (
|
116
105
|
<li>
|
117
|
-
<Link
|
118
|
-
href={generateLanguageUrl(lang.code)}
|
119
|
-
class:list={[
|
120
|
-
{
|
121
|
-
'bg-primary text-white': currentLocale === lang.code,
|
122
|
-
},
|
123
|
-
]}>
|
124
|
-
{lang.name}
|
125
|
-
</Link>
|
106
|
+
<Link href={generateLanguageUrl(lang.code)}>{lang.name}</Link>
|
126
107
|
</li>
|
127
108
|
))
|
128
109
|
}
|
129
110
|
</ul>
|
130
111
|
</div>
|
131
112
|
<div class="dropdown dropdown-end">
|
132
|
-
<
|
133
|
-
<RiSunCloudyLine class="w-5 h-5" />
|
134
|
-
</
|
113
|
+
<Button variant="ghost" size="sm" class="p-1">
|
114
|
+
<RiSunCloudyLine class="w-5 h-5" slot="icon-left" />
|
115
|
+
</Button>
|
135
116
|
<ul
|
136
117
|
tabindex={0}
|
137
|
-
class="dropdown-content menu bg-neutral-900 dark:bg-neutral-800 rounded-box z-[1] w-56 p-2 shadow-lg"
|
118
|
+
class="dropdown-content menu bg-neutral-900 dark:bg-neutral-800 rounded-box z-[1] w-56 p-2 shadow-lg"
|
119
|
+
>
|
138
120
|
{themes.map((theme) => <ThemeItem theme={theme.id} label={theme.name} />)}
|
139
121
|
</ul>
|
140
122
|
</div>
|
141
|
-
<
|
142
|
-
<RiSearch2Line class="w-5 h-5" />
|
143
|
-
</
|
123
|
+
<Button variant="ghost" size="sm" class="p-1" onClick="my_modal_1.showModal()">
|
124
|
+
<RiSearch2Line class="w-5 h-5" slot="icon-left" />
|
125
|
+
</Button>
|
144
126
|
</div>
|
145
127
|
</header>
|
146
128
|
|
@@ -153,7 +135,8 @@ function generateLanguageUrl(langCode: string): string {
|
|
153
135
|
<div class="flex flex-col gap-2">
|
154
136
|
{
|
155
137
|
navItems.map((item) => (
|
156
|
-
<
|
138
|
+
<Link
|
139
|
+
href={item.href}
|
157
140
|
class:list={[
|
158
141
|
'btn btn-sm w-full text-left justify-start',
|
159
142
|
{
|
@@ -161,22 +144,22 @@ function generateLanguageUrl(langCode: string): string {
|
|
161
144
|
'btn-primary': item.match(currentPath),
|
162
145
|
},
|
163
146
|
]}
|
164
|
-
|
147
|
+
>
|
165
148
|
{item.label}
|
166
|
-
</
|
149
|
+
</Link>
|
167
150
|
))
|
168
151
|
}
|
169
152
|
</div>
|
170
153
|
<div class="modal-action">
|
171
154
|
<form method="dialog">
|
172
|
-
<
|
155
|
+
<Button>
|
173
156
|
{currentLocale === 'zh-cn' ? '关闭' : 'Close'}
|
174
|
-
</
|
157
|
+
</Button>
|
175
158
|
</form>
|
176
159
|
</div>
|
177
160
|
</div>
|
178
161
|
<form method="dialog" class="modal-backdrop bg-black/20 backdrop-blur-sm">
|
179
|
-
<
|
162
|
+
<Button variant="ghost">关闭</Button>
|
180
163
|
</form>
|
181
164
|
</dialog>
|
182
165
|
|
@@ -284,7 +267,7 @@ function generateLanguageUrl(langCode: string): string {
|
|
284
267
|
<script>
|
285
268
|
function initializeThemeSwitch() {
|
286
269
|
const themeItems = document.querySelectorAll('[data-set-theme]');
|
287
|
-
const updateActiveTheme = (currentTheme
|
270
|
+
const updateActiveTheme = (currentTheme) => {
|
288
271
|
themeItems.forEach((item) => {
|
289
272
|
const themeId = item.getAttribute('data-set-theme');
|
290
273
|
const isActive = themeId === currentTheme;
|
@@ -300,8 +283,8 @@ function generateLanguageUrl(langCode: string): string {
|
|
300
283
|
item.addEventListener('click', handleThemeClick);
|
301
284
|
});
|
302
285
|
|
303
|
-
function handleThemeClick(event
|
304
|
-
const item = event.currentTarget
|
286
|
+
function handleThemeClick(event) {
|
287
|
+
const item = event.currentTarget;
|
305
288
|
const theme = item.getAttribute('data-set-theme');
|
306
289
|
document.documentElement.setAttribute('data-theme', theme ?? 'default');
|
307
290
|
localStorage.setItem('theme', theme ?? 'default');
|
@@ -0,0 +1,69 @@
|
|
1
|
+
---
|
2
|
+
import Link from './Link.astro';
|
3
|
+
|
4
|
+
interface Link {
|
5
|
+
text: string;
|
6
|
+
href: string;
|
7
|
+
}
|
8
|
+
|
9
|
+
interface Props {
|
10
|
+
title: string;
|
11
|
+
description: string;
|
12
|
+
image?: {
|
13
|
+
src: string;
|
14
|
+
alt: string;
|
15
|
+
};
|
16
|
+
links: Link[];
|
17
|
+
}
|
18
|
+
|
19
|
+
const { title, description, image, links = [] } = Astro.props;
|
20
|
+
---
|
21
|
+
|
22
|
+
<div class="py-16 px-8 text-center w-full min-h-screen relative overflow-hidden">
|
23
|
+
<div class="relative z-10 rounded-lg w-full h-full">
|
24
|
+
{image && <img src={image.src} alt={image.alt} class="h-1/2 mx-auto mb-8 drop-shadow-xl" />}
|
25
|
+
|
26
|
+
<h2 class="text-4xl mb-4 animate-fade-up">{title}</h2>
|
27
|
+
<p class="text-lg mb-12 text-center max-w-2xl mx-auto">
|
28
|
+
{description}
|
29
|
+
</p>
|
30
|
+
|
31
|
+
<div class="my-12 w-full">
|
32
|
+
<slot name="app" />
|
33
|
+
</div>
|
34
|
+
|
35
|
+
<div class="flex flex-row justify-center gap-8 mx-auto w-full">
|
36
|
+
{
|
37
|
+
links.map((link) => (
|
38
|
+
<Link
|
39
|
+
href={link.href}
|
40
|
+
external
|
41
|
+
variant="cta"
|
42
|
+
animation="hover-lift"
|
43
|
+
size="lg"
|
44
|
+
>
|
45
|
+
{link.text}
|
46
|
+
</Link>
|
47
|
+
))
|
48
|
+
}
|
49
|
+
</div>
|
50
|
+
</div>
|
51
|
+
</div>
|
52
|
+
|
53
|
+
<style>
|
54
|
+
@keyframes fade-up {
|
55
|
+
from {
|
56
|
+
opacity: 0;
|
57
|
+
transform: translateY(-20px);
|
58
|
+
}
|
59
|
+
|
60
|
+
to {
|
61
|
+
opacity: 1;
|
62
|
+
transform: translateY(0);
|
63
|
+
}
|
64
|
+
}
|
65
|
+
|
66
|
+
.animate-fade-up {
|
67
|
+
animation: fade-up 0.8s ease-out forwards;
|
68
|
+
}
|
69
|
+
</style>
|
@@ -1,16 +1,82 @@
|
|
1
1
|
---
|
2
|
-
|
2
|
+
import type { HTMLAttributes } from 'astro/types';
|
3
|
+
|
4
|
+
interface Props extends HTMLAttributes<'a'> {
|
3
5
|
href: string;
|
4
6
|
external?: boolean;
|
5
7
|
class?: string;
|
8
|
+
'class:list'?: any;
|
9
|
+
variant?: 'default' | 'primary' | 'secondary' | 'text' | 'cta' | 'ghost';
|
10
|
+
animation?: 'none' | 'hover-lift' | 'hover-glow' | 'hover-scale';
|
11
|
+
size?: 'sm' | 'md' | 'lg';
|
6
12
|
}
|
7
13
|
|
8
|
-
const {
|
14
|
+
const {
|
15
|
+
href,
|
16
|
+
external = false,
|
17
|
+
variant = 'default',
|
18
|
+
animation = 'none',
|
19
|
+
size = 'md',
|
20
|
+
class: className = '',
|
21
|
+
'class:list': classList,
|
22
|
+
...rest
|
23
|
+
} = Astro.props;
|
24
|
+
|
25
|
+
// 基础样式
|
26
|
+
const baseStyles = "link no-underline hover:no-underline transition-all duration-200 ease-in-out";
|
27
|
+
|
28
|
+
// 尺寸变体
|
29
|
+
const sizeStyles = {
|
30
|
+
sm: "px-4 py-2 text-sm",
|
31
|
+
md: "px-6 py-3 text-base",
|
32
|
+
lg: "px-8 py-4 text-lg"
|
33
|
+
};
|
34
|
+
|
35
|
+
// 主题变体
|
36
|
+
const variantStyles = {
|
37
|
+
default: "link-hover text-current hover:text-primary",
|
38
|
+
primary: "rounded-lg bg-blue-600 text-white font-medium hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 active:bg-blue-800",
|
39
|
+
secondary: "rounded-lg bg-gray-100 text-gray-800 font-medium hover:bg-gray-200 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 active:bg-gray-300",
|
40
|
+
text: "text-current hover:text-primary underline hover:no-underline",
|
41
|
+
cta: "rounded-lg bg-gradient-to-r from-blue-600 to-indigo-600 text-white font-medium hover:from-blue-700 hover:to-indigo-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 active:from-blue-800 active:to-indigo-800",
|
42
|
+
ghost: "text-current hover:text-primary bg-transparent hover:bg-base-200/50 rounded-lg"
|
43
|
+
};
|
44
|
+
|
45
|
+
// 动画效果
|
46
|
+
const animationStyles = {
|
47
|
+
none: "",
|
48
|
+
"hover-lift": "hover:-translate-y-0.5 hover:shadow-lg",
|
49
|
+
"hover-glow": "hover:shadow-[0_0_15px_rgba(59,130,246,0.5)]",
|
50
|
+
"hover-scale": "hover:scale-105"
|
51
|
+
};
|
52
|
+
|
53
|
+
// 合并所有样式
|
54
|
+
const finalClassName = [
|
55
|
+
baseStyles,
|
56
|
+
sizeStyles[size],
|
57
|
+
variantStyles[variant],
|
58
|
+
animationStyles[animation],
|
59
|
+
className
|
60
|
+
].join(' ');
|
9
61
|
---
|
10
62
|
|
11
63
|
<a
|
12
64
|
href={href}
|
13
|
-
class={
|
14
|
-
{...external ? { target: '_blank', rel: 'noopener noreferrer' } : {}}
|
65
|
+
class:list={[finalClassName, classList]}
|
66
|
+
{...external ? { target: '_blank', rel: 'noopener noreferrer' } : {}}
|
67
|
+
{...rest}
|
68
|
+
>
|
15
69
|
<slot />
|
16
70
|
</a>
|
71
|
+
|
72
|
+
<style>
|
73
|
+
/* 添加渐变动画 */
|
74
|
+
.bg-gradient-to-r {
|
75
|
+
background-size: 200% auto;
|
76
|
+
transition: background-position 0.3s ease-in-out;
|
77
|
+
}
|
78
|
+
|
79
|
+
.bg-gradient-to-r:hover {
|
80
|
+
background-position: right center;
|
81
|
+
}
|
82
|
+
</style>
|
@@ -0,0 +1,67 @@
|
|
1
|
+
---
|
2
|
+
interface Props {
|
3
|
+
/**
|
4
|
+
* Modal 的唯一标识符
|
5
|
+
*/
|
6
|
+
id: string;
|
7
|
+
/**
|
8
|
+
* 模态框的标题
|
9
|
+
*/
|
10
|
+
title?: string;
|
11
|
+
/**
|
12
|
+
* 是否显示关闭按钮
|
13
|
+
* @default true
|
14
|
+
*/
|
15
|
+
showCloseButton?: boolean;
|
16
|
+
/**
|
17
|
+
* 自定义类名
|
18
|
+
*/
|
19
|
+
class?: string;
|
20
|
+
}
|
21
|
+
|
22
|
+
const {
|
23
|
+
id,
|
24
|
+
title,
|
25
|
+
showCloseButton = true,
|
26
|
+
class: className = '',
|
27
|
+
} = Astro.props;
|
28
|
+
---
|
29
|
+
|
30
|
+
<dialog id={id} class="modal">
|
31
|
+
<div class:list={["modal-box relative", className]}>
|
32
|
+
{showCloseButton && (
|
33
|
+
<form method="dialog">
|
34
|
+
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2">✕</button>
|
35
|
+
</form>
|
36
|
+
)}
|
37
|
+
|
38
|
+
{title && <h3 class="font-bold text-lg mb-4">{title}</h3>}
|
39
|
+
|
40
|
+
<div class="modal-content">
|
41
|
+
<slot />
|
42
|
+
</div>
|
43
|
+
|
44
|
+
<div class="modal-action">
|
45
|
+
<slot name="actions" />
|
46
|
+
</div>
|
47
|
+
</div>
|
48
|
+
|
49
|
+
<form method="dialog" class="modal-backdrop">
|
50
|
+
<button>关闭</button>
|
51
|
+
</form>
|
52
|
+
</dialog>
|
53
|
+
|
54
|
+
<script define:vars={{ id }}>
|
55
|
+
// 为了方便使用,我们提供一些辅助方法
|
56
|
+
document.addEventListener('DOMContentLoaded', () => {
|
57
|
+
const modal = document.getElementById(id);
|
58
|
+
if (!modal) return;
|
59
|
+
|
60
|
+
// 为所有触发这个模态框的按钮添加点击事件
|
61
|
+
document.querySelectorAll(`[data-modal-target="${id}"]`).forEach(trigger => {
|
62
|
+
trigger.addEventListener('click', () => {
|
63
|
+
modal.showModal();
|
64
|
+
});
|
65
|
+
});
|
66
|
+
});
|
67
|
+
</script>
|
@@ -0,0 +1,68 @@
|
|
1
|
+
---
|
2
|
+
import { Image } from 'astro:assets';
|
3
|
+
import type { ImageMetadata } from 'astro';
|
4
|
+
import Link from './Link.astro';
|
5
|
+
|
6
|
+
interface SocialLink {
|
7
|
+
platform: 'github' | 'twitter' | 'linkedin' | 'website' | 'email';
|
8
|
+
url: string;
|
9
|
+
}
|
10
|
+
|
11
|
+
export interface Props {
|
12
|
+
name: string;
|
13
|
+
role: string;
|
14
|
+
avatar: ImageMetadata | string;
|
15
|
+
bio: string;
|
16
|
+
socialLinks?: SocialLink[];
|
17
|
+
class?: string;
|
18
|
+
}
|
19
|
+
|
20
|
+
const { name, role, avatar, bio, socialLinks, class: className = '' } = Astro.props;
|
21
|
+
|
22
|
+
const socialIcons = {
|
23
|
+
github: `<svg class="w-5 h-5" viewBox="0 0 24 24" fill="currentColor"><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg>`,
|
24
|
+
twitter: `<svg class="w-5 h-5" viewBox="0 0 24 24" fill="currentColor"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/></svg>`,
|
25
|
+
linkedin: `<svg class="w-5 h-5" viewBox="0 0 24 24" fill="currentColor"><path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/></svg>`,
|
26
|
+
website: `<svg class="w-5 h-5" viewBox="0 0 24 24" fill="currentColor"><path d="M12 0C5.373 0 0 5.373 0 12s5.373 12 12 12 12-5.373 12-12S18.627 0 12 0zm1 16.057v-3.05c.959-.69 2-1.367 2-3.007 0-2.233-1.5-3-2-3s-2 .767-2 3c0 1.64 1.041 2.317 2 3.007v3.05c-6.497 1.199-10 3.777-10 6.443h20c0-2.666-3.503-5.244-10-6.443z"/></svg>`,
|
27
|
+
email: `<svg class="w-5 h-5" viewBox="0 0 24 24" fill="currentColor"><path d="M1.5 8.67v8.58a3 3 0 003 3h15a3 3 0 003-3V8.67l-8.928 5.493a3 3 0 01-3.144 0L1.5 8.67z"/><path d="M22.5 6.908V6.75a3 3 0 00-3-3h-15a3 3 0 00-3 3v.158l9.714 5.978a1.5 1.5 0 001.572 0L22.5 6.908z"/></svg>`
|
28
|
+
} as const;
|
29
|
+
---
|
30
|
+
|
31
|
+
<div class:list={["card bg-base-100 shadow-xl hover:shadow-2xl transition-shadow duration-300", className]}>
|
32
|
+
<figure class="px-6 pt-6">
|
33
|
+
{typeof avatar === 'string' ? (
|
34
|
+
<img
|
35
|
+
src={avatar}
|
36
|
+
alt={`${name}'s avatar`}
|
37
|
+
class="rounded-xl w-48 h-48 object-cover"
|
38
|
+
/>
|
39
|
+
) : (
|
40
|
+
<Image
|
41
|
+
src={avatar}
|
42
|
+
alt={`${name}'s avatar`}
|
43
|
+
class="rounded-xl w-48 h-48 object-cover"
|
44
|
+
/>
|
45
|
+
)}
|
46
|
+
</figure>
|
47
|
+
<div class="card-body items-center text-center">
|
48
|
+
<h2 class="card-title text-2xl font-bold">{name}</h2>
|
49
|
+
<p class="text-primary font-medium">{role}</p>
|
50
|
+
<p class="text-base-content/80">{bio}</p>
|
51
|
+
|
52
|
+
{socialLinks && socialLinks.length > 0 && (
|
53
|
+
<div class="flex gap-4 mt-4">
|
54
|
+
{socialLinks.map(link => (
|
55
|
+
<Link
|
56
|
+
href={link.url}
|
57
|
+
external
|
58
|
+
variant="ghost"
|
59
|
+
class="p-2 hover:text-primary"
|
60
|
+
aria-label={`Visit ${name}'s ${link.platform} profile`}
|
61
|
+
>
|
62
|
+
<Fragment set:html={socialIcons[link.platform]} />
|
63
|
+
</Link>
|
64
|
+
))}
|
65
|
+
</div>
|
66
|
+
)}
|
67
|
+
</div>
|
68
|
+
</div>
|
@@ -0,0 +1,43 @@
|
|
1
|
+
---
|
2
|
+
import type { ImageMetadata } from 'astro';
|
3
|
+
import TeamMember from './TeamMember.astro';
|
4
|
+
|
5
|
+
interface SocialLink {
|
6
|
+
platform: 'github' | 'twitter' | 'linkedin' | 'website' | 'email';
|
7
|
+
url: string;
|
8
|
+
}
|
9
|
+
|
10
|
+
interface TeamMemberData {
|
11
|
+
name: string;
|
12
|
+
role: string;
|
13
|
+
avatar: ImageMetadata | string;
|
14
|
+
bio: string;
|
15
|
+
socialLinks?: SocialLink[];
|
16
|
+
}
|
17
|
+
|
18
|
+
interface Props {
|
19
|
+
members: TeamMemberData[];
|
20
|
+
columns?: 2 | 3 | 4;
|
21
|
+
class?: string;
|
22
|
+
}
|
23
|
+
|
24
|
+
const {
|
25
|
+
members,
|
26
|
+
columns = 3,
|
27
|
+
class: className = ''
|
28
|
+
} = Astro.props;
|
29
|
+
|
30
|
+
const gridCols = {
|
31
|
+
2: 'grid-cols-1 md:grid-cols-2',
|
32
|
+
3: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3',
|
33
|
+
4: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-4'
|
34
|
+
} as const;
|
35
|
+
---
|
36
|
+
|
37
|
+
<div class:list={["w-full", className]}>
|
38
|
+
<div class:list={["grid gap-8", gridCols[columns]]}>
|
39
|
+
{members.map(member => (
|
40
|
+
<TeamMember {...member} />
|
41
|
+
))}
|
42
|
+
</div>
|
43
|
+
</div>
|
@@ -1,14 +1,21 @@
|
|
1
1
|
---
|
2
|
+
import Button from './Button.astro';
|
3
|
+
|
2
4
|
interface Props {
|
3
|
-
|
4
|
-
|
5
|
+
theme: string;
|
6
|
+
label: string;
|
5
7
|
}
|
6
8
|
|
7
9
|
const { theme, label } = Astro.props;
|
8
10
|
---
|
9
11
|
|
10
|
-
<
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
12
|
+
<Button
|
13
|
+
variant="ghost"
|
14
|
+
size="sm"
|
15
|
+
block
|
16
|
+
class="text-left justify-start"
|
17
|
+
data-set-theme={theme}
|
18
|
+
>
|
19
|
+
{label}
|
20
|
+
</Button>
|
21
|
+
|