@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.
- package/app.vue +172 -0
- package/assets/css/base.scss +96 -0
- package/composables/api/article.ts +25 -0
- package/composables/api/category.ts +19 -0
- package/composables/api/comment.ts +25 -0
- package/composables/api/createApi.ts +242 -0
- package/composables/api/feedback.ts +14 -0
- package/composables/api/friend.ts +14 -0
- package/composables/api/moment.ts +10 -0
- package/composables/api/music.ts +39 -0
- package/composables/api/notification.ts +19 -0
- package/composables/api/stats.ts +14 -0
- package/composables/api/subscribe.ts +13 -0
- package/composables/api/sysconfig.ts +9 -0
- package/composables/api/tag.ts +19 -0
- package/composables/api/theme.ts +9 -0
- package/composables/api/upload.ts +13 -0
- package/composables/api/user.ts +77 -0
- package/composables/useArticle.ts +227 -0
- package/composables/useAuth.ts +180 -0
- package/composables/useComment.ts +143 -0
- package/composables/useDarkMode.ts +29 -0
- package/composables/useEmoji.ts +39 -0
- package/composables/useFeedback.ts +38 -0
- package/composables/useFriendList.ts +100 -0
- package/composables/useMermaid.ts +30 -0
- package/composables/useMoment.ts +86 -0
- package/composables/useMusic.ts +37 -0
- package/composables/useSearchState.ts +90 -0
- package/composables/useStores.ts +557 -0
- package/composables/useSubscribe.ts +13 -0
- package/composables/useTheme.ts +61 -0
- package/composables/useUser.ts +202 -0
- package/layouts/default.vue +1 -0
- package/nuxt.config.ts +170 -0
- package/package.json +58 -0
- package/pages/index.vue +1 -0
- package/plugins/console-banner.client.ts +11 -0
- package/plugins/custom-code.ts +121 -0
- package/plugins/syncThemeMeta.ts +28 -0
- package/plugins/tracker.client.ts +107 -0
- package/server/plugins/sitemap.ts +170 -0
- package/server/routes/atom.xml.ts +21 -0
- package/server/routes/manifest.json.ts +45 -0
- package/server/routes/rss.xml.ts +21 -0
- package/types/article.ts +48 -0
- package/types/auth.ts +8 -0
- package/types/category.ts +12 -0
- package/types/comment.ts +72 -0
- package/types/emoji.ts +10 -0
- package/types/feedback.ts +31 -0
- package/types/friend.ts +63 -0
- package/types/markdown.ts +7 -0
- package/types/moment.ts +85 -0
- package/types/notification.ts +56 -0
- package/types/request.ts +30 -0
- package/types/stats.ts +38 -0
- package/types/sysconfig.ts +2 -0
- package/types/tag.ts +11 -0
- package/types/theme.ts +26 -0
- package/types/upload.ts +5 -0
- package/types/user.ts +106 -0
- package/utils/avatar.ts +21 -0
- package/utils/date.ts +136 -0
- package/utils/download.ts +11 -0
- package/utils/emoji.ts +90 -0
- package/utils/format.ts +42 -0
- package/utils/markdown.ts +1458 -0
- package/utils/scroll.ts +31 -0
- package/utils/upload.ts +57 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { ref, watch } from 'vue';
|
|
2
|
+
|
|
3
|
+
export const isDark = ref(false);
|
|
4
|
+
|
|
5
|
+
let initialized = false;
|
|
6
|
+
|
|
7
|
+
export function initDarkMode(): void {
|
|
8
|
+
if (initialized || !import.meta.client) return;
|
|
9
|
+
initialized = true;
|
|
10
|
+
|
|
11
|
+
const currentTheme = document.documentElement.getAttribute('data-theme');
|
|
12
|
+
isDark.value = currentTheme === 'dark';
|
|
13
|
+
|
|
14
|
+
watch(isDark, dark => {
|
|
15
|
+
document.documentElement.setAttribute('data-theme', dark ? 'dark' : 'light');
|
|
16
|
+
localStorage.setItem('theme', dark ? 'dark' : 'light');
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
|
20
|
+
mediaQuery.addEventListener('change', e => {
|
|
21
|
+
if (!localStorage.getItem('theme')) {
|
|
22
|
+
isDark.value = e.matches;
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function toggleDarkMode(): void {
|
|
28
|
+
isDark.value = !isDark.value;
|
|
29
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { EmojiGroup } from '@@/types/emoji';
|
|
2
|
+
import { loadAllEmojiGroups, getAllEmojiGroupsSync } from '@/utils/emoji';
|
|
3
|
+
|
|
4
|
+
export function useEmoji() {
|
|
5
|
+
const { basicConfig } = useSysConfig();
|
|
6
|
+
|
|
7
|
+
const emojiGroups = ref<EmojiGroup[]>([]);
|
|
8
|
+
const loading = ref(true);
|
|
9
|
+
const error = ref('');
|
|
10
|
+
|
|
11
|
+
const load = async () => {
|
|
12
|
+
// 优先读取已有缓存(app.vue 初始化时已加载)
|
|
13
|
+
const cached = getAllEmojiGroupsSync();
|
|
14
|
+
if (cached && cached.length > 0) {
|
|
15
|
+
emojiGroups.value = cached;
|
|
16
|
+
loading.value = false;
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const emojisUrl = basicConfig.value.emojis;
|
|
21
|
+
if (!emojisUrl) {
|
|
22
|
+
error.value = '未配置表情包';
|
|
23
|
+
loading.value = false;
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
const groups = await loadAllEmojiGroups(emojisUrl);
|
|
29
|
+
if (groups.length === 0) throw new Error('加载表情包失败');
|
|
30
|
+
emojiGroups.value = groups;
|
|
31
|
+
} catch (err: unknown) {
|
|
32
|
+
error.value = (err as Error).message || '加载表情包失败';
|
|
33
|
+
} finally {
|
|
34
|
+
loading.value = false;
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
return { emojiGroups, loading, error, load };
|
|
39
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import {
|
|
2
|
+
submitFeedback as submitFeedbackApi,
|
|
3
|
+
getFeedbackByTicketNo as getFeedbackApi,
|
|
4
|
+
} from '@/composables/api/feedback';
|
|
5
|
+
import type { SubmitFeedbackParams, Feedback } from '@@/types/feedback';
|
|
6
|
+
|
|
7
|
+
export function useFeedback() {
|
|
8
|
+
const loading = ref(false);
|
|
9
|
+
const error = ref<Error | null>(null);
|
|
10
|
+
|
|
11
|
+
async function submitFeedback(params: SubmitFeedbackParams): Promise<Feedback> {
|
|
12
|
+
loading.value = true;
|
|
13
|
+
error.value = null;
|
|
14
|
+
try {
|
|
15
|
+
return await submitFeedbackApi(params);
|
|
16
|
+
} catch (e) {
|
|
17
|
+
error.value = e instanceof Error ? e : new Error(String(e));
|
|
18
|
+
throw e;
|
|
19
|
+
} finally {
|
|
20
|
+
loading.value = false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function getFeedbackByTicketNo(ticketNo: string): Promise<Feedback> {
|
|
25
|
+
loading.value = true;
|
|
26
|
+
error.value = null;
|
|
27
|
+
try {
|
|
28
|
+
return await getFeedbackApi(ticketNo);
|
|
29
|
+
} catch (e) {
|
|
30
|
+
error.value = e instanceof Error ? e : new Error(String(e));
|
|
31
|
+
throw e;
|
|
32
|
+
} finally {
|
|
33
|
+
loading.value = false;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return { submitFeedback, getFeedbackByTicketNo, loading, error };
|
|
38
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { getFriends, applyFriend as applyFriendApi } from '@/composables/api/friend';
|
|
2
|
+
import type { Friend, FriendGroup, FriendApplyRequest } from '@@/types/friend';
|
|
3
|
+
|
|
4
|
+
export function useFriendList() {
|
|
5
|
+
const friends = useState<Friend[]>('friends', () => []);
|
|
6
|
+
const allGroups = useState<FriendGroup[]>('friend-groups', () => []);
|
|
7
|
+
const loading = ref(false);
|
|
8
|
+
const error = ref<Error | null>(null);
|
|
9
|
+
|
|
10
|
+
async function fetchFriends() {
|
|
11
|
+
loading.value = true;
|
|
12
|
+
error.value = null;
|
|
13
|
+
try {
|
|
14
|
+
const data = await getFriends();
|
|
15
|
+
const all: Friend[] = [];
|
|
16
|
+
data.groups?.forEach(group => {
|
|
17
|
+
group.friends.forEach(friend => {
|
|
18
|
+
if (!friend.is_invalid) {
|
|
19
|
+
all.push(friend);
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
friends.value = all;
|
|
24
|
+
allGroups.value = data.groups || [];
|
|
25
|
+
} catch (e) {
|
|
26
|
+
console.error('获取友链失败:', e);
|
|
27
|
+
error.value = e instanceof Error ? e : new Error(String(e));
|
|
28
|
+
friends.value = [];
|
|
29
|
+
allGroups.value = [];
|
|
30
|
+
} finally {
|
|
31
|
+
loading.value = false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const { data: initialData } = useAsyncData(
|
|
36
|
+
'footer-friends',
|
|
37
|
+
async () => {
|
|
38
|
+
try {
|
|
39
|
+
const data = await getFriends();
|
|
40
|
+
const all: Friend[] = [];
|
|
41
|
+
data.groups?.forEach(group => {
|
|
42
|
+
group.friends.forEach(friend => {
|
|
43
|
+
if (!friend.is_invalid) {
|
|
44
|
+
all.push(friend);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
return { friends: all, groups: data.groups || [] };
|
|
49
|
+
} catch {
|
|
50
|
+
return { friends: [], groups: [] };
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
{ lazy: true }
|
|
54
|
+
);
|
|
55
|
+
watchEffect(() => {
|
|
56
|
+
if (initialData.value) {
|
|
57
|
+
friends.value = initialData.value.friends;
|
|
58
|
+
allGroups.value = initialData.value.groups;
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
function getRandom(count: number): Friend[] {
|
|
63
|
+
if (friends.value.length <= count) return friends.value;
|
|
64
|
+
return [...friends.value].sort(() => Math.random() - 0.5).slice(0, count);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const friendGroups = computed(() =>
|
|
68
|
+
allGroups.value
|
|
69
|
+
.map(g => ({ ...g, friends: g.friends.filter(f => !f.is_invalid) }))
|
|
70
|
+
.filter(g => g.friends.length > 0)
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
const invalidFriendGroups = computed(() =>
|
|
74
|
+
allGroups.value
|
|
75
|
+
.map(g => ({ ...g, friends: g.friends.filter(f => f.is_invalid) }))
|
|
76
|
+
.filter(g => g.friends.length > 0)
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
const isEmpty = computed(
|
|
80
|
+
() => allGroups.value.length === 0 || allGroups.value.every(g => g.friends.length === 0)
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
async function applyFriend(data: FriendApplyRequest): Promise<void> {
|
|
84
|
+
await applyFriendApi(data);
|
|
85
|
+
await fetchFriends();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
friends,
|
|
90
|
+
allGroups,
|
|
91
|
+
friendGroups,
|
|
92
|
+
invalidFriendGroups,
|
|
93
|
+
isEmpty,
|
|
94
|
+
loading,
|
|
95
|
+
error,
|
|
96
|
+
fetchFriends,
|
|
97
|
+
getRandom,
|
|
98
|
+
applyFriend,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import mermaid from 'mermaid';
|
|
2
|
+
|
|
3
|
+
let initialized = false;
|
|
4
|
+
|
|
5
|
+
export function useMermaid() {
|
|
6
|
+
const init = () => {
|
|
7
|
+
if (initialized) return;
|
|
8
|
+
mermaid.initialize({
|
|
9
|
+
startOnLoad: false,
|
|
10
|
+
theme: 'default',
|
|
11
|
+
securityLevel: 'loose',
|
|
12
|
+
});
|
|
13
|
+
initialized = true;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const renderDiagrams = async () => {
|
|
17
|
+
const elements = document.querySelectorAll('.mermaid:not(:has(svg))');
|
|
18
|
+
|
|
19
|
+
for (const element of elements) {
|
|
20
|
+
try {
|
|
21
|
+
const { svg } = await mermaid.render(`mermaid-${Date.now()}`, element.textContent || '');
|
|
22
|
+
element.innerHTML = svg;
|
|
23
|
+
} catch (error) {
|
|
24
|
+
console.error('Mermaid 渲染失败:', error);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
return { init, renderDiagrams };
|
|
30
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { getMoments } from '@/composables/api/moment';
|
|
2
|
+
import type { Moment } from '@@/types/moment';
|
|
3
|
+
|
|
4
|
+
export function useMoments() {
|
|
5
|
+
const moments = useState<Moment[]>('moments', () => []);
|
|
6
|
+
const total = useState<number>('moments-total', () => 0);
|
|
7
|
+
const currentPage = useState<number>('moments-currentPage', () => 1);
|
|
8
|
+
const pageSize = useState<number>('moments-pageSize', () => 30);
|
|
9
|
+
const loading = ref(false);
|
|
10
|
+
const error = ref<Error | null>(null);
|
|
11
|
+
|
|
12
|
+
const fetchMoments = async (page: number = 1, forceRefresh = false) => {
|
|
13
|
+
if (page) currentPage.value = page;
|
|
14
|
+
if (!forceRefresh && moments.value.length) return;
|
|
15
|
+
|
|
16
|
+
loading.value = true;
|
|
17
|
+
error.value = null;
|
|
18
|
+
try {
|
|
19
|
+
const {
|
|
20
|
+
list,
|
|
21
|
+
total: resTotal,
|
|
22
|
+
page: resPage,
|
|
23
|
+
page_size: resPageSize,
|
|
24
|
+
} = await getMoments({
|
|
25
|
+
page: currentPage.value,
|
|
26
|
+
page_size: pageSize.value,
|
|
27
|
+
});
|
|
28
|
+
moments.value = list || [];
|
|
29
|
+
total.value = resTotal || 0;
|
|
30
|
+
currentPage.value = resPage || 1;
|
|
31
|
+
pageSize.value = resPageSize || 30;
|
|
32
|
+
} catch (e) {
|
|
33
|
+
console.error('获取动态列表失败:', e);
|
|
34
|
+
error.value = e instanceof Error ? e : new Error(String(e));
|
|
35
|
+
moments.value = [];
|
|
36
|
+
total.value = 0;
|
|
37
|
+
} finally {
|
|
38
|
+
loading.value = false;
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
async function useSSR(key: string, pageSizeOverride?: number) {
|
|
43
|
+
const { data } = await useAsyncData(key, () =>
|
|
44
|
+
getMoments({ page: 1, page_size: pageSizeOverride || pageSize.value })
|
|
45
|
+
);
|
|
46
|
+
if (data.value) {
|
|
47
|
+
moments.value = data.value.list || [];
|
|
48
|
+
total.value = data.value.total || 0;
|
|
49
|
+
pageSize.value = data.value.page_size || pageSizeOverride || pageSize.value;
|
|
50
|
+
currentPage.value = 1;
|
|
51
|
+
}
|
|
52
|
+
return { data };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* 供小组件/侧边栏使用,不会污染全局 moments state
|
|
57
|
+
*/
|
|
58
|
+
async function fetchWidgetMoments(pageSize: number): Promise<Moment[]> {
|
|
59
|
+
try {
|
|
60
|
+
const { list } = await getMoments({ page: 1, page_size: pageSize });
|
|
61
|
+
return list || [];
|
|
62
|
+
} catch {
|
|
63
|
+
return [];
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* 小组件 SSR 数据获取(不污染全局 state)
|
|
69
|
+
*/
|
|
70
|
+
async function useWidgetSSR(key: string, pageSize: number) {
|
|
71
|
+
return useAsyncData(key, () => fetchWidgetMoments(pageSize));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
moments,
|
|
76
|
+
total,
|
|
77
|
+
currentPage,
|
|
78
|
+
pageSize,
|
|
79
|
+
loading,
|
|
80
|
+
error,
|
|
81
|
+
fetchMoments,
|
|
82
|
+
useSSR,
|
|
83
|
+
fetchWidgetMoments,
|
|
84
|
+
useWidgetSSR,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { MusicTrack, LyricLine } from '@@/types/moment';
|
|
2
|
+
import { fetchMetingMusic, fetchLyricsText } from '@/composables/api/music';
|
|
3
|
+
|
|
4
|
+
export function useMusic() {
|
|
5
|
+
const fetchTracks = async (
|
|
6
|
+
apiUrl: string,
|
|
7
|
+
params: { server: string; type: string; id: string }
|
|
8
|
+
): Promise<MusicTrack[]> => {
|
|
9
|
+
return fetchMetingMusic(apiUrl, params);
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const fetchLyrics = async (lrcUrl: string): Promise<string> => {
|
|
13
|
+
return fetchLyricsText(lrcUrl);
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const parseLyrics = (lrcText: string): LyricLine[] => {
|
|
17
|
+
if (!lrcText) return [];
|
|
18
|
+
const lines = lrcText.split('\n');
|
|
19
|
+
const lyricLines: LyricLine[] = [];
|
|
20
|
+
|
|
21
|
+
for (const line of lines) {
|
|
22
|
+
const match = line.match(/\[(\d{2}):(\d{2})(?:\.(\d{2,3}))?\](.*)/);
|
|
23
|
+
if (match && match[1] && match[2] && match[4]) {
|
|
24
|
+
const minutes = parseInt(match[1]);
|
|
25
|
+
const seconds = parseInt(match[2]);
|
|
26
|
+
const milliseconds = match[3] ? parseInt(match[3].padEnd(3, '0')) : 0;
|
|
27
|
+
const text = match[4].trim();
|
|
28
|
+
if (text) {
|
|
29
|
+
lyricLines.push({ time: minutes * 60 + seconds + milliseconds / 1000, text });
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return lyricLines.sort((a, b) => a.time - b.time);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
return { fetchTracks, fetchLyrics, parseLyrics };
|
|
37
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { searchArticles } from '@/composables/api/article';
|
|
2
|
+
import type { Article } from '@@/types/article';
|
|
3
|
+
|
|
4
|
+
export function useSearchState() {
|
|
5
|
+
const keyword = ref('');
|
|
6
|
+
const articles = ref<Article[]>([]);
|
|
7
|
+
const total = ref(0);
|
|
8
|
+
const page = ref(1);
|
|
9
|
+
const pageSize = 5;
|
|
10
|
+
const loading = ref(false);
|
|
11
|
+
const error = ref<Error | null>(null);
|
|
12
|
+
|
|
13
|
+
const totalPages = computed(() => Math.ceil(total.value / pageSize));
|
|
14
|
+
const hasSearched = computed(() => keyword.value.trim().length > 0);
|
|
15
|
+
|
|
16
|
+
function highlight(text: string): string {
|
|
17
|
+
const kw = keyword.value.trim();
|
|
18
|
+
if (!kw || !text) return text;
|
|
19
|
+
const escaped = kw.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
20
|
+
const regex = new RegExp(`(${escaped})`, 'gi');
|
|
21
|
+
return text.replace(regex, '<mark>$1</mark>');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function reset() {
|
|
25
|
+
keyword.value = '';
|
|
26
|
+
articles.value = [];
|
|
27
|
+
total.value = 0;
|
|
28
|
+
page.value = 1;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function search(newPage = 1) {
|
|
32
|
+
const searchTerm = keyword.value.trim();
|
|
33
|
+
if (!searchTerm) {
|
|
34
|
+
articles.value = [];
|
|
35
|
+
total.value = 0;
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
loading.value = true;
|
|
40
|
+
error.value = null;
|
|
41
|
+
page.value = newPage;
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
const data = await searchArticles(searchTerm, { page: newPage, page_size: pageSize });
|
|
45
|
+
articles.value = data.list;
|
|
46
|
+
total.value = data.total;
|
|
47
|
+
} catch (e) {
|
|
48
|
+
console.error('搜索失败:', e);
|
|
49
|
+
error.value = e instanceof Error ? e : new Error(String(e));
|
|
50
|
+
articles.value = [];
|
|
51
|
+
total.value = 0;
|
|
52
|
+
} finally {
|
|
53
|
+
loading.value = false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const debouncedSearch = useDebounceFn(() => search(1), 500);
|
|
58
|
+
|
|
59
|
+
function onKeywordChange() {
|
|
60
|
+
page.value = 1;
|
|
61
|
+
debouncedSearch();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function prevPage() {
|
|
65
|
+
if (page.value > 1) search(page.value - 1);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function nextPage() {
|
|
69
|
+
if (page.value < totalPages.value) search(page.value + 1);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
keyword,
|
|
74
|
+
articles,
|
|
75
|
+
total,
|
|
76
|
+
page,
|
|
77
|
+
pageSize,
|
|
78
|
+
loading,
|
|
79
|
+
error,
|
|
80
|
+
totalPages,
|
|
81
|
+
hasSearched,
|
|
82
|
+
highlight,
|
|
83
|
+
reset,
|
|
84
|
+
search,
|
|
85
|
+
debouncedSearch,
|
|
86
|
+
onKeywordChange,
|
|
87
|
+
prevPage,
|
|
88
|
+
nextPage,
|
|
89
|
+
};
|
|
90
|
+
}
|