@coffic/cosy-ui 0.5.8 → 0.5.14
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/LICENSE +21 -0
- package/README.md +14 -46
- package/dist/app.css +1 -1
- package/dist/collections/ArticleCollection.ts +19 -0
- package/dist/collections/BlogCollection.ts +28 -0
- package/dist/collections/CourseCollection.ts +11 -0
- package/dist/collections/ExperimentCollection.ts +18 -0
- package/dist/collections/LessonCollection.ts +25 -0
- package/dist/collections/MetaCollection.ts +17 -0
- package/dist/components/data-display/TeamMembers.astro +1 -1
- package/dist/components/display/Card.astro +0 -3
- package/dist/components/display/CodeBlock.astro +1 -2
- package/dist/components/display/Modal.astro +1 -2
- package/dist/components/icons/SearchIcon.astro +30 -34
- package/dist/components/icons/SunCloudyIcon.astro +35 -39
- package/dist/components/layouts/BaseLayout.astro +3 -2
- package/dist/components/navigation/TableOfContents.astro +6 -3
- package/dist/components/typography/Text.astro +1 -1
- package/dist/database/BlogDB.ts +199 -0
- package/dist/database/CourseDB.ts +85 -0
- package/dist/database/ExperimentDB.ts +103 -0
- package/dist/database/LessonDB.ts +103 -0
- package/dist/database/MetaDB.ts +75 -0
- package/dist/entities/BaseDoc.ts +170 -0
- package/dist/entities/BlogDoc.ts +53 -0
- package/dist/entities/CourseDoc.ts +56 -0
- package/dist/entities/ExperimentDoc.ts +117 -0
- package/dist/entities/Heading.ts +13 -0
- package/dist/entities/LessonDoc.ts +114 -0
- package/dist/entities/MetaDoc.ts +82 -0
- package/dist/entities/Tag.ts +42 -0
- package/dist/index.ts +9 -1
- package/dist/types/static-path.ts +8 -0
- package/dist/utils/image.ts +74 -70
- package/dist/utils/lang_package.ts +205 -206
- package/dist/utils/language.ts +0 -7
- package/dist/utils/link.ts +245 -239
- package/dist/utils/logger.ts +101 -126
- package/dist/utils/social.ts +1 -1
- package/package.json +24 -18
- package/dist/models/BaseDoc.ts +0 -164
@@ -0,0 +1,18 @@
|
|
1
|
+
import { defineCollection, z } from 'astro:content';
|
2
|
+
import { glob } from 'astro/loaders';
|
3
|
+
|
4
|
+
export const experimentSchema = z.object({
|
5
|
+
title: z.string(),
|
6
|
+
description: z.string().optional(),
|
7
|
+
pubDate: z.date().optional(),
|
8
|
+
});
|
9
|
+
|
10
|
+
export const makeExperimentCollection = (base: string) => {
|
11
|
+
return defineCollection({
|
12
|
+
loader: glob({
|
13
|
+
pattern: '**/*.{md,mdx}',
|
14
|
+
base,
|
15
|
+
}),
|
16
|
+
schema: experimentSchema,
|
17
|
+
});
|
18
|
+
};
|
@@ -0,0 +1,25 @@
|
|
1
|
+
import { defineCollection, z } from 'astro:content';
|
2
|
+
import { glob } from 'astro/loaders';
|
3
|
+
|
4
|
+
export const lessonSchema = z.object({
|
5
|
+
title: z.string(),
|
6
|
+
description: z.string().optional(),
|
7
|
+
authors: z
|
8
|
+
.array(
|
9
|
+
z.object({
|
10
|
+
name: z.string(),
|
11
|
+
picture: z.string().optional(),
|
12
|
+
})
|
13
|
+
)
|
14
|
+
.optional(),
|
15
|
+
});
|
16
|
+
|
17
|
+
export const makeLessonCollection = (base: string) => {
|
18
|
+
return defineCollection({
|
19
|
+
loader: glob({
|
20
|
+
pattern: '**/*.{md,mdx}',
|
21
|
+
base,
|
22
|
+
}),
|
23
|
+
schema: lessonSchema,
|
24
|
+
});
|
25
|
+
};
|
@@ -0,0 +1,17 @@
|
|
1
|
+
import { defineCollection, z } from 'astro:content';
|
2
|
+
import { glob } from 'astro/loaders';
|
3
|
+
|
4
|
+
export const metaSchema = z.object({
|
5
|
+
title: z.string(),
|
6
|
+
description: z.string().optional(),
|
7
|
+
});
|
8
|
+
|
9
|
+
export const makeMetaCollection = (base: string) => {
|
10
|
+
return defineCollection({
|
11
|
+
loader: glob({
|
12
|
+
pattern: '**/*.{md,mdx}',
|
13
|
+
base,
|
14
|
+
}),
|
15
|
+
schema: metaSchema,
|
16
|
+
});
|
17
|
+
};
|
@@ -93,7 +93,7 @@ const getGridClasses = (cols: 2 | 3 | 4) => {
|
|
93
93
|
<div class:list={['cosy:w-full cosy:mx-auto cosy:px-4', className]}>
|
94
94
|
<div class:list={['cosy:grid cosy:gap-6', ...getGridClasses(columnsValue)]}>
|
95
95
|
{
|
96
|
-
members.map((member: TeamMemberData
|
96
|
+
members.map((member: TeamMemberData) => (
|
97
97
|
<div class="cosy:transition-all cosy:hover:-translate-y-1 cosy:duration-300 cosy:transform">
|
98
98
|
<TeamMember {...member} />
|
99
99
|
</div>
|
@@ -51,12 +51,11 @@ import '../../app.css';
|
|
51
51
|
|
52
52
|
interface Props {
|
53
53
|
code: string;
|
54
|
-
lang?: string;
|
55
54
|
title?: string;
|
56
55
|
showLineNumbers?: boolean;
|
57
56
|
}
|
58
57
|
|
59
|
-
const { code,
|
58
|
+
const { code, title, showLineNumbers = true } = Astro.props;
|
60
59
|
|
61
60
|
// 移除代码字符串开头和结尾的空行
|
62
61
|
const trimmedCode = code.trim();
|
@@ -103,7 +103,7 @@ const { id, title, showCloseButton = true, class: className = '' } = Astro.props
|
|
103
103
|
</form>
|
104
104
|
</dialog>
|
105
105
|
|
106
|
-
<script define:vars={{ id }}>
|
106
|
+
<script is:inline define:vars={{ id }}>
|
107
107
|
// 为了方便使用,我们提供一些辅助方法
|
108
108
|
document.addEventListener('DOMContentLoaded', () => {
|
109
109
|
const modal = document.getElementById(id);
|
@@ -117,4 +117,3 @@ const { id, title, showCloseButton = true, class: className = '' } = Astro.props
|
|
117
117
|
});
|
118
118
|
});
|
119
119
|
</script>
|
120
|
-
|
@@ -1,40 +1,36 @@
|
|
1
1
|
---
|
2
2
|
interface Props {
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
/**
|
18
|
-
* 插槽名称
|
19
|
-
*/
|
20
|
-
slot?: string;
|
3
|
+
/**
|
4
|
+
* 图标的大小
|
5
|
+
* @default "24px"
|
6
|
+
*/
|
7
|
+
size?: string;
|
8
|
+
/**
|
9
|
+
* 图标的颜色
|
10
|
+
* @default "currentColor"
|
11
|
+
*/
|
12
|
+
color?: string;
|
13
|
+
/**
|
14
|
+
* 自定义类名
|
15
|
+
*/
|
16
|
+
class?: string;
|
21
17
|
}
|
22
18
|
|
23
|
-
const { size = '24px', color = 'currentColor', class: className = ''
|
19
|
+
const { size = '24px', color = 'currentColor', class: className = '' } = Astro.props;
|
24
20
|
---
|
25
21
|
|
26
|
-
<svg
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
>
|
38
|
-
|
39
|
-
|
40
|
-
|
22
|
+
<svg
|
23
|
+
xmlns="http://www.w3.org/2000/svg"
|
24
|
+
width={size}
|
25
|
+
height={size}
|
26
|
+
viewBox="0 0 24 24"
|
27
|
+
fill="none"
|
28
|
+
stroke={color}
|
29
|
+
stroke-width="2"
|
30
|
+
stroke-linecap="round"
|
31
|
+
stroke-linejoin="round"
|
32
|
+
class={className}>
|
33
|
+
<circle cx="11" cy="11" r="8"></circle>
|
34
|
+
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
35
|
+
</svg>
|
36
|
+
|
@@ -1,45 +1,41 @@
|
|
1
1
|
---
|
2
2
|
interface Props {
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
/**
|
18
|
-
* 插槽名称
|
19
|
-
*/
|
20
|
-
slot?: string;
|
3
|
+
/**
|
4
|
+
* 图标的大小
|
5
|
+
* @default "24px"
|
6
|
+
*/
|
7
|
+
size?: string;
|
8
|
+
/**
|
9
|
+
* 图标的颜色
|
10
|
+
* @default "currentColor"
|
11
|
+
*/
|
12
|
+
color?: string;
|
13
|
+
/**
|
14
|
+
* 自定义类名
|
15
|
+
*/
|
16
|
+
class?: string;
|
21
17
|
}
|
22
18
|
|
23
|
-
const { size = '24px', color = 'currentColor', class: className = ''
|
19
|
+
const { size = '24px', color = 'currentColor', class: className = '' } = Astro.props;
|
24
20
|
---
|
25
21
|
|
26
|
-
<svg
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
>
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
22
|
+
<svg
|
23
|
+
xmlns="http://www.w3.org/2000/svg"
|
24
|
+
width={size}
|
25
|
+
height={size}
|
26
|
+
viewBox="0 0 24 24"
|
27
|
+
fill="none"
|
28
|
+
stroke={color}
|
29
|
+
stroke-width="2"
|
30
|
+
stroke-linecap="round"
|
31
|
+
stroke-linejoin="round"
|
32
|
+
class={className}>
|
33
|
+
<path d="M12 2v2"></path>
|
34
|
+
<path d="M2 12h2"></path>
|
35
|
+
<path d="M20 12h2"></path>
|
36
|
+
<path d="M4.93 4.93l1.41 1.41"></path>
|
37
|
+
<path d="M17.66 4.93l-1.41 1.41"></path>
|
38
|
+
<path d="M12 19a4 4 0 100-8 4 4 0 000 8z"></path>
|
39
|
+
<path d="M12 15a7 7 0 00-7 7h14a7 7 0 00-7-7z"></path>
|
40
|
+
</svg>
|
41
|
+
|
@@ -50,7 +50,7 @@
|
|
50
50
|
*/
|
51
51
|
|
52
52
|
import '../../app.css';
|
53
|
-
import type
|
53
|
+
import { LinkUtil, type MetaProps } from '../../index';
|
54
54
|
|
55
55
|
export interface Props extends MetaProps {
|
56
56
|
debug?: boolean;
|
@@ -71,6 +71,7 @@ const {
|
|
71
71
|
|
72
72
|
// 处理类名
|
73
73
|
let bodyClasses = debug ? 'cosy:border cosy:border-red-500' : className || '';
|
74
|
+
const faviconPath = LinkUtil.createUrl('/favicon.png');
|
74
75
|
---
|
75
76
|
|
76
77
|
<!doctype html>
|
@@ -82,7 +83,7 @@ let bodyClasses = debug ? 'cosy:border cosy:border-red-500' : className || '';
|
|
82
83
|
{description && <meta name="description" content={description} />}
|
83
84
|
{keywords && <meta name="keywords" content={keywords} />}
|
84
85
|
<meta name="generator" content={Astro.generator} />
|
85
|
-
<link rel="icon" type="image/svg+xml" href=
|
86
|
+
<link rel="icon" type="image/svg+xml" href={faviconPath} />
|
86
87
|
|
87
88
|
<!-- 自定义样式 -->
|
88
89
|
{customStyles && <style set:html={customStyles} />}
|
@@ -125,8 +125,7 @@ const tocId = `toc-${Math.random().toString(36).substring(2, 9)}`;
|
|
125
125
|
<div
|
126
126
|
class={`toc-container toc-scroll-container ${fixed ? 'cosy:w-64' : 'cosy:w-full cosy:max-w-xs'}`}
|
127
127
|
id={`${tocId}-container`}
|
128
|
-
style="display: none;"
|
129
|
-
>
|
128
|
+
style="display: none;">
|
130
129
|
<div class="cosy:bg-base-100 cosy:shadow-inner cosy:card">
|
131
130
|
<div class="cosy:p-4 cosy:card-body">
|
132
131
|
<div class="cosy:mb-2 cosy:font-bold cosy:text-lg cosy:card-title">{titleText}</div>
|
@@ -140,7 +139,11 @@ const tocId = `toc-${Math.random().toString(36).substring(2, 9)}`;
|
|
140
139
|
</div>
|
141
140
|
</aside>
|
142
141
|
|
143
|
-
<script
|
142
|
+
<script
|
143
|
+
is:inline
|
144
|
+
define:vars={{ selector, maxDepth, tocId, containerSelector, minHeadings, langInfo }}
|
145
|
+
>
|
146
|
+
// @ts-nocheck
|
144
147
|
// 在页面加载完成后生成目录
|
145
148
|
function generateTOC() {
|
146
149
|
// 使用指定的容器选择器查找内容容器
|
@@ -0,0 +1,199 @@
|
|
1
|
+
import BlogDoc from '../entities/BlogDoc';
|
2
|
+
import type Tag from '../entities/Tag';
|
3
|
+
import { logger } from '../utils/logger';
|
4
|
+
import { type CollectionEntry } from 'astro:content';
|
5
|
+
import { BaseDB } from './BaseDB';
|
6
|
+
|
7
|
+
export const COLLECTION_NAME = 'blogs' as const;
|
8
|
+
export type BlogEntry = CollectionEntry<typeof COLLECTION_NAME>;
|
9
|
+
|
10
|
+
/**
|
11
|
+
* 博客数据库类,用于管理博客内容集合。
|
12
|
+
*
|
13
|
+
* 目录结构:
|
14
|
+
* ```
|
15
|
+
* blogs/
|
16
|
+
* ├── zh-cn/
|
17
|
+
* │ ├── typescript-intro.md # 文章内容
|
18
|
+
* │ ├── images/ # 文章相关图片
|
19
|
+
* │ └── web-performance.md
|
20
|
+
* └── en/
|
21
|
+
* └── typescript-intro.md
|
22
|
+
* └── images/
|
23
|
+
* └── web-performance.md
|
24
|
+
* ```
|
25
|
+
*/
|
26
|
+
class BlogDB extends BaseDB<typeof COLLECTION_NAME, BlogEntry, BlogDoc> {
|
27
|
+
protected collectionName = COLLECTION_NAME;
|
28
|
+
|
29
|
+
protected createDoc(entry: BlogEntry): BlogDoc {
|
30
|
+
return BlogDoc.fromEntry(entry);
|
31
|
+
}
|
32
|
+
|
33
|
+
/**
|
34
|
+
* 获取所有博客
|
35
|
+
*
|
36
|
+
* @returns {Promise<BlogDoc[]>} 返回所有博客
|
37
|
+
*/
|
38
|
+
async allBlogs(): Promise<BlogDoc[]> {
|
39
|
+
const debug = false;
|
40
|
+
const entries = await this.getDocsByDepth(2);
|
41
|
+
|
42
|
+
if (debug) {
|
43
|
+
logger.array('所有博客文档', entries);
|
44
|
+
}
|
45
|
+
|
46
|
+
return entries;
|
47
|
+
}
|
48
|
+
|
49
|
+
/**
|
50
|
+
* 获取指定语言的所有博客
|
51
|
+
*
|
52
|
+
* @param lang - 语言代码
|
53
|
+
* @returns {Promise<BlogDoc[]>} 返回指定语言的所有博客
|
54
|
+
*/
|
55
|
+
async allBlogsByLang(lang: string): Promise<BlogDoc[]> {
|
56
|
+
const docs = await this.allBlogs();
|
57
|
+
const filteredEntries = docs.filter((doc) => doc.getId().startsWith(lang));
|
58
|
+
|
59
|
+
return filteredEntries;
|
60
|
+
}
|
61
|
+
|
62
|
+
/**
|
63
|
+
* 获取用于 Astro 静态路由生成的路径参数,专门配合 [lang]/blogs/[slug].astro 使用
|
64
|
+
*
|
65
|
+
* @returns 返回路径参数数组
|
66
|
+
*/
|
67
|
+
async getStaticPaths() {
|
68
|
+
const debug = false;
|
69
|
+
const docs = await this.allBlogs();
|
70
|
+
|
71
|
+
const paths = docs.map((doc) => {
|
72
|
+
return {
|
73
|
+
params: {
|
74
|
+
lang: doc.getLang(),
|
75
|
+
slug: doc.getSlug(),
|
76
|
+
},
|
77
|
+
};
|
78
|
+
});
|
79
|
+
|
80
|
+
if (debug) {
|
81
|
+
logger.array('所有博客文档的路径', paths);
|
82
|
+
}
|
83
|
+
|
84
|
+
return paths;
|
85
|
+
}
|
86
|
+
|
87
|
+
/**
|
88
|
+
* 获取所有博客标签
|
89
|
+
* 标签会根据语言和名称去重,使用复合键 "lang:name" 确保唯一性
|
90
|
+
*
|
91
|
+
* @returns 返回所有标签数组
|
92
|
+
* 返回格式:
|
93
|
+
* [
|
94
|
+
* { name: 'typescript', lang: 'zh-cn', count: 5 },
|
95
|
+
* { name: 'javascript', lang: 'en', count: 3 }
|
96
|
+
* ]
|
97
|
+
*/
|
98
|
+
async getTags(): Promise<Tag[]> {
|
99
|
+
const tagsMap = new Map<string, Tag>();
|
100
|
+
const posts = await this.allBlogs();
|
101
|
+
|
102
|
+
posts.forEach((post) => {
|
103
|
+
post.getTags().forEach((tag) => {
|
104
|
+
const key = `${tag.lang}:${tag.name}`;
|
105
|
+
if (!tagsMap.has(key)) {
|
106
|
+
tagsMap.set(key, tag);
|
107
|
+
}
|
108
|
+
});
|
109
|
+
});
|
110
|
+
|
111
|
+
return Array.from(tagsMap.values());
|
112
|
+
}
|
113
|
+
|
114
|
+
/**
|
115
|
+
* 获取指定语言的博客标签
|
116
|
+
*
|
117
|
+
* @param lang - 语言代码(如 'zh-cn', 'en')
|
118
|
+
* @returns 返回指定语言的标签数组
|
119
|
+
*/
|
120
|
+
async getTagsByLang(lang: string): Promise<Tag[]> {
|
121
|
+
const debug = false;
|
122
|
+
const tagsMap = new Map<string, Tag>();
|
123
|
+
const posts = await this.allBlogsByLang(lang);
|
124
|
+
|
125
|
+
if (debug) {
|
126
|
+
logger.array('posts', posts);
|
127
|
+
}
|
128
|
+
|
129
|
+
if (posts.length === 0) {
|
130
|
+
return [];
|
131
|
+
}
|
132
|
+
|
133
|
+
posts.forEach((post) => {
|
134
|
+
post.getTags().forEach((tag) => {
|
135
|
+
const key = `${tag.lang}:${tag.name}`;
|
136
|
+
if (!tagsMap.has(key)) {
|
137
|
+
tagsMap.set(key, tag);
|
138
|
+
}
|
139
|
+
});
|
140
|
+
});
|
141
|
+
|
142
|
+
if (debug) {
|
143
|
+
logger.array('tags', Array.from(tagsMap.values()));
|
144
|
+
}
|
145
|
+
|
146
|
+
return Array.from(tagsMap.values());
|
147
|
+
}
|
148
|
+
|
149
|
+
/**
|
150
|
+
* 获取指定标签和语言的博客文章
|
151
|
+
*
|
152
|
+
* @param tag - 标签名称
|
153
|
+
* @param lang - 语言代码
|
154
|
+
* @returns 返回匹配的博客文档数组
|
155
|
+
* @example
|
156
|
+
* ```typescript
|
157
|
+
* const posts = await blogDB.getBlogsByTag('typescript', 'zh-cn');
|
158
|
+
* // 返回所有包含 'typescript' 标签的中文博客
|
159
|
+
* ```
|
160
|
+
*/
|
161
|
+
async getBlogsByTag(tag: string, lang: string): Promise<BlogDoc[]> {
|
162
|
+
const posts = await this.allBlogsByLang(lang);
|
163
|
+
return posts.filter((post) => post.getTags().some((t) => t.name === tag));
|
164
|
+
}
|
165
|
+
|
166
|
+
/**
|
167
|
+
* 获取标签的静态路径参数,用于生成标签页面的路由,专门配合 [lang]/blogs/tag/[name].astro 使用
|
168
|
+
*
|
169
|
+
* @returns 返回所有标签的路径参数数组
|
170
|
+
* 返回格式:
|
171
|
+
* [
|
172
|
+
* { params: { lang: 'zh-cn', name: 'typescript' } },
|
173
|
+
* { params: { lang: 'en', name: 'javascript' } }
|
174
|
+
* ]
|
175
|
+
*/
|
176
|
+
async getTagsStaticPaths() {
|
177
|
+
const debug = false;
|
178
|
+
const tags = await this.getTags();
|
179
|
+
|
180
|
+
const paths = tags.map((tag) => {
|
181
|
+
return {
|
182
|
+
params: {
|
183
|
+
lang: tag.lang,
|
184
|
+
name: tag.name,
|
185
|
+
},
|
186
|
+
};
|
187
|
+
});
|
188
|
+
|
189
|
+
if (debug) {
|
190
|
+
logger.array('所有的标签路径', paths);
|
191
|
+
}
|
192
|
+
|
193
|
+
return paths;
|
194
|
+
}
|
195
|
+
}
|
196
|
+
|
197
|
+
// 创建并导出单例实例
|
198
|
+
const blogDB = new BlogDB();
|
199
|
+
export default blogDB;
|
@@ -0,0 +1,85 @@
|
|
1
|
+
import CourseDoc from '../entities/CourseDoc';
|
2
|
+
import { getCollection, type CollectionEntry } from 'astro:content';
|
3
|
+
import { BaseDB } from './BaseDB';
|
4
|
+
|
5
|
+
export const COLLECTION_NAME = 'courses' as const;
|
6
|
+
export type CourseEntry = CollectionEntry<typeof COLLECTION_NAME>;
|
7
|
+
|
8
|
+
/**
|
9
|
+
* 课程数据库类,用于管理课程内容集合。
|
10
|
+
*
|
11
|
+
* 目录结构:
|
12
|
+
* ```
|
13
|
+
* courses/
|
14
|
+
* ├── zh-cn/
|
15
|
+
* │ ├── web-development/
|
16
|
+
* │ │ ├── index.md
|
17
|
+
* │ │ ├── chapter1
|
18
|
+
* │ │ │ ├── index.md
|
19
|
+
* │ │ │ ├── content.md
|
20
|
+
* │ │ │ └── ...
|
21
|
+
* │ │ └── chapter2
|
22
|
+
* │ │ ├── index.md
|
23
|
+
* │ │ ├── content.md
|
24
|
+
* │ │ └── ...
|
25
|
+
* │ └── mobile-dev/
|
26
|
+
* │ ├── index.md
|
27
|
+
* │ └── ...
|
28
|
+
* └── en/
|
29
|
+
* └── ...
|
30
|
+
* ```
|
31
|
+
*/
|
32
|
+
class CourseDB extends BaseDB<typeof COLLECTION_NAME, CourseEntry, CourseDoc> {
|
33
|
+
protected collectionName = COLLECTION_NAME;
|
34
|
+
|
35
|
+
protected createDoc(entry: CourseEntry): CourseDoc {
|
36
|
+
return new CourseDoc(entry);
|
37
|
+
}
|
38
|
+
|
39
|
+
/**
|
40
|
+
* 获取指定语言的所有顶级课程
|
41
|
+
*
|
42
|
+
* @param lang - 语言代码
|
43
|
+
* @returns 返回指定语言的顶级课程数组
|
44
|
+
*/
|
45
|
+
async allCoursesByLang(lang: string): Promise<CourseDoc[]> {
|
46
|
+
const entries = await getCollection(COLLECTION_NAME, ({ id }) => {
|
47
|
+
return id.startsWith(lang) && id.split('/').length === 2;
|
48
|
+
});
|
49
|
+
return entries.map((entry) => new CourseDoc(entry));
|
50
|
+
}
|
51
|
+
|
52
|
+
/**
|
53
|
+
* 获取用于 Astro 静态路由生成的路径参数,专门配合 [lang]/courses/[...slug].astro 使用
|
54
|
+
*
|
55
|
+
* @returns 返回路径参数数组
|
56
|
+
*/
|
57
|
+
async getStaticPaths(): Promise<{ params: { lang: string; slug: string } }[]> {
|
58
|
+
const entries = await getCollection(COLLECTION_NAME);
|
59
|
+
return entries.map((entry) => {
|
60
|
+
const doc = new CourseDoc(entry);
|
61
|
+
return {
|
62
|
+
params: {
|
63
|
+
lang: doc.getLang(),
|
64
|
+
slug: doc.getSlug(),
|
65
|
+
},
|
66
|
+
};
|
67
|
+
});
|
68
|
+
}
|
69
|
+
|
70
|
+
/**
|
71
|
+
* 获取精选课程
|
72
|
+
* 返回指定语言的前4个顶级课程文档
|
73
|
+
*
|
74
|
+
* @param lang - 语言代码(如 'zh-cn', 'en')
|
75
|
+
* @returns 返回精选课程文档数组(最多4个)
|
76
|
+
*/
|
77
|
+
async getFamousCourses(lang: string): Promise<CourseDoc[]> {
|
78
|
+
const courses = await this.allCoursesByLang(lang);
|
79
|
+
return courses.slice(0, 4);
|
80
|
+
}
|
81
|
+
}
|
82
|
+
|
83
|
+
// 创建并导出单例实例
|
84
|
+
const courseDB = new CourseDB();
|
85
|
+
export default courseDB;
|