├── article ├── about.md └── post │ └── 项目结构.md ├── .gitignore ├── pnpm-workspace.yaml ├── packages ├── theme-nana │ ├── README.md │ ├── src │ │ ├── store │ │ │ ├── index.ts │ │ │ ├── crud.js │ │ │ └── api.ts │ │ ├── page │ │ │ ├── 404.vue │ │ │ ├── home │ │ │ │ ├── Xmind.vue │ │ │ │ ├── Flag.vue │ │ │ │ ├── index.vue │ │ │ │ ├── SimplePostList.vue │ │ │ │ └── Toc.vue │ │ │ ├── doc.vue │ │ │ ├── layout │ │ │ │ ├── Navbar.vue │ │ │ │ ├── index.vue │ │ │ │ └── ChooserPane.vue │ │ │ ├── about.vue │ │ │ ├── thinking.vue │ │ │ ├── post.detail.vue │ │ │ └── post.vue │ │ ├── service │ │ │ ├── index.js │ │ │ ├── dev.ts │ │ │ ├── screen.ts │ │ │ ├── data.ts │ │ │ ├── time.ts │ │ │ ├── aes.ts │ │ │ └── localstorage.service.ts │ │ ├── hooks │ │ │ ├── index.ts │ │ │ ├── useLoading.ts │ │ │ └── useTheme.ts │ │ ├── assets │ │ │ ├── font │ │ │ │ └── sweet.ttf │ │ │ ├── image │ │ │ │ ├── mobile.webp │ │ │ │ ├── treecat.png │ │ │ │ ├── post.cover.jpg │ │ │ │ ├── pane_about.dark.svg │ │ │ │ ├── pane_about.svg │ │ │ │ ├── pane_preview.svg │ │ │ │ ├── pane_blog.svg │ │ │ │ ├── pane_preview.dark.svg │ │ │ │ ├── pane_blog.dark.svg │ │ │ │ ├── pane_thinking.dark.svg │ │ │ │ └── pane_thinking.svg │ │ │ └── style │ │ │ │ ├── var.less │ │ │ │ ├── doc.less │ │ │ │ ├── code.less │ │ │ │ └── global.less │ │ ├── App.vue │ │ ├── component │ │ │ ├── Loading.vue │ │ │ ├── Waiting.vue │ │ │ ├── LazyImage.vue │ │ │ ├── Cat.vue │ │ │ ├── ThemeController.vue │ │ │ ├── Chat.vue │ │ │ ├── Navigation.vue │ │ │ ├── ImageGallery.vue │ │ │ ├── part │ │ │ │ ├── _Loading.vue │ │ │ │ └── _Cat.vue │ │ │ └── Searcher.vue │ │ ├── main.js │ │ ├── config.ts │ │ ├── router │ │ │ └── router.ts │ │ └── auto-import.d.ts │ ├── jsconfig.json │ ├── .gitignore │ ├── components.d.ts │ ├── tsconfig.json │ ├── index.html │ ├── vite.config.js │ └── package.json └── vector │ ├── src │ ├── engine │ │ ├── index.ts │ │ ├── sync.cos.ts │ │ └── setup.ts │ ├── runtime │ │ ├── index.ts │ │ ├── engine.runtime.ts │ │ └── config.ts │ ├── cli.ts │ ├── utils │ │ ├── image │ │ │ ├── image.schedule.ts │ │ │ ├── image.cache.ts │ │ │ ├── image.render.ts │ │ │ ├── index.ts │ │ │ └── image.handle.ts │ │ ├── index.ts │ │ ├── schedule.ts │ │ ├── count.ts │ │ ├── common.ts │ │ ├── git.ts │ │ ├── hook.ts │ │ ├── file.reader.ts │ │ ├── io.ts │ │ └── markdown.ts │ ├── hook │ │ ├── hook.ts │ │ ├── index.ts │ │ ├── before.save.hook.ts │ │ └── init.hook.ts │ └── index.ts │ ├── jest.config.js │ ├── babel.config.cjs │ ├── tsconfig.json │ ├── test │ ├── src │ │ └── utils │ │ │ └── io.test.ts │ └── file.reader.test.js │ ├── package.json │ └── vector.config.ts ├── README.md ├── vector.config.js ├── .babelrc ├── package.json └── rollup.config.js /article/about.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | _tmp 3 | dist -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - "packages/*" 3 | -------------------------------------------------------------------------------- /packages/theme-nana/README.md: -------------------------------------------------------------------------------- 1 | # Nana 主题 2 | 3 | Nana 是 Vector 博客生成器的默认主题。 -------------------------------------------------------------------------------- /packages/theme-nana/src/store/index.ts: -------------------------------------------------------------------------------- 1 | export * from './crud'; 2 | 3 | -------------------------------------------------------------------------------- /packages/vector/src/engine/index.ts: -------------------------------------------------------------------------------- 1 | export { setup } from "./setup"; 2 | -------------------------------------------------------------------------------- /packages/vector/src/engine/sync.cos.ts: -------------------------------------------------------------------------------- 1 | export async function async2Cos() {} 2 | -------------------------------------------------------------------------------- /packages/theme-nana/src/page/404.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /packages/theme-nana/src/service/index.js: -------------------------------------------------------------------------------- 1 | export * from './time'; 2 | export * from './screen'; 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vector 2 | 3 | Vector 是一个非常强大的个人博客生成器。 4 | 5 | [在线演示](https://treecat.cn) 6 | 7 | 8 | -------------------------------------------------------------------------------- /packages/vector/src/runtime/index.ts: -------------------------------------------------------------------------------- 1 | export { initEngineRuntime, getRuntime } from "./engine.runtime"; 2 | -------------------------------------------------------------------------------- /packages/theme-nana/src/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export { useLoading } from './useLoading'; 2 | export * from './useTheme'; 3 | -------------------------------------------------------------------------------- /packages/vector/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: "ts-jest", 3 | testEnvironment: "node", 4 | }; 5 | -------------------------------------------------------------------------------- /packages/vector/babel.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [["@babel/preset-env", { targets: { node: "current" } }]], 3 | }; 4 | -------------------------------------------------------------------------------- /packages/theme-nana/src/assets/font/sweet.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/byteluo/MD-Render/HEAD/packages/theme-nana/src/assets/font/sweet.ttf -------------------------------------------------------------------------------- /packages/vector/src/cli.ts: -------------------------------------------------------------------------------- 1 | import { setup } from "./engine"; 2 | 3 | async function start() { 4 | await setup(); 5 | } 6 | 7 | start(); 8 | -------------------------------------------------------------------------------- /packages/theme-nana/src/assets/image/mobile.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/byteluo/MD-Render/HEAD/packages/theme-nana/src/assets/image/mobile.webp -------------------------------------------------------------------------------- /packages/theme-nana/src/assets/image/treecat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/byteluo/MD-Render/HEAD/packages/theme-nana/src/assets/image/treecat.png -------------------------------------------------------------------------------- /packages/vector/src/utils/image/image.schedule.ts: -------------------------------------------------------------------------------- 1 | import { Scheduler } from "../schedule"; 2 | 3 | export const imageSchedule = new Scheduler(); 4 | -------------------------------------------------------------------------------- /packages/theme-nana/src/assets/image/post.cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/byteluo/MD-Render/HEAD/packages/theme-nana/src/assets/image/post.cover.jpg -------------------------------------------------------------------------------- /packages/theme-nana/src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /packages/vector/src/utils/image/image.cache.ts: -------------------------------------------------------------------------------- 1 | const processedImages = new Set(); 2 | 3 | export function checkImageInCache(url: string): [boolean, string] { 4 | return [false, ""]; 5 | } 6 | -------------------------------------------------------------------------------- /vector.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | module.exports = { 4 | dataDir: path.resolve(__dirname, "article"), 5 | theme: path.resolve(__dirname, "packages/theme-nana") 6 | }; 7 | -------------------------------------------------------------------------------- /packages/vector/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./hook"; 2 | export * from "./file.reader"; 3 | export * from "./markdown"; 4 | export * from "./git"; 5 | export * from "./file.reader"; 6 | export * from "./io"; 7 | -------------------------------------------------------------------------------- /packages/theme-nana/src/component/Loading.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | -------------------------------------------------------------------------------- /packages/vector/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "esModuleInterop": true, 6 | "sourceMap": true, 7 | "resolveJsonModule": true, 8 | "outDir": "./dist" 9 | }, 10 | "include": ["src/**/*"], 11 | "exclude": ["node_modules"] 12 | } 13 | -------------------------------------------------------------------------------- /packages/theme-nana/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "paths": { 5 | "@/*": ["src/*"] 6 | }, 7 | "target": "ES6", 8 | "allowSyntheticDefaultImports": true 9 | }, 10 | "include": ["src/**/*"], 11 | "exclude": ["node_modules"] 12 | } 13 | -------------------------------------------------------------------------------- /packages/theme-nana/src/hooks/useLoading.ts: -------------------------------------------------------------------------------- 1 | import { ref } from "vue"; 2 | import Loading from "@/component/Loading.vue"; 3 | 4 | export function useLoading() { 5 | const isLoading = ref(true); 6 | const setLoading = (b: boolean) => { 7 | isLoading.value = b; 8 | }; 9 | 10 | return { 11 | isLoading, 12 | setLoading, 13 | Loading, 14 | }; 15 | } 16 | -------------------------------------------------------------------------------- /article/post/项目结构.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 项目结构 3 | --- 4 | 5 | Vector2 整体采用 memorepo 对项目进行分包处理。 6 | 7 | - apis 8 | 9 | 主题 API 的网络请求。这个包为了让用户构建主题,能够共有相关的 CRUD API,网络请求相关的内容写在这里面。 10 | 11 | - packages/vector-core 12 | 13 | Vector2 核心代码,用于将数据文件渲染成相应的数据结构。 14 | 15 | - packages/vector-data 16 | 17 | 存在数据,Vector2 默认从这个里面读取数据。 18 | 19 | - packages/vector-ui 20 | 21 | vector 可视化界面。 22 | -------------------------------------------------------------------------------- /packages/theme-nana/src/service/dev.ts: -------------------------------------------------------------------------------- 1 | export function isDevMode() { 2 | // @ts-ignore 3 | return import.meta.env.MODE === "development"; 4 | } 5 | 6 | export function getBackendPath() { 7 | if (isDevMode()) { 8 | return "http://127.0.0.1:8081"; 9 | } else { 10 | return "https://mongo-demo-eyqyo1-1252406184.ap-shanghai.app.tcloudbase.com/awesome"; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/vector/src/utils/schedule.ts: -------------------------------------------------------------------------------- 1 | export class Scheduler { 2 | private tasks: Array> = []; 3 | 4 | public async addAsyncTask(asyncTask: () => Promise) { 5 | this.tasks.push(asyncTask()); 6 | } 7 | 8 | public async waitForAllTasks(): Promise { 9 | const res = await Promise.all(this.tasks); 10 | this.tasks = []; 11 | return res; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "targets": { 7 | "chrome": "58", 8 | "ie": "11" 9 | }, 10 | "useBuiltIns": "usage", 11 | "corejs": "3" 12 | } 13 | ] 14 | ], 15 | "plugins": [ 16 | [ 17 | "@babel/plugin-transform-runtime", 18 | { 19 | "corejs": 3 20 | } 21 | ] 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /packages/theme-nana/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | 26 | 27 | public/image/* 28 | 29 | .temp -------------------------------------------------------------------------------- /packages/theme-nana/src/component/Waiting.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | 9 | 21 | -------------------------------------------------------------------------------- /packages/theme-nana/src/service/screen.ts: -------------------------------------------------------------------------------- 1 | import {ref} from "vue"; 2 | 3 | const isMobile = ref(true); 4 | 5 | 6 | function justifyIsMobile() { 7 | return document.body.clientWidth < 750 8 | } 9 | 10 | isMobile.value = justifyIsMobile(); 11 | 12 | window.addEventListener("resize", function () { 13 | isMobile.value = justifyIsMobile(); 14 | }); 15 | 16 | export function useScreen() { 17 | return { 18 | isMobile 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/theme-nana/components.d.ts: -------------------------------------------------------------------------------- 1 | // generated by unplugin-vue-components 2 | // We suggest you to commit this file into source control 3 | // Read more: https://github.com/vuejs/vue-next/pull/3399 4 | import '@vue/runtime-core' 5 | 6 | declare module '@vue/runtime-core' { 7 | export interface GlobalComponents { 8 | RouterLink: typeof import('vue-router')['RouterLink'] 9 | RouterView: typeof import('vue-router')['RouterView'] 10 | } 11 | } 12 | 13 | export {} 14 | -------------------------------------------------------------------------------- /packages/vector/src/engine/setup.ts: -------------------------------------------------------------------------------- 1 | import { executeHooks } from "../hook"; 2 | import { getRuntime, initEngineRuntime } from "../runtime"; 3 | import { renderMarkdownFile, getMarkdownFiles } from "../utils"; 4 | 5 | export async function setup() { 6 | await initEngineRuntime(); 7 | const config = getRuntime().getConfig(); 8 | const markdownFiles = await getMarkdownFiles(config.dataDir); 9 | const jobs = markdownFiles.map(renderMarkdownFile); 10 | const hookObj = await Promise.all(jobs); 11 | executeHooks(hookObj); 12 | } 13 | -------------------------------------------------------------------------------- /packages/theme-nana/src/service/data.ts: -------------------------------------------------------------------------------- 1 | import {reactive} from "vue"; 2 | 3 | export function useRemoteData(fetchFunc: Function) { 4 | const arr = reactive([] as any[]); 5 | fetchFunc().then((res: any[]) => { 6 | arr.push(...res); 7 | }); 8 | return arr; 9 | } 10 | 11 | export function arr2Map(arr: any[], key: string) { 12 | const m = {} as { [key: string]: any }; 13 | arr.forEach(object => { 14 | m[object[key]] = object; 15 | }); 16 | return m; 17 | } 18 | 19 | export function run(f: Function) { 20 | f(); 21 | } 22 | -------------------------------------------------------------------------------- /packages/theme-nana/src/page/home/Xmind.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 13 | 14 | 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Vector2", 3 | "version": "1.0.0", 4 | "type": "module", 5 | "description": "", 6 | "scripts": { 7 | "dev": "npm-run-all --parallel dev:*", 8 | "dev:vector": "cd packages/vector && npm run dev", 9 | "dev:nana": "cd packages/theme-nana && npm run dev" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "ISC", 14 | "dependencies": { 15 | "@types/node": "^18.7.23", 16 | "jest": "^29.4.3", 17 | "typescript": "^5.0.2" 18 | }, 19 | "devDependencies": { 20 | "npm-run-all": "^4.1.5" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/vector/src/utils/count.ts: -------------------------------------------------------------------------------- 1 | export class Count { 2 | postCount = 0; 3 | wordCount = 0; 4 | imageCount = 0; 5 | totalTime = 0; 6 | lastModified = 0; 7 | 8 | private constructor() {} 9 | 10 | private static instance = new Count(); 11 | static getInstance() { 12 | return this.instance; 13 | } 14 | 15 | countWord(n: number) { 16 | this.wordCount += n; 17 | } 18 | countPost(n: number) { 19 | this.postCount += n; 20 | } 21 | updateLastModified(time: number) { 22 | if (time > this.lastModified) { 23 | this.lastModified = time; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/theme-nana/src/service/time.ts: -------------------------------------------------------------------------------- 1 | export function transformTimeReadable( 2 | time: number, 3 | format = 'YYYY-MM-DD' 4 | ) { 5 | const date = new Date(time); 6 | const config: any = { 7 | YYYY: date.getFullYear(), 8 | MM: 9 | date.getMonth() + 1 < 10 10 | ? `0${date.getMonth() + 1}` 11 | : date.getMonth() + 1, 12 | DD: date.getDate(), 13 | HH: date.getHours(), 14 | mm: date.getMinutes(), 15 | ss: date.getSeconds(), 16 | }; 17 | for (let key in config) { 18 | format = format.replace(key, config[key]); 19 | } 20 | return format; 21 | } 22 | -------------------------------------------------------------------------------- /packages/vector/src/hook/hook.ts: -------------------------------------------------------------------------------- 1 | import { PostHookObj, HookObj } from "../utils"; 2 | 3 | export type InitHook = ( 4 | hookObjs: Array 5 | ) => Array | Promise>; 6 | 7 | export type BeforeSaveHook = ( 8 | hookObjs: Array 9 | ) => Array | Promise>; 10 | 11 | export const hooks = { 12 | init: [] as Array, 13 | beforeSave: [] as Array 14 | }; 15 | 16 | export function registInitHook(hook: InitHook) { 17 | hooks.init.push(hook); 18 | } 19 | 20 | export function registBeforeSaveHook(hook: BeforeSaveHook) { 21 | hooks.beforeSave.push(hook); 22 | } 23 | -------------------------------------------------------------------------------- /packages/theme-nana/src/hooks/useTheme.ts: -------------------------------------------------------------------------------- 1 | import { useLocalStorage } from '@vueuse/core'; 2 | 3 | const isDark = useLocalStorage( 4 | 'user-dark-choosed', 5 | window.matchMedia('(prefers-color-scheme: dark)').matches 6 | ); 7 | 8 | if (!isDark.value) { 9 | const bodyDom = document.querySelector('body'); 10 | bodyDom?.classList.toggle('brightness'); 11 | } 12 | 13 | const toggleTheme = () => { 14 | isDark.value = !isDark.value; 15 | const bodyDom = document.querySelector('body'); 16 | bodyDom?.classList.toggle('brightness'); 17 | }; 18 | 19 | export function useTheme() { 20 | return { 21 | isDark, 22 | toggleTheme, 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /packages/vector/src/index.ts: -------------------------------------------------------------------------------- 1 | // import path from "path-browserify"; 2 | 3 | async function importData(key: string) { 4 | return import("../dist/" + key); 5 | } 6 | 7 | export async function getAboutInfo(lang) { 8 | if (lang === "cn") { 9 | return importData("about.json"); 10 | } else { 11 | return importData("about.en.json"); 12 | } 13 | } 14 | 15 | export async function getPostList() { 16 | return (await importData("post/list.json")).items; 17 | } 18 | 19 | export async function getPostDetail(id: string) { 20 | return importData("post/" + id + ".json"); 21 | } 22 | 23 | export async function getThinkingData() { 24 | return importData("thinking.json"); 25 | } 26 | -------------------------------------------------------------------------------- /packages/vector/src/utils/common.ts: -------------------------------------------------------------------------------- 1 | import { createHash } from "crypto"; 2 | import memoize from "lodash/memoize"; 3 | 4 | /** 5 | * 获取字符串的 md5 值。 6 | */ 7 | export const getStringMD5 = (function () { 8 | function getMD5Hash(input: string) { 9 | const hash = createHash("md5"); 10 | hash.update(input); 11 | return hash.digest("hex"); 12 | } 13 | return memoize(getMD5Hash); 14 | })(); 15 | 16 | /** 17 | * 判断 URI 是否为图片。 18 | */ 19 | export function isImageURI(str: string): boolean { 20 | return !!str.match(/\.(jpeg|jpg|png|gif)$/i); 21 | } 22 | 23 | /** 24 | * 判断 URI 是否为网络路径。 25 | */ 26 | export function isHttpURI(str: string): boolean { 27 | return !!str.match(/^https?:\/\//i); 28 | } 29 | -------------------------------------------------------------------------------- /packages/vector/src/utils/git.ts: -------------------------------------------------------------------------------- 1 | interface GitInfo { 2 | time: number; 3 | author: string; 4 | commitInfo: string; 5 | } 6 | 7 | import { execSync } from "child_process"; 8 | 9 | /** 10 | * 获取指定存储库中特定文件的最新提交信息 11 | * @param repoPath 存储库路径 12 | * @param filePath 文件路径 13 | * @returns 包含提交哈希、作者、日期和消息的对象 14 | */ 15 | export function getLatestFileCommitInfo( 16 | repoPath: string, 17 | filePath: string 18 | ): GitInfo { 19 | const latestCommit = execSync( 20 | `cd ${repoPath} && git log --pretty=format:"{time: %ct000, commitInfo: '%s', author: '%cn'}" -- ${filePath}` 21 | ) 22 | .toString() 23 | .trim(); 24 | 25 | // 将输出转换为 JSON 对象 26 | const commitInfo = JSON.parse(latestCommit); 27 | 28 | return commitInfo; 29 | } 30 | -------------------------------------------------------------------------------- /packages/vector/src/hook/index.ts: -------------------------------------------------------------------------------- 1 | import { PostHookObj } from "../utils"; 2 | export { registInitHook, registBeforeSaveHook } from "./hook"; 3 | import { hooks } from "./hook"; 4 | import { initHooks } from "./init.hook"; 5 | import { beforeSaveHooks } from "./before.save.hook"; 6 | 7 | export async function executeHooks(data: PostHookObj[]) { 8 | const bundleHooks = [ 9 | ...hooks.init, 10 | ...initHooks, 11 | ...hooks.beforeSave, 12 | ...beforeSaveHooks 13 | ]; 14 | for (let i = 0; i < bundleHooks.length; i++) { 15 | const hook = bundleHooks[i]; 16 | const res = hook(data); 17 | if (res instanceof Promise) { 18 | data = (await res) as any; 19 | } else { 20 | data = res as any; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/vector/test/src/utils/io.test.ts: -------------------------------------------------------------------------------- 1 | import rewire from "rewire" 2 | const io = rewire("../../../src/utils/io") 3 | const ensureDirExists = io.__get__("ensureDirExists") 4 | // @ponicode 5 | describe("ensureDirExists", () => { 6 | test("0", async () => { 7 | await ensureDirExists("path/to/folder/") 8 | }) 9 | 10 | test("1", async () => { 11 | await ensureDirExists("./path/to/file") 12 | }) 13 | 14 | test("2", async () => { 15 | await ensureDirExists("path/to/file.ext") 16 | }) 17 | 18 | test("3", async () => { 19 | await ensureDirExists("/path/to/file") 20 | }) 21 | 22 | test("4", async () => { 23 | await ensureDirExists(".") 24 | }) 25 | 26 | test("5", async () => { 27 | await ensureDirExists("") 28 | }) 29 | }) 30 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | // rollup.config.js 2 | import { nodeResolve } from "@rollup/plugin-node-resolve"; 3 | import commonjs from "@rollup/plugin-commonjs"; 4 | import babel from "@rollup/plugin-babel"; 5 | 6 | export default { 7 | input: "src/index.js", // 入口文件 8 | output: [ 9 | { 10 | file: "dist/my-library.js", // 输出文件路径 11 | format: "umd", // 输出格式 12 | name: "MyLibrary", // UMD 包名称 13 | }, 14 | { 15 | file: "dist/my-library.esm.js", // 输出文件路径 16 | format: "esm", // 输出格式 17 | }, 18 | ], 19 | plugins: [ 20 | nodeResolve(), // 解析第三方模块 21 | commonjs(), // 转换 CommonJS 模块为 ES6 模块 22 | babel({ 23 | // Babel 转换 24 | exclude: "node_modules/**", // 排除 node_modules 目录 25 | babelHelpers: "bundled", // 使用 Bundled 模式 26 | }), 27 | ], 28 | }; 29 | -------------------------------------------------------------------------------- /packages/theme-nana/src/component/LazyImage.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 25 | -------------------------------------------------------------------------------- /packages/theme-nana/src/service/aes.ts: -------------------------------------------------------------------------------- 1 | import CryptoJS from 'crypto-js'; 2 | 3 | export function verifyPassword(key: string) { 4 | console.log(key); 5 | return decodeString(key, 'VTJGc2RHVmtYMStKY2J0b1lzT01IN2JPYWFXUW0zaEl2KytCT1JPOC9NWT0=') === 'treecat'; 6 | } 7 | 8 | export function decodeString(key: string, content: string) { 9 | const c = CryptoJS.enc.Base64.parse(content + "").toString(CryptoJS.enc.Utf8) 10 | const decrypted = CryptoJS.AES.decrypt(c, key + "", { 11 | iv: CryptoJS.enc.Utf8.parse(''), 12 | mode: CryptoJS.mode.CBC, 13 | padding: CryptoJS.pad.Pkcs7 14 | }); 15 | try { 16 | console.log("解锁结果", key, decrypted.toString(CryptoJS.enc.Utf8).toString()) 17 | return decrypted.toString(CryptoJS.enc.Utf8).toString(); 18 | } catch (e) { 19 | console.log("解锁失败", key, "?") 20 | return '' 21 | 22 | } 23 | } 24 | 25 | -------------------------------------------------------------------------------- /packages/vector/src/utils/hook.ts: -------------------------------------------------------------------------------- 1 | interface TocItem { 2 | text: string; 3 | type: "h1" | "h2" | "h3" | "h4" | "h5" | "h6"; 4 | target: string; 5 | } 6 | 7 | export interface PostHookObj { 8 | type: "PostHookObj"; 9 | ctime: number; 10 | mtime: number; 11 | // 目录 12 | toc: Array; 13 | cover?: string; 14 | _private: { 15 | filePath: string; 16 | except: boolean; 17 | // 文章内容 18 | rawContent: string; 19 | rawContentBody: string; 20 | }; 21 | title: string; 22 | id: string; 23 | md5: string; 24 | content: string; 25 | word: number; 26 | } 27 | 28 | export interface IndexHookObj { 29 | type: "IndexHookObj"; 30 | items: Array<{ mtime: number; cover: string; id: string; title: string }>; 31 | _private: { 32 | filePath: string; 33 | }; 34 | } 35 | 36 | export type HookObj = PostHookObj | IndexHookObj; 37 | -------------------------------------------------------------------------------- /packages/theme-nana/src/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from "vue"; 2 | import "./assets/style/global.less"; 3 | import App from "./App.vue"; 4 | import router from "./router/router"; 5 | import VueAnimateOnScroll from "vue3-animate-onscroll"; 6 | import animated from "animate.css"; 7 | import { library } from "@fortawesome/fontawesome-svg-core"; 8 | import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome"; 9 | 10 | import { 11 | faUser, 12 | faAngleLeft, 13 | faEdit, 14 | faSun, 15 | faMessage, 16 | faMoon, 17 | faFileWord, 18 | faSearch, 19 | faCheck, 20 | faTag, 21 | } from "@fortawesome/free-solid-svg-icons"; 22 | 23 | [ 24 | faFileWord, 25 | faSearch, 26 | faEdit, 27 | faCheck, 28 | faTag, 29 | faUser, 30 | faAngleLeft, 31 | faMoon, 32 | faSun, 33 | faMessage, 34 | ].forEach((el) => { 35 | library.add(el); 36 | }); 37 | 38 | const app = createApp(App); 39 | 40 | app.component("icon", FontAwesomeIcon); 41 | app.use(animated); 42 | app.use(VueAnimateOnScroll); 43 | app.use(router); 44 | app.mount("#app"); 45 | -------------------------------------------------------------------------------- /packages/theme-nana/src/component/Cat.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 26 | 27 | 41 | -------------------------------------------------------------------------------- /packages/theme-nana/src/component/ThemeController.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 24 | 25 | 40 | -------------------------------------------------------------------------------- /packages/theme-nana/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "strict": true, 6 | "jsx": "preserve", 7 | "moduleResolution": "node", 8 | "experimentalDecorators": true, 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "forceConsistentCasingInFileNames": true, 14 | "useDefineForClassFields": true, 15 | "sourceMap": true, 16 | "baseUrl": ".", 17 | "resolveJsonModule": true, 18 | "types": [ 19 | "webpack-env" 20 | ], 21 | "paths": { 22 | "@/*": [ 23 | "src/*" 24 | ] 25 | }, 26 | "lib": [ 27 | "esnext", 28 | "dom", 29 | "dom.iterable", 30 | "scripthost" 31 | ] 32 | }, 33 | "include": [ 34 | "src/**/*.ts", 35 | "src/**/*.tsx", 36 | "src/**/*.vue", 37 | "tests/**/*.ts", 38 | "tests/**/*.tsx" 39 | , "src/store/crud.js" ], 40 | "exclude": [ 41 | "node_modules", 42 | "node_modules/**/**" 43 | ] 44 | } 45 | -------------------------------------------------------------------------------- /packages/vector/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vector-core", 3 | "version": "1.0.0", 4 | "module": "src/index.ts", 5 | "types": "src/index.ts", 6 | "scripts": { 7 | "dev": "ts-node src/cli.ts", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "anywhere": "^1.6.0", 15 | "axios": "^1.3.3", 16 | "front-matter": "^4.0.2", 17 | "js-yaml": "^4.1.0", 18 | "lodash": "^4.17.21", 19 | "markdown-it": "^13.0.1", 20 | "path-browserify": "^1.0.1", 21 | "rewire": "^6.0.0", 22 | "ts-node": "^10.9.1", 23 | "typescript": "^4.9.5" 24 | }, 25 | "devDependencies": { 26 | "@babel/core": "^7.20.12", 27 | "@babel/preset-env": "^7.20.2", 28 | "@types/jest": "^29.4.0", 29 | "@types/js-yaml": "^4.0.5", 30 | "@types/lodash": "^4.14.191", 31 | "@types/markdown-it": "^12.2.3", 32 | "@types/node": "^18.15.5", 33 | "babel-jest": "^29.4.3", 34 | "jest": "^29.4.3", 35 | "ts-jest": "^29.0.5" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /packages/theme-nana/src/component/Chat.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 27 | 28 | 41 | -------------------------------------------------------------------------------- /packages/theme-nana/src/page/doc.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 26 | 27 | 40 | -------------------------------------------------------------------------------- /packages/vector/src/utils/image/image.render.ts: -------------------------------------------------------------------------------- 1 | import yaml from "js-yaml"; 2 | import { localImageHandler, urlImageHandler } from "./image.handle"; 3 | import { isImageURI, isHttpURI } from "../common"; 4 | import path from "path"; 5 | 6 | function processObject(filePath: string, obj: any) { 7 | for (const key in obj) { 8 | let objValue = obj[key]; 9 | if (typeof objValue === "string") { 10 | if (!isImageURI(objValue)) continue; 11 | if (!isHttpURI(objValue)) { 12 | objValue = path.resolve(path.dirname(filePath), objValue); 13 | } 14 | const handler = isHttpURI(objValue) ? urlImageHandler : localImageHandler; 15 | obj[key] = handler(objValue); 16 | } else if (typeof obj[key] === "object") { 17 | processObject(filePath, obj[key]); 18 | } 19 | } 20 | } 21 | 22 | export const markdownImageRender = { 23 | renderHeader(filePath: string, content: string) { 24 | const doc = yaml.load(content); 25 | processObject(filePath, doc); 26 | return yaml.dump(doc); 27 | }, 28 | renderBody(filePath: string, content: string) { 29 | return content; 30 | }, 31 | }; 32 | -------------------------------------------------------------------------------- /packages/theme-nana/src/service/localstorage.service.ts: -------------------------------------------------------------------------------- 1 | import { reactive, toRefs } from 'vue'; 2 | 3 | export enum LocalStorageKey { 4 | WebSiteSetting = 'page.website.setting.post.filter', 5 | Locker = 'cn.treecat.auth.locker.localstorage', 6 | Cat = 'component.cat.show.cat', 7 | } 8 | 9 | export function useLocalStorage( 10 | key: LocalStorageKey, 11 | defaultValue: T 12 | ) { 13 | const save = () => { 14 | localStorage.setItem(key.toString(), JSON.stringify(data)); 15 | }; 16 | let s = localStorage.getItem(key.toString()); 17 | const data = reactive(defaultValue); 18 | if (s) { 19 | Object.assign(data, JSON.parse(s)); 20 | } else { 21 | save(); 22 | } 23 | 24 | return { 25 | data, 26 | save, 27 | }; 28 | } 29 | 30 | export function useLocalStoragePlain(key: LocalStorageKey, defaultValue: T) { 31 | const { data, save } = useLocalStorage(key, { v: defaultValue }); 32 | const { v } = toRefs(data); 33 | return { 34 | data: v, 35 | save: function () { 36 | data.v = v.value; 37 | save(); 38 | }, 39 | }; 40 | } 41 | -------------------------------------------------------------------------------- /packages/theme-nana/src/page/home/Flag.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 18 | 19 | 56 | -------------------------------------------------------------------------------- /packages/theme-nana/src/page/home/index.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 40 | 41 | 54 | -------------------------------------------------------------------------------- /packages/theme-nana/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | Treecat 15 | 16 | 26 | 27 | 28 |
29 | 30 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /packages/theme-nana/src/config.ts: -------------------------------------------------------------------------------- 1 | export const devApiUrl = 'http://127.0.0.1:4000/'; 2 | export const productionApiUrl = '/api'; 3 | export const devBackendUrl = 'http://127.0.0.1:8080'; 4 | export const productionBackendUrl = ''; 5 | export const leastLoadInterval = 200; 6 | 7 | export const siteConfig = reactive({ 8 | avatar: 'https://static-1252406184.cos.ap-guangzhou.myqcloud.com/awesome/avatar.jpg', 9 | nickname: 'Treecat', 10 | phone: 'Collected by BOX Spider', 11 | postDefaultCover: 12 | 'https://static-1252406184.cos.ap-guangzhou.myqcloud.com/cover_w800/6.jpg', 13 | homeAsideSpeaks: [ 14 | '山风微微,像月光下晃动的海浪,温和而柔软,停留在时光的背后,变成小时候听过的故事。在遥远的城市,陌生的地方,有他未曾见过的山和海。', 15 | '云边有个小卖部,货架堆着岁月和夕阳,背后就是山,老人靠着躺椅假装睡着,小孩子偷走了一块糖,泪水几点钟落地,飞鸟要去向何方,人们聚和离,云朵来又往,讲故事的人,总有一个故事不愿讲,时光飞逝,悄悄话变成纸张。', 16 | ], 17 | flags: [ 18 | { 19 | text: '🎈 刷完力扣500题', 20 | status: '完成175题', 21 | }, 22 | { 23 | text: '🚀 做 React 版的主题', 24 | status: '正在进行', 25 | }, 26 | { 27 | text: '🍳 看 React 源码并总结', 28 | status: '正在进行', 29 | }, 30 | { 31 | text: '🕚 习惯:9点睡觉4点起', 32 | status: '正在养成', 33 | }, 34 | ], 35 | }); 36 | -------------------------------------------------------------------------------- /packages/theme-nana/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import vue from "@vitejs/plugin-vue"; 3 | import * as path from "path"; 4 | import AutoImport from "unplugin-auto-import/vite"; 5 | import viteCompression from "vite-plugin-compression"; 6 | 7 | export default defineConfig({ 8 | plugins: [ 9 | vue(), 10 | AutoImport({ 11 | imports: ["vue", "vue-router"], // 自动导入vue和vue-router相关函数 12 | dts: "src/auto-import.d.ts", // 生成 `auto-import.d.ts` 全局声明 13 | }), 14 | viteCompression({ 15 | verbose: true, 16 | disable: false, 17 | deleteOriginFile: false, 18 | threshold: 10240, 19 | algorithm: "gzip", 20 | ext: ".gz", 21 | }), 22 | ], 23 | resolve: { 24 | alias: { 25 | "@": path.join(__dirname, "./src"), 26 | "@store": path.join(__dirname, "./src/store"), 27 | "@component": path.join(__dirname, "./src/component"), 28 | "@config": path.join(__dirname, "./src/config.ts"), 29 | "@page": path.join(__dirname, "./src/page"), 30 | "@image": path.join(__dirname, "./src/assets/image"), 31 | }, 32 | }, 33 | css: { 34 | preprocessorOptions: { 35 | less: { 36 | charset: false, 37 | additionalData: '@import "./src/assets/style/global.less";', 38 | }, 39 | }, 40 | }, 41 | }); 42 | -------------------------------------------------------------------------------- /packages/vector/src/runtime/engine.runtime.ts: -------------------------------------------------------------------------------- 1 | import { config, initConfig } from "./config"; 2 | import path from "path"; 3 | 4 | export async function initEngineRuntime() { 5 | await initConfig(); 6 | } 7 | 8 | /** 9 | * 引擎运行时,包含一些关键性的方法和数据 10 | */ 11 | export function getRuntime() { 12 | return { 13 | getConfig() { 14 | return config; 15 | }, 16 | getImageRenderUrl(imageName: string) {}, 17 | getImageVectorPath(imageName: string) { 18 | return path.resolve(config.distDir, config.imageDirName, imageName); 19 | }, 20 | getMarkdownRenderPath(filePath: string, targetName: string) { 21 | const relativePath = filePath.replace(config.dataDir, ""); 22 | const dirName = path.dirname(relativePath); 23 | if (dirName == path.sep) { 24 | return path 25 | .join(config.distDir, relativePath) 26 | .replace(/\.md$/, ".json"); 27 | } else { 28 | if (filePath.endsWith("index.json")) { 29 | return path.join(config.distDir, dirName, "index.json"); 30 | } else { 31 | return path.join(config.distDir, dirName, targetName + ".json"); 32 | } 33 | } 34 | } 35 | }; 36 | } 37 | -------------------------------------------------------------------------------- /packages/vector/src/utils/image/index.ts: -------------------------------------------------------------------------------- 1 | import { markdownImageRender } from "./image.render"; 2 | 3 | const SPLIT_FLAG = "---\n"; 4 | 5 | /** 6 | * 用来处理文本中的图片数据,考虑到我们的程序会进行大量的计算, 7 | * 所以图片的处理,全部交给调度器来处理 8 | */ 9 | export function renderImage( 10 | filePath: string, 11 | content: string, 12 | type: "markdown" = "markdown" 13 | ) { 14 | if (type === "markdown") { 15 | const { renderBody, renderHeader } = markdownImageRender; 16 | const frontMatterRegex = /^---\r?\n([\s\S]*?)\n---\r?\n/; 17 | 18 | const frontMatterMatch = content.match(frontMatterRegex); 19 | let header = frontMatterMatch ? frontMatterMatch[1] : ""; 20 | let body = content.replace(frontMatterRegex, ""); 21 | if (header) { 22 | header = renderHeader(filePath, header); 23 | body = renderBody(filePath, body); 24 | return `---\n${header}---\n${body}`; 25 | } else { 26 | return renderBody(filePath, body); 27 | } 28 | 29 | // if (wrapper.length < 3) { 30 | // console.log(filePath); 31 | // return renderBody(filePath, content); 32 | // } else { 33 | // let [_, header, body] = content.split(SPLIT_FLAG); 34 | // header = renderHeader(filePath, header); 35 | // body = renderBody(filePath, content); 36 | // return header + SPLIT_FLAG + body; 37 | // } 38 | } 39 | return ""; 40 | } 41 | -------------------------------------------------------------------------------- /packages/vector/src/utils/file.reader.ts: -------------------------------------------------------------------------------- 1 | import memoize from "lodash/memoize"; 2 | import fs from "fs"; 3 | import crypto from "crypto"; 4 | 5 | interface FileDescriptor { 6 | filePath: string; 7 | content: string; 8 | md5: string; 9 | ctime: number; 10 | mtime: number; 11 | isReady: () => boolean; 12 | ready: () => Promise; 13 | } 14 | 15 | class FileReader { 16 | private fileDescriptor: FileDescriptor; 17 | 18 | constructor(filePath: string) { 19 | this.fileDescriptor = { 20 | filePath, 21 | content: "", 22 | md5: "", 23 | ctime: 0, 24 | mtime: 0, 25 | isReady: () => false, 26 | ready: async () => { 27 | const fileStat = await fs.promises.stat(filePath); 28 | this.fileDescriptor.ctime = fileStat.ctimeMs; 29 | this.fileDescriptor.mtime = fileStat.mtimeMs; 30 | const fileContent = await fs.promises.readFile(filePath, "utf8"); 31 | this.fileDescriptor.content = fileContent; 32 | const md5sum = crypto.createHash("md5"); 33 | md5sum.update(fileContent); 34 | this.fileDescriptor.md5 = md5sum.digest("hex"); 35 | this.fileDescriptor.isReady = () => true; 36 | }, 37 | }; 38 | } 39 | 40 | public getFileDescriptor(): FileDescriptor { 41 | return this.fileDescriptor; 42 | } 43 | } 44 | 45 | export const getFileReader = memoize( 46 | (filePath: string) => new FileReader(filePath) 47 | ); 48 | -------------------------------------------------------------------------------- /packages/vector/src/utils/image/image.handle.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import path from "path"; 3 | import fs from "fs"; 4 | import { imageSchedule } from "./image.schedule"; 5 | import { ensureDirExists } from "../io"; 6 | import { getRuntime } from "../../runtime"; 7 | import { getStringMD5 } from "../common"; 8 | 9 | function getImageSavePath(url: string) { 10 | const extName = path.extname(url); 11 | const md5 = getStringMD5(url); 12 | return getRuntime().getImageVectorPath(md5 + extName); 13 | } 14 | 15 | function getImageRenderUrl(url: string) { 16 | const extName = path.extname(url); 17 | const md5 = getStringMD5(url); 18 | return getRuntime().getImageRenderUrl(md5 + extName); 19 | } 20 | 21 | export function urlImageHandler(url: string) { 22 | const destinationPath = getImageSavePath(url); 23 | imageSchedule.addAsyncTask(async () => { 24 | const response = await axios({ 25 | method: "get", 26 | url, 27 | responseType: "stream", 28 | }); 29 | const stream = fs.createWriteStream(destinationPath); 30 | response.data.pipe(stream); 31 | }); 32 | 33 | return getImageRenderUrl(url); 34 | } 35 | 36 | export function localImageHandler(imagePath: string) { 37 | const destinationPath = getImageSavePath(imagePath); 38 | imageSchedule.addAsyncTask(async () => { 39 | await ensureDirExists(path.dirname(destinationPath)); 40 | await fs.promises.copyFile(imagePath, destinationPath); 41 | }); 42 | return getImageRenderUrl(imagePath); 43 | } 44 | -------------------------------------------------------------------------------- /packages/theme-nana/src/router/router.ts: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHashHistory } from 'vue-router'; 2 | import NotFind from '@/page/404.vue'; 3 | 4 | const routes = [ 5 | { 6 | path: '/', 7 | component: () => import('../page/layout/index.vue'), 8 | redirect: '/home', 9 | children: [ 10 | { 11 | path: '/home', 12 | component: () => import('../page/home/index.vue'), 13 | }, 14 | { path: '/post', component: () => import('../page/post.vue') }, 15 | { 16 | path: '/post/page/:page', 17 | component: () => import('../page/post.vue'), 18 | }, 19 | { 20 | path: '/thinking', 21 | component: () => import('../page/thinking.vue'), 22 | }, 23 | { 24 | path: '/about', 25 | component: () => import('../page/about.vue'), 26 | }, 27 | ], 28 | }, 29 | { 30 | path: '/post/:id', 31 | component: () => import('@/page/post.detail.vue'), 32 | }, 33 | { 34 | path: '/doc/:module', 35 | component: () => import('../page/doc.vue'), 36 | }, 37 | { 38 | path: '/404', 39 | component: NotFind, 40 | }, 41 | { 42 | path: '/:catchAll(.*)', 43 | redirect: '/404', 44 | }, 45 | ]; 46 | 47 | const router = createRouter({ 48 | history: createWebHashHistory(), 49 | routes, 50 | }); 51 | 52 | export default router; 53 | -------------------------------------------------------------------------------- /packages/vector/src/runtime/config.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import fs from "fs/promises"; 3 | import { merge } from "lodash"; 4 | 5 | interface VectorConfig { 6 | tmpDir: string; 7 | dataDir: string; 8 | rootUrl: string; 9 | imageDirName: string; 10 | distDir: string; 11 | } 12 | 13 | export const config: VectorConfig = { uninitialized: true } as any; 14 | 15 | export async function initConfig() { 16 | if (!(config as any).uninitialized) { 17 | return; 18 | } 19 | const rootDir = path.resolve(__dirname, "../../../.."); 20 | const defultTmpDir = path.resolve(__dirname, "../..", "_tmp"); 21 | const distDir = path.resolve(__dirname, "../..", "dist"); 22 | const defualtDataDir = path.resolve(rootDir, "packages/vector-data"); 23 | const rootUrl = "http://127.0.0.1:3000"; 24 | const userConfig = {}; 25 | try { 26 | const data = await fs.readFile( 27 | path.resolve(rootDir, "vector.config.js") 28 | ); 29 | const __dirname = rootDir; 30 | const obj = eval(data.toString()); 31 | Object.assign(userConfig, obj); 32 | } catch (err) { 33 | console.log("配置失败"); 34 | } 35 | Object.assign( 36 | config, 37 | merge( 38 | { 39 | tmpDir: defultTmpDir, 40 | dataDir: defualtDataDir, 41 | distDir: distDir, 42 | rootUrl, 43 | imageDirName: "__image__" 44 | } as VectorConfig, 45 | userConfig 46 | ) 47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /packages/vector/vector.config.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import fs from "fs"; 3 | import os from "os"; 4 | 5 | const config = { 6 | tmpDir: os.tmpdir(), 7 | dataDir: "C:\\Users\\Treecat\\Desktop\\vector\\source", 8 | sourceDir: path.resolve("source"), 9 | distDir: path.resolve("api"), 10 | restfulRoot: "/api", 11 | postUrl: "/#/post/:id", 12 | encryptKey: "123456", 13 | imageDirPath: path.resolve("api/__web_images__"), 14 | imageReplacer: (el: any) => { 15 | function getImagePrefix() { 16 | if (process.env.mode === "dev") { 17 | return "http://127.0.0.1:4000/images/"; 18 | } else { 19 | return "http://treecat.cn/api/images/"; 20 | } 21 | } 22 | return getImagePrefix() + el + "?imageMogr2/format/webp"; 23 | }, 24 | listFileName: "list.json", 25 | listMapFileName: "list.map.json", 26 | errorImage: "http://treecat.cn/api/images/error.jpg?imageMogr2/format/webp", 27 | sourceRepoes: "git@gitee.com:bytesci/treecat-doc.git", 28 | theme: { 29 | path: path.resolve("theme/nana"), 30 | productPath: path.resolve("theme/nana/dist"), 31 | script: { 32 | dev: "yarn dev", 33 | build: "yarn build", 34 | }, 35 | }, 36 | cos: { 37 | secretConfig: { SecretId: "", SecretKey: "" }, 38 | Bucket: "treecat-cn-1252406184", 39 | Region: "ap-guangzhou", 40 | }, 41 | }; 42 | 43 | try { 44 | const secretConfigFile = fs.readFileSync("./secret.json").toString(); 45 | Object.assign(config.cos.secretConfig, JSON.parse(secretConfigFile)); 46 | } catch (err) {} 47 | 48 | export default config; 49 | -------------------------------------------------------------------------------- /packages/theme-nana/src/store/crud.js: -------------------------------------------------------------------------------- 1 | import { sendStaticGetRequest } from "@/store/api"; 2 | import { isDevMode } from "@/service/dev"; 3 | export { 4 | getAboutInfo, 5 | getPostList, 6 | getPostDetail, 7 | getThinkingData 8 | } from "vector-core"; 9 | 10 | export function useData(data, fetch) { 11 | const loaded = ref(false); 12 | let isDesctoryed = false; 13 | const invokeQueue = []; 14 | const stopWatch = watch( 15 | loaded, 16 | (v) => { 17 | if (v) { 18 | invokeQueue.forEach((cb) => { 19 | cb(); 20 | }); 21 | invokeQueue.length = 0; 22 | stopWatch(); 23 | } 24 | }, 25 | { immediate: true } 26 | ); 27 | const getData = async () => { 28 | const res = await fetch(); 29 | loaded.value = true; 30 | Object.assign(data, res); 31 | if (isDevMode()) { 32 | getData(); 33 | } 34 | }; 35 | getData(); 36 | const onDataLoaded = (cb) => { 37 | if (loaded.value) { 38 | cb(); 39 | } else { 40 | invokeQueue.push(cb); 41 | } 42 | }; 43 | const destory = () => { 44 | stopWatch(); 45 | isDesctoryed = true; 46 | }; 47 | return { 48 | onDataLoaded, 49 | loaded, 50 | destory 51 | }; 52 | } 53 | 54 | export async function getModuleData(module) { 55 | return sendStaticGetRequest(`/${module}.json`); 56 | } 57 | 58 | export async function getPostListMap() { 59 | return (await sendStaticGetRequest(`/post/list.map.json`)).map; 60 | } 61 | 62 | export function usePostDetail() {} 63 | -------------------------------------------------------------------------------- /packages/vector/test/file.reader.test.js: -------------------------------------------------------------------------------- 1 | import { getFileReader } from "../utils/file.reader"; 2 | 3 | describe("getFileReader", () => { 4 | it("returns the same instance when called with the same file path", () => { 5 | const filePath = "test/fixtures/file.txt"; 6 | const reader1 = getFileReader(filePath); 7 | const reader2 = getFileReader(filePath); 8 | 9 | expect(reader1).toBe(reader2); 10 | }); 11 | 12 | it("returns different instances when called with different file paths", () => { 13 | const filePath1 = "test/fixtures/file1.txt"; 14 | const filePath2 = "test/fixtures/file2.txt"; 15 | const reader1 = getFileReader(filePath1); 16 | const reader2 = getFileReader(filePath2); 17 | 18 | expect(reader1).not.toBe(reader2); 19 | }); 20 | 21 | it("returns instances with the correct file path", () => { 22 | const filePath = "test/fixtures/file.txt"; 23 | const reader = getFileReader(filePath); 24 | 25 | expect(reader.getFileDescriptor().filePath).toBe(filePath); 26 | }); 27 | 28 | it("returns instances with the correct content", async () => { 29 | const filePath = "test/fixtures/file.txt"; 30 | const reader = getFileReader(filePath); 31 | 32 | await reader.getFileDescriptor().ready(); 33 | 34 | expect(reader.getFileDescriptor().content).toBe("Hello, world!\n"); 35 | }); 36 | 37 | it("returns instances with the correct md5", async () => { 38 | const filePath = "test/fixtures/file.txt"; 39 | const reader = getFileReader(filePath); 40 | 41 | await reader.getFileDescriptor().ready(); 42 | 43 | expect(reader.getFileDescriptor().md5).toBe( 44 | "6cd3556deb0da54bca060b4c39479839" 45 | ); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /packages/vector/src/utils/io.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs/promises"; 2 | import path from "path"; 3 | 4 | /** 5 | * 确保目录存在,如果不存在,则创建之。 6 | */ 7 | export const ensureDirExists = (function () { 8 | const dirCache = new Set(); 9 | return async function (filePath: string) { 10 | if (dirCache.has(filePath)) { 11 | return; 12 | } 13 | try { 14 | const stat = await fs.stat(filePath); 15 | if (!stat.isDirectory()) { 16 | throw new Error(`${filePath} exists but is not a directory`); 17 | } 18 | } catch (err) { 19 | if (err.code === "ENOENT") { 20 | // 双重校验锁 21 | if (dirCache.has(filePath)) { 22 | return; 23 | } 24 | await fs.mkdir(filePath, { recursive: true }); 25 | dirCache.add(filePath); 26 | } else { 27 | throw err; 28 | } 29 | } 30 | }; 31 | })(); 32 | 33 | /** 34 | * 获取目录下的全部 Markdown 文件。 35 | */ 36 | export async function getMarkdownFiles(folderPath: string): Promise { 37 | const result = []; 38 | async function traverse(folderPath: string) { 39 | const markdownFiles: string[] = []; 40 | const files = await fs.readdir(folderPath); 41 | for (const file of files) { 42 | const filePath = path.join(folderPath, file); 43 | const stats = await fs.stat(filePath); 44 | if (stats.isDirectory()) { 45 | if (file.startsWith(".")) { 46 | continue; 47 | } else { 48 | await traverse(filePath); 49 | } 50 | } else if (stats.isFile() && path.extname(filePath) === ".md") { 51 | markdownFiles.push(filePath); 52 | } 53 | } 54 | result.push(...markdownFiles); 55 | } 56 | await traverse(folderPath); 57 | return result; 58 | } 59 | -------------------------------------------------------------------------------- /packages/theme-nana/src/assets/image/pane_about.dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 11 | 13 | 16 | -------------------------------------------------------------------------------- /packages/theme-nana/src/assets/image/pane_about.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 11 | 13 | 16 | -------------------------------------------------------------------------------- /packages/theme-nana/src/assets/style/var.less: -------------------------------------------------------------------------------- 1 | body.brightness { 2 | --color-card-background-secondary: #d0d0d0; 3 | --color-card-background: #e1e1e1; 4 | --color-meta-text: #333; 5 | --color-normal-text: #222; 6 | --color-primary: rgb(0, 174, 174); 7 | --color-light: rgba(0, 174, 174, 0.473); 8 | --color-primary-dark: rgb(0, 174, 174); 9 | --shadow-1: rgb(0 0 0 / 8%) 0px 3px 6px, rgb(0 0 0 / 10%) 0px 3px 6px; 10 | --shadow-2: rgb(0 0 0 / 8%) 0px 3px 3px, rgb(0 0 0 / 10%) 0px 3px 3px; 11 | } 12 | 13 | // --------------- color 14 | @color-primary: var(--color-primary, rgb(255, 232, 158)); 15 | @color-primary-light: var(--color-primary, rgb(101, 101, 101)); 16 | @color-primary-dark: var(--color-primary-dark, rgb(255, 221, 108)); 17 | @color-card-background-secondary: var(--color-card-background-secondary, #444); 18 | @color-card-background: var(--color-card-background, #333); 19 | @color-normal-text: var(--color-normal-text, #ccc); 20 | @color-meta-text: var(--color-meta-text, #999); 21 | 22 | // --------------- size 23 | @size-max-valid-content: 1000px; 24 | @size-mobile: 750px; 25 | @size-card-radius: 8px; 26 | 27 | @size-meta-font: 13px; 28 | @size-normal-font: 17px; 29 | @size-title-font: 21px; 30 | 31 | @size-card-padding-big: 24px 40px; 32 | @size-card-padding: 16px 24px; 33 | @size-card-padding-small: 12px 12px 12px 16px; 34 | 35 | // --------------- font 36 | @font-serif: Lora, Noto Serif SC, ui-serif, Georgia, Cambria, times new roman, 37 | Times, serif; 38 | 39 | // --------------- effect 40 | @shadow-1: var( 41 | --shadow-1, 42 | rgb(0 0 0 / 30%) 0px 3px 3px, 43 | rgb(0 0 0 / 15%) 0px 3px 3px 44 | ); 45 | 46 | @shadow-2: var( 47 | --shadow-2, 48 | rgb(0 0 0 / 30%) 0px 3px 3px, 49 | rgb(0 0 0 / 15%) 0px 3px 3px 50 | ); 51 | -------------------------------------------------------------------------------- /packages/theme-nana/src/assets/image/pane_preview.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /packages/theme-nana/src/assets/image/pane_blog.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/theme-nana/src/assets/image/pane_preview.dark.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 9 | 12 | 15 | 18 | -------------------------------------------------------------------------------- /packages/theme-nana/src/assets/image/pane_blog.dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 11 | 14 | -------------------------------------------------------------------------------- /packages/theme-nana/src/page/layout/Navbar.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 26 | 27 | 76 | -------------------------------------------------------------------------------- /packages/vector/src/hook/before.save.hook.ts: -------------------------------------------------------------------------------- 1 | import { Scheduler } from "../utils/schedule"; 2 | import fs from "fs/promises"; 3 | import path from "path"; 4 | import { getRuntime } from "../runtime"; 5 | import { PostHookObj, HookObj, IndexHookObj } from "../utils"; 6 | import { ensureDirExists } from "../utils/io"; 7 | import { BeforeSaveHook } from "./hook"; 8 | 9 | /** 10 | * 生成索引文件 11 | */ 12 | function generateIndexFile(hookObjs: PostHookObj[]): Array { 13 | const map = {} as Record; 14 | hookObjs.forEach((obj) => { 15 | const dir = path.dirname(obj._private.filePath); 16 | map[dir] = map[dir] || { 17 | items: [], 18 | _private: { filePath: dir }, 19 | type: "IndexHookObj" 20 | }; 21 | map[dir].items.push({ 22 | mtime: obj.mtime, 23 | cover: obj.cover, 24 | id: obj.id, 25 | title: obj.title 26 | }); 27 | }); 28 | return [...Object.values(map), ...hookObjs]; 29 | } 30 | 31 | /** 32 | * 把内容存下来 33 | */ 34 | function saveObj(hookObjs: HookObj[]) { 35 | const saveScheduler = new Scheduler(); 36 | hookObjs.forEach((obj) => { 37 | saveScheduler.addAsyncTask(async () => { 38 | let savePath = ""; 39 | if (obj.type === "IndexHookObj") { 40 | savePath = getRuntime().getMarkdownRenderPath( 41 | obj._private.filePath, 42 | "index" 43 | ); 44 | } else if (obj.type === "PostHookObj") { 45 | savePath = getRuntime().getMarkdownRenderPath( 46 | obj._private.filePath, 47 | obj.id 48 | ); 49 | } 50 | 51 | const { _private, ...saveObj } = obj; 52 | await ensureDirExists(path.dirname(savePath)); 53 | await fs.writeFile(savePath, JSON.stringify(saveObj)); 54 | }); 55 | }); 56 | return hookObjs; 57 | } 58 | 59 | export const beforeSaveHooks = [ 60 | generateIndexFile, 61 | saveObj 62 | ] as Array; 63 | -------------------------------------------------------------------------------- /packages/theme-nana/src/page/about.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 49 | 50 | 88 | -------------------------------------------------------------------------------- /packages/theme-nana/src/store/api.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosResponse } from "axios"; 2 | import { 3 | devApiUrl, 4 | devBackendUrl, 5 | productionApiUrl, 6 | productionBackendUrl, 7 | } from "@/config"; 8 | import { isDevMode } from "@/service/dev"; 9 | 10 | let api = axios.create({ 11 | headers: { 12 | // 'content-type': 'application/x-www-form-urlencoded' 13 | }, 14 | }) as any; 15 | 16 | api.interceptors.request.use( 17 | (config: any) => { 18 | // const {currentUser} = useUser(); 19 | // if(currentUser.value) { 20 | // config.headers.authorization = currentUser.value.ticket 21 | // } 22 | return config; 23 | }, 24 | (err: any) => { 25 | return Promise.reject(err); 26 | } 27 | ); 28 | 29 | api.interceptors.response.use( 30 | (response: AxiosResponse) => { 31 | //拦截响应,做统一处理 32 | // if (response.data.code !== 200) { 33 | // if (response.data.msg) { 34 | // Toast.fail(response.data.msg); 35 | // } 36 | // throw new Error(response.data.msg || "状态码错误"); 37 | // } 38 | // response = ; 39 | return response.data; 40 | }, 41 | //接口错误状态处理,也就是说无响应时的处理 42 | () => { 43 | // Toast.fail("网络请求错误"); 44 | throw new Error(); 45 | } 46 | ); 47 | 48 | // api.sendGetRequest = function (url: string, params?: Object) { 49 | // return api.get(url, params) 50 | // } 51 | 52 | let staticUrl = productionApiUrl; 53 | let backendUrl = productionBackendUrl; 54 | if (isDevMode()) { 55 | staticUrl = devApiUrl; 56 | backendUrl = devBackendUrl; 57 | } 58 | 59 | export async function sendStaticGetRequest( 60 | url: string, 61 | params?: any 62 | ): Promise { 63 | const startTime = new Date().getTime(); 64 | const result = await api.get(url.startsWith("http") ? url : staticUrl + url, { 65 | params, 66 | }); 67 | const endTime = new Date().getTime(); 68 | return new Promise((res) => { 69 | setTimeout( 70 | () => { 71 | res(result); 72 | }, 73 | isDevMode() ? 100 - (endTime - startTime) : 0 74 | ); 75 | }); 76 | } 77 | 78 | export function sendPostRequest( 79 | url: string, 80 | params?: { [key: string]: string } 81 | ): Promise { 82 | return api.post(url.startsWith("https") ? url : backendUrl + url, params); 83 | } 84 | -------------------------------------------------------------------------------- /packages/vector/src/hook/init.hook.ts: -------------------------------------------------------------------------------- 1 | import { PostHookObj } from "../utils"; 2 | import { getStringMD5 } from "../utils/common"; 3 | import { InitHook } from "./hook"; 4 | 5 | /** 6 | * 过滤不渲染的文章。 7 | */ 8 | function filterExcept(hookObjs: PostHookObj[]) { 9 | return hookObjs.filter((el) => !el._private.except); 10 | } 11 | 12 | /** 13 | * 给文章生成目录。 14 | */ 15 | function generatePostToc(hookObjs: PostHookObj[]) { 16 | hookObjs 17 | .filter((item) => item.content) 18 | .forEach((item) => { 19 | const reg = /<(h[1-6])>(.*?)<\/\1>/g; 20 | const content = item.content; 21 | // 给内容增加 id 22 | item.content = content.replace(reg, (_, g1, g2) => { 23 | return `<${g1} id=${getStringMD5( 24 | item.title + g2 25 | )}>${g2}`; 26 | }); 27 | // 生成 toc 28 | const list = Array.from(content.matchAll(new RegExp(reg))).map( 29 | (el) => { 30 | return { 31 | text: el[2], 32 | type: el[1], 33 | target: getStringMD5(item.title + el[2]) 34 | }; 35 | } 36 | ); 37 | // 把 toc 分组 38 | const groupList = []; 39 | list.forEach((item) => { 40 | if (item.type === "h2") { 41 | groupList.push(item); 42 | } else if (groupList.length > 0) { 43 | const lastItem = groupList[groupList.length - 1]; 44 | lastItem.items = lastItem.items || []; 45 | lastItem.items.push(item); 46 | } 47 | }); 48 | item.toc = groupList; 49 | }); 50 | return hookObjs; 51 | } 52 | 53 | /** 54 | * 生成封面 55 | */ 56 | function generateCover(hookObjs: PostHookObj[]) { 57 | hookObjs 58 | .filter((obj) => !obj.cover) 59 | .forEach((hookObj) => { 60 | const res = hookObj.content.match(//); 61 | if (res) { 62 | hookObj.cover = res[1]; 63 | } 64 | }); 65 | return hookObjs; 66 | } 67 | 68 | export const initHooks = [ 69 | filterExcept, 70 | generateCover, 71 | generatePostToc 72 | ] as Array; 73 | -------------------------------------------------------------------------------- /packages/theme-nana/src/assets/image/pane_thinking.dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 11 | 14 | 17 | -------------------------------------------------------------------------------- /packages/theme-nana/src/assets/image/pane_thinking.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 11 | 14 | 17 | -------------------------------------------------------------------------------- /packages/vector/src/utils/markdown.ts: -------------------------------------------------------------------------------- 1 | import fm from "front-matter"; 2 | import MarkdownIt from "markdown-it"; 3 | import { getFileReader } from "./file.reader"; 4 | import { PostHookObj } from "./hook"; 5 | import { renderImage } from "./image"; 6 | 7 | const md = MarkdownIt({ html: true }); 8 | 9 | function getValueFromObjects( 10 | key: string, 11 | obj1: Record = {}, 12 | obj2: Record = {}, 13 | defaultValue: any 14 | ): any { 15 | if (key in obj1) { 16 | return obj1[key]; 17 | } else if (key in obj2) { 18 | return obj2[key]; 19 | } else { 20 | return defaultValue; 21 | } 22 | } 23 | 24 | export async function renderMarkdownFile( 25 | filePath: string 26 | ): Promise { 27 | try { 28 | const fileDescriptor = getFileReader(filePath).getFileDescriptor(); 29 | if (!fileDescriptor.isReady()) { 30 | await fileDescriptor.ready(); 31 | } 32 | 33 | fileDescriptor.content = renderImage( 34 | filePath, 35 | fileDescriptor.content, 36 | "markdown" 37 | ); 38 | 39 | const rawContent = fileDescriptor.content; 40 | let { attributes, body: markdownBody } = fm(rawContent) as any; 41 | 42 | const [id, title, ctime, mtime] = [ 43 | ["id", fileDescriptor.md5], 44 | ["title", ""], 45 | ["ctime", 0], 46 | ["mtime", 0] 47 | ].map(([key, defaultValue]) => 48 | getValueFromObjects( 49 | key as string, 50 | attributes, 51 | fileDescriptor, 52 | defaultValue 53 | ) 54 | ); 55 | 56 | return { 57 | type: "PostHookObj", 58 | id, 59 | md5: fileDescriptor.md5, 60 | ctime, 61 | title, 62 | cover: attributes.cover, 63 | toc: [], 64 | mtime, 65 | word: markdownBody.length, 66 | content: md.render(rawContent), 67 | _private: { 68 | filePath, 69 | except: !!attributes.except, 70 | rawContent, 71 | rawContentBody: markdownBody 72 | } 73 | }; 74 | } catch (error) { 75 | console.error(`渲染 ${filePath} 失败\n` + error); 76 | return null; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /packages/theme-nana/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vector-theme-nana", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "build": "vite build", 7 | "dev": "vite serve --mode development" 8 | }, 9 | "dependencies": { 10 | "@cloudbase/js-sdk": "1.7.2", 11 | "@fortawesome/fontawesome-svg-core": "^6.1.1", 12 | "@fortawesome/free-brands-svg-icons": "^6.1.1", 13 | "@fortawesome/free-regular-svg-icons": "^6.1.1", 14 | "@fortawesome/free-solid-svg-icons": "^6.1.1", 15 | "@fortawesome/vue-fontawesome": "^3.0.0-5", 16 | "@tiptap/starter-kit": "^2.0.0-beta.199", 17 | "@tiptap/vue-3": "^2.0.0-beta.199", 18 | "@vueuse/core": "^9.11.1", 19 | "@wangeditor/editor": "^5.1.1", 20 | "@wangeditor/editor-for-vue": "^5.1.12", 21 | "animate.css": "4.1.1", 22 | "axios": "0.26.1", 23 | "crypto-js": "4.1.1", 24 | "dayjs": "^1.11.1", 25 | "echarts": "^5.3.2", 26 | "pinia": "^2.0.14", 27 | "pinia-plugin-persist": "^1.0.0", 28 | "prosemirror-example-setup": "^1.2.1", 29 | "prosemirror-model": "^1.18.1", 30 | "prosemirror-schema-basic": "^1.2.0", 31 | "prosemirror-state": "^1.4.1", 32 | "vector-core": "workspace:^1.0.0", 33 | "vite-plugin-compression": "^0.5.1", 34 | "vite-plugin-pages": "^0.26.0", 35 | "vue": "3.2.31", 36 | "vue-animate-onscroll": "1.0.8", 37 | "vue-class-component": "^8.0.0-0", 38 | "vue-router": "4.0.13", 39 | "vue-video-player": "5.0.2", 40 | "vue3-animate-onscroll": "1.0.6", 41 | "vuedraggable": "^4.1.0", 42 | "vuex": "4.0.2" 43 | }, 44 | "devDependencies": { 45 | "@types/crypto-js": "^4.1.1", 46 | "@types/lodash": "^4.14.182", 47 | "@types/markdown-it": "^12.2.3", 48 | "@types/node": "^18.7.23", 49 | "@vitejs/plugin-vue": "2.3.1", 50 | "@vue/cli-plugin-typescript": "~5.0.0", 51 | "@vue/cli-service": "^5.0.4", 52 | "concurrently": "^7.4.0", 53 | "cross-env": "^7.0.3", 54 | "front-matter": "^4.0.2", 55 | "koa": "2.13.4", 56 | "koa-router": "10.1.1", 57 | "koa2-cors": "2.0.6", 58 | "less": "^4.1.2", 59 | "less-loader": "10.2.0", 60 | "lodash": "^4.17.21", 61 | "lowdb": "^3.0.0", 62 | "markdown-it": "^13.0.1", 63 | "nodemon": "2.0.15", 64 | "ts-node": "^10.9.1", 65 | "typescript": "~4.5.5", 66 | "unplugin-auto-import": "^0.11.2", 67 | "unplugin-vue-components": "^0.19.5", 68 | "vite": "2.9.1", 69 | "vite-plugin-restart": "^0.2.0", 70 | "vite-plugin-style-import": "^1.4.1" 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /packages/theme-nana/src/assets/style/doc.less: -------------------------------------------------------------------------------- 1 | .doc { 2 | color: @color-normal-text; 3 | overflow-wrap: break-word; 4 | 5 | 6 | pre { 7 | overflow: auto; 8 | } 9 | 10 | ol { 11 | list-style: decimal; 12 | margin-left: 24px; 13 | padding: 0; 14 | } 15 | 16 | ul { 17 | list-style-type: disc; 18 | margin-left: 24px; 19 | 20 | li { 21 | margin-bottom: 12px; 22 | } 23 | } 24 | 25 | table { 26 | margin-top: 8px; 27 | } 28 | 29 | td, 30 | th { 31 | border: 2px solid rgb(152, 152, 152); 32 | padding: 8px 16px; 33 | } 34 | 35 | blockquote { 36 | background: @color-card-background-secondary; 37 | margin: 0; 38 | margin: 8px 0; 39 | border-radius: 8px; 40 | padding: 8px 16px; 41 | 42 | ul, 43 | ol { 44 | li { 45 | margin-bottom: 16px !important; 46 | } 47 | } 48 | 49 | ol { 50 | list-style: decimal; 51 | } 52 | 53 | p { 54 | margin-bottom: 0; 55 | } 56 | } 57 | 58 | p + p { 59 | margin: 16px 0 0; 60 | } 61 | 62 | p { 63 | font-size: @size-normal-font; 64 | line-height: 1.8; 65 | margin: 0; 66 | color: @color-normal-text; 67 | } 68 | 69 | pre { 70 | background: @color-card-background-secondary; 71 | padding: 8px; 72 | border-radius: 8px; 73 | } 74 | 75 | @media screen and (max-width: @size-mobile) { 76 | } 77 | 78 | h2, 79 | h3, 80 | h4 { 81 | font-weight: 600; 82 | margin-bottom: 8px; 83 | margin-top: 24px; 84 | color: @color-normal-text; 85 | } 86 | 87 | h2 { 88 | font-size: 22px; 89 | } 90 | 91 | h3 { 92 | font-size: 18px; 93 | } 94 | 95 | h4 { 96 | font-size: 14px; 97 | } 98 | 99 | img { 100 | display: block; 101 | margin: auto; 102 | border-radius: 8px; 103 | box-sizing: border-box; 104 | max-width: 100%; 105 | min-width: 200px; 106 | max-height: 600px; 107 | object-fit: cover; 108 | border: 1px solid @color-card-background-secondary; 109 | } 110 | 111 | video { 112 | width: 100%; 113 | } 114 | 115 | .hljs { 116 | background: @color-card-background-secondary; 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /packages/theme-nana/src/component/Navigation.vue: -------------------------------------------------------------------------------- 1 | 21 | 30 | 103 | -------------------------------------------------------------------------------- /packages/theme-nana/src/page/home/SimplePostList.vue: -------------------------------------------------------------------------------- 1 | 29 | 48 | 95 | -------------------------------------------------------------------------------- /packages/theme-nana/src/component/ImageGallery.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 46 | 47 | -------------------------------------------------------------------------------- /packages/theme-nana/src/page/layout/index.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 39 | 40 | 96 | -------------------------------------------------------------------------------- /packages/theme-nana/src/assets/style/code.less: -------------------------------------------------------------------------------- 1 | pre code.hljs { 2 | display: block; 3 | overflow-x: auto; 4 | } 5 | .hljs { 6 | font-size: 15px; 7 | padding: 3px 5px; 8 | color: #ddd; 9 | background: #303030; 10 | font-family: 'Arial', 'Microsoft YaHei', '黑体', '宋体', sans-serif; 11 | } 12 | .hljs-keyword, 13 | .hljs-link, 14 | .hljs-literal, 15 | .hljs-section, 16 | .hljs-selector-tag { 17 | color: #fff; 18 | } 19 | .hljs-addition, 20 | .hljs-attribute, 21 | .hljs-built_in, 22 | .hljs-bullet, 23 | .hljs-name, 24 | .hljs-string, 25 | .hljs-symbol, 26 | .hljs-template-tag, 27 | .hljs-template-variable, 28 | .hljs-title, 29 | .hljs-type, 30 | .hljs-variable { 31 | color: #d88; 32 | } 33 | .hljs-comment, 34 | .hljs-deletion, 35 | .hljs-meta, 36 | .hljs-quote { 37 | color: #979797; 38 | } 39 | .hljs-doctag, 40 | .hljs-keyword, 41 | .hljs-literal, 42 | .hljs-name, 43 | .hljs-section, 44 | .hljs-selector-tag, 45 | .hljs-strong, 46 | .hljs-title, 47 | .hljs-type { 48 | font-weight: 700; 49 | } 50 | .hljs-emphasis { 51 | font-style: italic; 52 | } 53 | 54 | .brightness { 55 | pre code.hljs { 56 | display: block; 57 | overflow-x: auto; 58 | padding: 1em; 59 | } 60 | code.hljs { 61 | padding: 3px 5px; 62 | } 63 | .hljs { 64 | color: #24292e; 65 | } 66 | .hljs-doctag, 67 | .hljs-keyword, 68 | .hljs-meta .hljs-keyword, 69 | .hljs-template-tag, 70 | .hljs-template-variable, 71 | .hljs-type, 72 | .hljs-variable.language_ { 73 | color: #d73a49; 74 | } 75 | .hljs-title, 76 | .hljs-title.class_, 77 | .hljs-title.class_.inherited__, 78 | .hljs-title.function_ { 79 | color: #6f42c1; 80 | } 81 | .hljs-attr, 82 | .hljs-attribute, 83 | .hljs-literal, 84 | .hljs-meta, 85 | .hljs-number, 86 | .hljs-operator, 87 | .hljs-selector-attr, 88 | .hljs-selector-class, 89 | .hljs-selector-id, 90 | .hljs-variable { 91 | color: #005cc5; 92 | } 93 | .hljs-meta .hljs-string, 94 | .hljs-regexp, 95 | .hljs-string { 96 | color: #032f62; 97 | } 98 | .hljs-built_in, 99 | .hljs-symbol { 100 | color: #e36209; 101 | } 102 | .hljs-code, 103 | .hljs-comment, 104 | .hljs-formula { 105 | color: #6a737d; 106 | } 107 | .hljs-name, 108 | .hljs-quote, 109 | .hljs-selector-pseudo, 110 | .hljs-selector-tag { 111 | color: #22863a; 112 | } 113 | .hljs-subst { 114 | color: #24292e; 115 | } 116 | .hljs-section { 117 | color: #005cc5; 118 | font-weight: 700; 119 | } 120 | .hljs-bullet { 121 | color: #735c0f; 122 | } 123 | .hljs-emphasis { 124 | color: #24292e; 125 | font-style: italic; 126 | } 127 | .hljs-strong { 128 | color: #24292e; 129 | font-weight: 700; 130 | } 131 | .hljs-addition { 132 | color: #22863a; 133 | background-color: #f0fff4; 134 | } 135 | .hljs-deletion { 136 | color: #b31d28; 137 | background-color: #ffeef0; 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /packages/theme-nana/src/assets/style/global.less: -------------------------------------------------------------------------------- 1 | @import './var.less'; 2 | @import './doc.less'; 3 | @import './code.less'; 4 | 5 | body { 6 | background-color: @color-card-background-secondary; 7 | font-family: Helvetica, 'Hiragino Sans GB', 'Microsoft Yahei', '微软雅黑', 8 | Arial, sans-serif; 9 | a { 10 | text-decoration: none; 11 | color: @color-primary; 12 | 13 | &:hover { 14 | color: @color-primary-dark; 15 | } 16 | } 17 | } 18 | 19 | .clickable { 20 | cursor: pointer; 21 | border-radius: 6px; 22 | &:hover { 23 | background: @color-card-background-secondary; 24 | } 25 | } 26 | 27 | div, 28 | p { 29 | color: @color-normal-text; 30 | } 31 | 32 | .root { 33 | width: 100%; 34 | height: 100vh; 35 | position: fixed; 36 | padding: 0; 37 | overflow: auto; 38 | margin: 0; 39 | background: @color-card-background; 40 | } 41 | 42 | .clickable { 43 | cursor: pointer; 44 | } 45 | 46 | .rotation { 47 | /*动画属性*/ 48 | animation: myAnimation 3s; 49 | animation-iteration-count: infinite; 50 | animation-timing-function: linear; 51 | } 52 | 53 | ul { 54 | margin: 0; 55 | padding: 0; 56 | list-style: none; 57 | } 58 | 59 | /* 定义动画如何执行 */ 60 | @keyframes myAnimation { 61 | /* 写法1 这种写法只能定义开始和结束时的动画 */ 62 | form { 63 | transform: rotate(0deg); 64 | } 65 | 66 | to { 67 | transform: rotate(360deg); 68 | } 69 | } 70 | 71 | .card { 72 | background: @color-card-background !important; 73 | overflow: hidden; 74 | box-shadow: @shadow-2; 75 | border-radius: @size-card-radius; 76 | } 77 | 78 | body.brightness { 79 | .knowledge-graph { 80 | img { 81 | filter: brightness(1); 82 | } 83 | } 84 | 85 | .brand { 86 | img { 87 | filter: invert(1); 88 | } 89 | } 90 | 91 | .chooser-cover-wrapper { 92 | filter: brightness(0.8) !important; 93 | } 94 | 95 | .page-post-detail { 96 | .post-header { 97 | .post-cover-wrapper { 98 | filter: brightness(0.8) !important; 99 | } 100 | .post-title, 101 | .post-meta-list { 102 | filter: invert(1); 103 | } 104 | } 105 | } 106 | 107 | .cat-container { 108 | filter: brightness(1); 109 | } 110 | 111 | .flag-list { 112 | .flag-item { 113 | color: @color-normal-text; 114 | } 115 | } 116 | } 117 | 118 | .search-dialog, 119 | .post-toc-content, 120 | .doc { 121 | ::-webkit-scrollbar { 122 | width: 6px; 123 | height: 6px; 124 | } 125 | ::-webkit-scrollbar-thumb, 126 | ::-webkit-scrollbar-thumb:horizontal { 127 | border-radius: 16px; 128 | background-color: @color-primary-light; 129 | } 130 | ::-webkit-scrollbar-track, 131 | ::-webkit-scrollbar-track:horizontal { 132 | background-color: @color-card-background-secondary; 133 | &:hover { 134 | width: 15px; 135 | } 136 | } 137 | ::-webkit-scrollbar-corner { 138 | background-color: @color-primary; 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /packages/theme-nana/src/auto-import.d.ts: -------------------------------------------------------------------------------- 1 | // Generated by 'unplugin-auto-import' 2 | export {} 3 | declare global { 4 | const EffectScope: typeof import('vue')['EffectScope'] 5 | const computed: typeof import('vue')['computed'] 6 | const createApp: typeof import('vue')['createApp'] 7 | const customRef: typeof import('vue')['customRef'] 8 | const defineAsyncComponent: typeof import('vue')['defineAsyncComponent'] 9 | const defineComponent: typeof import('vue')['defineComponent'] 10 | const effectScope: typeof import('vue')['effectScope'] 11 | const getCurrentInstance: typeof import('vue')['getCurrentInstance'] 12 | const getCurrentScope: typeof import('vue')['getCurrentScope'] 13 | const h: typeof import('vue')['h'] 14 | const inject: typeof import('vue')['inject'] 15 | const isProxy: typeof import('vue')['isProxy'] 16 | const isReactive: typeof import('vue')['isReactive'] 17 | const isReadonly: typeof import('vue')['isReadonly'] 18 | const isRef: typeof import('vue')['isRef'] 19 | const markRaw: typeof import('vue')['markRaw'] 20 | const nextTick: typeof import('vue')['nextTick'] 21 | const onActivated: typeof import('vue')['onActivated'] 22 | const onBeforeMount: typeof import('vue')['onBeforeMount'] 23 | const onBeforeRouteLeave: typeof import('vue-router')['onBeforeRouteLeave'] 24 | const onBeforeRouteUpdate: typeof import('vue-router')['onBeforeRouteUpdate'] 25 | const onBeforeUnmount: typeof import('vue')['onBeforeUnmount'] 26 | const onBeforeUpdate: typeof import('vue')['onBeforeUpdate'] 27 | const onDeactivated: typeof import('vue')['onDeactivated'] 28 | const onErrorCaptured: typeof import('vue')['onErrorCaptured'] 29 | const onMounted: typeof import('vue')['onMounted'] 30 | const onRenderTracked: typeof import('vue')['onRenderTracked'] 31 | const onRenderTriggered: typeof import('vue')['onRenderTriggered'] 32 | const onScopeDispose: typeof import('vue')['onScopeDispose'] 33 | const onServerPrefetch: typeof import('vue')['onServerPrefetch'] 34 | const onUnmounted: typeof import('vue')['onUnmounted'] 35 | const onUpdated: typeof import('vue')['onUpdated'] 36 | const provide: typeof import('vue')['provide'] 37 | const reactive: typeof import('vue')['reactive'] 38 | const readonly: typeof import('vue')['readonly'] 39 | const ref: typeof import('vue')['ref'] 40 | const resolveComponent: typeof import('vue')['resolveComponent'] 41 | const resolveDirective: typeof import('vue')['resolveDirective'] 42 | const shallowReactive: typeof import('vue')['shallowReactive'] 43 | const shallowReadonly: typeof import('vue')['shallowReadonly'] 44 | const shallowRef: typeof import('vue')['shallowRef'] 45 | const toRaw: typeof import('vue')['toRaw'] 46 | const toRef: typeof import('vue')['toRef'] 47 | const toRefs: typeof import('vue')['toRefs'] 48 | const triggerRef: typeof import('vue')['triggerRef'] 49 | const unref: typeof import('vue')['unref'] 50 | const useAttrs: typeof import('vue')['useAttrs'] 51 | const useCssModule: typeof import('vue')['useCssModule'] 52 | const useCssVars: typeof import('vue')['useCssVars'] 53 | const useLink: typeof import('vue-router')['useLink'] 54 | const useRoute: typeof import('vue-router')['useRoute'] 55 | const useRouter: typeof import('vue-router')['useRouter'] 56 | const useSlots: typeof import('vue')['useSlots'] 57 | const watch: typeof import('vue')['watch'] 58 | const watchEffect: typeof import('vue')['watchEffect'] 59 | const watchPostEffect: typeof import('vue')['watchPostEffect'] 60 | const watchSyncEffect: typeof import('vue')['watchSyncEffect'] 61 | } 62 | -------------------------------------------------------------------------------- /packages/theme-nana/src/component/part/_Loading.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 143 | -------------------------------------------------------------------------------- /packages/theme-nana/src/page/home/Toc.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 80 | 81 | 157 | -------------------------------------------------------------------------------- /packages/theme-nana/src/page/layout/ChooserPane.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 87 | 88 | 175 | -------------------------------------------------------------------------------- /packages/theme-nana/src/page/thinking.vue: -------------------------------------------------------------------------------- 1 | 48 | 49 | 66 | 67 | 177 | -------------------------------------------------------------------------------- /packages/theme-nana/src/component/Searcher.vue: -------------------------------------------------------------------------------- 1 | 46 | 47 | 133 | 134 | 204 | -------------------------------------------------------------------------------- /packages/theme-nana/src/page/post.detail.vue: -------------------------------------------------------------------------------- 1 | 55 | 56 | 173 | 174 | 310 | -------------------------------------------------------------------------------- /packages/theme-nana/src/page/post.vue: -------------------------------------------------------------------------------- 1 | 75 | 76 | 196 | 197 | 368 | -------------------------------------------------------------------------------- /packages/theme-nana/src/component/part/_Cat.vue: -------------------------------------------------------------------------------- 1 | 201 | 202 | 261 | 262 | 1310 | --------------------------------------------------------------------------------