@flecblog/core-nuxt 0.1.0

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.
Files changed (70) hide show
  1. package/app.vue +172 -0
  2. package/assets/css/base.scss +96 -0
  3. package/composables/api/article.ts +25 -0
  4. package/composables/api/category.ts +19 -0
  5. package/composables/api/comment.ts +25 -0
  6. package/composables/api/createApi.ts +242 -0
  7. package/composables/api/feedback.ts +14 -0
  8. package/composables/api/friend.ts +14 -0
  9. package/composables/api/moment.ts +10 -0
  10. package/composables/api/music.ts +39 -0
  11. package/composables/api/notification.ts +19 -0
  12. package/composables/api/stats.ts +14 -0
  13. package/composables/api/subscribe.ts +13 -0
  14. package/composables/api/sysconfig.ts +9 -0
  15. package/composables/api/tag.ts +19 -0
  16. package/composables/api/theme.ts +9 -0
  17. package/composables/api/upload.ts +13 -0
  18. package/composables/api/user.ts +77 -0
  19. package/composables/useArticle.ts +227 -0
  20. package/composables/useAuth.ts +180 -0
  21. package/composables/useComment.ts +143 -0
  22. package/composables/useDarkMode.ts +29 -0
  23. package/composables/useEmoji.ts +39 -0
  24. package/composables/useFeedback.ts +38 -0
  25. package/composables/useFriendList.ts +100 -0
  26. package/composables/useMermaid.ts +30 -0
  27. package/composables/useMoment.ts +86 -0
  28. package/composables/useMusic.ts +37 -0
  29. package/composables/useSearchState.ts +90 -0
  30. package/composables/useStores.ts +557 -0
  31. package/composables/useSubscribe.ts +13 -0
  32. package/composables/useTheme.ts +61 -0
  33. package/composables/useUser.ts +202 -0
  34. package/layouts/default.vue +1 -0
  35. package/nuxt.config.ts +170 -0
  36. package/package.json +58 -0
  37. package/pages/index.vue +1 -0
  38. package/plugins/console-banner.client.ts +11 -0
  39. package/plugins/custom-code.ts +121 -0
  40. package/plugins/syncThemeMeta.ts +28 -0
  41. package/plugins/tracker.client.ts +107 -0
  42. package/server/plugins/sitemap.ts +170 -0
  43. package/server/routes/atom.xml.ts +21 -0
  44. package/server/routes/manifest.json.ts +45 -0
  45. package/server/routes/rss.xml.ts +21 -0
  46. package/types/article.ts +48 -0
  47. package/types/auth.ts +8 -0
  48. package/types/category.ts +12 -0
  49. package/types/comment.ts +72 -0
  50. package/types/emoji.ts +10 -0
  51. package/types/feedback.ts +31 -0
  52. package/types/friend.ts +63 -0
  53. package/types/markdown.ts +7 -0
  54. package/types/moment.ts +85 -0
  55. package/types/notification.ts +56 -0
  56. package/types/request.ts +30 -0
  57. package/types/stats.ts +38 -0
  58. package/types/sysconfig.ts +2 -0
  59. package/types/tag.ts +11 -0
  60. package/types/theme.ts +26 -0
  61. package/types/upload.ts +5 -0
  62. package/types/user.ts +106 -0
  63. package/utils/avatar.ts +21 -0
  64. package/utils/date.ts +136 -0
  65. package/utils/download.ts +11 -0
  66. package/utils/emoji.ts +90 -0
  67. package/utils/format.ts +42 -0
  68. package/utils/markdown.ts +1458 -0
  69. package/utils/scroll.ts +31 -0
  70. package/utils/upload.ts +57 -0
