diff --git a/docs/.vuepress/config.ts b/docs/.vuepress/config.ts index d1e140ff..2e003178 100644 --- a/docs/.vuepress/config.ts +++ b/docs/.vuepress/config.ts @@ -7,7 +7,7 @@ import { enNotes, zhNotes } from './notes.js' export default defineUserConfig({ base: '/', - lang: 'zh', + lang: 'zh-CN', title: 'Plume Theme', description: '', source: path.resolve(__dirname, '../'), @@ -16,7 +16,7 @@ export default defineUserConfig({ '/': { title: 'Plume主题', description: '', - lang: 'zh', + lang: 'zh-CN', }, '/en/': { title: 'Plume Theme', @@ -77,6 +77,7 @@ export default defineUserConfig({ }, ], }, + { text: '友情链接', link: '/friends/', icon: 'emojione-monotone:roller-coaster' }, ], footer: { copyright: 'Copyright © 2022-present pengzhanbo', diff --git a/docs/2.preview/主题效果预览.md b/docs/2.preview/主题效果预览.md index 192edd81..3755c67c 100644 --- a/docs/2.preview/主题效果预览.md +++ b/docs/2.preview/主题效果预览.md @@ -140,10 +140,14 @@ function foo() { } ``` -::: info 注释 +::: note 注释 注释内容 ::: +::: info 信息 +信息内容 +::: + ::: tip 提示 提示内容 ::: diff --git a/docs/friends.md b/docs/friends.md new file mode 100644 index 00000000..f2aa75f2 --- /dev/null +++ b/docs/friends.md @@ -0,0 +1,42 @@ +--- +friends: true +title: 友情链接 +description: 这里是友情链接的描述文字 +permalink: /friends/ +list: + - + name: pengzhanbo + link: https://github.com/pengzhanbo + avatar: https://github.com/pengzhanbo.png + desc: 即使慢,驰而不息,纵会落后,纵会失败,但必须能够到达他所向的目标。 + - + name: pengzhanbo + link: https://github.com/pengzhanbo + avatar: https://github.com/pengzhanbo.png + desc: 即使慢,驰而不息,纵会落后,纵会失败,但必须能够到达他所向的目标。 + - + name: pengzhanbo + link: https://github.com/pengzhanbo + avatar: https://github.com/pengzhanbo.png + desc: 即使慢,驰而不息,纵会落后,纵会失败,但必须能够到达他所向的目标。 + - + name: pengzhanbo + link: https://github.com/pengzhanbo + avatar: https://github.com/pengzhanbo.png + desc: 即使慢,驰而不息,纵会落后,纵会失败,但必须能够到达他所向的目标。 + - + name: pengzhanbo + link: https://github.com/pengzhanbo + avatar: https://github.com/pengzhanbo.png + desc: 即使慢,驰而不息,纵会落后,纵会失败,但必须能够到达他所向的目标。 + - + name: pengzhanbo + link: https://github.com/pengzhanbo + avatar: https://github.com/pengzhanbo.png + desc: 即使慢,驰而不息,纵会落后,纵会失败,但必须能够到达他所向的目标。 + - + name: pengzhanbo + link: https://github.com/pengzhanbo + avatar: https://github.com/pengzhanbo.png + desc: 即使慢,驰而不息,纵会落后,纵会失败,但必须能够到达他所向的目标。 +--- 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 @@ + + + + + + {{ archivesLink.text }} + + + + + {{ archive.label }} + + + + + + + + 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 @@ + + + + + + + + + + {{ avatar.name }} + {{ avatar.description }} + + + + + + {{ tags.text }} + + + + {{ archives.text }} + + + + + + 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 @@ - - - - - - - - - - {{ avatar.name }} - {{ avatar.description }} - - - - - - diff --git a/theme/src/client/components/Friends.vue b/theme/src/client/components/Friends.vue new file mode 100644 index 00000000..987db8d4 --- /dev/null +++ b/theme/src/client/components/Friends.vue @@ -0,0 +1,123 @@ + + + + + {{ matter.title || 'My Friends' }} + {{ matter.description }} + + + + + + + + {{ editNavLink.text }} + + + + + diff --git a/theme/src/client/components/FriendsItem.vue b/theme/src/client/components/FriendsItem.vue new file mode 100644 index 00000000..c744d62a --- /dev/null +++ b/theme/src/client/components/FriendsItem.vue @@ -0,0 +1,78 @@ + + + + + + + + + {{ friend.name }} + {{ friend.desc }} + + + + + diff --git a/theme/src/client/components/LocalNav.vue b/theme/src/client/components/LocalNav.vue index 70cb4b39..dd149b1c 100644 --- a/theme/src/client/components/LocalNav.vue +++ b/theme/src/client/components/LocalNav.vue @@ -44,12 +44,18 @@ const classes = computed(() => { } }) +const showLocalNav = computed(() => { + return (hasSidebar.value || page.value.isBlogPost) && (!empty.value || y.value >= navHeight.value) +}) + - + { transition: color 0.5s; } +.menu.hidden { + visibility: hidden; +} + .menu:hover { color: var(--vp-c-text-1); transition: color 0.25s; 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/PageMeta.vue b/theme/src/client/components/PageMeta.vue index 4a711443..66e73638 100644 --- a/theme/src/client/components/PageMeta.vue +++ b/theme/src/client/components/PageMeta.vue @@ -32,7 +32,7 @@ const tags = computed(() => { return [] }) -const hasMeta = computed(() => tags.value.length || createTime.value) +const hasMeta = computed(() => readingTime.value.times || tags.value.length || createTime.value) -import { usePageLang } from '@vuepress/client' -import { useBlogPostData } from '@vuepress-plume/plugin-blog-data/client' -import { computed } from 'vue' -import type { Ref } from 'vue' -import type { PlumeThemeBlogPostItem } from '../../shared/index.js' +import { usePostListControl } from '../composables/index.js' import PostItem from './PostItem.vue' -const locale = usePageLang() - -const list = useBlogPostData() as unknown as Ref - -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 { + pagination, + postList, + page, + totalPage, + isLastPage, + isFirstPage, + isPaginationEnabled, + changePage, +} = usePostListControl() + + + {{ pagination?.prevPageText || 'Prev' }} + + {{ page }} / {{ totalPage }} + + {{ pagination?.nextPageText || 'Next' }} + + @@ -38,4 +33,34 @@ const postList = computed(() => { padding-top: 2rem; flex: 1; } + +.pagination { + display: flex; + align-items: center; + justify-content: space-between; + padding: 2rem 1.25rem 4rem; +} + +.btn { + color: var(--vp-c-brand-1); + font-weight: 500; + border: 1px solid var(--vp-c-brand-1); + padding: 0 4px; + border-radius: 4px; + transition: all var(--t-color); +} + +.btn:hover { + color: var(--vp-c-brand-2); + border-color: var(--vp-c-brand-2); +} +.btn[disabled] { + color: var(--vp-c-gray-1); + border-color: var(--vp-c-divider); +} + +.page-info { + color: var(--vp-c-brand-2); + font-weight: 500; +} 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 @@ + + + + + + {{ post.title }} + {{ post.createTime }} + + + + + 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 @@ + + + + + + + {{ tagsLink.text }} + + + + {{ tag.name }} + ({{ tag.count }}) + + + + + + + + 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/components/icons/IconPrint.vue b/theme/src/client/components/icons/IconPrint.vue new file mode 100644 index 00000000..860906b6 --- /dev/null +++ b/theme/src/client/components/icons/IconPrint.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 @@ @@ -49,7 +52,8 @@ provide('is-sidebar-open', isSidebarOpen) - + + diff --git a/theme/src/client/styles/md-enhance.scss b/theme/src/client/styles/md-enhance.scss index 0dbad564..0ced9a1e 100644 --- a/theme/src/client/styles/md-enhance.scss +++ b/theme/src/client/styles/md-enhance.scss @@ -1,90 +1,151 @@ -.plume-content .hint-container { - border-radius: 8px; - padding: 16px 16px 8px; - line-height: 24px; - font-size: var(--vp-custom-block-font-size); - color: var(--vp-c-text-2); -} - -.plume-content .hint-container .hint-container-title { - font-weight: 600; - margin-top: 0; -} - -.hint-container.note { - color: var(--vp-custom-block-info-text); -} - -.hint-container.tip { - color: var(--vp-custom-block-tip-text); -} - -.hint-container.warning { - color: var(--vp-custom-block-warning-text); -} - -.hint-container.caution { - color: var(--vp-custom-block-danger-text); -} - -.hint-container.detail { - color: var(--vp-custom-block-detail-text); -} - -.plume-content .hint-container.details summary { - margin: -1.5rem -1.5rem -1.1rem; - font-weight: 700; - cursor: pointer; - color: var(--vp-c-text-1); -} - -.plume-content .hint-container.details summary + p { - margin: 8px 0; -} - -.plume-content .hint-container p + p { - margin: 8px 0; -} - -.plume-content .hint-container code { - font-size: var(--vp-custom-block-code-font-size); -} - -.plume-content .hint-container { - &.note, - &.tip, - &.detail, - &.important { - a, - code { - color: var(--vp-c-brand-1); - } - a:hover { - color: var(--vp-c-brand-2); - } - } - &.warning { - a, - code { - color: var(--vp-c-warning-1); - } - a:hover { - color: var(--vp-c-warning-2); - } - } - &.caution { - a, - code { - color: var(--vp-c-danger-1); - } - a:hover { - color: var(--vp-c-danger-2); - } - } - - th, - blockquote > p { +.plume-content { + .hint-container { + border-radius: 8px; + padding: 16px 16px 8px; + line-height: 24px; font-size: var(--vp-custom-block-font-size); - color: inherit; + color: var(--vp-c-text-2); + + .hint-container-title { + font-weight: 600; + margin-top: 0; + } + + &.note { + border-radius: 0; + color: var(--vp-c-text-3); + } + + &.info { + color: var(--vp-custom-block-info-text); + } + + &.tip { + color: var(--vp-custom-block-tip-text); + } + + &.warning { + color: var(--vp-custom-block-warning-text); + } + + &.caution { + color: var(--vp-custom-block-danger-text); + } + + &.detail { + color: var(--vp-custom-block-detail-text); + + summary { + margin: -1.5rem -1.5rem -1.1rem; + font-weight: 700; + cursor: pointer; + color: var(--vp-c-text-1); + } + + summary + p { + margin: 8px 0; + } + } + + p + p { + margin: 8px 0; + } + + code { + font-size: var(--vp-custom-block-code-font-size); + } + + &.note, + &.tip, + &.detail, + &.important { + a, + code { + color: var(--vp-c-brand-1); + } + a:hover { + color: var(--vp-c-brand-2); + } + } + &.warning { + a, + code { + color: var(--vp-c-warning-1); + } + a:hover { + color: var(--vp-c-warning-2); + } + } + &.caution { + a, + code { + color: var(--vp-c-danger-1); + } + a:hover { + color: var(--vp-c-danger-2); + } + } + + th, + blockquote > p { + font-size: var(--vp-custom-block-font-size); + color: inherit; + } + } + + .vp-code-demo { + border: solid 1px var(--vp-c-divider); + overflow: hidden; + + &:hover { + box-shadow: none; + } + + .vp-code-demo-header { + padding: 8px 12px; + } + + .vp-code-demo-code-wrapper { + margin-bottom: -0.9rem; + } + + .vp-code-demo-toggle-button { + margin: 0 12px 0 8px; + background-color: var(--vp-c-gray-2); + + &:hover { + background-color: var(--vp-c-gray-1); + } + } + + .vp-code-demo-title { + font-size: 1rem; + line-height: 1.75; + } + + .vp-code-demo-display { + border-bottom: transparent; + } + + .code-demo-jsfiddle .jsfiddle-button, + .code-demo-codepen .codepen-button { + background-color: transparent; + } + + .vp-code-demo-codes div[class*='language-'] { + border-bottom: 2px dashed var(--vp-c-divider); + + &:first-of-type { + border-top: 1px solid var(--vp-c-divider); + } + &:last-of-type { + border-bottom: none; + } + } + + .vp-code-demo-codes div[class*='language-'] pre { + margin-bottom: 0; + border-radius: 0; + } } } diff --git a/theme/src/client/styles/vars.scss b/theme/src/client/styles/vars.scss index 3c77a222..af471b17 100644 --- a/theme/src/client/styles/vars.scss +++ b/theme/src/client/styles/vars.scss @@ -69,6 +69,8 @@ --vp-c-red-3: #e0575b; --vp-c-red-soft: rgba(244, 63, 94, 0.14); + --vp-c-purple: #f4eefe; + --vp-c-sponsor: #db2777; } @@ -93,6 +95,8 @@ --vp-c-yellow-3: #a46a0a; --vp-c-yellow-soft: rgba(234, 179, 8, 0.16); + --vp-c-purple: #423655; + --vp-c-red-1: #f66f81; --vp-c-red-2: #f14158; --vp-c-red-3: #b62a3c; @@ -297,7 +301,7 @@ --vp-code-bg: var(--vp-c-default-soft); --vp-code-block-color: var(--vp-c-text-2); - --vp-code-block-bg: var(--vp-c-bg-alt); + --vp-code-block-bg: var(--vp-c-bg-soft); --vp-code-block-divider-color: var(--vp-c-gutter); --vp-code-lang-color: var(--vp-c-text-3); @@ -476,6 +480,15 @@ --vp-c-text-hero-text: var(--vp-c-text-dark-1); } +/** + * Component: Friends + * -------------------------------------------------------------------------- */ +:root { + --vp-friends-text-color: var(--vp-c-text-1); + --vp-friends-bg-color: var(--vp-c-bg); + --vp-friends-link-color: var(--vp-c-brand-1); + --vp-friends-border-color: var(--vp-c-border); +} /** * Component: Badge * -------------------------------------------------------------------------- */ @@ -549,12 +562,14 @@ } /* md enhance hints */ -:root { - // important +:root, +html.dark { + /* important */ --important-title-color: var(--vp-c-text-1); - --important-bg-color: #f4eefe; - --important-border-color: #f4eefe; + --important-bg-color: var(--vp-c-purple); + --important-border-color: var(--vp-c-purple); --important-code-bg-color: rgb(163 113 247 / 10%); + // info --info-title-color: var(--vp-c-text-1); --info-bg-color: var(--vp-custom-block-info-bg); @@ -563,7 +578,7 @@ // note --note-title-color: var(--vp-c-text-3); - --note-bg-color: var(--vp-c-bg-elv); + --note-bg-color: var(--vp-c-bg); --note-border-color: var(--vp-c-divider); --note-code-bg-color: var(--vp-c-default-soft); @@ -591,6 +606,11 @@ --detail-code-bg-color: var(--vp-custom-block-details-code-bg); } +/* md enhance code-demo */ +:root { + --code-demo-header-bg-color: var(--vp-c-bg-soft); +} + :root { --t-color: 250ms ease; --code-bg-color: var(--vp-code-block-bg); diff --git a/theme/src/client/utils/base.ts b/theme/src/client/utils/base.ts new file mode 100644 index 00000000..fa7c644c --- /dev/null +++ b/theme/src/client/utils/base.ts @@ -0,0 +1,3 @@ +export const toArray = (value: T | T[]): T[] => { + return Array.isArray(value) ? value : [value] +} diff --git a/theme/src/client/utils/index.ts b/theme/src/client/utils/index.ts index 3961eb76..14a67353 100644 --- a/theme/src/client/utils/index.ts +++ b/theme/src/client/utils/index.ts @@ -4,3 +4,4 @@ export * from './socialIcons.js' export * from './dom.js' export * from './resolveEditLink.js' export * from './resolveRepoType.js' +export * from './base.js' diff --git a/theme/src/node/autoFrontmatter.ts b/theme/src/node/autoFrontmatter.ts index 706612fd..513f17f2 100644 --- a/theme/src/node/autoFrontmatter.ts +++ b/theme/src/node/autoFrontmatter.ts @@ -50,12 +50,14 @@ export default function autoFrontmatter( .filter(Boolean) const baseFrontmatter: FrontmatterObject = { - author(author: string) { + author(author: string, _, data: any) { if (author) return author + if (data.friends) return return localeOption.avatar?.name || pkg.author || '' }, - createTime(formatTime: string, { createTime }) { + createTime(formatTime: string, { createTime }, data: any) { if (formatTime) return formatTime + if (data.friends) return return format(new Date(createTime), 'yyyy/MM/dd HH:mm:ss') }, } @@ -107,8 +109,9 @@ export default function autoFrontmatter( return getCurrentDirname(note, filepath) || '' }, ...baseFrontmatter, - permalink(permalink: string, { filepath }) { + permalink(permalink: string, { filepath }, data: any) { if (permalink) return permalink + if (data.friends) return const locale = resolveLocale(filepath) const notes = notesByLocale(locale) const note = findNote(filepath) @@ -136,8 +139,9 @@ export default function autoFrontmatter( return basename }, ...baseFrontmatter, - permalink(permalink: string, { filepath }) { + permalink(permalink: string, { filepath }, data: any) { if (permalink) return permalink + if (data.friends) return const locale = resolveLocale(filepath) const note = findNote(filepath) const notes = notesByLocale(locale) diff --git a/theme/src/node/setupPages.ts b/theme/src/node/setupPages.ts index b1c67042..3bf01528 100644 --- a/theme/src/node/setupPages.ts +++ b/theme/src/node/setupPages.ts @@ -7,6 +7,10 @@ import type { PlumeThemePageData, } from '../shared/index.js' +const normalizePath = (dir: string) => { + return dir.replace(/\\+/g, '/') +} + export async function setupPage( app: App, localeOption: PlumeThemeLocaleOptions @@ -14,20 +18,68 @@ export async function setupPage( const locales = Object.keys(app.siteData.locales || {}) for (const [, locale] of locales.entries()) { const blog = localeOption.locales?.[locale]?.blog + const defaultBlog = localeOption.blog + const link = blog?.link + ? blog.link + : normalizePath(path.join('/', locale, defaultBlog?.link || '')) const blogPage = await createPage(app, { - path: blog?.link - ? blog.link - : path.join('/', locale, localeOption.blog?.link || ''), + path: link, frontmatter: { lang: locale.replace(/^\/|\/$/g, '') || app.siteData.lang, type: 'blog', }, }) - app.pages.push(blogPage) + + if (blog?.tags !== false || defaultBlog?.tags !== false) { + const tagsPage = await createPage(app, { + path: normalizePath(path.join(link, 'tags/')), + frontmatter: { + lang: locale.replace(/^\/|\/$/g, '') || app.siteData.lang, + type: 'blog-tags', + }, + }) + app.pages.push(tagsPage) + } + + if (blog?.archives !== false || defaultBlog?.archives !== false) { + const archivesPage = await createPage(app, { + path: normalizePath(path.join(link, 'archives/')), + frontmatter: { + lang: locale.replace(/^\/|\/$/g, '') || app.siteData.lang, + type: 'blog-archives', + }, + }) + app.pages.push(archivesPage) + } } } +export function extendsPageData( + app: App, + page: Page, + localeOptions: PlumeThemeLocaleOptions +) { + page.data.filePathRelative = page.filePathRelative + page.routeMeta.title = page.title + + if (page.frontmatter.friends) { + page.frontmatter.article = false + page.frontmatter.type = 'friends' + page.data.isBlogPost = false + page.permalink = page.permalink ?? '/friends/' + } + + if ((page.frontmatter.type as string)?.startsWith('blog')) { + page.data.isBlogPost = false + page.frontmatter.article = false + page.data.type = page.frontmatter.type as any + } + + autoCategory(app, page, localeOptions) + pageContentRendered(page) +} + let uuid = 10000 const cache: Record = {} const RE_CATEGORY = /^(\d+)?(?:\.?)([^]+)$/ diff --git a/theme/src/node/theme.ts b/theme/src/node/theme.ts index 8f5f0391..6383310a 100644 --- a/theme/src/node/theme.ts +++ b/theme/src/node/theme.ts @@ -1,12 +1,12 @@ import type { App, Page, Theme } from '@vuepress/core' -import { fs, getDirname, path } from '@vuepress/utils' +import { getDirname, path } from '@vuepress/utils' import type { PlumeThemeOptions, PlumeThemePageData } from '../shared/index.js' import { mergeLocaleOptions } from './defaultOptions.js' import { setupPlugins } from './plugins.js' -import { autoCategory, pageContentRendered, setupPage } from './setupPages.js' +import { extendsPageData, setupPage } from './setupPages.js' const __dirname = getDirname(import.meta.url) -const name = '@vuepress-plume/theme-plume' +const name = 'vuepress-theme-plume' const resolve = (...args: string[]) => path.resolve(__dirname, '../', ...args) const templates = (url: string) => resolve('../templates', url) @@ -20,25 +20,11 @@ export const plumeTheme = ({ name, templateBuild: templates('build.html'), clientConfigFile: resolve('client/config.js'), - alias: { - ...Object.fromEntries( - fs - .readdirSync(resolve('client/components')) - .filter((file) => file.endsWith('.vue')) - .map((file) => [ - `@theme/${file}`, - resolve('client/components', file), - ]) - ), - }, plugins: setupPlugins(app, themePlugins, localeOptions), onInitialized: async (app) => await setupPage(app, localeOptions), - extendsPage: (page: Page) => { - page.data.filePathRelative = page.filePathRelative - page.routeMeta.title = page.title - autoCategory(app, page, localeOptions) - pageContentRendered(page) - }, + extendsPage: (page: Page) => + extendsPageData(app, page, localeOptions) + , } } } diff --git a/theme/src/shared/frontmatter.ts b/theme/src/shared/frontmatter.ts index 8ed637c8..193ba0c4 100644 --- a/theme/src/shared/frontmatter.ts +++ b/theme/src/shared/frontmatter.ts @@ -39,3 +39,17 @@ export interface PlumeThemePostFrontmatter extends PlumeThemePageFrontmatter { export interface PlumeThemeNoteFrontmatter extends PlumeThemePageFrontmatter { createTime?: string } + +export interface FriendsItem { + name: string + link: string + avatar?: string + desc?: string +} + +export interface PlumeThemeFriendsFrontmatter { + friends: boolean + title?: string + description?: string + list?: FriendsItem[] +} diff --git a/theme/src/shared/options/locale.ts b/theme/src/shared/options/locale.ts index cb2261a1..e543477b 100644 --- a/theme/src/shared/options/locale.ts +++ b/theme/src/shared/options/locale.ts @@ -1,8 +1,6 @@ import type { LocaleData } from '@vuepress/core' import type { NotesDataOptions } from '@vuepress-plume/plugin-notes-data' import type { NavItem } from './navbar.js' -// import type { NavbarConfig, NavLink } from '../layout/index.js' -// import type { PlumeThemeNotesOptions } from './notes.js' export interface PlumeThemeAvatar { /** @@ -39,6 +37,67 @@ export type SocialLinkIcon = | 'bilibili' | { svg: string } +export interface PlumeThemeBlog { + /** + * blog 文章读取目录 + * + * @default './' 即 vuepress 配置的 source 目录 + */ + dir?: string + + /** + * blog list link + * + * @default '/blog/' + */ + link?: string + + /** + * 在 `blog.dir` 目录中,通过 glob string 配置包含文件 + * + * @default - ['**\*.md'] + */ + include?: string[] + + /** + * 在 `blog.dir` 目录中,通过 glob string 配置排除的文件 + * + * _README.md 文件一般作为主页或者某个目录下的主页,不应该被读取为 blog文章_ + * + * @default - ['.vuepress/', 'node_modules/', '{README,index}.md'] + */ + exclude?: string[] + + pagination?: false | { + /** + * 每页显示的文章数量 + * @default 20 + */ + perPage?: number + /** + * 前一页的文本 + * @default 'Prev' + */ + prevPageText?: string + /** + * 后一页的文本 + * @default 'Next' + */ + nextPageText?: string + } + + /** + * 是否启用标签页 + * @default true + */ + tags?: boolean + /** + * 是否启用归档页 + * @default true + */ + archives?: boolean +} + export interface PlumeThemeLocaleData extends LocaleData { /** * 网站站点首页 @@ -76,37 +135,10 @@ export interface PlumeThemeLocaleData extends LocaleData { */ social?: SocialLink[] - blog?: { - /** - * blog 文章读取目录 - * - * @default './' 即 vuepress 配置的 source 目录 - */ - dir?: string - - /** - * blog list link - * - * @default '/blog/' - */ - link?: string - - /** - * 在 `blog.dir` 目录中,通过 glob string 配置包含文件 - * - * @default - ['**\*.md'] - */ - include?: string[] - - /** - * 在 `blog.dir` 目录中,通过 glob string 配置排除的文件 - * - * _README.md 文件一般作为主页或者某个目录下的主页,不应该被读取为 blog文章_ - * - * @default - ['.vuepress/', 'node_modules/', '{README,index}.md'] - */ - exclude?: string[] - } + /** + * 博客配置 + */ + blog?: PlumeThemeBlog /** * 文章链接前缀 @@ -115,29 +147,6 @@ export interface PlumeThemeLocaleData extends LocaleData { */ article?: string - /** - * 标签页链接 与 navbar配置 - * - * @def:{ text: '标签', link: '/tag/' } - */ - // tag?: false | NavItemWithLink - - /** - * 文章分类 与 navbar配置 - * - * @default: { text: '分类', link: '/category/ } - */ - // category?: false | NavItemWithLink - - /** - * 归档页 链接与 navbar 配置 - * - * (注,由于页面样式为 timeline, 所以默认链接为 timeline ) - * - * @default: { text: '归档', link: '/timeline/' } - */ - // archive?: false | NavItemWithLink - /** * 笔记配置, 笔记中的文章默认不会出现在首页文章列表 * diff --git a/theme/src/shared/page.ts b/theme/src/shared/page.ts index b38bd16b..d74431f5 100644 --- a/theme/src/shared/page.ts +++ b/theme/src/shared/page.ts @@ -9,7 +9,7 @@ interface ReadingTime { export interface PlumeThemePageData extends GitPluginPageData { isBlogPost: boolean - type: 'blog' | 'product' + type: 'blog' | 'friends' | 'blog-tags' | 'blog-archives' categoryList?: PageCategoryData[] filePathRelative: string | null readingTime?: ReadingTime
+ +
{{ avatar.description }}
- -
{{ matter.description }}
{{ friend.desc }}
+ {{ post.title }} + {{ post.createTime }} +
+ {{ tag.name }} + ({{ tag.count }}) +