refactor(theme): 🔥 theme moved to https://github.com/project-trans/vitepress-theme-project-trans
parent
ce3fc14441
commit
37c40df9c3
29
package.json
29
package.json
|
@ -10,30 +10,29 @@
|
|||
"dev:wrangler": "wrangler pages dev ./docs/.vitepress/dist/",
|
||||
"build": "vitepress build docs",
|
||||
"preview": "vitepress preview docs",
|
||||
"update-package": "pnpm dlx vp-update",
|
||||
"postinstall": "pnpm --filter @project-trans/* build"
|
||||
"update-package": "pnpm dlx vp-update"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@antfu/eslint-config": "^2.21.1",
|
||||
"@cloudflare/workers-types": "^4.20240620.0",
|
||||
"@iconify-json/carbon": "^1.1.36",
|
||||
"@iconify-json/icon-park-outline": "^1.1.15",
|
||||
"@iconify-json/octicon": "^1.1.55",
|
||||
"@antfu/eslint-config": "^2.27.3",
|
||||
"@cloudflare/workers-types": "^4.20240919.0",
|
||||
"@iconify-json/carbon": "^1.2.1",
|
||||
"@iconify-json/icon-park-outline": "^1.2.0",
|
||||
"@iconify-json/octicon": "^1.2.0",
|
||||
"@project-trans/suggestion-box": "^0.0.9",
|
||||
"@project-trans/vitepress-theme-project-trans": "workspace:*",
|
||||
"@types/markdown-it": "^13.0.8",
|
||||
"@project-trans/vitepress-theme-project-trans": "^0.3.1726846403",
|
||||
"@types/markdown-it": "^13.0.9",
|
||||
"@types/markdown-it-footnote": "^3.0.4",
|
||||
"@unocss/eslint-plugin": "^0.59.4",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint": "^8.57.1",
|
||||
"eslint-plugin-format": "^0.1.2",
|
||||
"simple-git": "^3.25.0",
|
||||
"simple-git": "^3.27.0",
|
||||
"unbuild": "^2.0.0",
|
||||
"unocss": "^0.58.9",
|
||||
"unplugin-vue-components": "^0.26.0",
|
||||
"vite": "^5.3.1",
|
||||
"vitepress": "^1.2.3",
|
||||
"vitepress-sidebar": "^1.23.2",
|
||||
"vue": "^3.4.30"
|
||||
"vite": "^5.4.7",
|
||||
"vitepress": "^1.3.4",
|
||||
"vitepress-sidebar": "^1.27.0",
|
||||
"vue": "^3.5.6"
|
||||
},
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
|
|
|
@ -1,44 +0,0 @@
|
|||
# VitePress Theme Project Trans
|
||||
|
||||
这是一个由 Project Trans 开发的 VitePress 主题。
|
||||
|
||||
## 使用方式
|
||||
|
||||
修改下述两个文件:
|
||||
|
||||
```typescript
|
||||
// docs/.vitepress/config.ts
|
||||
import genConfig from '@project-trans/vitepress-theme-project-trans/config'
|
||||
import type { SidebarOptions } from '@project-trans/vitepress-theme-project-trans/theme'
|
||||
import type { ThemeContext } from '@project-trans/vitepress-theme-project-trans/utils'
|
||||
import { withThemeContext } from '@project-trans/vitepress-theme-project-trans/utils'
|
||||
|
||||
const themeConfig: ThemeContext = {
|
||||
siteTitle: 'RLE.wiki',
|
||||
siteDescription: '一份 RLE 指北',
|
||||
/** Repo */
|
||||
githubRepoLink: 'https://github.com/project-trans/RLE-wiki',
|
||||
/** vitepress 根目录 */
|
||||
rootDir: 'docs',
|
||||
/** 文档所在目录(目前似未使用此项) */
|
||||
include: ['campus', 'contributor-guide', 'fashion'],
|
||||
nav,
|
||||
sidebarOptions,
|
||||
}
|
||||
|
||||
// https://vitepress.dev/reference/site-config
|
||||
export default withThemeContext(themeConfig, genConfig)
|
||||
```
|
||||
|
||||
```typescript
|
||||
// docs/.vitepress/theme/index.ts
|
||||
// https://vitepress.dev/guide/custom-theme
|
||||
import PtjsTheme from '@project-trans/vitepress-theme-project-trans/theme'
|
||||
|
||||
import 'uno.css'
|
||||
import './style.css'
|
||||
|
||||
export default {
|
||||
extends: PtjsTheme,
|
||||
}
|
||||
```
|
|
@ -1,33 +0,0 @@
|
|||
import { defineBuildConfig } from 'unbuild'
|
||||
|
||||
export default defineBuildConfig({
|
||||
entries: [
|
||||
{
|
||||
builder: 'mkdist',
|
||||
input: './src',
|
||||
outDir: './dist',
|
||||
pattern: ['**/*.ts', '**/*.css'],
|
||||
format: 'esm',
|
||||
loaders: ['js'],
|
||||
},
|
||||
{
|
||||
builder: 'mkdist',
|
||||
input: './src',
|
||||
outDir: './dist',
|
||||
pattern: ['**/*.ts', '**/*.css'],
|
||||
format: 'cjs',
|
||||
loaders: ['js'],
|
||||
},
|
||||
{
|
||||
builder: 'mkdist',
|
||||
input: './src',
|
||||
outDir: './dist',
|
||||
pattern: ['**/*.vue'],
|
||||
loaders: ['vue'],
|
||||
},
|
||||
],
|
||||
declaration: true,
|
||||
clean: true,
|
||||
sourcemap: true,
|
||||
rollup: { emitCJS: true },
|
||||
})
|
|
@ -1,71 +0,0 @@
|
|||
{
|
||||
"name": "@project-trans/vitepress-theme-project-trans",
|
||||
"type": "module",
|
||||
"version": "0.0.1",
|
||||
"description": "A VitePress theme made with ❤️ by project-trans",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/project-trans/RLE-wiki"
|
||||
},
|
||||
"exports": {
|
||||
"./config": {
|
||||
"types": "./dist/config.d.ts",
|
||||
"import": "./dist/config.mjs",
|
||||
"require": "./dist/config.js"
|
||||
},
|
||||
"./utils": {
|
||||
"types": "./dist/utils/index.d.ts",
|
||||
"import": "./dist/utils/index.mjs",
|
||||
"require": "./dist/utils/index.js"
|
||||
},
|
||||
"./theme": {
|
||||
"types": "./dist/theme.d.ts",
|
||||
"import": "./dist/theme.mjs",
|
||||
"require": "./dist/theme.js"
|
||||
},
|
||||
"./components": {
|
||||
"types": "./dist/components.d.ts",
|
||||
"import": "./dist/components.mjs",
|
||||
"require": "./dist/components.js"
|
||||
}
|
||||
},
|
||||
"main": "./dist/config.js",
|
||||
"module": "./dist/config.mjs",
|
||||
"types": "./dist/config.d.ts",
|
||||
"scripts": {
|
||||
"build": "unbuild",
|
||||
"dev": "vitepress dev",
|
||||
"dev:wrangler": "wrangler pages dev ./dist/",
|
||||
"preview": "vitepress preview",
|
||||
"update-package": "pnpm dlx vp-update"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@antfu/eslint-config": "^2.21.1",
|
||||
"@cloudflare/workers-types": "^4.20240620.0",
|
||||
"@iconify-json/octicon": "^1.1.55",
|
||||
"@nolebase/vitepress-plugin-enhanced-readabilities": "^2.1.2",
|
||||
"@nolebase/vitepress-plugin-git-changelog": "^2.1.2",
|
||||
"@nolebase/vitepress-plugin-highlight-targeted-heading": "^2.1.2",
|
||||
"@nolebase/vitepress-plugin-meta": "^2.2.1",
|
||||
"@project-trans/suggestion-box": "^0.0.9",
|
||||
"@types/markdown-it": "^14.1.1",
|
||||
"@types/markdown-it-footnote": "^3.0.4",
|
||||
"@types/node": "^20.14.8",
|
||||
"@unocss/eslint-plugin": "^0.61.0",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-plugin-format": "^0.1.2",
|
||||
"markdown-it-footnote": "^4.0.0",
|
||||
"markdown-it-katex": "^2.0.3",
|
||||
"markdown-it-pangu": "^1.0.2",
|
||||
"md5": "^2.3.0",
|
||||
"simple-git": "^3.25.0",
|
||||
"unocss": "^0.61.0",
|
||||
"unplugin-vue-components": "^0.27.0",
|
||||
"vite": "^5.3.1",
|
||||
"vitepress": "^1.2.3",
|
||||
"vitepress-sidebar": "^1.23.2",
|
||||
"vue": "^3.4.30",
|
||||
"wrangler": "^3.61.0"
|
||||
}
|
||||
}
|
|
@ -1,50 +0,0 @@
|
|||
<script lang="ts" setup>
|
||||
import DefaultTheme from 'vitepress/theme'
|
||||
import {
|
||||
NolebaseHighlightTargetedHeading,
|
||||
} from '@nolebase/vitepress-plugin-highlight-targeted-heading/client'
|
||||
import {
|
||||
NolebaseEnhancedReadabilitiesMenu,
|
||||
NolebaseEnhancedReadabilitiesScreenMenu,
|
||||
} from '@nolebase/vitepress-plugin-enhanced-readabilities/client'
|
||||
|
||||
import { useData } from 'vitepress'
|
||||
import CopyrightInfo from './components/CopyrightInfo.vue'
|
||||
import AppFooter from './components/AppFooter.vue'
|
||||
import AppearanceToggle from './components/AppearanceToggle.vue'
|
||||
import PageInfo from './components/PageInfo.vue'
|
||||
|
||||
const { Layout } = DefaultTheme
|
||||
const { frontmatter } = useData()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AppearanceToggle>
|
||||
<Layout>
|
||||
<template #doc-before>
|
||||
<NolebaseHighlightTargetedHeading />
|
||||
<div class="vp-doc vp-doc-before">
|
||||
<h1>{{ frontmatter.title }}</h1>
|
||||
<PageInfo />
|
||||
<CopyrightInfo />
|
||||
</div>
|
||||
</template>
|
||||
<template #doc-after>
|
||||
<AppFooter />
|
||||
</template>
|
||||
<template #nav-bar-content-after>
|
||||
<NolebaseEnhancedReadabilitiesMenu />
|
||||
</template>
|
||||
<template #nav-screen-content-after>
|
||||
<NolebaseEnhancedReadabilitiesScreenMenu />
|
||||
</template>
|
||||
</Layout>
|
||||
</AppearanceToggle>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.vp-doc-before + main > div > div > h2:first-of-type {
|
||||
border-top: unset;
|
||||
margin-top: 0;
|
||||
}
|
||||
</style>
|
|
@ -1,19 +0,0 @@
|
|||
import AppearanceToggle from './components/AppearanceToggle.vue'
|
||||
import AppFooter from './components/AppFooter.vue'
|
||||
import AppSBox from './components/AppSBox.vue'
|
||||
import ArticlesMenu from './components/ArticlesMenu.vue'
|
||||
import CopyrightInfo from './components/CopyrightInfo.vue'
|
||||
import HomeContent from './components/HomeContent.vue'
|
||||
import PageInfo from './components/PageInfo.vue'
|
||||
import ReadingTime from './components/ReadingTime.vue'
|
||||
|
||||
export {
|
||||
AppFooter,
|
||||
AppSBox,
|
||||
AppearanceToggle,
|
||||
ArticlesMenu,
|
||||
CopyrightInfo,
|
||||
HomeContent,
|
||||
PageInfo,
|
||||
ReadingTime,
|
||||
}
|
|
@ -1,39 +0,0 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, watch, } from 'vue';
|
||||
import { NolebaseGitChangelog } from '@nolebase/vitepress-plugin-git-changelog/client';
|
||||
import { useRoute } from 'vitepress';
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
// 定义一个 ref 来存储动态 key
|
||||
const componentKey = ref(0);
|
||||
const frontmatter = ref({});
|
||||
const isFrontmatterLoaded = ref(false);
|
||||
|
||||
// 更新 key 和 frontmatter 的函数
|
||||
const updateKeyAndFrontmatter = () => {
|
||||
componentKey.value += 1;
|
||||
frontmatter.value = route.data?.frontmatter || {};
|
||||
isFrontmatterLoaded.value = true;
|
||||
};
|
||||
|
||||
// 监听路由变化,更新 key 和 frontmatter
|
||||
watch(() => route.path, () => {
|
||||
isFrontmatterLoaded.value = false;
|
||||
updateKeyAndFrontmatter();
|
||||
}, { immediate: true }); // 在组件挂载时立即执行一次,确保第一次渲染时 key 和 frontmatter 是正确的
|
||||
|
||||
// 在组件挂载时更新 key 和 frontmatter
|
||||
// onMounted(updateKeyAndFrontmatter);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :key="componentKey" class="vp-doc">
|
||||
<h2 id="意见反馈">
|
||||
意见反馈
|
||||
</h2>
|
||||
<AppSBox />
|
||||
<!-- 仅在 Frontmatter 加载完成且未设置 hideChangelog 时渲染 GitChangelog -->
|
||||
<NolebaseGitChangelog v-if="isFrontmatterLoaded && !frontmatter.hideChangelog" />
|
||||
</div>
|
||||
</template>
|
|
@ -1,11 +0,0 @@
|
|||
<script setup lang="ts">
|
||||
import SuggestionBox from '@project-trans/suggestion-box'
|
||||
import '@project-trans/suggestion-box/dist/style.css'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SuggestionBox
|
||||
text-content-placeholder="这个页面有什么问题?或者有什么建议?请在下方留言,我们会尽快回复和处理。"
|
||||
target-url="/api/v1/suggestion"
|
||||
/>
|
||||
</template>
|
|
@ -1,76 +0,0 @@
|
|||
<script setup lang="ts">
|
||||
import { useData } from 'vitepress'
|
||||
import { nextTick, provide } from 'vue'
|
||||
|
||||
const { isDark } = useData()
|
||||
|
||||
const isSSR = typeof window === 'undefined'
|
||||
|
||||
function enableTransitions() {
|
||||
return isSSR
|
||||
? false
|
||||
: 'startViewTransition' in document
|
||||
&& window.matchMedia('(prefers-reduced-motion: no-preference)').matches
|
||||
}
|
||||
|
||||
provide('toggle-appearance', async ({ clientX: x, clientY: y }: MouseEvent) => {
|
||||
if (!enableTransitions()) {
|
||||
isDark.value = !isDark.value
|
||||
return
|
||||
}
|
||||
|
||||
if (!document.documentElement.classList.contains('VPSwitchAppearance-ViewTransition'))
|
||||
document.documentElement.classList.add('VPSwitchAppearance-ViewTransition')
|
||||
|
||||
const clipPath = [
|
||||
`circle(0px at ${x}px ${y}px)`,
|
||||
`circle(${Math.hypot(
|
||||
Math.max(x, innerWidth - x),
|
||||
Math.max(y, innerHeight - y),
|
||||
)}px at ${x}px ${y}px)`,
|
||||
]
|
||||
|
||||
await (document as any).startViewTransition(async () => {
|
||||
isDark.value = !isDark.value
|
||||
await nextTick()
|
||||
}).ready
|
||||
|
||||
document.documentElement.animate(
|
||||
{ clipPath: isDark.value ? clipPath.reverse() : clipPath },
|
||||
{
|
||||
duration: 500,
|
||||
easing: 'ease-in',
|
||||
pseudoElement: `::view-transition-${isDark.value ? 'old' : 'new'}(root)`,
|
||||
},
|
||||
)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<slot />
|
||||
</template>
|
||||
|
||||
<style>
|
||||
::view-transition-old(root),
|
||||
::view-transition-new(root) {
|
||||
animation: none;
|
||||
mix-blend-mode: normal;
|
||||
}
|
||||
|
||||
::view-transition-old(root),
|
||||
.dark::view-transition-new(root) {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
::view-transition-new(root),
|
||||
.dark::view-transition-old(root) {
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.VPSwitchAppearance-ViewTransition .VPSwitchAppearance .check {
|
||||
transition: transform 350ms 0ms !important;
|
||||
}
|
||||
.VPSwitchAppearance-ViewTransition.dark .VPSwitchAppearance .check {
|
||||
transition: transform 350ms 500ms !important;
|
||||
}
|
||||
</style>
|
|
@ -1,32 +0,0 @@
|
|||
<script setup lang="ts">
|
||||
import { useRoute } from 'vitepress'
|
||||
import { computed } from 'vue'
|
||||
import { data } from './articlesmenu.data'
|
||||
|
||||
const route = useRoute()
|
||||
const articles = computed(() =>
|
||||
data
|
||||
.filter((article) => {
|
||||
if (!article.url.startsWith(route.path))
|
||||
return false
|
||||
if (article.url === route.path)
|
||||
return false
|
||||
const relateUrl = article.url.replace(route.path, '')
|
||||
const slashCount = relateUrl.split('/').length - 1
|
||||
if (slashCount > 1)
|
||||
return false
|
||||
if (slashCount === 1 && !relateUrl.endsWith('/'))
|
||||
return false
|
||||
return true
|
||||
})
|
||||
.map(article => ({ link: article.url, text: article.title })),
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ul>
|
||||
<li v-for="article in articles" :key="article.link">
|
||||
<a :href="article.link">{{ article.text }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
|
@ -1,68 +0,0 @@
|
|||
<script setup lang="ts">
|
||||
import { useData } from 'vitepress'
|
||||
import { computed } from 'vue'
|
||||
import type { Node, Trie } from '../plugins/CopyrightLoader.data'
|
||||
import { data } from '../plugins/CopyrightLoader.data'
|
||||
|
||||
function searchClosestInTrie(
|
||||
that: Trie<Record<string, any>>,
|
||||
path: string[],
|
||||
node: Node<Record<string, any>> = that.root,
|
||||
): Record<string, any> | null {
|
||||
if (path.length === 0)
|
||||
return node.value
|
||||
|
||||
if (path[0] in node.children) {
|
||||
let value = searchClosestInTrie(
|
||||
that,
|
||||
path.slice(1),
|
||||
node.children[path[0]],
|
||||
)
|
||||
if (value === null)
|
||||
value = node.value
|
||||
|
||||
return value
|
||||
}
|
||||
return node.value
|
||||
}
|
||||
|
||||
const paths = useData()
|
||||
.page.value.relativePath.replace('.md', '').split('/')
|
||||
.filter((item: string) => item !== '')
|
||||
const attrs = computed(() => searchClosestInTrie(data, paths))
|
||||
const frontmatter = useData().frontmatter
|
||||
|
||||
const originUrlExists = computed(() => (attrs.value?.copyright?.url ?? null) != null)
|
||||
const originUrl = computed(() => attrs.value?.copyright?.url ?? 'javascript:void(0)')
|
||||
|
||||
const license = computed(() => attrs.value?.copyright?.license ?? null)
|
||||
const licenseExists = computed(() => license.value != null)
|
||||
const licenseUrlExists = computed(() => (attrs.value?.copyright?.licenseUrl ?? null) != null)
|
||||
const licenseUrl = computed(() => attrs.value?.copyright?.licenseUrl ?? 'javascript:void(0)')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="attrs?.copyright?.enable ?? false">
|
||||
<div class="tip custom-block">
|
||||
<p class="custom-block-title">
|
||||
Copyright
|
||||
</p>
|
||||
<p>
|
||||
<span>这篇文章 </span>
|
||||
<a v-if="originUrlExists" :href="originUrl">{{ frontmatter.title }}</a>
|
||||
<span v-else>{{ frontmatter.title }}</span>
|
||||
<span> 由 </span>
|
||||
<span v-for="author in attrs?.author" :key="author">{{ author }}</span>
|
||||
<span> 创作</span>
|
||||
<span v-if="licenseExists">
|
||||
<span>,Project Trans 在 </span>
|
||||
<a v-if="licenseUrlExists" :href="licenseUrl">{{ license }}</a>
|
||||
<span v-else>{{ license }}</span>
|
||||
<span> 许可下使用</span>
|
||||
</span>
|
||||
<span>。</span>
|
||||
</p>
|
||||
</div>
|
||||
<hr>
|
||||
</div>
|
||||
</template>
|
|
@ -1,27 +0,0 @@
|
|||
<template>
|
||||
<div class="vp-doc container">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.container {
|
||||
margin: auto;
|
||||
width: 100%;
|
||||
max-width: 1280px;
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.container {
|
||||
padding: 0 48px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.container {
|
||||
width: 100%;
|
||||
padding: 0 64px;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -1,70 +0,0 @@
|
|||
<script setup lang="ts">
|
||||
import { useData } from 'vitepress'
|
||||
import { computed, onMounted, ref, watchEffect } from 'vue'
|
||||
import ReadingTime from './ReadingTime.vue' // 导入 ReadingTime 组件
|
||||
|
||||
// 从 VitePress 获取页面数据
|
||||
const { frontmatter, page, theme, lang } = useData()
|
||||
|
||||
// 计算页面的最后更新时间
|
||||
const date = computed(
|
||||
() => new Date(frontmatter.value.lastUpdated ?? page.value.lastUpdated),
|
||||
)
|
||||
|
||||
// 计算 ISO 格式的日期时间字符串
|
||||
const isoDatetime = computed(() => date.value.toISOString())
|
||||
|
||||
// 定义一个响应式变量来存储格式化后的日期时间字符串
|
||||
const datetime = ref('')
|
||||
|
||||
// 避免 hydration 错误,在组件挂载后执行
|
||||
onMounted(() => {
|
||||
watchEffect(() => {
|
||||
// 使用国际化 API 格式化日期时间
|
||||
datetime.value = new Intl.DateTimeFormat(
|
||||
theme.value.lastUpdated?.formatOptions?.forceLocale ? lang.value : undefined,
|
||||
theme.value.lastUpdated?.formatOptions ?? {
|
||||
dateStyle: 'short',
|
||||
timeStyle: 'short',
|
||||
},
|
||||
).format(date.value)
|
||||
})
|
||||
})
|
||||
|
||||
// 计算页面的作者信息
|
||||
const authors = computed(() => {
|
||||
let author = (frontmatter.value?.author ?? []) as string[]
|
||||
if (!Array.isArray(author))
|
||||
author = [author]
|
||||
return author
|
||||
})
|
||||
|
||||
// 计算显示的作者信息
|
||||
const displayAuthors = computed(() => {
|
||||
if (authors.value.length === 0) {
|
||||
return '匿名'
|
||||
}
|
||||
else {
|
||||
return `${authors.value.join(', ')} 等`
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mb-10 mt-4 flex flex-wrap gap-4">
|
||||
<div class="inline-flex items-center gap-1">
|
||||
<span class="i-octicon:person" />
|
||||
<span>作者:</span>
|
||||
<span>{{ displayAuthors }}</span>
|
||||
</div>
|
||||
|
||||
<div class="inline-flex items-center gap-1">
|
||||
<span class="i-octicon:calendar-16" />
|
||||
<span>{{ theme.lastUpdated?.text || 'Last updated' }}:</span>
|
||||
<time :datetime="isoDatetime">{{ datetime }}</time>
|
||||
</div>
|
||||
<ClientOnly>
|
||||
<ReadingTime /> <!-- 添加 ReadingTime 组件 -->
|
||||
</ClientOnly>
|
||||
</div>
|
||||
</template>
|
|
@ -1,26 +0,0 @@
|
|||
<script setup lang="ts">
|
||||
import { useData } from 'vitepress'
|
||||
|
||||
// 获取页面数据
|
||||
const { frontmatter } = useData()
|
||||
|
||||
// 计算阅读时间的函数
|
||||
function calculateReadingTime(wordCount: number) {
|
||||
const wordsPerMinute = 500 // 假设中文阅读速度为每分钟500字
|
||||
return Math.ceil(wordCount / wordsPerMinute) // 计算预计阅读时间
|
||||
}
|
||||
|
||||
// 从 frontmatter 获取字数和计算阅读时间
|
||||
const wordCount = frontmatter.value.wordCount || 0
|
||||
const readingTime = calculateReadingTime(wordCount)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<p>字数: {{ wordCount }} 预计阅读时间: {{ readingTime }} 分钟</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* 这里可以添加样式 */
|
||||
</style>
|
|
@ -1,10 +0,0 @@
|
|||
import { createContentLoader } from 'vitepress'
|
||||
|
||||
declare const data: { url: string, title: string }[]
|
||||
|
||||
export { data }
|
||||
|
||||
export default createContentLoader('**/*.md', {
|
||||
transform: list =>
|
||||
list.map(item => ({ url: item.url, title: item.frontmatter.title })),
|
||||
})
|
|
@ -1,230 +0,0 @@
|
|||
import fs from 'node:fs'
|
||||
import path, { dirname, resolve } from 'node:path'
|
||||
import process from 'node:process'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
import { GitChangelog } from '@nolebase/vitepress-plugin-git-changelog/vite'
|
||||
import { transformHeadMeta } from '@nolebase/vitepress-plugin-meta'
|
||||
import footnote from 'markdown-it-footnote'
|
||||
import katex from 'markdown-it-katex'
|
||||
import mdPangu from 'markdown-it-pangu'
|
||||
import UnoCSS from 'unocss/vite'
|
||||
import Components from 'unplugin-vue-components/vite'
|
||||
import { defineConfig } from 'vitepress'
|
||||
|
||||
import { generateSidebar } from './sidebar'
|
||||
import { useThemeContext } from './utils/themeContext'
|
||||
|
||||
// 从文件系统读取 Markdown 文件内容
|
||||
function readMarkdownFileContent(filePath: string): string {
|
||||
if (fs.existsSync(filePath)) {
|
||||
return fs.readFileSync(filePath, 'utf-8')
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
// 统计文档的字数函数
|
||||
function countWords(content: string): number {
|
||||
const cleanedContent = content
|
||||
.replace(/```[\s\S]*?```/g, '') // 移除代码块
|
||||
.replace(/!\[.*?\]\(.*?\)/g, '') // 移除图片链接
|
||||
.replace(/\[.*?\]\(.*?\)/g, '') // 移除普通链接
|
||||
.replace(/<[^>]+(>|$)/g, '') // 移除 HTML 标签
|
||||
.replace(/[.,/#!$%^&*;:{}=\-_`~()]/g, '') // 移除标点符号
|
||||
.replace(/\s+/g, ' ') // 将多余的空格归为一个空格
|
||||
.trim() // 去除首尾空格
|
||||
|
||||
const chineseCharacters = cleanedContent.match(/[\u4E00-\u9FFF\uFF01-\uFFE5]/g) || []
|
||||
const words = cleanedContent.split(/\s+/).filter(Boolean)
|
||||
|
||||
return chineseCharacters.length + words.length
|
||||
}
|
||||
|
||||
// https://vitepress.dev/reference/site-config
|
||||
function genConfig() {
|
||||
const themeConfig = useThemeContext()
|
||||
const { siteTitle, githubRepoLink, nav }
|
||||
= themeConfig
|
||||
return defineConfig({
|
||||
lang: 'zh-CN',
|
||||
title: siteTitle,
|
||||
cleanUrls: true,
|
||||
markdown: {
|
||||
config(md) {
|
||||
md.use(mdPangu)
|
||||
md.use(footnote)
|
||||
md.use(katex)
|
||||
},
|
||||
},
|
||||
head: [
|
||||
[
|
||||
'link',
|
||||
{
|
||||
rel: 'apple-touch-icon',
|
||||
sizes: '180x180',
|
||||
href: '/apple-touch-icon.png',
|
||||
},
|
||||
],
|
||||
[
|
||||
'link',
|
||||
{
|
||||
rel: 'icon',
|
||||
type: 'image/png',
|
||||
sizes: '32x32',
|
||||
href: '/favicon-32x32.png',
|
||||
},
|
||||
],
|
||||
[
|
||||
'link',
|
||||
{
|
||||
rel: 'icon',
|
||||
type: 'image/png',
|
||||
sizes: '16x16',
|
||||
href: '/favicon-16x16.png',
|
||||
},
|
||||
],
|
||||
['link', { rel: 'manifest', href: '/site.webmanifest' }],
|
||||
['meta', { name: 'msapplication-TileColor', content: '#4c4c4c' }],
|
||||
['meta', { name: 'theme-color', content: '#ffffff' }],
|
||||
['meta', { property: 'og:site_name', content: siteTitle }],
|
||||
],
|
||||
themeConfig: {
|
||||
// https://vitepress.dev/reference/default-theme-config
|
||||
siteTitle: false,
|
||||
logo: {
|
||||
src: '/logo-horizontal.svg',
|
||||
alt: 'Logo: RLE.wiki',
|
||||
},
|
||||
nav,
|
||||
sidebar: generateSidebar(),
|
||||
socialLinks: [{ icon: 'github', link: githubRepoLink }],
|
||||
editLink: {
|
||||
pattern: `${githubRepoLink}/edit/main/docs/:path`,
|
||||
text: '在 GitHub 上编辑此页面', // label localization
|
||||
},
|
||||
// label localization
|
||||
outline: { label: '本页大纲', level: 'deep' },
|
||||
lastUpdated: { text: '最后更新' },
|
||||
darkModeSwitchLabel: '深色模式',
|
||||
sidebarMenuLabel: '目录',
|
||||
returnToTopLabel: '返回顶部',
|
||||
docFooter: {
|
||||
prev: '上一页',
|
||||
next: '下一页',
|
||||
},
|
||||
search: {
|
||||
provider: 'local',
|
||||
options: {
|
||||
locales: {
|
||||
root: {
|
||||
translations: {
|
||||
button: {
|
||||
buttonText: '搜索文档',
|
||||
buttonAriaLabel: '搜索文档',
|
||||
},
|
||||
modal: {
|
||||
noResultsText: '无法找到相关结果',
|
||||
resetButtonTitle: '清除查询条件',
|
||||
displayDetails: '显示详细列表',
|
||||
footer: {
|
||||
selectText: '选择',
|
||||
navigateText: '切换',
|
||||
closeText: '关闭',
|
||||
// 无障碍(ARIA)标签,用于描述键盘导航操作
|
||||
navigateUpKeyAriaLabel: '上箭头',
|
||||
navigateDownKeyAriaLabel: '下箭头',
|
||||
selectKeyAriaLabel: '回车',
|
||||
closeKeyAriaLabel: '退出',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
// Add title field in frontmatter to search
|
||||
// You can exclude a page from search by adding search: false to the page's frontmatter.
|
||||
_render(src, env, md) {
|
||||
if (env.frontmatter?.search === false)
|
||||
return ''
|
||||
let html = md.render(src, env)
|
||||
if (env.frontmatter?.title)
|
||||
html = md.render(`# ${env.frontmatter.title}\n`) + html
|
||||
return html
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
transformHead: async (context) => {
|
||||
let head = [...context.head]
|
||||
|
||||
const returnedHead = await transformHeadMeta()(head, context)
|
||||
if (typeof returnedHead !== 'undefined')
|
||||
head = returnedHead
|
||||
|
||||
return head
|
||||
},
|
||||
vite: {
|
||||
plugins: [
|
||||
GitChangelog({
|
||||
repoURL: githubRepoLink,
|
||||
}),
|
||||
// GitChangelogMarkdownSection({
|
||||
// sections: {
|
||||
// disableChangelog: false,
|
||||
// disableContributors: true,
|
||||
// },
|
||||
// getChangelogTitle: (): string => {
|
||||
// return '文件历史'
|
||||
// },
|
||||
// excludes: [],
|
||||
// exclude: (_, { helpers }): boolean => {
|
||||
// if (helpers.idEquals('index.md'))
|
||||
// return true
|
||||
|
||||
// return false
|
||||
// },
|
||||
// }),
|
||||
Components({
|
||||
dirs: [
|
||||
'docs/.vitepress/theme/components',
|
||||
resolve(
|
||||
typeof dirname(fileURLToPath(import.meta.url)) === 'string'
|
||||
? dirname(fileURLToPath(import.meta.url))
|
||||
: __dirname,
|
||||
'./components',
|
||||
),
|
||||
],
|
||||
include: [/\.vue$/, /\.vue\?vue/, /\.md$/],
|
||||
dts: './.vitepress/components.d.ts',
|
||||
transformer: 'vue3',
|
||||
}),
|
||||
UnoCSS(),
|
||||
],
|
||||
ssr: {
|
||||
noExternal: [
|
||||
'@nolebase/vitepress-plugin-enhanced-readabilities',
|
||||
'@nolebase/vitepress-plugin-highlight-targeted-heading',
|
||||
],
|
||||
},
|
||||
},
|
||||
transformPageData(pageData) {
|
||||
// 构建 Markdown 文件路径
|
||||
const markdownFile = `${pageData.relativePath}`
|
||||
const filePath = path.join(process.cwd(), 'docs', markdownFile)
|
||||
|
||||
// 从文件系统读取文件内容
|
||||
const content = readMarkdownFileContent(filePath)
|
||||
|
||||
// 统计字数并插入到 Frontmatter
|
||||
const wordCount = countWords(content)
|
||||
|
||||
return {
|
||||
frontmatter: {
|
||||
...pageData.frontmatter,
|
||||
wordCount, // 将字数写入 Frontmatter
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export default genConfig
|
|
@ -1,4 +0,0 @@
|
|||
:root {
|
||||
--vp-font-family-base: sans-serif;
|
||||
--vp-font-family-mono: monospace;
|
||||
}
|
|
@ -1,62 +0,0 @@
|
|||
import type { ContentData } from 'vitepress'
|
||||
import { createContentLoader, defineLoader } from 'vitepress'
|
||||
|
||||
const contentLoader = createContentLoader('/**/*.md')
|
||||
|
||||
export interface Node<T> {
|
||||
value: T | null
|
||||
children: { [key: string]: Node<T> }
|
||||
}
|
||||
|
||||
export interface Trie<T> {
|
||||
root: Node<T>
|
||||
insert: ((path: string[], value: T, node: Node<T>) => void) & ((path: string[], value: T) => void)
|
||||
}
|
||||
|
||||
declare const data: Trie<Record<string, any>>
|
||||
export { data }
|
||||
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
export default defineLoader({
|
||||
watch: contentLoader.watch,
|
||||
async load(): Promise<Trie<Record<string, any>>> {
|
||||
const raw: ContentData[] = await contentLoader.load()
|
||||
const trie: Trie<Record<string, any>> = {
|
||||
root: { value: null, children: {} },
|
||||
|
||||
insert(this: Trie<Record<string, any>>, path, value, node: Node<Record<string, any>> = this.root) {
|
||||
if (path.length === 0) {
|
||||
node.value = value
|
||||
}
|
||||
else if (path.length === 1) {
|
||||
if (!(path[0] in node.children))
|
||||
node.children[path[0]] = { value, children: {} }
|
||||
else
|
||||
node.children[path[0]].value = value
|
||||
}
|
||||
else {
|
||||
if (!(path[0] in node.children)) {
|
||||
const new_node = { value: null, children: {} }
|
||||
this.insert(path.slice(1), value, new_node)
|
||||
node.children[path[0]] = new_node
|
||||
}
|
||||
else {
|
||||
this.insert(path.slice(1), value, node.children[path[0]])
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
for (const v of raw) {
|
||||
const frontmatter = v.frontmatter ?? null
|
||||
const copyright = frontmatter?.copyright ?? null
|
||||
if (copyright !== null) {
|
||||
trie.insert(v.url.split('/').filter((item, _index, _array) => item !== ''), {
|
||||
author: frontmatter.author ?? null,
|
||||
title: frontmatter.title ?? null,
|
||||
copyright,
|
||||
})
|
||||
}
|
||||
}
|
||||
return trie
|
||||
},
|
||||
})
|
|
@ -1,29 +0,0 @@
|
|||
import type {
|
||||
SidebarItem,
|
||||
SidebarMultiItem,
|
||||
} from 'vitepress-sidebar'
|
||||
import { generateSidebar as genSidebar } from 'vitepress-sidebar'
|
||||
import { useThemeContext } from './utils/themeContext'
|
||||
|
||||
export function generateSidebar() {
|
||||
const { sidebarOptions } = useThemeContext()
|
||||
const sidebar = genSidebar(sidebarOptions)
|
||||
for (const key in sidebar) {
|
||||
const sidebarMultiItem: SidebarMultiItem = (sidebar as any)[key]
|
||||
sidebarMultiItem.items.sort(sidebarTitleSorter)
|
||||
}
|
||||
return sidebar
|
||||
}
|
||||
|
||||
function sidebarTitleSorter(infoA: SidebarItem, infoB: SidebarItem): number {
|
||||
const textA = infoA.text
|
||||
const textB = infoB.text
|
||||
if (textA === undefined || textB === undefined)
|
||||
return 0
|
||||
|
||||
const infoANfc = textA.normalize('NFC')
|
||||
const infoBNfc = textB.normalize('NFC')
|
||||
return infoANfc.localeCompare(infoBNfc, 'zh', {
|
||||
numeric: true,
|
||||
})
|
||||
}
|
|
@ -1,42 +0,0 @@
|
|||
// https://vitepress.dev/guide/custom-theme
|
||||
import type { Theme } from 'vitepress'
|
||||
import DefaultTheme from 'vitepress/theme-without-fonts'
|
||||
import { h } from 'vue'
|
||||
import './custom-font.css'
|
||||
|
||||
import {
|
||||
NolebaseEnhancedReadabilitiesPlugin,
|
||||
} from '@nolebase/vitepress-plugin-enhanced-readabilities/client'
|
||||
|
||||
import {
|
||||
NolebaseGitChangelogPlugin,
|
||||
} from '@nolebase/vitepress-plugin-git-changelog/client'
|
||||
import type Options from 'vitepress-sidebar'
|
||||
|
||||
import '@nolebase/vitepress-plugin-enhanced-readabilities/client/style.css'
|
||||
import '@nolebase/vitepress-plugin-git-changelog/client/style.css'
|
||||
import '@nolebase/vitepress-plugin-highlight-targeted-heading/client/style.css'
|
||||
|
||||
import Layout from './Layout.vue'
|
||||
|
||||
export type SidebarOptions = Options
|
||||
|
||||
export default {
|
||||
extends: DefaultTheme,
|
||||
Layout,
|
||||
enhanceApp({ app }) {
|
||||
app.use(NolebaseEnhancedReadabilitiesPlugin, {
|
||||
spotlight: {
|
||||
defaultToggle: true,
|
||||
},
|
||||
})
|
||||
|
||||
app.use(NolebaseGitChangelogPlugin, {
|
||||
locales: {
|
||||
'zh-CN': {
|
||||
lastEditedDateFnsLocaleName: 'zhCN',
|
||||
},
|
||||
},
|
||||
})
|
||||
},
|
||||
} satisfies Theme
|
|
@ -1,2 +0,0 @@
|
|||
export * from './path'
|
||||
export * from './themeContext'
|
|
@ -1,13 +0,0 @@
|
|||
import { normalizePath } from 'vite'
|
||||
|
||||
export function pathEquals(path: string, equals: string): boolean {
|
||||
return normalizePath(path) === (normalizePath(equals))
|
||||
}
|
||||
|
||||
export function pathStartsWith(path: string, startsWith: string): boolean {
|
||||
return normalizePath(path).startsWith(normalizePath(startsWith))
|
||||
}
|
||||
|
||||
export function pathEndsWith(path: string, startsWith: string): boolean {
|
||||
return normalizePath(path).endsWith(normalizePath(startsWith))
|
||||
}
|
|
@ -1,26 +0,0 @@
|
|||
import { AsyncLocalStorage } from 'node:async_hooks'
|
||||
import type { DefaultTheme } from 'vitepress'
|
||||
import type { generateSidebar } from 'vitepress-sidebar'
|
||||
import type Options from 'vitepress-sidebar'
|
||||
|
||||
type NavConfig = DefaultTheme.Config['nav']
|
||||
|
||||
export interface ThemeContext {
|
||||
siteTitle: string
|
||||
siteDescription: string
|
||||
githubRepoLink: string
|
||||
rootDir: string
|
||||
include: string[]
|
||||
nav: NavConfig
|
||||
sidebarOptions: Options | Options[]
|
||||
}
|
||||
|
||||
const themeContext = new AsyncLocalStorage<ThemeContext>()
|
||||
|
||||
export function withThemeContext<T>(context: ThemeContext, fn: () => T): T {
|
||||
return themeContext.run(context, fn)
|
||||
}
|
||||
|
||||
export function useThemeContext(): ThemeContext {
|
||||
return themeContext.getStore()!
|
||||
}
|
4932
pnpm-lock.yaml
4932
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
|
@ -1,3 +1,2 @@
|
|||
packages:
|
||||
- functions
|
||||
- packages/*
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { defineConfig, presetAttributify, presetIcons, presetUno } from 'unocss';
|
||||
import presetSBox from '@project-trans/suggestion-box/dist/preset';
|
||||
import { defineConfig, presetAttributify, presetIcons, presetUno } from 'unocss'
|
||||
import presetSBox from '@project-trans/suggestion-box/dist/preset'
|
||||
|
||||
export default defineConfig({
|
||||
shortcuts: [
|
||||
|
|
Loading…
Reference in New Issue