📝 Add quick-start env

This commit is contained in:
xtaodada 2023-10-21 01:15:41 +08:00
parent fbc8bfb2cc
commit 2c48ede0be
Signed by: xtaodada
GPG Key ID: 4CBB3F4FA8C85659
56 changed files with 11324 additions and 193 deletions

9
.eslintignore Normal file
View File

@ -0,0 +1,9 @@
dist/
node_modules
node_modules/
types/
cache/
!docs/.vitepress
!/.eslintrc.js
!.test
.temp

65
.eslintrc.cjs Normal file
View File

@ -0,0 +1,65 @@
const restricted = [
'..',
'../..',
]
module.exports = {
root: true,
env: {
browser: true,
node: true,
},
extends: '@antfu',
rules: {
'vue/no-deprecated-functional-template': 'off',
'vue/one-component-per-file': 'off',
'vue/no-template-shadow': 'off',
'vue/require-prop-types': 'off',
'spaced-comment': ['error', 'always', { exceptions: ['#__PURE__'] }],
'no-restricted-imports': [
'error',
{
paths: restricted,
},
],
'node/no-callback-literal': 'off',
'import/namespace': 'off',
'import/default': 'off',
'import/no-named-as-default': 'off',
'import/no-named-as-default-member': 'off',
},
overrides: [
{
files: ['**/*.md', '**/*.md/*.*', 'demo.vue', 'demo.client.vue', 'scripts/*.ts', '*.test.ts', 'utils.ts'],
rules: {
'no-alert': 'off',
'no-console': 'off',
'no-undef': 'off',
'no-unused-vars': 'off',
'no-restricted-imports': 'off',
'@typescript-eslint/no-unused-vars': 'off',
'@typescript-eslint/no-redeclare': 'off',
'@typescript-eslint/no-invalid-this': 'off',
'unused-imports/no-unused-vars': 'off',
'@typescript-eslint/no-this-alias': [
'error',
{
allowedNames: ['self', 'instance'],
},
],
},
},
{
files: ['docs/.vitepress/**/*.*'],
rules: {
'no-restricted-imports': 'off',
},
},
{
files: ['docs/.vitepress/theme/plugins/**/*.*'],
rules: {
'prefer-rest-params': 'off',
},
},
],
}

4
.gitignore vendored
View File

@ -1,3 +1,3 @@
.vitepress/dist
.vitepress/cache
docs/.vitepress/dist
docs/.vitepress/cache
node_modules/

View File

@ -1,28 +0,0 @@
import { defineConfig } from 'vitepress'
// https://vitepress.dev/reference/site-config
export default defineConfig({
title: "GramBot",
description: "Telegram robot, query the official game information",
themeConfig: {
// https://vitepress.dev/reference/default-theme-config
nav: [
{ text: 'Home', link: '/' },
{ text: 'Examples', link: '/markdown-examples' }
],
sidebar: [
{
text: 'Examples',
items: [
{ text: 'Markdown Examples', link: '/markdown-examples' },
{ text: 'Runtime API Examples', link: '/api-examples' }
]
}
],
socialLinks: [
{ icon: 'github', link: 'https://github.com/vuejs/vitepress' }
]
}
})

View File