@@ -0,0 +1,170 @@
1
+ // 动态生成 sitemap 路由
2
+
3
+ interface SitemapArticle {
4
+ url: string;
5
+ title?: string;
6
+ cover?: string;
7
+ update_time?: string;
8
+ publish_time?: string;
9
+ }
10
+
11
+ interface SitemapCategory {
12
+ url: string;
13
+ }
14
+
15
+ interface SitemapTag {
16
+ url: string;
17
+ }
18
+
19
+ interface ArticlesResponse {
20
+ data?: {
21
+ list?: SitemapArticle[];
22
+ };
23
+ }
24
+
25
+ interface CategoriesResponse {
26
+ data?: {
27
+ list?: SitemapCategory[];
28
+ };
29
+ }
30
+
31
+ interface TagsResponse {
32
+ data?: {
33
+ list?: SitemapTag[];
34
+ };
35
+ }
36
+
37
+ interface SitemapImage {
38
+ loc: string;
39
+ caption?: string;
40
+ }
41
+
42
+ interface SitemapUrl {
43
+ loc: string;
44
+ lastmod?: string;
45
+ changefreq: 'weekly';
46
+ priority: number;
47
+ images?: SitemapImage[];
48
+ }
49
+
50
+ /**
51
+ * 将日期字符串转换为 W3C 标准的 ISO 8601 格式
52
+ * @param dateStr - 后端返回的日期字符串 (如 "2026-04-20 12:09:14")
53
+ * @returns ISO 8601 格式的日期字符串 (如 "2026-04-20T12:09:14+00:00")
54
+ */
55
+ function formatW3CDate(dateStr: string | undefined): string | undefined {
56
+ if (!dateStr) return undefined;
57
+
58
+ // 将 "2026-04-20 12:09:14" 转换为 Date 对象
59
+ const date = new Date(dateStr.replace(' ', 'T'));
60
+
61
+ if (isNaN(date.getTime())) return undefined;
62
+
63
+ // 返回 ISO 8601 格式,带时区偏移
64
+ return date.toISOString();
65
+ }
66
+
67
+ /**
68
+ * 获取构建时间作为静态页面的 lastmod
69
+ * @returns ISO 8601 格式的构建时间
70
+ */
71
+ function getBuildTime(): string {
72
+ // 使用当前时间作为构建时间
73
+ return new Date().toISOString();
74
+ }
75
+
76
+ /**
77
+ * 判断是否为静态页面(非动态内容页面)
78
+ * @param url - 页面 URL
79
+ * @returns 是否为静态页面
80
+ */
81
+ function isStaticPage(url: string): boolean {
82
+ // 静态页面列表
83
+ const staticPages = [
84
+ '/',
85
+ '/about',
86
+ '/ask',
87
+ '/cookies',
88
+ '/copyright',
89
+ '/friend',
90
+ '/message',
91
+ '/moment',
92
+ '/privacy',
93
+ '/statistics',
94
+ '/categories',
95
+ '/tags',
96
+ '/archive',
97
+ ];
98
+ return staticPages.includes(url);
99
+ }
100
+
101
+ export default defineNitroPlugin(async nitroApp => {
102
+ nitroApp.hooks.hook('sitemap:resolved', async ctx => {
103
+ try {
104
+ const config = useRuntimeConfig();
105
+ const apiUrl = config.public.apiUrl;
106
+ const buildTime = getBuildTime();
107
+
108
+ // 从后端 API 获取文章列表
109
+ const articlesRes = await $fetch<ArticlesResponse>(`${apiUrl}/articles`).catch(() => null);
110
+
111
+ // 添加文章路由到 sitemap
112
+ const articles = articlesRes?.data?.list || [];
113
+ articles.forEach((article: SitemapArticle) => {
114
+ const sitemapUrl: SitemapUrl = {
115
+ loc: article.url,
116
+ lastmod: formatW3CDate(article.update_time || article.publish_time),
117
+ changefreq: 'weekly',
118
+ priority: 0.8,
119
+ };
120
+
121
+ // 如果有封面图,添加到图片 sitemap
122
+ if (article.cover) {
123
+ sitemapUrl.images = [
124
+ {
125
+ loc: article.cover,
126
+ caption: article.title,
127
+ },
128
+ ];
129
+ }
130
+
131
+ ctx.urls.push(sitemapUrl);
132
+ });
133
+
134
+ // 添加分类路由
135
+ const categoriesRes = await $fetch<CategoriesResponse>(`${apiUrl}/categories`).catch(
136
+ () => null
137
+ );
138
+ const categories = categoriesRes?.data?.list || [];
139
+ categories.forEach((category: SitemapCategory) => {
140
+ ctx.urls.push({
141
+ loc: category.url,
142
+ lastmod: buildTime,
143
+ changefreq: 'weekly',
144
+ priority: 0.6,
145
+ } as SitemapUrl);
146
+ });
147
+
148
+ // 添加标签路由
149
+ const tagsRes = await $fetch<TagsResponse>(`${apiUrl}/tags`).catch(() => null);
150
+ const tags = tagsRes?.data?.list || [];
151
+ tags.forEach((tag: SitemapTag) => {
152
+ ctx.urls.push({
153
+ loc: tag.url,
154
+ lastmod: buildTime,
155
+ changefreq: 'weekly',
156
+ priority: 0.5,
157
+ } as SitemapUrl);
158
+ });
159
+
160
+ // 为已存在的静态页面添加 lastmod(构建时间)
161
+ ctx.urls.forEach((url: SitemapUrl) => {
162
+ if (isStaticPage(url.loc) && !url.lastmod) {
163
+ url.lastmod = buildTime;
164
+ }
165
+ });
166
+ } catch {
167
+ // sitemap 生成失败不影响应用运行,静默忽略
168
+ }
169
+ });
170
+ });
@@ -0,0 +1,21 @@
1
+ export default defineEventHandler(async event => {
2
+ const config = useRuntimeConfig();
3
+ const backendUrl = config.public.apiUrl.replace(/\/+$/, '').replace(/\/api\/v\d+$/, '');
4
+
5
+ try {
6
+ const response = await fetch(`${backendUrl}/atom.xml`);
7
+
8
+ if (!response.ok) {
9
+ throw new Error(`Backend returned ${response.status}`);
10
+ }
11
+
12
+ const atomFeed = await response.text();
13
+ setResponseHeader(event, 'Content-Type', 'application/atom+xml; charset=utf-8');
14
+ return atomFeed;
15
+ } catch {
16
+ throw createError({
17
+ statusCode: 500,
18
+ statusMessage: 'Failed to fetch Atom feed',
19
+ });
20
+ }
21
+ });
@@ -0,0 +1,45 @@
1
+ export default defineEventHandler(async event => {
2
+ try {
3
+ const config = useRuntimeConfig();
4
+ const apiUrl = (config.public.apiUrl as string).replace(/\/+$/, '');
5
+ const res = await $fetch<{ code: number; data: Record<string, string> }>(
6
+ `${apiUrl}/settings/basic`
7
+ );
8
+ const cfg = res.code === 0 ? res.data : {};
9
+
10
+ const manifest = {
11
+ name: cfg.title || 'FlecBlog',
12
+ short_name: cfg.title?.substring(0, 12) || 'Flec',
13
+ description: cfg.description || '个人博客',
14
+ theme_color: '#f7f7f7',
15
+ background_color: '#ffffff',
16
+ display: 'standalone',
17
+ start_url: '/',
18
+ icons: [
19
+ {
20
+ src: cfg.favicon || '/favicon.ico',
21
+ sizes: '192x192',
22
+ type: 'image/png',
23
+ },
24
+ {
25
+ src: cfg.favicon || '/favicon.ico',
26
+ sizes: '512x512',
27
+ type: 'image/png',
28
+ },
29
+ ],
30
+ };
31
+
32
+ setHeader(event, 'Content-Type', 'application/manifest+json');
33
+ return manifest;
34
+ } catch {
35
+ return {
36
+ name: 'FlecBlog',
37
+ short_name: 'Flec',
38
+ theme_color: '#f7f7f7',
39
+ background_color: '#ffffff',
40
+ display: 'standalone',
41
+ start_url: '/',
42
+ icons: [{ src: '/favicon.ico', sizes: '192x192', type: 'image/png' }],
43
+ };
44
+ }
45
+ });
@@ -0,0 +1,21 @@
1
+ export default defineEventHandler(async event => {
2
+ const config = useRuntimeConfig();
3
+ const backendUrl = config.public.apiUrl.replace(/\/+$/, '').replace(/\/api\/v\d+$/, '');
4
+
5
+ try {
6
+ const response = await fetch(`${backendUrl}/rss.xml`);
7
+
8
+ if (!response.ok) {
9
+ throw new Error(`Backend returned ${response.status}`);
10
+ }
11
+
12
+ const rssFeed = await response.text();
13
+ setResponseHeader(event, 'Content-Type', 'application/rss+xml; charset=utf-8');
14
+ return rssFeed;
15
+ } catch {
16
+ throw createError({
17
+ statusCode: 500,
18
+ statusMessage: 'Failed to fetch RSS feed',
19
+ });
20
+ }
21
+ });
@@ -0,0 +1,48 @@
1
+ export interface ArticleNav {
2
+ title: string;
3
+ url: string;
4
+ }
5
+
6
+ export interface Article {
7
+ id: number;
8
+ title: string;
9
+ slug?: string;
10
+ url: string;
11
+ content?: string;
12
+ summary: string;
13
+ ai_summary?: string;
14
+ excerpt?: string;
15
+ cover?: string;
16
+ is_top: boolean;
17
+ is_essence: boolean;
18
+ is_outdated?: boolean;
19
+ view_count?: number;
20
+ comment_count: number;
21
+ publish_time: string;
22
+ update_time: string;
23
+ location?: string;
24
+ category: {
25
+ id: number;
26
+ name: string;
27
+ url: string;
28
+ };
29
+ tags: Array<{
30
+ id: number;
31
+ name: string;
32
+ url: string;
33
+ }>;
34
+ prev?: ArticleNav;
35
+ next?: ArticleNav;
36
+ }
37
+
38
+ /**
39
+ * 文章查询参数(所有参数可选,支持灵活组合)
40
+ */
41
+ export interface ArticleQuery {
42
+ page?: number; // 页码;不传返回全部数据
43
+ page_size?: number; // 每页数量;不传返回全部数据
44
+ year?: string; // 按年份筛选
45
+ month?: string; // 按月份筛选
46
+ category?: string; // 按分类筛选(slug)
47
+ tag?: string; // 按标签筛选(slug)
48
+ }
package/types/auth.ts ADDED
@@ -0,0 +1,8 @@
1
+ /** 微信扫码登录状态 */
2
+ export interface WechatQRState {
3
+ visible: boolean;
4
+ imageUrl: string;
5
+ scene: string;
6
+ status: 'idle' | 'loading' | 'scanning' | 'confirmed' | 'expired' | 'error';
7
+ error: string;
8
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * 分类类型定义
3
+ */
4
+ export interface Category {
5
+ id: number;
6
+ name: string;
7
+ slug: string;
8
+ url: string;
9
+ description: string;
10
+ count: number;
11
+ sort: number;
12
+ }
@@ -0,0 +1,72 @@
1
+ import type { UserRole } from './user';
2
+
3
+ /**
4
+ * 评论目标类型
5
+ */
6
+ export type CommentTargetType = 'article' | 'page' | 'moment';
7
+
8
+ /**
9
+ * 评论数据结构
10
+ */
11
+ export interface Comment {
12
+ id: number;
13
+ content: string;
14
+ is_deleted: boolean;
15
+ is_pinned: boolean; // 是否置顶
16
+ parent_id: number | null;
17
+ created_at: string;
18
+ location?: string; // 地理位置
19
+ browser?: string; // 浏览器内核
20
+ os?: string; // 操作系统
21
+ user: {
22
+ role: UserRole;
23
+ badge?: string;
24
+ id: number;
25
+ email_hash: string;
26
+ nickname: string;
27
+ avatar: string;
28
+ website?: string;
29
+ };
30
+ reply_user?: {
31
+ role: UserRole;
32
+ badge?: string;
33
+ id: number;
34
+ email_hash: string;
35
+ nickname: string;
36
+ avatar: string;
37
+ website?: string;
38
+ };
39
+ replies: Comment[];
40
+ }
41
+
42
+ /**
43
+ * 创建评论参数
44
+ */
45
+ export interface CreateCommentParams {
46
+ target_type: CommentTargetType;
47
+ target_key: string | number;
48
+ content: string;
49
+ parent_id?: number;
50
+
51
+ // 游客信息(可选,未登录时使用)
52
+ nickname?: string;
53
+ email?: string;
54
+ website?: string;
55
+ }
56
+
57
+ /**
58
+ * 扁平化评论(用于虚拟列表等场景)
59
+ */
60
+ export interface FlatComment {
61
+ comment: Comment;
62
+ depth: number;
63
+ }
64
+
65
+ /**
66
+ * 游客信息(本地存储)
67
+ */
68
+ export interface GuestInfo {
69
+ nickname: string;
70
+ email: string;
71
+ website?: string;
72
+ }
package/types/emoji.ts ADDED
@@ -0,0 +1,10 @@
1
+ export interface EmojiItem {
2
+ key: string;
3
+ val: string;
4
+ }
5
+
6
+ export interface EmojiGroup {
7
+ name: string;
8
+ type: 'emoji' | 'image' | 'emoticon';
9
+ items: EmojiItem[];
10
+ }
@@ -0,0 +1,31 @@
1
+ // 反馈类型
2
+ export type ReportType = 'copyright' | 'inappropriate' | 'summary' | 'suggestion';
3
+
4
+ // 反馈状态
5
+ export type FeedbackStatus = 'pending' | 'resolved' | 'closed';
6
+
7
+ // 反馈对象
8
+ export interface Feedback {
9
+ id: number;
10
+ ticket_no: string;
11
+ report_url: string;
12
+ report_type: ReportType;
13
+ form_content: Record<string, unknown>;
14
+ email: string;
15
+ status: FeedbackStatus;
16
+ admin_reply: string;
17
+ reply_time?: string;
18
+ user_agent: string;
19
+ ip: string;
20
+ feedback_time: string;
21
+ }
22
+
23
+ // 提交反馈参数
24
+ export interface SubmitFeedbackParams {
25
+ reportUrl: string;
26
+ reportType: ReportType;
27
+ email?: string;
28
+ description: string;
29
+ reason?: string;
30
+ attachmentFiles?: string[];
31
+ }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * 友链类型
3
+ */
4
+ export interface FriendType {
5
+ id: number;
6
+ name: string;
7
+ is_visible: boolean;
8
+ sort: number;
9
+ }
10
+
11
+ /**
12
+ * 友链数据结构
13
+ */
14
+ export interface Friend {
15
+ id: number;
16
+ name: string;
17
+ url: string;
18
+ description?: string;
19
+ avatar?: string;
20
+ screenshot?: string;
21
+ sort: number;
22
+ is_invalid: boolean;
23
+ type_id?: number;
24
+ type?: FriendType;
25
+ }
26
+
27
+ /**
28
+ * 友链分组(用于展示)
29
+ */
30
+ export interface FriendGroup {
31
+ type_id: number | null;
32
+ type_name: string;
33
+ type_sort: number;
34
+ friends: Friend[];
35
+ }
36
+
37
+ /**
38
+ * 友链分组响应
39
+ */
40
+ export interface FriendGroupedResponse {
41
+ groups: FriendGroup[];
42
+ total_groups: number;
43
+ total_friends: number;
44
+ }
45
+
46
+ /**
47
+ * 友链查询参数
48
+ */
49
+ export interface FriendQueryParams {
50
+ page?: number;
51
+ page_size?: number;
52
+ }
53
+
54
+ /**
55
+ * 友链申请请求
56
+ */
57
+ export interface FriendApplyRequest {
58
+ name: string; // 网站名称
59
+ url: string; // 网站链接
60
+ description: string; // 网站描述
61
+ avatar: string; // 网站头像/logo
62
+ screenshot?: string; // 网站截图(可选)
63
+ }
@@ -0,0 +1,7 @@
1
+ /** 目录项 */
2
+ export interface TocItem {
3
+ id: string;
4
+ level: number;
5
+ text: string;
6
+ children?: TocItem[];
7
+ }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * 视频内容
3
+ */
4
+ export interface MomentVideo {
5
+ url: string;
6
+ platform?: 'youtube' | 'bilibili' | 'local';
7
+ video_id?: string;
8
+ }
9
+
10
+ /**
11
+ * 音频内容
12
+ */
13
+ export interface MomentAudio {
14
+ url: string;
15
+ }
16
+
17
+ /**
18
+ * 音乐内容
19
+ */
20
+ export interface MomentMusic {
21
+ server: 'netease' | 'tencent';
22
+ type: 'song' | 'playlist' | 'album' | 'artist';
23
+ id: string;
24
+ }
25
+
26
+ /**
27
+ * 链接内容
28
+ */
29
+ export interface MomentLink {
30
+ url: string;
31
+ title: string;
32
+ favicon?: string;
33
+ }
34
+
35
+ /**
36
+ * 动态内容
37
+ */
38
+ export interface MomentContent {
39
+ text?: string;
40
+ images?: string[];
41
+ location?: string;
42
+ tags?: string;
43
+ video?: MomentVideo;
44
+ audio?: MomentAudio;
45
+ music?: MomentMusic;
46
+ link?: MomentLink;
47
+ }
48
+
49
+ /**
50
+ * 动态
51
+ */
52
+ export interface Moment {
53
+ id: number;
54
+ content: MomentContent;
55
+ publish_time: string;
56
+ }
57
+
58
+ /**
59
+ * 音乐播放器曲目
60
+ */
61
+ export interface MusicTrack {
62
+ name: string;
63
+ artist: string;
64
+ url: string;
65
+ cover: string;
66
+ lrc?: string;
67
+ }
68
+
69
+ /**
70
+ * 歌词行
71
+ */
72
+ export interface LyricLine {
73
+ time: number;
74
+ text: string;
75
+ }
76
+
77
+ /**
78
+ * 动态列表响应
79
+ */
80
+ export interface MomentListResponse {
81
+ list: Moment[];
82
+ total: number;
83
+ page: number;
84
+ page_size: number;
85
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * 通知类型枚举(前台用户)
3
+ */
4
+ export type NotificationType = 'comment_reply'; // 评论回复
5
+
6
+ /**
7
+ * 评论通知数据类型
8
+ */
9
+ export interface CommentNotificationData {
10
+ article_title: string;
11
+ article_slug: string;
12
+ comment_id: number;
13
+ comment_content: string;
14
+ parent_comment_id?: number;
15
+ }
16
+
17
+ /**
18
+ * 通知对象(前台用户)
19
+ */
20
+ export interface Notification {
21
+ id: number;
22
+ type: NotificationType;
23
+ type_text: string; // 类型中文文本(后端提供,前端直接显示)
24
+
25
+ // 前端显示字段(直接使用)
26
+ title: string;
27
+ content: string;
28
+ link: string;
29
+
30
+ // 详细数据(备用)
31
+ data: CommentNotificationData | Record<string, unknown>;
32
+ target_id?: number;
33
+ is_read: boolean;
34
+ read_at: string | null;
35
+ created_at: string;
36
+ sender: string | null;
37
+ }
38
+
39
+ /**
40
+ * 通知列表响应
41
+ */
42
+ export interface NotificationListResponse {
43
+ list: Notification[];
44
+ total: number;
45
+ page: number;
46
+ page_size: number;
47
+ unread_count: number; // 未读数量
48
+ }
49
+
50
+ /**
51
+ * 获取通知列表参数
52
+ */
53
+ export interface GetNotificationsParams {
54
+ page: number;
55
+ page_size: number;
56
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * API响应数据结构
3
+ * @template T 响应数据类型
4
+ */
5
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- 泛型默认值,允许调用方不指定类型
6
+ export interface ApiResponse<T = any> {
7
+ code: number;
8
+ message: string;
9
+ data: T;
10
+ }
11
+
12
+ /**
13
+ * 分页查询参数
14
+ */
15
+ export interface PaginationQuery {
16
+ page?: number;
17
+ page_size?: number;
18
+ }
19
+
20
+ /**
21
+ * 分页响应数据结构
22
+ * @template T 列表项数据类型
23
+ */
24
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- 泛型默认值,允许调用方不指定类型
25
+ export interface PaginationData<T = any> {
26
+ list: T[];
27
+ total: number;
28
+ page: number;
29
+ page_size: number;
30
+ }