commit
4bad4b90d6
@ -22,31 +22,36 @@ export const zhNotes = definePlumeNotesConfig({
|
||||
{
|
||||
text: 'markdown',
|
||||
icon: 'material-symbols:markdown-outline',
|
||||
dir: 'markdown',
|
||||
prefix: 'markdown',
|
||||
collapsed: true,
|
||||
items: ['基础', '扩展', '进阶'],
|
||||
},
|
||||
{
|
||||
text: '代码块',
|
||||
dir: '代码',
|
||||
prefix: '代码',
|
||||
icon: 'ph:code-bold',
|
||||
collapsed: true,
|
||||
items: ['介绍', '特性支持', '代码组', '导入代码', 'twoslash'],
|
||||
},
|
||||
{
|
||||
text: '代码演示',
|
||||
dir: '代码演示',
|
||||
prefix: '代码演示',
|
||||
icon: 'carbon:demo',
|
||||
collapsed: true,
|
||||
items: ['前端', 'rust', 'golang', 'kotlin', 'codepen', 'jsFiddle', 'codeSandbox', 'replit'],
|
||||
},
|
||||
{
|
||||
text: '图表',
|
||||
icon: 'mdi:chart-line',
|
||||
dir: '图表',
|
||||
prefix: '图表',
|
||||
collapsed: true,
|
||||
items: ['chart', 'echarts', 'mermaid', 'flowchart'],
|
||||
},
|
||||
{
|
||||
text: '资源嵌入',
|
||||
icon: 'dashicons:embed-video',
|
||||
dir: '嵌入',
|
||||
prefix: '嵌入',
|
||||
collapsed: true,
|
||||
items: ['pdf', 'bilibili', 'youtube'],
|
||||
},
|
||||
],
|
||||
@ -55,8 +60,13 @@ export const zhNotes = definePlumeNotesConfig({
|
||||
text: '功能',
|
||||
icon: 'lucide:box',
|
||||
collapsed: false,
|
||||
dir: '功能',
|
||||
items: ['图标', '代码复制', '内容搜索', '评论', '加密', '组件', '文章水印', '友情链接页', 'seo', 'sitemap'],
|
||||
prefix: '功能',
|
||||
items: ['图标', '代码复制', '内容搜索', '评论', '加密', '组件', '文章水印', '友情链接页', 'seo', 'sitemap', {
|
||||
text: '非内置功能',
|
||||
icon: 'system-uicons:box-add',
|
||||
collapsed: true,
|
||||
items: ['repoCard', 'npmBadge'],
|
||||
}],
|
||||
},
|
||||
{
|
||||
text: '自定义',
|
||||
@ -89,13 +99,13 @@ export const zhNotes = definePlumeNotesConfig({
|
||||
},
|
||||
{
|
||||
text: 'frontmatter',
|
||||
dir: 'frontmatter',
|
||||
prefix: 'frontmatter',
|
||||
collapsed: false,
|
||||
items: ['basic', 'home', 'post', 'friend'],
|
||||
},
|
||||
{
|
||||
text: '内置插件',
|
||||
dir: 'plugins',
|
||||
prefix: 'plugins',
|
||||
collapsed: false,
|
||||
items: ['', '代码高亮', '搜索', '阅读统计', 'markdown增强', 'markdownPower', '百度统计', '水印'],
|
||||
},
|
||||
@ -109,7 +119,7 @@ export const zhNotes = definePlumeNotesConfig({
|
||||
text: '插件',
|
||||
items: [
|
||||
// 'caniuse',
|
||||
'iconify',
|
||||
// 'iconify',
|
||||
'shiki',
|
||||
'md-power',
|
||||
'content-updated',
|
||||
|
||||
@ -179,6 +179,8 @@ interface BlogOptions {
|
||||
|
||||
::: warning
|
||||
该字段不支持在 [主题配置文件 `plume.config.js`](./配置说明.md#主题配置文件) 中进行配置。
|
||||
|
||||
为了使缓存能够生效,您应该 **删除** `package.json` 中 `vuepress dev` 开发服务启动脚本中的 `--clean-cache` 参数。
|
||||
:::
|
||||
|
||||
### locales
|
||||
|
||||
151
docs/notes/theme/guide/功能/npmBadge.md
Normal file
151
docs/notes/theme/guide/功能/npmBadge.md
Normal file
@ -0,0 +1,151 @@
|
||||
---
|
||||
title: Npm 徽章
|
||||
author: pengzhanbo
|
||||
icon: akar-icons:npm-fill
|
||||
createTime: 2024/07/26 22:07:23
|
||||
permalink: /guide/npm-badge/
|
||||
---
|
||||
|
||||
<script setup>
|
||||
import NpmBadge from 'vuepress-theme-plume/features/NpmBadge.vue'
|
||||
import NpmBadgeGroup from 'vuepress-theme-plume/features/NpmBadgeGroup.vue'
|
||||
</script>
|
||||
|
||||
## 概述
|
||||
|
||||
Npm 徽章组件 用于显示 npm 包信息,并提供相关的链接。
|
||||
|
||||
徽章由 <https://shields.io> 提供支持。
|
||||
|
||||
## 使用
|
||||
|
||||
使用该组件需要你手动导入 `NpmBadge` 或 `NpmBadgeGroup` 组件:
|
||||
|
||||
```md :no-line-numbers
|
||||
<!-- 在 markdown 中导入 -->
|
||||
<script setup>
|
||||
import NpmBadge from 'vuepress-theme-plume/features/NpmBadge.vue'
|
||||
import NpmBadgeGroup from 'vuepress-theme-plume/features/NpmBadgeGroup.vue'
|
||||
</script>
|
||||
|
||||
<!-- 导入后,即可在 markdown 中使用 -->
|
||||
<NpmBadge name="vuepress-theme-plume" type="dm" />
|
||||
|
||||
<!-- 并排显示多个 npm badge -->
|
||||
<NpmBadgeGroup name="vuepress-theme-plume" items="version,dm" />
|
||||
```
|
||||
|
||||
<NpmBadge name="vuepress-theme-plume" type="dm" />
|
||||
|
||||
<NpmBadgeGroup name="vuepress-theme-plume" items="version,dm" />
|
||||
|
||||
## `<NpmBadge />`
|
||||
|
||||
单个 npm badge
|
||||
|
||||
### Props
|
||||
|
||||
| 名称 | 类型 | 必填 | 默认值 | 说明 |
|
||||
| ---------- | --------------- | --------------- | ----------- | --------------------------------------- |
|
||||
| name | `string` | 否 | `''` | npm 包名,为空则从 `repo` 中获取 |
|
||||
| repo | `string` | name 为空时必填 | `''` | 包 github 仓库地址, 格式为 `owner/repo` |
|
||||
| type | `NpmBadgeType` | 是 | - | 徽章类型 |
|
||||
| theme | `NpmBadgeTheme` | 否 | `'flat'` | 徽章主题 |
|
||||
| label | `string` | 否 | `''` | 徽章标签 |
|
||||
| color | `string` | 否 | `'#32A9C3'` | 徽章颜色 |
|
||||
| labelColor | `string` | 否 | `'#1B3C4A'` | 徽章标签颜色 |
|
||||
| branch | `string` | 否 | `'main'` | 仓库分支 |
|
||||
| dir | `string` | 否 | `''` | 包所在仓库目录,适用于 monorepo 项目 |
|
||||
|
||||
### Types
|
||||
|
||||
```ts
|
||||
type NpmBadgeType =
|
||||
// github
|
||||
| 'source' // github source
|
||||
| 'stars' // github stars
|
||||
| 'forks' // github forks
|
||||
| 'license' // github license
|
||||
// npm
|
||||
| 'version' // npm version
|
||||
| 'dt' // alias d18m
|
||||
| 'd18m' // npm downloads last 18 months
|
||||
| 'dw' // npm downloads weekly
|
||||
| 'dm' // npm downloads monthly
|
||||
| 'dy' // npm downloads yearly
|
||||
|
||||
type NpmBadgeTheme = 'flat' | 'flat-square' | 'plastic' | 'for-the-badge' | 'social'
|
||||
```
|
||||
|
||||
### 示例
|
||||
|
||||
- `<NpmBadge repo="pengzhanbo/vuepress-theme-plume" type="source" />` - <NpmBadge repo="pengzhanbo/vuepress-theme-plume" type="source" />
|
||||
- `<NpmBadge repo="pengzhanbo/vuepress-theme-plume" type="stars" />` - <NpmBadge repo="pengzhanbo/vuepress-theme-plume" type="stars" />
|
||||
- `<NpmBadge repo="pengzhanbo/vuepress-theme-plume" type="forks" />` - <NpmBadge repo="pengzhanbo/vuepress-theme-plume" type="forks" />
|
||||
- `<NpmBadge repo="pengzhanbo/vuepress-theme-plume" type="license" />` - <NpmBadge repo="pengzhanbo/vuepress-theme-plume" type="license" />
|
||||
- `<NpmBadge name="vuepress-theme-plume" type="version" />` - <NpmBadge repo="pengzhanbo/vuepress-theme-plume" type="version" />
|
||||
- `<NpmBadge name="vuepress-theme-plume" type="dt" />` - <NpmBadge repo="pengzhanbo/vuepress-theme-plume" type="dt" />
|
||||
- `<NpmBadge name="vuepress-theme-plume" type="d18m" />` - <NpmBadge repo="pengzhanbo/vuepress-theme-plume" type="d18m" />
|
||||
- `<NpmBadge name="vuepress-theme-plume" type="dy" />` - <NpmBadge repo="pengzhanbo/vuepress-theme-plume" type="dy" />
|
||||
- `<NpmBadge name="vuepress-theme-plume" type="dm" />` - <NpmBadge repo="pengzhanbo/vuepress-theme-plume" type="dm" />
|
||||
- `<NpmBadge name="vuepress-theme-plume" type="dw" />` - <NpmBadge repo="pengzhanbo/vuepress-theme-plume" type="dw" />
|
||||
|
||||
## `<NpmBadgeGroup />`
|
||||
|
||||
组合多个 npm badge
|
||||
|
||||
### Props
|
||||
|
||||
| 名称 | 类型 | 必填 | 默认值 | 说明 |
|
||||
| ---------- | --------------- | --------------- | ------ | --------------------------------------- |
|
||||
| name | `string` | 否 | `''` | npm 包名,为空则从 `repo` 中获取 |
|
||||
| repo | `string` | name 为空时必填 | `''` | 包 github 仓库地址, 格式为 `owner/repo` |
|
||||
| items | `string \| NpmBadgeType[]` | 否 | - | 徽章类型列表, 传入 `string` 时用 `','`分隔,会自动转换为 `NpmBadgeType[]` |
|
||||
| theme | `NpmBadgeTheme` | 否 | `''` | 徽章主题 |
|
||||
| color | `string` | 否 | `''` | 徽章颜色 |
|
||||
| labelColor | `string` | 否 | `''` | 徽章标签颜色 |
|
||||
| branch | `string` | 否 | `''` | 仓库分支 |
|
||||
| dir | `string` | 否 | `''` | 包所在仓库目录,适用于 monorepo 项目 |
|
||||
|
||||
### Slots
|
||||
|
||||
`<NpmBadgeGroup />` 支持传入 多个 `<NpmBadge />` 组件。
|
||||
|
||||
`<NpmBadgeGroup />` 声明的 `Props` 将被注入到 `<NpmBadge />` 组件中。通过这种方式来实现和简化徽章组合。
|
||||
|
||||
### 示例
|
||||
|
||||
**输入:**
|
||||
|
||||
```md :no-line-numbers
|
||||
<NpmBadgeGroup
|
||||
repo="pengzhanbo/vuepress-theme-plume"
|
||||
items="stars,version,dm,source"
|
||||
/>
|
||||
```
|
||||
|
||||
**输出:**
|
||||
|
||||
<NpmBadgeGroup repo="pengzhanbo/vuepress-theme-plume" items="stars,version,dm,source" />
|
||||
|
||||
使用 `<slot />` 灵活定义徽章组合:
|
||||
|
||||
**输入:**
|
||||
|
||||
```md :no-line-numbers
|
||||
<NpmBadgeGroup repo="pengzhanbo/vuepress-theme-plume">
|
||||
<NpmBadge type="stars" />
|
||||
<NpmBadge type="version" label="npm" />
|
||||
<NpmBadge type="dm" />
|
||||
<NpmBadge type="source" />
|
||||
</NpmBadgeGroup>
|
||||
```
|
||||
|
||||
**输出:**
|
||||
|
||||
<NpmBadgeGroup repo="pengzhanbo/vuepress-theme-plume">
|
||||
<NpmBadge type="stars" />
|
||||
<NpmBadge type="version" label="npm" />
|
||||
<NpmBadge type="dm" />
|
||||
<NpmBadge type="source" />
|
||||
</NpmBadgeGroup>
|
||||
91
docs/notes/theme/guide/功能/repoCard.md
Normal file
91
docs/notes/theme/guide/功能/repoCard.md
Normal file
@ -0,0 +1,91 @@
|
||||
---
|
||||
title: Repo 卡片
|
||||
author: pengzhanbo
|
||||
icon: octicon:repo-16
|
||||
createTime: 2024/07/26 21:11:56
|
||||
permalink: /guide/github-repo-card/
|
||||
---
|
||||
|
||||
<script setup>
|
||||
import RepoCard from 'vuepress-theme-plume/features/RepoCard.vue'
|
||||
</script>
|
||||
|
||||
## 概述
|
||||
|
||||
Repo 卡片组件 用于显示 GitHub 仓库信息。
|
||||
|
||||
## 使用
|
||||
|
||||
使用该组件需要你手动导入 `RepoCard` 组件:
|
||||
|
||||
```md :no-line-numbers
|
||||
<!-- 在 markdown 中导入 -->
|
||||
<script setup>
|
||||
import RepoCard from 'vuepress-theme-plume/features/RepoCard.vue'
|
||||
</script>
|
||||
|
||||
<!-- 导入后,即可在 markdown 中使用 -->
|
||||
<RepoCard repo="pengzhanbo/vuepress-theme-plume" />
|
||||
```
|
||||
|
||||
注册为全局组件:
|
||||
|
||||
::: code-tabs
|
||||
@tab .vuepress/client.ts
|
||||
|
||||
```ts
|
||||
import { defineClientConfig } from 'vuepress/client'
|
||||
import RepoCard from 'vuepress-theme-plume/features/RepoCard.vue'
|
||||
|
||||
export default defineClientConfig({
|
||||
enhance({ app }) {
|
||||
app.component('RepoCard', RepoCard)
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
全局组件可在 其他任意 markdown 文件中使用
|
||||
|
||||
```md
|
||||
<RepoCard repo="pengzhanbo/vuepress-theme-plume" />
|
||||
```
|
||||
|
||||
### Props
|
||||
|
||||
`RepoCard` 组件的 接收一个 `repo` 参数,传入的是仓库的地址,格式为 `owner/repo`。
|
||||
|
||||
## 示例
|
||||
|
||||
### 单卡片
|
||||
|
||||
**输入:**
|
||||
|
||||
```md
|
||||
<RepoCard repo="pengzhanbo/vuepress-theme-plume" />
|
||||
```
|
||||
|
||||
**输出:**
|
||||
|
||||
<RepoCard repo="pengzhanbo/vuepress-theme-plume" />
|
||||
|
||||
### 多卡片
|
||||
|
||||
如果希望以紧凑的方式并排展示多个卡片,可以使用 `CardGrid` 组件。
|
||||
|
||||
**输入:**
|
||||
|
||||
```md
|
||||
<CardGrid>
|
||||
<RepoCard repo="vuepress/core" />
|
||||
<RepoCard repo="vuepress/ecosystem" />
|
||||
</CardGrid>
|
||||
```
|
||||
|
||||
**输出:**
|
||||
|
||||
<CardGrid>
|
||||
<RepoCard repo="vuepress/core" />
|
||||
<RepoCard repo="vuepress/ecosystem" />
|
||||
</CardGrid>
|
||||
@ -37,6 +37,9 @@
|
||||
"types": "./lib/client/composables/index.d.ts",
|
||||
"import": "./lib/client/composables/index.js"
|
||||
},
|
||||
"./features/*": {
|
||||
"import": "./lib/client/features/components/*"
|
||||
},
|
||||
"./shared": {
|
||||
"types": "./lib/shared/index.d.ts",
|
||||
"import": "./lib/shared/index.js"
|
||||
|
||||
@ -174,7 +174,7 @@ watch(
|
||||
|
||||
@media (min-width: 1440px) {
|
||||
.vp-doc-container:not(.has-sidebar) .content {
|
||||
max-width: 784px;
|
||||
max-width: 884px;
|
||||
}
|
||||
|
||||
.vp-doc-container.is-blog:not(.has-sidebar.has-aside) .content {
|
||||
@ -275,6 +275,6 @@ watch(
|
||||
}
|
||||
|
||||
.vp-doc-container.has-aside .content-container {
|
||||
max-width: 688px;
|
||||
max-width: 788px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -61,7 +61,7 @@ const bind = computed<any>(() => ({
|
||||
<span v-if="!loaded" class="vp-icon" :style="{ color, width: size, height: size }" />
|
||||
<OfflineIcon
|
||||
v-else-if="icon"
|
||||
class="vp-iconify"
|
||||
class="vp-icon"
|
||||
v-bind="bind"
|
||||
/>
|
||||
</ClientOnly>
|
||||
|
||||
@ -29,8 +29,9 @@ export function useLangs({
|
||||
const { notFound, path } = resolveRoute(targetPath)
|
||||
if (!notFound)
|
||||
return path
|
||||
|
||||
const blog = theme.value.blog
|
||||
if (isBlogPost.value)
|
||||
if (isBlogPost.value && blog !== false)
|
||||
return withBase(blog?.link || normalizeLink(locale, 'blog/'))
|
||||
|
||||
const sidebarList = sidebar.value
|
||||
|
||||
@ -16,7 +16,6 @@ import {
|
||||
ref,
|
||||
watch,
|
||||
watchEffect,
|
||||
watchPostEffect,
|
||||
} from 'vue'
|
||||
import { sidebar as sidebarRaw } from '@internal/sidebar'
|
||||
import { isActive, normalizeLink, normalizePrefix, resolveNavLink } from '../utils/index.js'
|
||||
@ -368,7 +367,7 @@ export function useSidebarControl(item: ComputedRef<ResolvedSidebarItem>): Sideb
|
||||
)
|
||||
}
|
||||
|
||||
watch([page, item, () => route.hash], updateIsActiveLink)
|
||||
watch([() => page.value.path, item, () => route.hash], updateIsActiveLink)
|
||||
onMounted(updateIsActiveLink)
|
||||
|
||||
const hasActiveLink = computed(() => {
|
||||
@ -389,11 +388,11 @@ export function useSidebarControl(item: ComputedRef<ResolvedSidebarItem>): Sideb
|
||||
collapsed.value = !!(collapsible.value && item.value.collapsed)
|
||||
})
|
||||
|
||||
watchPostEffect(() => {
|
||||
watch(() => [page.value.path, isActiveLink.value, hasActiveLink.value], () => {
|
||||
if (isActiveLink.value || hasActiveLink.value) {
|
||||
collapsed.value = false
|
||||
}
|
||||
})
|
||||
}, { immediate: true, flush: 'post' })
|
||||
|
||||
const toggle = (): void => {
|
||||
if (collapsible.value) {
|
||||
|
||||
35
theme/src/client/features/components/NpmBadge.vue
Normal file
35
theme/src/client/features/components/NpmBadge.vue
Normal file
@ -0,0 +1,35 @@
|
||||
<script setup lang="ts">
|
||||
import { toRef } from 'vue'
|
||||
import type { NpmBadgeOptions } from '../composables/npm-badge.js'
|
||||
import { useNpmBadge } from '../composables/npm-badge.js'
|
||||
|
||||
const props = defineProps<NpmBadgeOptions>()
|
||||
|
||||
const info = useNpmBadge(toRef(() => props))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span class="vp-npm-badge">
|
||||
<img v-if="!info.link" :src="info.badgeUrl" :alt="info.alt">
|
||||
<a v-else :href="info.link" target="_blank" rel="noreferrer">
|
||||
<img :src="info.badgeUrl" :alt="info.alt">
|
||||
</a>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.vp-npm-badge {
|
||||
display: inline-flex;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
:where(div,p,h2,h3,h4,h5,h6,li):not(.vp-npm-badge-group) > .vp-npm-badge + .vp-npm-badge {
|
||||
margin-inline-start: 8px;
|
||||
}
|
||||
|
||||
.vp-npm-badge a {
|
||||
display: inline-flex;
|
||||
text-decoration: none;
|
||||
text-underline-offset: 0;
|
||||
}
|
||||
</style>
|
||||
37
theme/src/client/features/components/NpmBadgeGroup.vue
Normal file
37
theme/src/client/features/components/NpmBadgeGroup.vue
Normal file
@ -0,0 +1,37 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, toRef } from 'vue'
|
||||
import type { NpmBadgeGroupOptions, NpmBadgeType } from '../composables/npm-badge.js'
|
||||
import { useNpmBadgeGroup } from '../composables/npm-badge.js'
|
||||
import NpmBadge from './NpmBadge.vue'
|
||||
|
||||
const props = defineProps<NpmBadgeGroupOptions>()
|
||||
|
||||
useNpmBadgeGroup(toRef(() => props))
|
||||
|
||||
const list = computed(() => {
|
||||
if (!props.items)
|
||||
return []
|
||||
|
||||
if (Array.isArray(props.items))
|
||||
return props.items
|
||||
|
||||
return props.items.split(',').map(type => type.trim()) as NpmBadgeType[]
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<p class="vp-npm-badge-group">
|
||||
<slot>
|
||||
<NpmBadge v-for="(type, index) in list" :key="type + index" :type="type" />
|
||||
</slot>
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.vp-npm-badge-group {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
155
theme/src/client/features/components/RepoCard.vue
Normal file
155
theme/src/client/features/components/RepoCard.vue
Normal file
@ -0,0 +1,155 @@
|
||||
<script setup lang="ts">
|
||||
import { toRef } from 'vue'
|
||||
import { useGithubRepo } from '../composables/github-repo.js'
|
||||
|
||||
const props = defineProps<{
|
||||
repo: string
|
||||
}>()
|
||||
const { loaded, data } = useGithubRepo(toRef(props, 'repo'))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="loaded && data" class="vp-repo-card">
|
||||
<p class="repo-name">
|
||||
<span class="vpi-github-repo" />
|
||||
<span class="repo-link">
|
||||
<a :href="data.url" target="_blank" rel="noopener noreferrer">
|
||||
{{ data.ownerType === 'Organization' ? data.fullName : data.name }}
|
||||
</a>
|
||||
</span>
|
||||
<span class="repo-visibility">{{ data.visibility + (data.template ? ' Template' : '') }}</span>
|
||||
</p>
|
||||
<p class="repo-desc">
|
||||
{{ data.description }}
|
||||
</p>
|
||||
<div class="repo-info">
|
||||
<p v-if="data.language">
|
||||
<span
|
||||
class="repo-language" :style="{ 'background-color': data.languageColor }"
|
||||
/><span>{{ data.language }}</span>
|
||||
</p>
|
||||
<p><span class="vpi-github-star" /><span>{{ data.stars }}</span></p>
|
||||
<p><span class="vpi-github-fork" /><span>{{ data.forks }}</span></p>
|
||||
<p v-if="data.license">
|
||||
<span class="vpi-github-license" /><span>{{ data.license.name }}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.vp-repo-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 16px 20px;
|
||||
margin: 16px 0;
|
||||
border: solid 1px var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
transition: border-color var(--t-color);
|
||||
}
|
||||
|
||||
.vp-repo-card:hover {
|
||||
border-color: var(--vp-c-brand-2);
|
||||
}
|
||||
|
||||
.vp-repo-card p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.vp-repo-card .repo-name {
|
||||
display: flex;
|
||||
gap: 0 8px;
|
||||
align-items: center;
|
||||
max-width: 100%;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.vp-repo-card .repo-link {
|
||||
flex: 1;
|
||||
width: 1px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.vp-repo-card .repo-name a {
|
||||
max-width: 100%;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-brand-1);
|
||||
text-decoration: none;
|
||||
transition: color var(--t-color);
|
||||
}
|
||||
|
||||
.vp-repo-card .repo-name a:hover {
|
||||
color: var(--vp-c-brand-2);
|
||||
}
|
||||
|
||||
.vp-repo-card .repo-visibility {
|
||||
display: inline-block;
|
||||
padding: 0 8px;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
color: var(--vp-c-text-2);
|
||||
border: solid 1px var(--vp-c-divider);
|
||||
border-radius: 22px;
|
||||
transition: color var(--t-color), border var(--t-color);
|
||||
}
|
||||
|
||||
.vp-repo-card .repo-desc {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
color: var(--vp-c-text-2);
|
||||
transition: color var(--t-color);
|
||||
}
|
||||
|
||||
.vp-repo-card .repo-info {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
.vp-repo-card .repo-info p {
|
||||
display: flex;
|
||||
gap: 0 4px;
|
||||
align-items: center;
|
||||
color: var(--vp-c-text-2);
|
||||
transition: color var(--t-color);
|
||||
}
|
||||
|
||||
.vp-repo-card .repo-info p [class^="vpi-github-"] {
|
||||
color: var(--vp-c-text-1);
|
||||
transition: color var(--t-color);
|
||||
}
|
||||
|
||||
.vp-repo-card .repo-language {
|
||||
display: inline-block;
|
||||
width: 0.8em;
|
||||
height: 0.8em;
|
||||
border-radius: 100%;
|
||||
}
|
||||
|
||||
.vpi-github-repo {
|
||||
--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='1em' height='1em' viewBox='0 0 16 16'%3E%3Cpath fill='%23000' d='M2 2.5A2.5 2.5 0 0 1 4.5 0h8.75a.75.75 0 0 1 .75.75v12.5a.75.75 0 0 1-.75.75h-2.5a.75.75 0 0 1 0-1.5h1.75v-2h-8a1 1 0 0 0-.714 1.7a.75.75 0 1 1-1.072 1.05A2.5 2.5 0 0 1 2 11.5Zm10.5-1h-8a1 1 0 0 0-1 1v6.708A2.5 2.5 0 0 1 4.5 9h8ZM5 12.25a.25.25 0 0 1 .25-.25h3.5a.25.25 0 0 1 .25.25v3.25a.25.25 0 0 1-.4.2l-1.45-1.087a.25.25 0 0 0-.3 0L5.4 15.7a.25.25 0 0 1-.4-.2Z'/%3E%3C/svg%3E");
|
||||
|
||||
color: var(--vp-c-text-2);
|
||||
transition: color var(--t-color);
|
||||
transform: translateY(2px);
|
||||
}
|
||||
|
||||
.vpi-github-star {
|
||||
--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='1em' height='1em' viewBox='0 0 256 256'%3E%3Cpath fill='%23000' d='M243 96a20.33 20.33 0 0 0-17.74-14l-56.59-4.57l-21.84-52.81a20.36 20.36 0 0 0-37.66 0L87.35 77.44L30.76 82a20.45 20.45 0 0 0-11.66 35.88l43.18 37.24l-13.2 55.7A20.37 20.37 0 0 0 79.57 233L128 203.19L176.43 233a20.39 20.39 0 0 0 30.49-22.15l-13.2-55.7l43.18-37.24A20.43 20.43 0 0 0 243 96m-70.47 45.7a12 12 0 0 0-3.84 11.86L181.58 208l-47.29-29.08a12 12 0 0 0-12.58 0L74.42 208l12.89-54.4a12 12 0 0 0-3.84-11.86l-42.27-36.5l55.4-4.47a12 12 0 0 0 10.13-7.38L128 41.89l21.27 51.5a12 12 0 0 0 10.13 7.38l55.4 4.47Z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.vpi-github-fork {
|
||||
--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='1em' height='1em' viewBox='0 0 256 256'%3E%3Cpath fill='%23000' d='M228 64a36 36 0 1 0-48 33.94V112a4 4 0 0 1-4 4H80a4 4 0 0 1-4-4V97.94a36 36 0 1 0-24 0V112a28 28 0 0 0 28 28h36v18.06a36 36 0 1 0 24 0V140h36a28 28 0 0 0 28-28V97.94A36.07 36.07 0 0 0 228 64M64 52a12 12 0 1 1-12 12a12 12 0 0 1 12-12m64 152a12 12 0 1 1 12-12a12 12 0 0 1-12 12m64-128a12 12 0 1 1 12-12a12 12 0 0 1-12 12'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.vpi-github-license {
|
||||
--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='1em' height='1em' viewBox='0 0 16 16'%3E%3Cpath fill='none' stroke='%23000' stroke-linecap='round' stroke-linejoin='round' d='M4.5 13.5h7M8.01 1v12.06M1.5 3.5h3l1.5-1h4l1.5 1h3M.5 10L3 4.48L5.5 10C4 11 2 11 .5 10m10 0L13 4.48L15.5 10c-1.5 1-3.5 1-5 0'/%3E%3C/svg%3E");
|
||||
}
|
||||
</style>
|
||||
48
theme/src/client/features/composables/github-repo.ts
Normal file
48
theme/src/client/features/composables/github-repo.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import { computed, ref, toValue, watch } from 'vue'
|
||||
import type { MaybeRef } from 'vue'
|
||||
|
||||
export interface GithubRepoInfo {
|
||||
name: string
|
||||
fullName: string
|
||||
description: string
|
||||
url: string
|
||||
stars: number
|
||||
forks: number
|
||||
watchers: number
|
||||
language: string
|
||||
languageColor: string
|
||||
visibility: 'Private' | 'Public' // private, public
|
||||
template: boolean
|
||||
ownerType: 'User' | 'Organization'
|
||||
license: {
|
||||
name: string
|
||||
url: string
|
||||
} | null
|
||||
}
|
||||
|
||||
export function useGithubRepo(repo: MaybeRef<string>) {
|
||||
const repoRef = computed(() => {
|
||||
const info = toValue(repo)
|
||||
const [owner = '', name = ''] = info.split('/')
|
||||
return { owner, name }
|
||||
})
|
||||
const data = ref<GithubRepoInfo | null>(null)
|
||||
const loaded = ref(false)
|
||||
|
||||
async function fetchData() {
|
||||
const { owner, name } = repoRef.value
|
||||
if (__VUEPRESS_SSR__ || !owner || !name)
|
||||
return
|
||||
|
||||
loaded.value = false
|
||||
const res = await fetch(`https://api.pengzhanbo.cn/github/repo/${owner}/${name}`)
|
||||
.then(res => res.json()) as GithubRepoInfo
|
||||
|
||||
data.value = res
|
||||
loaded.value = true
|
||||
}
|
||||
|
||||
watch(repoRef, fetchData, { immediate: true })
|
||||
|
||||
return { data, loaded }
|
||||
}
|
||||
175
theme/src/client/features/composables/npm-badge.ts
Normal file
175
theme/src/client/features/composables/npm-badge.ts
Normal file
@ -0,0 +1,175 @@
|
||||
import { computed, inject, provide, ref, toValue } from 'vue'
|
||||
import type { ComputedRef, InjectionKey, Ref } from 'vue'
|
||||
|
||||
const DEFAULT_COLOR = '#32A9C3'
|
||||
const DEFAULT_LABEL_COLOR = '#1B3C4A'
|
||||
const BADGE_URL = 'https://img.shields.io'
|
||||
const GITHUB_URL = 'https://github.com'
|
||||
const NPM_URL = 'https://www.npmjs.com/package'
|
||||
|
||||
export type NpmBadgeType =
|
||||
// github
|
||||
| 'source' // github source
|
||||
| 'stars' // github stars
|
||||
| 'forks' // github forks
|
||||
| 'license' // github license
|
||||
// npm
|
||||
| 'version' // npm version
|
||||
| 'dt' // alias d18m
|
||||
| 'd18m' // npm downloads last 18 months
|
||||
| 'dw' // npm downloads weekly
|
||||
| 'dm' // npm downloads monthly
|
||||
| 'dy' // npm downloads yearly
|
||||
|
||||
export type NpmBadgeTheme = 'flat' | 'flat-square' | 'plastic' | 'for-the-badge' | 'social'
|
||||
|
||||
export interface NpmBadgeBaseOptions {
|
||||
// npm package name
|
||||
name?: string
|
||||
// github repo
|
||||
repo?: string
|
||||
// github branch
|
||||
branch?: string
|
||||
// github directory
|
||||
dir?: string
|
||||
// text color
|
||||
color?: string
|
||||
// label color
|
||||
labelColor?: string
|
||||
// badge style
|
||||
theme?: NpmBadgeTheme
|
||||
}
|
||||
|
||||
export interface NpmBadgeOptions extends NpmBadgeBaseOptions {
|
||||
type: NpmBadgeType
|
||||
// label text
|
||||
label?: string
|
||||
}
|
||||
|
||||
export interface NpmBadgeGroupOptions extends Omit<NpmBadgeBaseOptions, 'label'> {
|
||||
items?: string | NpmBadgeType | NpmBadgeType[]
|
||||
}
|
||||
|
||||
export interface NpmBadgeInfo {
|
||||
badgeUrl: string
|
||||
link?: string
|
||||
alt?: string
|
||||
}
|
||||
|
||||
type NpmBadgeBaseOptionsRef = Ref<NpmBadgeBaseOptions>
|
||||
|
||||
const NpmBadgeSymbol: InjectionKey<NpmBadgeBaseOptionsRef> = Symbol(__VUEPRESS_DEV__ ? 'NpmBadge' : '')
|
||||
|
||||
export function useNpmBadge(opt: Ref<NpmBadgeOptions>): ComputedRef<NpmBadgeInfo> {
|
||||
const parentOpt = inject(NpmBadgeSymbol, ref({}) as NpmBadgeBaseOptionsRef)
|
||||
|
||||
return computed(() => {
|
||||
const po = toValue(parentOpt)
|
||||
const o = toValue(opt)
|
||||
const res: NpmBadgeOptions = {
|
||||
name: o.name || po.name,
|
||||
repo: o.repo || po.repo,
|
||||
branch: o.branch || po.branch,
|
||||
dir: o.dir || po.dir,
|
||||
type: o.type,
|
||||
color: o.color || po.color,
|
||||
label: o.label,
|
||||
labelColor: o.labelColor || po.labelColor,
|
||||
theme: o.theme || po.theme,
|
||||
}
|
||||
return resolveNpmBadgeOptions(res)
|
||||
})
|
||||
}
|
||||
|
||||
export function useNpmBadgeGroup(opt: Ref<NpmBadgeGroupOptions>) {
|
||||
const baseOptions = computed<NpmBadgeBaseOptions>(() => {
|
||||
const o = toValue(opt)
|
||||
return {
|
||||
name: o.name,
|
||||
repo: o.repo,
|
||||
branch: o.branch,
|
||||
dir: o.dir,
|
||||
color: o.color,
|
||||
labelColor: o.labelColor,
|
||||
theme: o.theme,
|
||||
}
|
||||
})
|
||||
|
||||
provide(NpmBadgeSymbol, baseOptions)
|
||||
}
|
||||
|
||||
function resolveNpmBadgeOptions(options: NpmBadgeOptions): NpmBadgeInfo {
|
||||
let { name = '', repo = '', branch = 'main', dir = '', type, color, label, labelColor, theme = '' } = options
|
||||
name = name || repo.split('/')?.[1] || ''
|
||||
const normalizeName = encodeURIComponent(name)
|
||||
|
||||
const githubLink = repo ? `${GITHUB_URL}/${repo}${dir ? `/tree/${branch}/${dir}` : ''}` : ''
|
||||
const npmLink = `${NPM_URL}/${name}`
|
||||
|
||||
const params = new URLSearchParams()
|
||||
|
||||
if (type !== 'source' && type !== 'stars' && type !== 'forks') {
|
||||
params.append('style', theme || 'flat')
|
||||
params.append('color', color || DEFAULT_COLOR)
|
||||
params.append('labelColor', labelColor || DEFAULT_LABEL_COLOR)
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case 'source': {
|
||||
params.append('logo', 'github')
|
||||
params.append('color', labelColor || DEFAULT_LABEL_COLOR)
|
||||
return {
|
||||
badgeUrl: `${BADGE_URL}/badge/source-a?${params.toString()}`,
|
||||
link: githubLink,
|
||||
alt: 'github source',
|
||||
}
|
||||
}
|
||||
case 'stars':
|
||||
case 'forks': {
|
||||
params.append('style', theme || 'social')
|
||||
return {
|
||||
badgeUrl: `${BADGE_URL}/github/${type}/${repo}?${params.toString()}`,
|
||||
link: githubLink,
|
||||
alt: `github ${type}`,
|
||||
}
|
||||
}
|
||||
case 'license':
|
||||
return {
|
||||
badgeUrl: `${BADGE_URL}/github/license/${repo}?${params.toString()}`,
|
||||
link: githubLink,
|
||||
alt: 'license',
|
||||
}
|
||||
case 'version': {
|
||||
params.append('label', label || name || 'npm')
|
||||
return {
|
||||
badgeUrl: `${BADGE_URL}/npm/v/${normalizeName}?${params.toString()}`,
|
||||
link: npmLink,
|
||||
alt: 'npm version',
|
||||
}
|
||||
}
|
||||
case 'dt':
|
||||
case 'd18m': {
|
||||
params.append('label', label || 'downloads')
|
||||
return {
|
||||
badgeUrl: `${BADGE_URL}/npm/d18m/${normalizeName}?${params.toString()}`,
|
||||
link: npmLink,
|
||||
alt: 'npm downloads',
|
||||
}
|
||||
}
|
||||
case 'dm':
|
||||
case 'dy':
|
||||
case 'dw': {
|
||||
params.append('label', label || 'downloads')
|
||||
return {
|
||||
badgeUrl: `${BADGE_URL}/npm/${type}/${normalizeName}?${params.toString()}`,
|
||||
link: npmLink,
|
||||
alt: 'npm downloads',
|
||||
}
|
||||
}
|
||||
default:
|
||||
return {
|
||||
badgeUrl: `${BADGE_URL}/badge/unknown?${params.toString()}`,
|
||||
alt: 'unknown',
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -194,34 +194,36 @@ export function resolveOptions(
|
||||
include: '**/{readme,README,index}.md',
|
||||
frontmatter: {},
|
||||
},
|
||||
{
|
||||
include: localeOptions.blog?.include ?? ['**/*.md'],
|
||||
frontmatter: {
|
||||
...options.title !== false
|
||||
? {
|
||||
title(title: string, { relativePath }) {
|
||||
if (title)
|
||||
return title
|
||||
const basename = path.basename(relativePath || '', '.md')
|
||||
return basename
|
||||
},
|
||||
} as AutoFrontmatterObject
|
||||
: undefined,
|
||||
...baseFrontmatter,
|
||||
...options.permalink !== false
|
||||
? {
|
||||
permalink(permalink: string, { relativePath }) {
|
||||
if (permalink)
|
||||
return permalink
|
||||
const locale = resolveLocale(relativePath)
|
||||
const prefix = withBase(articlePrefix, locale)
|
||||
localeOptions.blog !== false
|
||||
? {
|
||||
include: localeOptions.blog?.include ?? ['**/*.md'],
|
||||
frontmatter: {
|
||||
...options.title !== false
|
||||
? {
|
||||
title(title: string, { relativePath }) {
|
||||
if (title)
|
||||
return title
|
||||
const basename = path.basename(relativePath || '', '.md')
|
||||
return basename
|
||||
},
|
||||
} as AutoFrontmatterObject
|
||||
: undefined,
|
||||
...baseFrontmatter,
|
||||
...options.permalink !== false
|
||||
? {
|
||||
permalink(permalink: string, { relativePath }) {
|
||||
if (permalink)
|
||||
return permalink
|
||||
const locale = resolveLocale(relativePath)
|
||||
const prefix = withBase(articlePrefix, locale)
|
||||
|
||||
return normalizePath(`${prefix}/${nanoid()}/`)
|
||||
},
|
||||
} as AutoFrontmatterObject
|
||||
: undefined,
|
||||
},
|
||||
},
|
||||
return normalizePath(`${prefix}/${nanoid()}/`)
|
||||
},
|
||||
} as AutoFrontmatterObject
|
||||
: undefined,
|
||||
},
|
||||
}
|
||||
: '',
|
||||
|
||||
{
|
||||
include: '*',
|
||||
|
||||
@ -35,8 +35,6 @@ export function resolveThemeData(app: App, options: PlumeThemeLocaleOptions): Pl
|
||||
}
|
||||
})
|
||||
|
||||
const blog = options.blog || {}
|
||||
const blogLink = blog.link || '/blog/'
|
||||
entries(options.locales || {}).forEach(([locale, opt]) => {
|
||||
// 注入预设 导航栏
|
||||
// home | blog | tags | archives
|
||||
@ -47,21 +45,25 @@ export function resolveThemeData(app: App, options: PlumeThemeLocaleOptions): Pl
|
||||
text: PRESET_LOCALES[localePath].home,
|
||||
link: locale,
|
||||
}]
|
||||
navbar.push({
|
||||
text: PRESET_LOCALES[localePath].blog,
|
||||
link: withBase(blogLink, locale),
|
||||
})
|
||||
if (blog.tags !== false) {
|
||||
if (options.blog !== false) {
|
||||
const blog = options.blog || {}
|
||||
const blogLink = blog.link || '/blog/'
|
||||
navbar.push({
|
||||
text: PRESET_LOCALES[localePath].tag,
|
||||
link: withBase(blog.tagsLink || `${blogLink}/tags/`, locale),
|
||||
})
|
||||
}
|
||||
if (blog.archives !== false) {
|
||||
navbar.push({
|
||||
text: PRESET_LOCALES[localePath].archive,
|
||||
link: withBase(blog.archivesLink || `${blogLink}/archives/`, locale),
|
||||
text: PRESET_LOCALES[localePath].blog,
|
||||
link: withBase(blogLink, locale),
|
||||
})
|
||||
if (blog.tags !== false) {
|
||||
navbar.push({
|
||||
text: PRESET_LOCALES[localePath].tag,
|
||||
link: withBase(blog.tagsLink || `${blogLink}/tags/`, locale),
|
||||
})
|
||||
}
|
||||
if (blog.archives !== false) {
|
||||
navbar.push({
|
||||
text: PRESET_LOCALES[localePath].archive,
|
||||
link: withBase(blog.archivesLink || `${blogLink}/archives/`, locale),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
themeData.locales![locale].navbar = navbar
|
||||
|
||||
@ -25,6 +25,12 @@ export async function preparedBlogData(
|
||||
localeOptions: PlumeThemeLocaleOptions,
|
||||
encrypt?: PlumeThemeEncrypt,
|
||||
): Promise<void> {
|
||||
if (localeOptions.blog === false) {
|
||||
const content = resolveContent(app, { name: 'blogPostData', content: [] })
|
||||
await writeTemp(app, 'internal/blogData.js', content)
|
||||
return
|
||||
}
|
||||
|
||||
const start = performance.now()
|
||||
|
||||
const blog = localeOptions.blog || {}
|
||||
|
||||
@ -16,8 +16,8 @@ interface IconData {
|
||||
type CollectMap = Record<string, string[]>
|
||||
type IconDataMap = Record<string, IconData>
|
||||
|
||||
const ICON_REGEXP = /<(?:VP)?Icon(?:ify)?([^>]*)>/g
|
||||
const ICON_NAME_REGEXP = /name="([^"]+)"/
|
||||
const ICON_REGEXP = /<(?:VP)?(Icon|Card|LinkCard)([^>]*)>/g
|
||||
const ICON_NAME_REGEXP = /(?:name|icon)="([^"]+)"/
|
||||
const URL_CONTENT_REGEXP = /(url\([\s\S]+\))/
|
||||
const JS_FILENAME = 'internal/iconify.js'
|
||||
const CSS_FILENAME = 'internal/iconify.css'
|
||||
|
||||
@ -18,6 +18,9 @@ export async function setupPage(
|
||||
app: App,
|
||||
localeOption: PlumeThemeLocaleOptions,
|
||||
) {
|
||||
if (localeOption.blog === false)
|
||||
return
|
||||
|
||||
const pageList: Promise<Page>[] = []
|
||||
const locales = localeOption.locales || {}
|
||||
const rootPath = getRootLangPath(app)
|
||||
|
||||
@ -64,7 +64,7 @@ export interface PlumeThemeLocaleData extends LocaleData {
|
||||
/**
|
||||
* 博客配置
|
||||
*/
|
||||
blog?: PlumeThemeBlog
|
||||
blog?: false | PlumeThemeBlog
|
||||
|
||||
/**
|
||||
* 文章链接前缀
|
||||
@ -274,7 +274,7 @@ export interface PlumeThemeLocaleData extends LocaleData {
|
||||
encryptPlaceholder?: string
|
||||
}
|
||||
|
||||
/** =========================== Avatar ================================ */
|
||||
/** =========================== Profile ================================ */
|
||||
|
||||
/**
|
||||
* 个人资料
|
||||
|
||||
@ -1,3 +1,6 @@
|
||||
import process from 'node:process'
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { type Options, defineConfig } from 'tsup'
|
||||
|
||||
const sharedExternal: (string | RegExp)[] = [
|
||||
@ -12,6 +15,11 @@ const clientExternal: (string | RegExp)[] = [
|
||||
/.*\.css$/,
|
||||
]
|
||||
|
||||
const featuresComposables = fs.readdirSync(
|
||||
path.join(process.cwd(), 'src/client/features/composables'),
|
||||
{ recursive: true, encoding: 'utf-8' },
|
||||
)
|
||||
|
||||
export default defineConfig((cli) => {
|
||||
const DEFAULT_OPTIONS: Options = {
|
||||
dts: !cli.watch,
|
||||
@ -35,6 +43,7 @@ export default defineConfig((cli) => {
|
||||
outDir: './lib/node',
|
||||
external: sharedExternal,
|
||||
target: 'node18',
|
||||
watch: false,
|
||||
},
|
||||
// client/utils/index.js
|
||||
{
|
||||
@ -77,5 +86,16 @@ export default defineConfig((cli) => {
|
||||
'./config.js',
|
||||
],
|
||||
},
|
||||
...featuresComposables.map(file => ({
|
||||
...DEFAULT_OPTIONS,
|
||||
entry: [`./src/client/features/composables/${file}`],
|
||||
outDir: `./lib/client/features/composables/`,
|
||||
external: [
|
||||
...clientExternal,
|
||||
'../../composables/index.js',
|
||||
'../../utils/index.js',
|
||||
...featuresComposables.map(file => `./${file.replace('.ts', '.js')}`),
|
||||
],
|
||||
})),
|
||||
]
|
||||
})
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user