@ -1,49 +0,0 @@
---
outline: deep
---
# Runtime API Examples
This page demonstrates usage of some of the runtime APIs provided by VitePress.
The main `useData()` API can be used to access site, theme, and page data for the current page. It works in both `.md` and `.vue` files:
```md
<script setup>
import { useData } from 'vitepress'
const { theme, page, frontmatter } = useData()
</script>
## Results
### Theme Data
<pre>{{ theme }}</pre>
### Page Data
<pre>{{ page }}</pre>
### Page Frontmatter
<pre>{{ frontmatter }}</pre>
```
<script setup>
import { useData } from 'vitepress'
const { site, theme, page, frontmatter } = useData()
</script>
## Results
### Theme Data
<pre>{{ theme }}</pre>
### Page Data
<pre>{{ page }}</pre>
### Page Frontmatter
<pre>{{ frontmatter }}</pre>
## More
Check out the documentation for the [full list of runtime APIs](https://vitepress.dev/reference/runtime-api#usedata).

23
docs/.vitepress/components.d.ts vendored Normal file
View File

@ -0,0 +1,23 @@
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
export {}
declare module 'vue' {
export interface GlobalComponents {
Badge: typeof import('./theme/components/Badge.vue')['default']
'Bi:fileEarmarkWordFill': typeof import('~icons/bi/file-earmark-word-fill')['default']
Card: typeof import('./theme/components/Card.vue')['default']
ChatMessage: typeof import('./theme/components/ChatMessage.vue')['default']
ChatPanel: typeof import('./theme/components/ChatPanel.vue')['default']
CopyRight: typeof import('./theme/components/CopyRight.vue')['default']
DataPanel: typeof import('./theme/components/DataPanel.vue')['default']
HomeContributors: typeof import('./theme/components/HomeContributors.vue')['default']
NavCard: typeof import('./theme/components/NavCard.vue')['default']
'Ooui:clock': typeof import('~icons/ooui/clock')['default']
PageInfo: typeof import('./theme/components/PageInfo.vue')['default']
VideoLink: typeof import('./theme/components/VideoLink.vue')['default']
}
}

87
docs/.vitepress/config.ts Normal file
View File

@ -0,0 +1,87 @@
import { defineConfig } from 'vitepress'
import { withPwa } from '@vite-pwa/vitepress'
import { description, docsVersion, github, keywords, name, site } from './meta'
import { pwa } from './plugins/pwa'
import sidebar from './sidebar'
import socialLinks from './link'
export default withPwa(defineConfig({
pwa,
outDir: '../dist',
title: name,
description,
appearance: true,
lastUpdated: true,
useWebFonts: false,
markdown: {
lineNumbers: true,
},
locales: {
root: { label: '简体中文', lang: 'zh-CN' },
},
themeConfig: {
logo: '/favicon.ico',
outline: 2,
docFooter: {
prev: '上一篇',
next: '下一篇',
},
returnToTopLabel: '返回顶部',
outlineTitle: '导航栏',
darkModeSwitchLabel: '外观',
sidebarMenuLabel: '归档',
editLink: {
pattern: `${github}/tree/vp/docs/:path`,
text: '在 GitHub 上编辑此页',
},
lastUpdatedText: '最后一次更新于',
footer: {
message: 'Telegram robot, query the official game information.',
copyright: 'Copyright © 2023 PaigramTeam. All rights reserved.',
},
nav: [
{
text: ' 📦️ Repo',
items: [
{ text: '✨ PaiGram', link: 'https://github.com/PaiGramTeam/PaiGram' },
{ text: '🚅 PamGram', link: 'https://github.com/PaiGramTeam/PamGram' },
{ text: '🎮 MibooGram', link: 'https://github.com/PaiGramTeam/MibooGram' },
],
},
{
text: `v${docsVersion}`,
items: [
{ text: '📝 Docs', link: github },
],
},
],
sidebar,
socialLinks,
},
head: [
['meta', { name: 'referrer', content: 'no-referrer-when-downgrade' }],
['meta', { name: 'keywords', content: keywords }],
['meta', { name: 'author', content: 'PaigramTeam' }],
['meta', { property: 'og:type', content: 'article' }],
['meta', { name: 'application-name', content: name }],
['meta', { name: 'apple-mobile-web-app-title', content: name }],
['meta', { name: 'apple-mobile-web-app-status-bar-style', content: 'default' }],
['link', { rel: 'shortcut icon', href: '/favicon.ico' }],
['link', { rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }],
['link', { rel: 'mask-icon', href: '/favicon.ico', color: '#06f' }],
['meta', { name: 'theme-color', content: '#06f' }],
['link', { rel: 'apple-touch-icon', sizes: '120x120', href: '/images/icons/apple-touch-icon.png' }],
// webfont
['link', { rel: 'dns-prefetch', href: 'https://fonts.googleapis.com' }],
['link', { rel: 'dns-prefetch', href: 'https://fonts.gstatic.com' }],
['link', { rel: 'preconnect', crossorigin: 'anonymous', href: 'https://fonts.googleapis.com' }],
['link', { rel: 'preconnect', crossorigin: 'anonymous', href: 'https://fonts.gstatic.com' }],
// og
['meta', { property: 'og:description', content: description }],
['meta', { property: 'og:url', content: site }],
['meta', { property: 'og:locale', content: 'zh_CN' }],
],
}))

10
docs/.vitepress/link.ts Normal file
View File

@ -0,0 +1,10 @@
import { github } from './meta'
const socialLinks = [
{
icon: 'github',
link: github,
},
]
export default socialLinks

21
docs/.vitepress/meta.ts Normal file
View File

@ -0,0 +1,21 @@
import { version } from '../../package.json'
// base info
export const name = 'GramBot'
export const site = 'https://docs.paimon.vip/'
export const logo = '../public/icon.png'
export const keywords = 'docs,paimon,paigram,pomgram,telegram,bot'
export const description = 'Telegram robot, query the official game information'
// social link
export const github = 'https://github.com/PaiGramTeam/PaiGramDocs'
// docs version
export const docsVersion = version
/* PWA runtime caching urlPattern regular expressions */
/* eslint-disable prefer-regex-literals */
export const githubSourceContentRegex = new RegExp('^https://(((raw|user-images|camo).githubusercontent.com))/.*', 'i')
export const googleFontRegex = new RegExp('^https://fonts.googleapis.com/.*', 'i')
export const googleStaticFontRegex = new RegExp('^https://fonts.gstatic.com/.*', 'i')
export const jsdelivrCDNRegex = new RegExp('^https://cdn.jsdelivr.net/.*', 'i')

View File

@ -0,0 +1,35 @@
import type { Plugin } from 'vite'
import { getReadingTime } from './../theme/utils'
export function MarkdownTransform(): Plugin {
return {
name: 'md-transform',
enforce: 'pre',
async transform(code, id) {
if (!id.match(/\.md\b/))
return null
// convert img
const imgRegex = /!\[(.+?)\]\((.+?)\)/g
let imgMatches = imgRegex.exec(code)
while (imgMatches) {
const [text, link] = imgMatches.slice(1)
code = code.replace(imgMatches[0], `<img src="${link}" alt="${text || 'img'}" />`)
imgMatches = imgRegex.exec(code)
}
// const { footer } = await getDocsMarkdown()
// code = replacer(code, footer, 'FOOTER', 'tail')
const { readTime, words } = getReadingTime(code)
code = code.replace(/(#\s\S.+)/, `$1\n\n<PageInfo readTime="${readTime}" words="${words}"/>\n`)
code = code.replace(/::: info([\s\S.]+)?:::/g, '::: info 📝 备注$1:::\n')
code = code.replace(/::: warning([\s\S.]+)?:::/g, '::: warning 🚨 警告$1:::\n')
code = code.replace(/::: tip([\s\S.]+)?:::/g, '::: tip 💡 提醒$1:::\n')
code = code.replace(/::: danger([\s\S.]+)?:::/g, '::: danger 🔥 危险$1:::\n')
return code
},
}
}

View File

@ -0,0 +1,110 @@
import fg from 'fast-glob'
import { resolve } from 'pathe'
import type { VitePWAOptions } from 'vite-plugin-pwa'
import {
description,
githubSourceContentRegex,
googleFontRegex,
googleStaticFontRegex,
jsdelivrCDNRegex,
name,
} from '../meta'
/**
* Vite Plugin PWA uses Workbox library to build the service worker
* can find more information on Workbox section.
* @see https://vite-plugin-pwa.netlify.app/
*/
export const pwa: Partial<VitePWAOptions> = {
outDir: '../dist',
registerType: 'autoUpdate',
// include all static assets under public/
includeAssets: fg.sync('**/*.{png,svg,gif,ico,txt}', { cwd: resolve(__dirname, '../../public') }),
manifest: {
id: '/',
name,
short_name: name,
description,
theme_color: '#06f',
icons: [
{
src: '/images/icons/apple-touch-120x120.png',
sizes: '120x120',
type: 'image/png',
},
{
src: '/images/icons/android-chrome-192x192.png',
sizes: '192x192',
type: 'image/png',
},
{
src: '/images/icons/android-chrome-512x512.png',
sizes: '512x512',
type: 'image/png',
},
],
},
workbox: {
navigateFallbackDenylist: [/^\/new$/],
globPatterns: ['**/*.{js,css,webp,png,svg,gif,ico,woff2}'],
navigateFallback: null,
runtimeCaching: [
{
urlPattern: googleFontRegex,
handler: 'CacheFirst',
options: {
cacheName: 'google-font-style-cache',
expiration: {
maxEntries: 10,
maxAgeSeconds: 60 * 60 * 24 * 365, // <== 365 days
},
cacheableResponse: {
statuses: [0, 200],
},
},
},
{
urlPattern: googleStaticFontRegex,
handler: 'CacheFirst',
options: {
cacheName: 'google-fonts-cache',
expiration: {
maxEntries: 10,
maxAgeSeconds: 60 * 60 * 24 * 365, // <== 365 days
},
cacheableResponse: {
statuses: [0, 200],
},
},
},
{
urlPattern: jsdelivrCDNRegex,
handler: 'CacheFirst',
options: {
cacheName: 'jsdelivr-cdn-cache',
expiration: {
maxEntries: 10,
maxAgeSeconds: 60 * 60 * 24 * 365, // <== 365 days
},
cacheableResponse: {
statuses: [0, 200],
},
},
},
{
urlPattern: githubSourceContentRegex,
handler: 'CacheFirst',
options: {
cacheName: 'githubusercontent-images-cache',
expiration: {
maxEntries: 10,
maxAgeSeconds: 60 * 60 * 24 * 365, // <== 365 days
},
cacheableResponse: {
statuses: [0, 200],
},
},
},
],
},
}

View File

@ -0,0 +1,14 @@
export default {
'/': [
{
text: '🚀 快速上手',
collapsed: true,
items: [
{ text: '环境检查', link: '/quick-start/env' },
{ text: '克隆项目', link: '/quick-start/install' },
{ text: '配置项目', link: '/quick-start/config' },
{ text: '启动项目', link: '/quick-start/run' },
],
},
],
}

View File

@ -0,0 +1,64 @@
<script setup lang="ts">
defineProps<{
text?: string
type?: 'info' | 'tip' | 'warning' | 'danger'
}>()
</script>
<template>
<span class="VPBadge" :class="type ?? 'tip'">
<slot>{{ text }}</slot>
</span>
</template>
<style scoped>
.VPBadge {
display: inline-block;
margin-left: 2px;
border: 1px solid transparent;
border-radius: 10px;
padding: 0 8px;
line-height: 18px;
font-size: 12px;
font-weight: 600;
transform: translateY(-2px);
}
h1 .VPBadge,
h2 .VPBadge,
h3 .VPBadge,
h4 .VPBadge,
h5 .VPBadge,
h6 .VPBadge {
vertical-align: top;
}
h2 .VPBadge {
border-radius: 11px;
line-height: 20px;
}
.VPBadge.info {
border-color: var(--vp-badge-info-border);
color: var(--vp-badge-info-text);
background-color: var(--vp-badge-info-bg);
}
.VPBadge.tip {
border-color: var(--vp-badge-tip-border);
color: var(--vp-badge-tip-text);
background-color: var(--vp-badge-tip-bg);
}
.VPBadge.warning {
border-color: var(--vp-badge-warning-border);
color: var(--vp-badge-warning-text);
background-color: var(--vp-badge-warning-bg);
}
.VPBadge.danger {
border-color: var(--vp-badge-danger-border);
color: var(--vp-badge-danger-text);
background-color: var(--vp-badge-danger-bg);
}
</style>

View File

@ -0,0 +1,139 @@
<script setup lang="ts">
import json from '../../../public/plugin_list.json'
</script>
<template>
<div display="flex" flex-wrap="wrap">
<div flex="~ wrap gap-1" justify-flex-start gap="20px">
<a
v-for="(item, index) in json.plugins" :key="index" :href="item.link"
>
<div class="card">
<h3 class="card__title">{{ index }}<Badge :type="item.type" :text="item.content" />
</h3>
<p class="card__content">{{ item.info }} </p>
<div class="so_top_icon">
<img
loading="lazy" :src="item.avatar" rounded-full min-w-10 min-h-10 h-10 w-10
:alt="`${name}'s avatar`"
>
</div>
<div class="card__arrow">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" height="15" width="15">
<path fill="#fff" d="M13.4697 17.9697C13.1768 18.2626 13.1768 18.7374 13.4697 19.0303C13.7626 19.3232 14.2374 19.3232 14.5303 19.0303L20.3232 13.2374C21.0066 12.554 21.0066 11.446 20.3232 10.7626L14.5303 4.96967C14.2374 4.67678 13.7626 4.67678 13.4697 4.96967C13.1768 5.26256 13.1768 5.73744 13.4697 6.03033L18.6893 11.25H4C3.58579 11.25 3.25 11.5858 3.25 12C3.25 12.4142 3.58579 12.75 4 12.75H18.6893L13.4697 17.9697Z" />
</svg>
</div>
</div>
</a>
</div>
</div>
</template>
<style>
a {
text-decoration: none !important;
}
.card {
--border-radius: 0.75rem;
--primary-color: var(--vp-c-brand);
--secondary-color: var(--vp-c-text-2);
width: 210px;
height: 130px;
font-family: "Arial";
padding: 1rem;
cursor: pointer;
border-radius: var(--border-radius);
background: var(--vp-c-bg-soft);
box-shadow: 0px 8px 16px 0px rgb(0 0 0 / 3%);
position: relative;
display: flex;
}
.card > * + * {
margin-top: 1.1em;
}
.card .card__content {
position: absolute;
top: 40px;
left: 10px;
right: 10px;
display: flex;
color: var(--secondary-color);
font-size: 0.86rem;
}
.card .card__title {
position: absolute;
top: -13px;
left: 45px;
color: var(--vp-c-text-1);
padding: 0;
font-size: 1rem;
font-weight: bold;
}
.card .card__date {
color: #6e6b80;
font-size: 0.8rem;
}
.card .card__arrow {
position: absolute;
background: var(--primary-color);
padding: 0.4rem;
border-top-left-radius: var(--border-radius);
border-bottom-right-radius: var(--border-radius);
bottom: 0;
right: 0;
transition: 0.2s;
display: flex;
justify-content: center;
align-items: center;
}
.card .so_top_icon {
position: absolute;
width: 30px;
height: 30px;
border-radius: 50%;
background: #000;
top: 0px;
left: 10px;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
}
.card .so_top_icon img {
width: 22px;
height: 22px;
object-fit: contain;
}
.card svg {
transition: 0.2s;
}
/* hover */
.card:hover {
cursor: pointer;
transition: transform 0.4s ease;
transform: scale(1.05);
}
.card:hover .card__title {
color: var(--primary-color);
text-decoration: underline;
}
.card:hover .card__arrow {
background: #111;
}
.card:hover .card__arrow svg {
transform: translateX(3px);
}
</style>

View File

@ -0,0 +1,231 @@
<script lang="ts" setup>
import { computed, getCurrentInstance, onBeforeUnmount, onMounted, ref, watch } from 'vue'
const props = defineProps<{
nickname: string
color: string
avatar: string
tag: string
type: string
}>()
const colorMap = {
Alice: '#cc0066',
Bob: '#00994d',
Carol: '#1e90ff',
Dave: '#f4a460',
}
const avatarMap = {
grambot: '',
b: '',
}
const typeMap = {
grambot: 'tip',
b: 'danger',
}
const tagMap = {
grambot: '机器人',
b: '用户',
}
const shown = ref(false)
const active = ref(false)
const moving = ref(false)
const root = ref<HTMLElement>()
const backgroundColor = computed(() => props.color || colorMap[props.nickname])
const avatar = computed(() => props.avatar || avatarMap[props.nickname])
const tag = computed(() => props.tag || tagMap[props.nickname])
const type = computed(() => props.type || typeMap[props.nickname])
function getPrevious() {
let last: Element
for (const current of document.querySelectorAll('.chat-message')) {
if (current === root.value)
return last
last = current
}
}
watch(active, (value) => {
if (!value)
return shown.value = false
const prev = getPrevious()
if (!prev)
return appear()
const rect = prev.getBoundingClientRect()
if (rect.bottom < 0)
return appear()
const prevExposed = prev.__vue__.exposed as typeof exposed
if (prevExposed.moving.value || !prevExposed.shown.value)
prevExposed.onappear(appear)
else
appear()
})
let appearCallback = () => {}
function appear() {
shown.value = true
moving.value = true
setTimeout(() => {
moving.value = false
appearCallback()
}, 100)
}
function handleScroll() {
const rect = root.value.getBoundingClientRect()
if (rect.top < innerHeight)
active.value = true
// active.value = rect.top < innerHeight
}
const instance = getCurrentInstance()
const exposed = {
moving,
shown,
onappear(callback: any) {
appearCallback = callback
},
}
defineExpose(exposed)
onMounted(() => {
root.value.__vue__ = instance
handleScroll()
addEventListener('scroll', handleScroll)
addEventListener('resize', handleScroll)
})
onBeforeUnmount(() => {
removeEventListener('scroll', handleScroll)
removeEventListener('resize', handleScroll)
})
</script>
<template>
<div ref="root" class="chat-message" :class="{ shown }">
<img v-if="avatar" class="avatar" :src="avatar">
<div v-else class="avatar" :style="{ backgroundColor }">
{{ nickname[0] }}
</div>
<div class="nickname">
{{ nickname }}
<Badge :type="type" :text="tag" />
</div>
<div class="message-box">
<slot>&nbsp;</slot>
</div>
</div>
</template>
<style lang="scss">
$avatar-size: 2.8rem;
$msgbox-left: 4.2rem;
.chat-message {
position: relative;
margin: 1rem 0;
opacity: 0;
transform: translateX(-20%);
transition: transform 0.3s ease-out, opacity 0.3s ease;
&.shown {
opacity: 1;
transform: translateX(0);
}
.avatar {
width: $avatar-size;
height: $avatar-size;
position: absolute;
border-radius: 100%;
transform: translateY(-1px);
user-select: none;
pointer-events: none;
text-align: center;
line-height: $avatar-size;
font-size: 1.6rem;
color: white;
font-family: "Comic Sans MS";
}
.nickname {
user-select: none;
position: relative;
margin: 0 0 0.4rem $msgbox-left;
font-weight: bold;
font-size: 0.9rem;
}
}
.chat-message:not(.no-padding) .message-box {
padding: 0.5rem 0.7rem;
}
.chat-message .message-box {
position: relative;
margin-left: $msgbox-left;
width: fit-content;
border-radius: 0.5rem;
background-color: var(--vp-c-bg);
word-break: break-all;
line-height: 26px !important;
> img {
border-radius: 0.5rem;
}
img {
vertical-align: middle;
}
p > img {
margin: 0.2rem 0;
}
&::before {
content: '';
position: absolute;
right: 100%;
top: 0px;
width: 12px;
height: 12px;
border: 0 solid transparent;
border-bottom-width: 8px;
border-bottom-color: currentColor;
border-radius: 0 0 0 32px;
color: var(--vp-c-bg);
}
p {
margin: 0 !important;
line-height: 26px !important;
}
p.indent-1 {
padding-left: 1rem;
}
p.indent-2 {
padding-left: 2rem;
}
blockquote {
font-size: 0.9rem;
margin: 0 0 0.2rem;
background-color: #f3f6f9;
border: none;
border-radius: 0.5rem;
padding: 0.2rem 0.6rem;
background-color: var(--vp-c-bg-alt);
color: var(--vp-c-text-2);
}
}
</style>

View File

@ -0,0 +1,129 @@
<script lang="ts">
export default {
props: {
controls: Boolean,
title: String,
},
data: () => ({
tab: 'default',
}),
computed: {
mini() {
return !this.controls && !this.title
},
},
}
</script>
<template>
<div class="panel-view" :class="{ mini }">
<div class="controls">
<div class="circle red" />
<div class="circle yellow" />
<div class="circle green" />
<div class="title">
<span v-if="title" class="title-text">{{ title }}</span>
</div>
</div>
<div class="content">
<slot />
</div>
</div>
</template>
<style lang="scss">
$circleRadius: 6px;
$circleSpacing: 19px;
$textShadow: 1px 1px 1px rgba(23, 31, 35, 0.5);
.panel-view {
outline: 2px;
outline-color: #ea1313;
outline-style: auto;
position: relative;
border-radius: 6px;
margin: 1rem 0;
overflow: auto hidden;
background-color: var(--vp-c-bg-alt);
&.manager, &.container {
background-color: #032f62;
}
.controls {
display: initial;
position: absolute;
top: 0.8rem;
width: 100%;
}
.circle {
position: absolute;
top: 8px - $circleRadius;
width: 2 * $circleRadius;
height: 2 * $circleRadius;
border-radius: $circleRadius;
&.red {
left: 17px;
background-color: #ff5f56;
}
&.yellow {
left: 17px + $circleSpacing;
background-color: #ffbd2e;
}
&.green {
left: 17px + 2 * $circleSpacing;
background-color: #27c93f;
}
}
.title {
text-align: center;
width: 100%;
font-size: 1rem;
font-weight: bold;
line-height: 1rem;
.tab {
color: gray;
cursor: pointer;
transition: .3s ease;
}
.tab.active {
color: white;
cursor: default;
}
.title-text:not(:last-child)::after {
color: gray;
content: " - ";
}
.tab + .tab::before {
cursor: default;
content: " | ";
color: gray;
}
}
.content {
padding: 0.2rem 1.2rem;
> p {
font-size: 0.8rem;
color: #909399;
text-align: center;
}
}
&.mini .controls {
display: none;
}
&:not(.mini) .content {
padding-top: 2rem;
}
}
</style>

View File

@ -0,0 +1,22 @@
<script setup lang="ts">
import { ref } from 'vue'
import { useData } from 'vitepress'
const defaultAuthor = 'PaigramTeam'
const { frontmatter } = useData()
const author = ref(defaultAuthor)
if (frontmatter.value?.author)
author.value = frontmatter.value?.author
function reName(name: string) {
return name
}
const pageHref = location.href
function getGithubLink(name: string) {
return `https://github.com/${reName(name)}`
}
</script>

View File

@ -0,0 +1,144 @@
<script setup lang="ts">
</script>
<template>
<div class="panel mt-[48px]">
<div class="mx-auto container max-w-[1152px] min-h-[32px] bg-[var(--vp-c-bg-soft)] w-full rounded-[8px]">
<section class="grid grid-cols-3 justify-items-center py-[12px] px-[24px] items-center">
<h2 class="leading-[24px] text-[16px] font-[600]">
本站总访问量 <span id="busuanzi_value_site_pv" class="font-bold" />
</h2>
<fa6-solid:heart-pulse class="heart" />
<h2 class="leading-[24px] text-[16px]">
本站访客数 <span id="busuanzi_value_site_uv" class="font-bold" /> 人次
</h2>
</section>
</div>
</div>
</template>
<style scoped>
.panel {
padding: 0 24px;
margin-top: 24px;
}
@media (min-width: 640px) {
.panel {
padding: 0 48px;
margin-top: 32px;
}
}
@media (min-width: 960px) {
.panel {
padding: 0 64px;
margin-top: 48px;
}
}
.heart {
color: red;
animation: iconAnimate 1.33s ease-in-out infinite
}
@-moz-keyframes iconAnimate {
0%,
100% {
transform: scale(1)
}
10%,
30% {
transform: scale(.9)
}
20%,
40%,
60%,
80% {
transform: scale(1.1)
}
50%,
70% {
transform: scale(1.1)
}
}
@-webkit-keyframes iconAnimate {
0%,
100% {
transform: scale(1)
}
10%,
30% {
transform: scale(.9)
}
20%,
40%,
60%,
80% {
transform: scale(1.1)
}
50%,
70% {
transform: scale(1.1)
}
}
@-o-keyframes iconAnimate {
0%,
100% {
transform: scale(1)
}
10%,
30% {
transform: scale(.9)
}
20%,
40%,
60%,
80% {
transform: scale(1.1)
}
50%,
70% {
transform: scale(1.1)
}
}
@keyframes iconAnimate {
0%,
100% {
transform: scale(1)
}
10%,
30% {
transform: scale(.9)
}
20%,
40%,
60%,
80% {
transform: scale(1.1)
}
50%,
70% {
transform: scale(1.1)
}
}
</style>

View File

@ -0,0 +1,25 @@
<script setup lang="ts">
import { contributors } from '../../../contributors'
</script>
<template>
<div class="vp-doc mx-auto">
<h2 op50 font-normal pt-5 text-center>
Contributors
</h2>
</div>
<div text-lg max-w-100 text-center leading-7 p-10 mx-auto>
<div flex="~ wrap gap-1" justify-center>
<a
v-for="{ name, avatar } of contributors" :key="name" :href="`https://github.com/${name}`" target="_blank" m-0
rel="noopener noreferrer" :aria-label="`${name} on GitHub`"
>
<img
loading="lazy" :src="avatar" width="40" height="40" rounded-full min-w-10 min-h-10 h-10 w-10
:alt="`${name}'s avatar`"
>
</a>
</div>
<br>
</div>
</template>

View File

@ -0,0 +1,28 @@
<script setup lang="ts">
import type { navItem } from '../../../favorites/types'
defineProps<{
navData: navItem[]
}>()
</script>
<template>
<div class="grid auto-rows-auto grid-cols-2 gap-[12px]">
<section v-for="navItem of navData" :key="navItem.id">
<a :href="navItem.link" rel="noreferrer" target="_blank" class="group">
<section
class="flex h-full flex-col border-1 border-solid border-[var(--vp-c-border)]/[.55] rounded-[8px] leading-[24px] px-[24px] py-[12px] group-hover:shadow"
>
<span class="text-gray-600 group-hover:text-gray-800 dark:text-gray-300 dark:group-hover:text-gray-100">
{{ navItem.text }}
</span>
<span
class="mb-auto text-sm text-gray-700 opacity-50 dark:text-gray-300 dark:group-hover:text-gray-50 min-h-[20px]"
>
{{ navItem.desc ?? navItem.text }}
</span>
</section>
</a>
</section>
</div>
</template>

View File

@ -0,0 +1,39 @@
<script setup lang="ts">
import { computed, ref } from 'vue'
import { useData } from 'vitepress'
import { getDate, getFromNow } from '../utils'
defineProps<{
readTime: string
words: string
}>()
const defaultAuthor = 'Choi Yang'
const author = ref(defaultAuthor)
const { frontmatter, page } = useData()
const publishedTime = getDate(frontmatter.value?.date)
if (frontmatter.value?.author)
author.value = frontmatter.value?.author
const lastUpdatedDate = computed(() => new Date(page.value.lastUpdated!))
const isoDatetime = computed(() => lastUpdatedDate.value.toISOString())
const timeFormNow = getFromNow(isoDatetime.value)
</script>
<template>
<div>
<section
class="border-b-1 border-[var(--vp-c-divider)] w-full border-b-solid mt-[24px] pb-[12px] flex gap-[12px] mb-[12px] flex-wrap max-w-[85%]"
>
<div class="flex gap-[4px] items-center">
<bi:file-earmark-word-fill />
字数统计:<span>{{ words }} </span>
</div>
<div class="flex gap-[4px] items-center">
<ooui:clock />
阅读时长:<span>{{ readTime }} 分钟</span>
</div>
</section>
</div>
</template>

View File

@ -0,0 +1,15 @@
<script setup lang="ts">
const props = defineProps<{
bvId: string
}>()
const link = `https://www.bilibili.com/video/${props.bvId}/`
</script>
<template>
<div class="video_link">
<a :href="link" target="_blank" bg-blue:10 px4 py3 rounded block mt2 flex items-center gap2>
<div i-carbon:play-filled flex-none text-lg />
<slot />
</a>
</div>
</template>

View File

@ -0,0 +1,52 @@
import { inBrowser, useRoute } from 'vitepress'
import type { EnhanceAppContext, Theme } from 'vitepress'
import DefaultTheme from 'vitepress/theme'
import { nextTick, onMounted, watch } from 'vue'
import busuanzi from 'busuanzi.pure.js'
import mediumZoom from 'medium-zoom'
import { registerAnalytics, siteIds, trackPageview } from './plugins/baidutongji'
import './styles/main.css'
import './styles/global.css'
import './styles/demo.css'
import './styles/utils.css'
import './styles/vars.css'
import './styles/custom-block.css'
import './styles/scrollBar.scss'
import 'uno.css'
if (inBrowser)
import('./plugins/pwa')
const theme: Theme = {
...DefaultTheme,
enhanceApp({ router }: EnhanceAppContext) {
if (inBrowser) {
registerAnalytics(siteIds)
window.addEventListener('hashchange', () => {
const { href: url } = window.location
trackPageview(siteIds, url)
})
router.onAfterRouteChanged = (to) => {
trackPageview(siteIds, to)
busuanzi.fetch()
}
}
},
setup() {
const route = useRoute()
const initZoom = () => {
mediumZoom('.main img', { background: 'var(--vp-c-bg)' }) // Should there be a new?
}
onMounted(() => {
initZoom()
})
watch(
() => route.path,
() => nextTick(() => initZoom()),
)
},
}
export default theme

View File

@ -0,0 +1,100 @@
import { join, resolve } from 'node:path'
import matter from 'gray-matter'
import fg from 'fast-glob'
import fs from 'fs-extra'
export interface Options {
base: string
title?: string
collapsed?: boolean
}
export const DIR_ROOT = resolve(__dirname, '../../../../')
export const DIR_SRC = resolve(DIR_ROOT, 'docs')
export function fastGlobSync(type: string, dir: string, ignore: string[] = []) {
const files = fg.sync('*', {
onlyDirectories: type === 'dir',
onlyFiles: type === 'file',
cwd: dir,
ignore: [
'_*',
'dist',
'node_modules',
...ignore,
],
})
files.sort()
return files
}
export const dirs = fastGlobSync('dir', DIR_SRC)
function getSidebar(dir: string, title: string | undefined) {
const curDir = resolve(DIR_SRC, dir)
const dirs = fastGlobSync('dir', curDir)
const res = []
if (dirs.length) {
// TODO 多级目录
dirs.forEach((e) => {
const childDir = resolve(curDir, e)
const mdFiles = fastGlobSync('file', childDir)
const sidebar = {
text: (e.charAt(0).toUpperCase() + e.slice(1)).replaceAll('-', ' '),
collapsed: false,
items: [] as any,
}
mdFiles.forEach((file) => {
if (file.endsWith('.md')) {
let prePath = join('/', dir, e, file)
if (file === 'index.md')
prePath = join('/', dir, e, '/')
const item = {
text: file.slice(0, -3),
link: prePath,
}
sidebar.items.push(item)
}
})
res.push(sidebar)
})
}
else {
const mdFiles = fastGlobSync('file', curDir)
const sidebar = {
text: title,
collapsed: false,
items: [] as any,
}
mdFiles.filter(e => e !== 'index.md').forEach((file) => {
if (file.endsWith('.md')) {
const prePath = join('/', dir, file)
const curDir = resolve(DIR_SRC, dir)
const mdPath = resolve(curDir, file)
const mdRaw = fs.readFileSync(mdPath, 'utf-8')
const { content: md } = matter(mdRaw)
const mdLine = md.split('\n').slice(0, 6)
const mdTitle = mdLine.filter(e => e.match(/^#\s*/))[0]?.replace('#', '').trim()
const item = {
text: mdTitle || file.slice(0, -3),
link: prePath,
}
sidebar.items.push(item)
}
})
res.push(sidebar)
}
return res
}
export default (options: Options) => {
options.collapsed = options?.collapsed ?? false
if (options.base !== '/') {
const dir = dirs.filter((dir: string) => dir.includes(options.base))[0]
if (dir) {
const sidebar = getSidebar(dir, options.title)
return sidebar
}
}
}

View File

@ -0,0 +1,49 @@
import { inBrowser } from 'vitepress'
/**
* ID
*/
export const siteIds = 'eb1ef90b1e9476b8d3d43088d3ac9c49'
declare global {
interface Window {
_hmt: any
}
}
/**
*
*/
export function registerAnalytics(siteId: string) {
if (!inBrowser)
return
if (document.querySelector(`#analytics-plugin-${siteId}`))
return
window._hmt = window._hmt ? window._hmt : []
const script = document.createElement('script')
script.id = `analytics-${siteId}`
script.async = true
script.src = `https://hm.baidu.com/hm.js?${siteId}`
document.querySelector('head')?.appendChild(script)
}
/**
* PV
* @param siteId - ID
* @param pageUrl - URL
*/
export function trackPageview(siteId: string, pageUrl: string) {
if (!inBrowser)
return
if (!pageUrl || typeof pageUrl !== 'string')
pageUrl = '/'
if (pageUrl.startsWith('http')) {
const urlFragment = pageUrl.split('/')
const origin = `${urlFragment[0]}//${urlFragment[2]}`
pageUrl = pageUrl.replace(origin, '')
}
window._hmt.push(['_setAccount', siteId])
window._hmt.push(['_trackPageview', pageUrl])
}

View File

@ -0,0 +1,30 @@
import process from 'node:process'
declare const dataLayer: any[]
declare const gtag: (...args: any[]) => void
declare global {
interface Window {
dataLayer?: typeof dataLayer
gtag?: typeof gtag
}
}
function mountGoogleAnalytics(id: string) {
if (window.dataLayer && window.gtag)
return
const gtagScript = document.createElement('script')
gtagScript.src = `https://www.googletagmanager.com/gtag/js?id=${id}`
gtagScript.async = true
document.head.appendChild(gtagScript)
window.dataLayer = window.dataLayer || []
window.gtag = function () {
dataLayer.push(arguments)
}
gtag('js', new Date())
gtag('config', id)
}
export default ({ id }) => {
if (process.env.NODE_ENV === 'production' && id && typeof window !== 'undefined')
mountGoogleAnalytics(id)
}

View File

@ -0,0 +1,4 @@
import { registerSW } from 'virtual:pwa-register'
/** @see https://vite-plugin-pwa.netlify.app/guide/auto-update.html#ssr-ssg */
registerSW({ immediate: true })

View File

@ -0,0 +1,5 @@
.custom-block-title
{
font-weight: 1200;
font-size: 20px;
}

View File

@ -0,0 +1,206 @@
.demo {
font-size: var(--vt-doc-code-font-size);
background: var(--cho-code-block-bg);
padding: 2em;
position: relative;
margin-bottom: 10px;
border-radius: 8px;
transition: background-color 0.5s;
}
.demo-source-link {
color: var(--vp-c-text-2) !important;
position: absolute;
top: 0;
right: 10px;
z-index: 2;
font-size: 12px;
font-weight: 500;
color: var(--vp-c-text-dark-3);
transition: color 0.5s;
}
@media (min-width: 720px) {
.demo {
--width: calc(
100vw - var(--sidebar-width) - var(--scrollbar-width)
) !important;
--content-width: min(48rem, var(--width)) !important;
--margin-left: calc(
calc(calc(var(--content-width) - var(--width)) / 2) - 1.5rem
) !important;
}
}
.note {
opacity: 0.7;
font-size: 0.9rem;
margin-bottom: -0.2rem;
}
.demo {
p {
margin: 0.5rem 0;
}
button {
padding: 3px 15px;
background-color: var(--vp-c-brand);
border: none;
outline: none;
color: white;
margin: 0.5rem 0;
border-bottom: 2px solid var(--vp-c-brand-dark);
text-shadow: 1px 1px 1px var(--vp-c-brand-dark);
border-radius: 4px;
font-size: 1rem;
box-sizing: border-box;
vertical-align: middle;
}
button.small {
padding: 0.25em 0.7em 0.2em 0.7em;
}
button.orange {
--vp-c-brand: #db8742;
--vp-c-brand-dark: #ad6e39;
--vp-c-brand-active: #d67e36;
}
button.red {
--c-brand: #f56c6c;
--c-brand-dark: #e41c1c;
--c-brand-active: #e94c4c;
}
button.gray {
color: var(--vp-c-disabled-fg);
--vp-c-brand: var(--vp-c-disabled-bg);
--vp-c-brand-dark: var(--vp-c-disabled-bg);
--vp-c-brand-active: rgba(125,125,125,.2);
}
button:hover {
background-color: var(--vp-c-brand-dark);
}
button:active {
border-bottom: 0;
border-top: 2px solid var(--vp-c-brand-dark);
}
button ~ button {
margin-left: 0.5rem;
}
button[disabled],
button.disabled {
cursor: default;
background-color: var(--vp-c-disabled-bg);
color: var(--vp-c-disabled-fg);
border-bottom: 2px solid var(--vp-c-disabled-bg);
pointer-events: none;
text-shadow: none;
border-top: 0;
opacity: 0.8;
}
textarea {
display: block;
min-width: 20rem;
font-size: 1.05rem;
padding: 0.5em 1em 0.4em 1em;
border-radius: 5px;
box-shadow: var(--vp-c-divider) 0px 0px 0px 1px;
margin: 0.5rem 0;
outline: none;
background: var(--vp-c-bg);
color: var(--vp-c-text);
transition: background-color 0.5s;
}
textarea[readonly] {
background: var(--vp-c-bg-light);
}
input[type="text"],
input[type="search"],
input[type="number"] {
display: block;
font-size: 0.9rem;
padding: 0.5em 1em 0.4em 1em;
border: 1px solid var(--vp-c-divider-light);
border-radius: 4px;
outline: none;
background: var(--vp-c-bg);
color: var(--vp-c-text);
min-width: 20rem;
margin: 0.5rem 0;
}
input[type="number"] {
min-width: 15rem;
outline: none;
}
input:focus {
border: 1px solid var(--vp-c-divider-dark-1);
}
pre,
.code-block {
opacity: 0.8;
overflow: auto;
}
.code-block {
background-color: var(--vp-c-bg-mute) !important;
padding: 4px 8px;
margin: 12px 0;
border-radius: 4px;
}
code {
background-color: var(--vp-c-bg-mute) !important;
}
.resizer {
resize: both;
padding: 1rem;
width: 200px;
height: 200px;
border: 1px solid var(--vp-c-divider-light);
border-radius: 4px;
background: white;
outline: none;
white-space: pre;
overflow-wrap: normal;
overflow: hidden;
overflow-x: hidden;
}
.float {
position: fixed;
bottom: 0;
right: 0;
padding: 1rem 2rem;
border: 1px solid var(--vp-c-divider-light);
background: var(--vp-c-bg);
z-index: 100;
min-width: 100px;
}
.area {
border-width: 2px;
border-style: dashed;
padding: 1rem;
}
}
html.dark .demo {
.resizer {
background: #222;
color: white;
}
}

View File

@ -0,0 +1,4 @@
iframe {
width: 100%;
min-height: 600px;
}

View File

@ -0,0 +1,47 @@
html.dark {
color-scheme: dark;
}
.VPContent {
background: url("/grid.svg") rgba(255, 255, 255, 0.6);
background-blend-mode: difference;
background-attachment: fixed;
}
.dark .VPContent {
background: rgb(40, 40, 40, 0.6) url("/grid.svg");
background-blend-mode: multiply;
background-attachment: fixed;
}
.vp-doc h2 {
border-top: 0px;
margin-top: 10px;
}
.VPMenuLink .link {
line-height: 28px !important;
}
.VPSidebarGroup .link {
padding: 3px 0 !important;
}
.VPTeamPageTitle .title {
line-height: 32px;
font-size: 24px;
opacity: 0.5;
}
.VPTeamPageTitle .lead {
font-size: 14px;
opacity: 0.4;
}
.medium-zoom-overlay {
z-index: 20;
}
.medium-zoom-image {
z-index: 21;
}

View File

@ -0,0 +1,25 @@
::-webkit-scrollbar
{
width: 3px;
height: 3px;
}
::-webkit-scrollbar-track-piece
{
background-color: white;
-webkit-border-radius: 3px;
border-radius: 3px;
}
::-webkit-scrollbar-thumb:vertical
{
background-color: #666;
-webkit-border-radius: 3px;
border-radius: 3px;
}
::-webkit-scrollbar-thumb:horizontal
{
background-color: #666;
-webkit-border-radius: 3px;
border-radius: 3px;
}

View File

@ -0,0 +1,3 @@
.text-orange {
color: #db8742;
}

View File

@ -0,0 +1,90 @@
/**
* Colors
* -------------------------------------------------------------------------- */
:root {
--vp-c-accent: #35495e;
--vp-c-brand: #d76be3;
--vp-c-brand-dark: #3385ff;
--vp-c-text-code: #5d6f5d;
/* --vp-code-block-bg: rgba(125, 125, 125, 0.04); */
--vp-c-disabled-bg: rgba(125, 125, 125, 0.2);
/* fix contrast on gray cards: used by --vp-c-text-2 */
--vp-c-brand-light: #ff8f91;
--vp-c-text-light-2: rgba(30, 30, 30, 0.7);
--cho-code-block-bg: rgba(125, 125, 125, 0.04);
--vp-c-bg-alt: rgba(235, 235, 235, 0.6);
}
.dark {
--vp-code-block-bg: rgba(0, 0, 0, 0.2);
--vp-c-text-code: #c0cec0;
/* fix contrast on gray cards: check the same above (this is the default) */
--vp-c-text-dark-2: rgba(235, 235, 235, 0.6);
--vp-c-bg: rgb(10, 11, 10);
--vp-c-bg-alt: rgba(10, 11, 10, 0.6);
}
/**
* Component: Code
* -------------------------------------------------------------------------- */
:root {
--vp-code-line-highlight-color: rgba(125, 125, 125, 0.2);
}
.dark {
--vp-code-line-highlight-color: rgba(0, 0, 0, 0.5);
}
/**
* Component: Button
* -------------------------------------------------------------------------- */
:root {
--vp-button-brand-border: var(--vp-c-brand-light);
--vp-button-brand-bg: var(--vp-c-brand);
--vp-button-brand-hover-border: var(--vp-c-brand-light);
--vp-button-brand-hover-bg: var(--vp-c-brand-light);
--vp-button-brand-active-border: var(--vp-c-brand-light);
--vp-button-brand-active-bg: var(--vp-button-brand-bg);
}
/**
* Component: Home
* -------------------------------------------------------------------------- */
:root {
--vp-home-hero-name-color: transparent;
--vp-home-hero-name-background: -webkit-linear-gradient(
120deg,
#ffffff -80%,
#ea0fff
);
--vp-home-hero-image-background-image: linear-gradient(
-45deg,
#ea0fff 30%,
#35495e80
);
--vp-home-hero-image-filter: blur(30px);
}
@media (min-width: 640px) {
:root {
--vp-home-hero-image-filter: blur(56px);
}
}
@media (min-width: 960px) {
:root {
--vp-home-hero-image-filter: blur(72px);
}
}
/**
* Component: Algolia
* -------------------------------------------------------------------------- */
.DocSearch {
--docsearch-primary-color: var(--vp-c-brand) !important;
}

View File

@ -0,0 +1,4 @@
export interface PageInfo {
readTime: number | string;
words: number | string;
}

View File

@ -0,0 +1,26 @@
import dayjs from 'dayjs'
import utc from 'dayjs/plugin/utc.js'
import relativeTime from 'dayjs/plugin/relativeTime'
import 'dayjs/locale/zh-cn'
dayjs.extend(utc)
dayjs.locale('zh-cn')
dayjs.extend(relativeTime)
export function getDate(date: string | Date | undefined): string | null {
if (date) {
const time = dayjs(date instanceof Date ? date : date.trim())
if (time.isValid()) {
const currentTime = dayjs(date).utc().local().format('YYYY-MM-DD')
return currentTime
}
}
return null
}
export function getFromNow(date: string | Date): string | null {
if (date)
return dayjs(date).utc().local().fromNow()
return null
}

View File

@ -0,0 +1,3 @@
export * from './md'
export * from './date'
export * from './pageInfo'

View File

@ -0,0 +1,22 @@
export function renderMarkdown(markdownText = '') {
const htmlText = markdownText
.replace(/^### (.*$)/gim, '<h3>$1</h3>')
.replace(/^## (.*$)/gim, '<h2>$1</h2>')
.replace(/^# (.*$)/gim, '<h1>$1</h1>')
.replace(/^\> (.*$)/gim, '<blockquote>$1</blockquote>')
.replace(/\*\*(.*)\*\*/gim, '<b>$1</b>')
.replace(/\*(.*)\*/gim, '<i>$1</i>')
.replace(/!\[(.*?)\]\((.*?)\)/gim, '<img alt=\'$1\' src=\'$2\' />')
.replace(/\[(.*?)\]\((.*?)\)/gim, '<a href=\'$2\'>$1</a>')
.replace(/`(.*?)`/gim, '<code>$1</code>')
.replace(/\n$/gim, '<br />')
return htmlText.trim()
}
export const EXTERNAL_URL_RE = /^[a-z]+:/i
export const PATHNAME_PROTOCOL_RE = /^pathname:\/\//
export function isExternal(path: string): boolean {
return EXTERNAL_URL_RE.test(path)
}

View File

@ -0,0 +1,43 @@
import type { PageInfo } from '../types'
export function getWords(content: string): RegExpMatchArray | null {
return content.match(/[\w\d\s,.\u00C0-\u024F\u0400-\u04FF]+/giu)
}
export function getChinese(content: string): RegExpMatchArray | null {
return content.match(/[\u4E00-\u9FD5]/gu)
}
export function getEnWordCount(content: string): number {
return getWords(content)?.reduce<number>(
(accumulator, word) =>
accumulator + (word.trim() === '' ? 0 : word.trim().split(/\s+/u).length),
0,
) || 0
}
export function getCnWordCount(content: string): number {
return getChinese(content)?.length || 0
}
export function getWordNumber(content: string): number {
return getEnWordCount(content) + getCnWordCount(content)
}
export function getReadingTime(content: string,
cnWordPerMinute = 350,
enwordPerMinute = 160): PageInfo {
const count = getWordNumber(content || '')
const words = count >= 1000 ? `${Math.round(count / 100) / 10}k` : count
const enWord = getEnWordCount(content)
const cnWord = getCnWordCount(content)
const readingTime = cnWord / cnWordPerMinute + enWord / enwordPerMinute
const readTime = readingTime < 1 ? '1' : Number.parseInt(`${readingTime}`, 10)
return {
readTime,
words,
}
}

19
docs/contributors.json Normal file
View File

@ -0,0 +1,19 @@
[
"luoshuijs",
"omg-xtao",
"karakoo",
"chuangbo",
"CHxCOOH",
"LittleMengBot",
"sihuan",
"zhxycn",
"Billyzou0741326",
"cworld1",
"StellaCaerulea",
"IJNKAWAKAZE",
"daviszerro",
"xr1s",
"Nene07210721",
"hakureiyuyuko",
"AnotiaWang"
]

31
docs/contributors.ts Normal file
View File

@ -0,0 +1,31 @@
import contributors from './contributors.json'
export interface Contributor {
name: string
avatar: string
}
export interface CoreTeam {
avatar: string
name: string
github: string
twitter?: string
sponsors?: boolean
description: string
packages?: string[]
functions?: string[]
}
const contributorsAvatars: Record<string, string> = {}
function getAvatarUrl(name: string) {
return `https://github.com/${name}.png`
}
const contributorList = (contributors as string[]).reduce((acc, name) => {
contributorsAvatars[name] = getAvatarUrl(name)
acc.push({ name, avatar: contributorsAvatars[name] })
return acc
}, [] as Contributor[])
export { contributorList as contributors }

34
docs/index.md Normal file
View File

@ -0,0 +1,34 @@
---
layout: home
title: GramBot
titleTemplate: Telegram robot, query the official game information
hero:
name: "GramBot"
text: ""
tagline: |
🔨 PaiGramTeam
image:
src: /icon_256px_round.png
alt: fav
actions:
- theme: brand
text: 快速上手
link: /quick-start/env
features:
- icon: 💻
title: 支持多种设备
details: windows、linux、mac...
- icon: 🤖
title: Telegram 平台
details: Telegram BOT
- icon: 🤝
title: 异步开发
details: 异步优先式开发,提高运行效率
- icon: 🔌
title: 插件系统
details: 插件化开发,模块化管理
---
<HomeContributors/>

BIN
docs/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

5
docs/public/grid.svg Normal file
View File

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100 100">
<path d="M96,95h4v1H96v4H95V96H86v4H85V96H76v4H75V96H66v4H65V96H56v4H55V96H46v4H45V96H36v4H35V96H26v4H25V96H16v4H15V96H0V95H15V86H0V85H15V76H0V75H15V66H0V65H15V56H0V55H15V46H0V45H15V36H0V35H15V26H0V25H15V16H0V15H15V0h1V15h9V0h1V15h9V0h1V15h9V0h1V15h9V0h1V15h9V0h1V15h9V0h1V15h9V0h1V15h9V0h1V15h4v1H96v9h4v1H96v9h4v1H96v9h4v1H96v9h4v1H96v9h4v1H96v9h4v1H96v9h4v1H96Zm-1,0V86H86v9ZM85,95V86H76v9ZM75,95V86H66v9ZM65,95V86H56v9ZM55,95V86H46v9ZM45,95V86H36v9ZM35,95V86H26v9ZM25,95V86H16v9ZM16,85h9V76H16Zm10,0h9V76H26Zm10,0h9V76H36Zm10,0h9V76H46Zm10,0h9V76H56Zm10,0h9V76H66Zm10,0h9V76H76Zm10,0h9V76H86Zm9-10V66H86v9ZM85,75V66H76v9ZM75,75V66H66v9ZM65,75V66H56v9ZM55,75V66H46v9ZM45,75V66H36v9ZM35,75V66H26v9ZM25,75V66H16v9ZM16,65h9V56H16Zm10,0h9V56H26Zm10,0h9V56H36Zm10,0h9V56H46Zm10,0h9V56H56Zm10,0h9V56H66Zm10,0h9V56H76Zm10,0h9V56H86Zm9-10V46H86v9ZM85,55V46H76v9ZM75,55V46H66v9ZM65,55V46H56v9ZM55,55V46H46v9ZM45,55V46H36v9ZM35,55V46H26v9ZM25,55V46H16v9ZM16,45h9V36H16Zm10,0h9V36H26Zm10,0h9V36H36Zm10,0h9V36H46Zm10,0h9V36H56Zm10,0h9V36H66Zm10,0h9V36H76Zm10,0h9V36H86Zm9-10V26H86v9ZM85,35V26H76v9ZM75,35V26H66v9ZM65,35V26H56v9ZM55,35V26H46v9ZM45,35V26H36v9ZM35,35V26H26v9ZM25,35V26H16v9ZM16,25h9V16H16Zm10,0h9V16H26Zm10,0h9V16H36Zm10,0h9V16H46Zm10,0h9V16H56Zm10,0h9V16H66Zm10,0h9V16H76Zm10,0h9V16H86Z" fill="rgba(255,255,255,0.2)" fill-rule="evenodd" opacity="0.2"/>
<path d="M6,5V0H5V5H0V6H5v94H6V6h94V5Z" fill="rgba(255,255,255,0.075)" fill-rule="evenodd"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
docs/public/icon.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

BIN
docs/public/icon_256px.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

56
docs/quick-start/env.md Normal file
View File

@ -0,0 +1,56 @@
# 环境检查<Badge type="tip" text="简单" />
## Python 环境检查教程
### 1.检查Python版本
首先我们需要确认你的系统中是否已经安装了Python 3.8。打开终端或命令提示符,然后输入以下命令:
```bash
python --version
```
如果你的系统已经安装了Python 3.8+那么你应该会看到类似于“Python 3.8.x” “Python 3.9.x” ... 的输出。
如果你看到的是其他版本的Python或者你的系统提示你没有安装Python那么你需要安装Python 3.8+。
GitHub Copilot: # Python Poetry环境检查教程
### 2.检查Poetry是否已安装
首先我们需要确认你的系统中是否已经安装了Poetry。打开终端或命令提示符然后输入以下命令
```bash
poetry --version
```
如果你的系统已经安装了Poetry那么你应该会看到类似于“Poetry version x.x.x”的输出。
如果你的系统提示你没有安装Poetry那么你需要安装Poetry。
#### 安装Poetry
打开终端或命令提示符,然后输入以下命令:
```bash
pip install poetry
poetry --version
```
现在你应该能看到“Poetry version x.x.x”的输出了。
GitHub Copilot: # Git环境检查教程
## 系统环境检查
### 1.检查Git是否已安装
首先我们需要确认你的系统中是否已经安装了Git。打开终端或命令提示符然后输入以下命令
```bash
git --version
```
如果你的系统已经安装了Git那么你应该会看到类似于“git version x.x.x”的输出。
如果你的系统提示你没有安装Git那么你需要安装Git。

55
docs/vite.config.ts Normal file
View File

@ -0,0 +1,55 @@
import { resolve } from 'node:path'
import { createRequire } from 'node:module'
import { defineConfig } from 'vite'
import type { UserConfig } from 'vite'
import UnoCSS from 'unocss/vite'
import Icons from 'unplugin-icons/vite'
import IconsResolver from 'unplugin-icons/resolver'
import Components from 'unplugin-vue-components/vite'
import { MarkdownTransform } from './.vitepress/plugins/markdownTransform'
const require = createRequire(import.meta.url)
export default defineConfig(async () => {
return <UserConfig>{
server: {
hmr: {
overlay: false,
},
fs: {
allow: [
resolve(__dirname, '..'),
],
},
},
plugins: [
// custom
MarkdownTransform(),
// plugins
Components({
dirs: resolve(__dirname, '.vitepress/theme/components'),
include: [/\.vue$/, /\.vue\?vue/, /\.md$/],
resolvers: [
IconsResolver({
componentPrefix: '',
}),
],
dts: './.vitepress/components.d.ts',
transformer: 'vue3',
}),
Icons({
compiler: 'vue3',
autoInstall: true,
defaultStyle: 'display: inline-block',
}),
UnoCSS(),
],
css: {
postcss: {
plugins: [
require('postcss-nested'),
],
},
},
}
})

View File

@ -1,25 +0,0 @@
---
# https://vitepress.dev/reference/default-theme-home-page
layout: home
hero:
name: "GramBot"
text: "Telegram robot, query the official game information"
tagline: My great project tagline
actions:
- theme: brand
text: Markdown Examples
link: /markdown-examples
- theme: alt
text: API Examples
link: /api-examples
features:
- title: Feature A
details: Lorem ipsum dolor sit amet, consectetur adipiscing elit
- title: Feature B
details: Lorem ipsum dolor sit amet, consectetur adipiscing elit
- title: Feature C
details: Lorem ipsum dolor sit amet, consectetur adipiscing elit
---

View File

@ -1,85 +0,0 @@
# Markdown Extension Examples
This page demonstrates some of the built-in markdown extensions provided by VitePress.
## Syntax Highlighting
VitePress provides Syntax Highlighting powered by [Shiki](https://github.com/shikijs/shiki), with additional features like line-highlighting:
**Input**
````
```js{4}
export default {
data () {
return {
msg: 'Highlighted!'
}
}
}
```
````
**Output**
```js{4}
export default {
data () {
return {
msg: 'Highlighted!'
}
}
}
```
## Custom Containers
**Input**
```md
::: info
This is an info box.
:::
::: tip
This is a tip.
:::
::: warning
This is a warning.
:::
::: danger
This is a dangerous warning.
:::
::: details
This is a details block.
:::
```
**Output**
::: info
This is an info box.
:::
::: tip
This is a tip.
:::
::: warning
This is a warning.
:::
::: danger
This is a dangerous warning.
:::
::: details
This is a details block.
:::
## More
Check out the documentation for the [full list of markdown extensions](https://vitepress.dev/guide/markdown).

View File

@ -1,7 +1,59 @@
{
"type": "module",
"version": "0.0.1",
"scripts": {
"docs:dev": "vitepress dev",
"docs:build": "vitepress build",
"docs:preview": "vitepress preview"
"dev": "vitepress dev docs --port 8080 --max-old-space-size=50000",
"build": "vitepress build docs",
"serve": "vitepress serve docs"
},
"devDependencies": {
"@antfu/eslint-config": "^0.42.0",
"@antfu/ni": "^0.21.8",
"@iconify/json": "^2.2.115",
"@types/fs-extra": "^11.0.1",
"@types/md5": "^2.3.2",
"@vite-pwa/vitepress": "^0.2.1",
"bumpp": "^9.2.0",
"busuanzi.pure.js": "^1.0.3",
"cloudinary-build-url": "^0.2.4",
"dayjs": "^1.11.9",
"eslint-plugin-import": "^2.28.1",
"esno": "^0.17.0",
"fast-glob": "^3.3.1",
"feed": "^4.2.2",
"fs-extra": "^11.1.1",
"gray-matter": "^4.0.3",
"less": "^4.2.0",
"lint-staged": "^14.0.0",
"md5": "^2.3.0",
"medium-zoom": "^1.0.8",
"moment": "^2.29.4",
"ohmyfetch": "^0.4.21",
"pathe": "^1.1.1",
"postcss": "^8.4.29",
"postcss-nested": "^6.0.1",
"sass": "^1.69.3",
"simple-git": "^3.19.1",
"simple-git-hooks": "^2.9.0",
"sitemap-ts": "^1.4.0",
"typescript": "^5.2.2",
"unocss": "^0.56.0",
"unplugin-icons": "^0.17.0",
"unplugin-vue-components": "^0.25.2",
"vite": "^4.4.9",
"vite-plugin-pwa": "^0.16.5",
"vitepress": "1.0.0-rc.20",
"vue": "^3.3.4"
},
"simple-git-hooks": {
"pre-commit": "npx lint-staged"
},
"lint-staged": {
"*.{js,ts,tsx,vue,md}": [
"eslint --cache --fix"
]
},
"dependencies": {
"vue-custom-scrollbar": "^1.4.4"
}
}
}

9056
pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load Diff

37
tsconfig.json Normal file
View File

@ -0,0 +1,37 @@
{
"compilerOptions": {
"target": "es2020",
"module": "esnext",
"types": [
"bun-types"
],
"lib": [
"ESNext",
"DOM",
"DOM.Iterable"
],
"moduleResolution": "Bundler",
"esModuleInterop": true,
"strict": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"declaration": true,
"resolveJsonModule": true,
"rootDir": ".",
"baseUrl": ".",
"jsx": "preserve",
"skipLibCheck": true,
"skipDefaultLibCheck": true,
"noUnusedLocals": true
},
"include": [
"docs",
"docs/src/*.json",
"docs/.vitepress/components/*.vue",
"docs/.vitepress/*.ts"
],
"exclude": [
"**/node_modules/**",
"**/dist/**"
]
}

36
unocss.config.ts Normal file
View File

@ -0,0 +1,36 @@
import {
defineConfig,
presetAttributify,
presetIcons,
presetUno,
transformerDirectives,
transformerVariantGroup,
} from 'unocss'
export default defineConfig({
shortcuts: {
'border-main': 'border-gray-400 border-opacity-30',
'bg-main': 'bg-gray-400',
'bg-base': 'bg-white dark:bg-hex-1a1a1a',
},
presets: [
presetUno(),
presetAttributify(),
presetIcons({
scale: 1.2,
warn: true,
}),
],
theme: {
colors: {
primary: '#3eaf7c',
},
fontFamily: {
mono: 'var(--vt-font-family-mono)',
},
},
transformers: [
transformerDirectives(),
transformerVariantGroup(),
],
})

14
vite.config.ts.js Normal file
View File

@ -0,0 +1,14 @@
import { defineConfig } from 'vite'
import vitepress from 'vitepress'
export default defineConfig({
plugins: [vitepress()],
build: {
// 指定输出目录,默认为 .vitepress/dist
outDir: 'dist',
// 指定静态资源目录,默认为 dist/assets
assetsDir: 'assets',
// 指定静态资源的基础路径,默认为 /
base: '/',
},
})