diff --git a/theme/src/client/components/Archives.vue b/theme/src/client/components/Archives.vue new file mode 100644 index 00000000..c7f9f1d9 --- /dev/null +++ b/theme/src/client/components/Archives.vue @@ -0,0 +1,61 @@ + + + + diff --git a/theme/src/client/components/Blog.vue b/theme/src/client/components/Blog.vue index fd6f311e..7432ed94 100644 --- a/theme/src/client/components/Blog.vue +++ b/theme/src/client/components/Blog.vue @@ -1,11 +1,20 @@ diff --git a/theme/src/client/components/BlogAside.vue b/theme/src/client/components/BlogAside.vue new file mode 100644 index 00000000..c977a251 --- /dev/null +++ b/theme/src/client/components/BlogAside.vue @@ -0,0 +1,96 @@ + + + + + diff --git a/theme/src/client/components/BlogAvatar.vue b/theme/src/client/components/BlogAvatar.vue deleted file mode 100644 index 1ba61122..00000000 --- a/theme/src/client/components/BlogAvatar.vue +++ /dev/null @@ -1,52 +0,0 @@ - - - - - diff --git a/theme/src/client/components/Nav/NavScreen.vue b/theme/src/client/components/Nav/NavScreen.vue index 0cb255f5..86cf10dc 100644 --- a/theme/src/client/components/Nav/NavScreen.vue +++ b/theme/src/client/components/Nav/NavScreen.vue @@ -35,7 +35,7 @@ const isLocked = useScrollLock(inBrowser ? document.body : null) diff --git a/theme/src/client/components/ShortPostList.vue b/theme/src/client/components/ShortPostList.vue new file mode 100644 index 00000000..4e5f48f7 --- /dev/null +++ b/theme/src/client/components/ShortPostList.vue @@ -0,0 +1,59 @@ + + + + + diff --git a/theme/src/client/components/Tags.vue b/theme/src/client/components/Tags.vue new file mode 100644 index 00000000..42d9ee69 --- /dev/null +++ b/theme/src/client/components/Tags.vue @@ -0,0 +1,82 @@ + + + + + diff --git a/theme/src/client/components/icons/IconArchive.vue b/theme/src/client/components/icons/IconArchive.vue new file mode 100644 index 00000000..12405033 --- /dev/null +++ b/theme/src/client/components/icons/IconArchive.vue @@ -0,0 +1,3 @@ + diff --git a/theme/src/client/composables/blog.ts b/theme/src/client/composables/blog.ts new file mode 100644 index 00000000..a2221f3b --- /dev/null +++ b/theme/src/client/composables/blog.ts @@ -0,0 +1,189 @@ +import { usePageLang } from '@vuepress/client' +import { useBlogPostData } from '@vuepress-plume/plugin-blog-data/client' +import { computed, ref } from 'vue' +import type { Ref } from 'vue' +import type { PlumeThemeBlogPostItem } from '../../shared/index.js' +import { useLocaleLink, useThemeLocaleData } from '../composables/index.js' +import { toArray } from '../utils/index.js' + +export const usePostListControl = () => { + const locale = usePageLang() + const themeData = useThemeLocaleData() + + const list = useBlogPostData() as unknown as Ref + const blog = computed(() => themeData.value.blog || {}) + const pagination = computed(() => blog.value.pagination || {}) + + const postList = computed(() => { + const stickyList = list.value.filter((item) => + typeof item.sticky === 'boolean' ? item.sticky : item.sticky >= 0 + ) + const otherList = list.value.filter( + (item) => item.sticky === undefined || item.sticky === false + ) + + return [ + ...stickyList.sort((prev, next) => { + if (next.sticky === true && prev.sticky === true) return 0 + return next.sticky > prev.sticky ? 1 : -1 + }), + ...otherList, + ].filter((item) => item.lang === locale.value) + }) + + const page = ref(1) + + const totalPage = computed(() => { + if (blog.value.pagination === false) return 0 + const perPage = blog.value.pagination?.perPage || 20 + return Math.ceil(postList.value.length / perPage) + }) + const isLastPage = computed(() => page.value >= totalPage.value) + const isFirstPage = computed(() => page.value <= 1) + const isPaginationEnabled = computed(() => blog.value.pagination !== false && totalPage.value > 1) + + const finalList = computed(() => { + if (blog.value.pagination === false) return postList.value + + const perPage = blog.value.pagination?.perPage || 20 + if (postList.value.length <= perPage) return postList.value + + return postList.value.slice( + (page.value - 1) * perPage, + page.value * perPage + ) + }) + + const changePage = (offset: number) => { + page.value += offset + window.scrollTo({ top: 0, left: 0, behavior: 'smooth' }) + } + + return { + pagination, + postList: finalList, + page, + totalPage, + isLastPage, + isFirstPage, + isPaginationEnabled, + changePage, + } +} + +const extractLocales: Record = { + 'zh-CN': { tags: '标签', archives: '归档' }, + en: { tags: 'Tags', archives: 'Archives' }, + 'zh-TW': { tags: '標籤', archives: '歸檔' }, +} + +export const useBlogExtract = () => { + const theme = useThemeLocaleData() + const locale = usePageLang() + + const hasBlogExtract = computed(() => theme.value.blog?.archives !== false || theme.value.blog?.tags !== false) + const tagsLink = useLocaleLink('blog/tags/') + const archiveLink = useLocaleLink('blog/archives/') + + const tags = computed(() => ({ + link: tagsLink.value, + text: extractLocales[locale.value]?.tags || extractLocales.en.tags, + })) + + const archives = computed(() => ({ + link: archiveLink.value, + text: extractLocales[locale.value]?.archives || extractLocales.en.archives, + })) + + return { + hasBlogExtract, + tags, + archives, + } +} + +export type ShortPostItem = Pick + +export const useTags = () => { + const locale = usePageLang() + const list = useBlogPostData() as unknown as Ref + const filteredList = computed(() => + list.value.filter((item) => item.lang === locale.value) + ) + + const tags = computed(() => { + const tagMap: Record = {} + filteredList.value.forEach((item) => { + if (item.tags) { + toArray(item.tags).forEach((tag) => { + if (tagMap[tag]) { + tagMap[tag] += 1 + } else { + tagMap[tag] = 1 + } + }) + } + }) + return Object.keys(tagMap).map((tag) => ({ + name: tag, + count: tagMap[tag], + })) + }) + + const postList = ref([]) + const currentTag = ref() + + const handleTagClick = (tag: string) => { + currentTag.value = tag + postList.value = filteredList.value.filter((item) => { + if (item.tags) { + return toArray(item.tags).includes(tag) + } + return false + }).map((item) => ({ + title: item.title, + path: item.path, + createTime: item.createTime.split(' ')[0], + })) + } + + return { + tags, + currentTag, + postList, + handleTagClick + } +} + + +export const useArchives = () => { + const locale = usePageLang() + const list = useBlogPostData() as unknown as Ref + const filteredList = computed(() => + list.value.filter((item) => item.lang === locale.value) + ) + const archives = computed(() => { + const archives: { label: string, list: ShortPostItem[] }[] = [] + + filteredList.value.forEach(item => { + const createTime = item.createTime.split(' ')[0] + const year = createTime.split('/')[0] + let current = archives.find(archive => archive.label === year) + if (!current) { + current = { label: year, list: [] } + archives.push(current) + } + current.list.push({ + title: item.title, + path: item.path, + createTime: createTime.slice(year.length + 1), + }) + }) + + return archives + }) + + return { + archives + } +} diff --git a/theme/src/client/composables/index.ts b/theme/src/client/composables/index.ts index e28eca86..77c180f4 100644 --- a/theme/src/client/composables/index.ts +++ b/theme/src/client/composables/index.ts @@ -6,3 +6,5 @@ export * from './sidebar.js' export * from './aside.js' export * from './page.js' export * from './readingTime.js' +export * from './blog.js' +export * from './locale.js' diff --git a/theme/src/client/composables/locale.ts b/theme/src/client/composables/locale.ts new file mode 100644 index 00000000..9995606c --- /dev/null +++ b/theme/src/client/composables/locale.ts @@ -0,0 +1,23 @@ +import { usePageLang, useSiteData } from '@vuepress/client' +import { computed } from 'vue' +import { normalizeLink } from '../utils' + +export const useLocaleLink = (link: string) => { + const site = useSiteData() + const locale = usePageLang() + + const links = computed(() => { + const locales = site.value.locales + const links: Record = {} + Object.keys(locales).forEach((key) => { + const locale = locales[key] + locale.lang && (links[locale.lang] = key) + }) + return links + }) + + return computed(() => { + const prefix = links.value[locale.value] || '/' + return normalizeLink(prefix + link) + }) +} diff --git a/theme/src/client/composables/readingTime.ts b/theme/src/client/composables/readingTime.ts index 99d70ae0..3a579a1c 100644 --- a/theme/src/client/composables/readingTime.ts +++ b/theme/src/client/composables/readingTime.ts @@ -14,13 +14,13 @@ export const readingTimeLocales = { time: "About $time min", }, - "zh": { + "zh-CN": { word: "约 $word 字", less1Minute: "小于 1 分钟", time: "大约 $time 分钟", }, - "zh-tw": { + "zh-TW": { word: "約 $word 字", less1Minute: "小於 1 分鐘", time: "大约 $time 分鐘", @@ -132,7 +132,7 @@ export const readingTimeLocales = { export const useReadingTime = () => { const page = usePageData() - return computed(() => { + return computed<{ times: string; words: string }>(() => { if (!page.value.readingTime) return { times: '', words: '' } const locale = readingTimeLocales[page.value.lang] ?? readingTimeLocales.en diff --git a/theme/src/client/layouts/Layout.vue b/theme/src/client/layouts/Layout.vue index ef3e2aca..d5876900 100644 --- a/theme/src/client/layouts/Layout.vue +++ b/theme/src/client/layouts/Layout.vue @@ -1,10 +1,11 @@