初始框架(1.0.0)
This commit is contained in:
3
.browserslistrc
Normal file
3
.browserslistrc
Normal file
@@ -0,0 +1,3 @@
|
||||
> 1%
|
||||
last 2 versions
|
||||
not dead
|
19
.eslintrc.js
Normal file
19
.eslintrc.js
Normal file
@@ -0,0 +1,19 @@
|
||||
// const isProduction = (process.env.NODE_ENV === 'production');
|
||||
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
node: true
|
||||
},
|
||||
'extends': [
|
||||
'plugin:vue/essential',
|
||||
'eslint:recommended',
|
||||
],
|
||||
parserOptions: {
|
||||
parser: '@babel/eslint-parser'
|
||||
},
|
||||
rules: {
|
||||
// 'no-console': (isProduction ? 'warn' : 'off'),
|
||||
// 'no-debugger': (isProduction ? 'warn' : 'off'),
|
||||
}
|
||||
};
|
5
babel.config.js
Normal file
5
babel.config.js
Normal file
@@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
presets: [
|
||||
'@vue/cli-plugin-babel/preset'
|
||||
]
|
||||
};
|
24
jsconfig.json
Normal file
24
jsconfig.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": "./",
|
||||
"lib": [
|
||||
"esnext",
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"scripthost"
|
||||
],
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"src/*"
|
||||
]
|
||||
},
|
||||
"sourceMap": false,
|
||||
"target": "es5"
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.vue",
|
||||
"src/**/*.js"
|
||||
]
|
||||
}
|
19861
package-lock.json
generated
Normal file
19861
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
45
package.json
Normal file
45
package.json
Normal file
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"name": "frost-zx-blog",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve --mode development",
|
||||
"serve-production": "vue-cli-service serve --mode production",
|
||||
"serve-all": "vue-cli-service serve --mode development --host 0.0.0.0 ",
|
||||
"serve-local": "vue-cli-service serve --mode development --host 127.0.0.1",
|
||||
"build": "vue-cli-service build --mode production",
|
||||
"build-modern": "vue-cli-service build --mode production --modern",
|
||||
"lint-check": "vue-cli-service lint --no-fix",
|
||||
"lint-fix": "vue-cli-service lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mdi/font": "^6.5.95",
|
||||
"axios": "^0.26.0",
|
||||
"buefy": "^0.9.17",
|
||||
"core-js": "^3.8.3",
|
||||
"github-markdown-css": "^5.1.0",
|
||||
"highlight.js": "^11.4.0",
|
||||
"markdown-it": "^12.3.2",
|
||||
"ress": "^4.0.0",
|
||||
"vue": "^2.6.14",
|
||||
"vue-router": "^3.5.1",
|
||||
"vuex": "^3.6.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.12.16",
|
||||
"@babel/eslint-parser": "^7.12.16",
|
||||
"@types/markdown-it": "^12.2.3",
|
||||
"@vue/cli-plugin-babel": "~5.0.0",
|
||||
"@vue/cli-plugin-eslint": "~5.0.0",
|
||||
"@vue/cli-plugin-router": "~5.0.0",
|
||||
"@vue/cli-plugin-vuex": "~5.0.0",
|
||||
"@vue/cli-service": "~5.0.0",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-plugin-vue": "^8.0.3",
|
||||
"less": "^4.0.0",
|
||||
"less-loader": "^8.0.0",
|
||||
"sass": "^1.32.7",
|
||||
"sass-loader": "^12.0.0",
|
||||
"vue-template-compiler": "^2.6.14"
|
||||
}
|
||||
}
|
5
public/config.js
Normal file
5
public/config.js
Normal file
@@ -0,0 +1,5 @@
|
||||
var appConfig = {
|
||||
|
||||
contentList: [],
|
||||
|
||||
};
|
1
public/contents/_index/contents.md
Normal file
1
public/contents/_index/contents.md
Normal file
@@ -0,0 +1 @@
|
||||
请在左侧的文章目录选择内容。
|
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.6 KiB |
16
public/index.html
Normal file
16
public/index.html
Normal file
@@ -0,0 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<meta name="description" content="一个使用 Vue + markdown-it 开发实现的静态博客。">
|
||||
<meta name="keywords" content="blog,frost-zx,markdown,博客,静态博客">
|
||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
||||
<title>Frost-ZX 的主页</title>
|
||||
<script src="./config.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
</body>
|
||||
</html>
|
86
src/App.vue
Normal file
86
src/App.vue
Normal file
@@ -0,0 +1,86 @@
|
||||
<template>
|
||||
<div id="app">
|
||||
|
||||
<!-- 导航栏 -->
|
||||
<b-navbar class="app-header">
|
||||
|
||||
<template #brand>
|
||||
<b-navbar-item tag="router-link" :to="{ name: 'Home' }">
|
||||
<img src="./assets/image/avatar.png" alt="Avatar" />
|
||||
</b-navbar-item>
|
||||
</template>
|
||||
|
||||
<template #start>
|
||||
<b-navbar-item
|
||||
tag="router-link"
|
||||
:to="{ name: 'Home' }"
|
||||
:active="routeName === 'Home'"
|
||||
>主页</b-navbar-item>
|
||||
<b-navbar-item
|
||||
tag="router-link"
|
||||
:to="{ name: 'ContentIndex' }"
|
||||
:active="['Content','ContentIndex'].includes(routeName)"
|
||||
>文章</b-navbar-item>
|
||||
<b-navbar-item
|
||||
tag="router-link"
|
||||
:to="{ name: 'About' }"
|
||||
:active="routeName === 'About'"
|
||||
>关于</b-navbar-item>
|
||||
</template>
|
||||
|
||||
<template #end>
|
||||
<b-navbar-item tag="div">
|
||||
<a
|
||||
class="button is-light"
|
||||
href="https://github.com/Frost-ZX"
|
||||
target="_blank"
|
||||
>
|
||||
<b-icon icon="github"></b-icon>
|
||||
<span>GitHub</span>
|
||||
</a>
|
||||
</b-navbar-item>
|
||||
</template>
|
||||
|
||||
</b-navbar>
|
||||
|
||||
<!-- 页面内容 -->
|
||||
<router-view class="app-content" />
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'App',
|
||||
computed: {
|
||||
|
||||
/** 当前路由名称 */
|
||||
routeName(vm) {
|
||||
return vm.$route.name;
|
||||
},
|
||||
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
#app {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
> * {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.app-header {
|
||||
flex-shrink: 0;
|
||||
box-shadow: 0 0 1rem 0.25rem rgba(10, 10, 10, 0.1);
|
||||
}
|
||||
|
||||
.app-content {
|
||||
flex-grow: 1;
|
||||
height: 0;
|
||||
}
|
||||
</style>
|
27
src/assets/css/buefy.scss
Normal file
27
src/assets/css/buefy.scss
Normal file
@@ -0,0 +1,27 @@
|
||||
@charset "utf-8";
|
||||
|
||||
// https://buefy.org/documentation/customization
|
||||
// https://bulma.io/documentation/customize/variables/
|
||||
|
||||
// 导入 Bulma 的核心样式
|
||||
@import "~bulma/sass/utilities/_all";
|
||||
|
||||
// 自定义颜色
|
||||
$primary: #1976D2;
|
||||
|
||||
// 设置链接颜色
|
||||
$link: $primary;
|
||||
$link-invert: $primary-invert;
|
||||
$link-focus-border: $primary;
|
||||
|
||||
// 更新 map(与 Bulma 的默认值相同)
|
||||
// $colors
|
||||
$colors: mergeColorMaps(("white": ($white, $black), "black": ($black, $white), "light": ($light, $light-invert), "dark": ($dark, $dark-invert), "primary": ($primary, $primary-invert, $primary-light, $primary-dark), "link": ($link, $link-invert, $link-light, $link-dark), "info": ($info, $info-invert, $info-light, $info-dark), "success": ($success, $success-invert, $success-light, $success-dark), "warning": ($warning, $warning-invert, $warning-light, $warning-dark), "danger": ($danger, $danger-invert, $danger-light, $danger-dark)), $custom-colors);
|
||||
// $shades
|
||||
$shades: mergeColorMaps(("black-bis": $black-bis, "black-ter": $black-ter, "grey-darker": $grey-darker, "grey-dark": $grey-dark, "grey": $grey, "grey-light": $grey-light, "grey-lighter": $grey-lighter, "white-ter": $white-ter, "white-bis": $white-bis), $custom-shades);
|
||||
// $sizes
|
||||
$sizes: $size-1 $size-2 $size-3 $size-4 $size-5 $size-6 $size-7;
|
||||
|
||||
// 导入 Bulma 和 Buefy 的样式
|
||||
@import "~bulma/bulma.sass";
|
||||
@import "~buefy/src/scss/buefy";
|
42
src/assets/css/main.less
Normal file
42
src/assets/css/main.less
Normal file
@@ -0,0 +1,42 @@
|
||||
:root {
|
||||
font-size: 16px;
|
||||
--color-primary: #1976D2;
|
||||
}
|
||||
|
||||
// 滚动条
|
||||
::-webkit-scrollbar {
|
||||
width: 0.5rem;
|
||||
height: 0.5rem;
|
||||
}
|
||||
::-webkit-scrollbar-track {
|
||||
background-color: transparent;
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
border-radius: 1rem;
|
||||
background-color: #DFDFDF;
|
||||
|
||||
&:hover {
|
||||
background-color: #CCCCCC;
|
||||
}
|
||||
}
|
||||
|
||||
html, body, #app {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #FFF;
|
||||
overflow: hidden;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/* Buefy */
|
||||
|
||||
// 导航栏
|
||||
.navbar-item.is-active {
|
||||
color: inherit !important;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
// 侧边栏
|
||||
.sidebar-content {
|
||||
padding: 1em;
|
||||
}
|
BIN
src/assets/image/avatar.png
Normal file
BIN
src/assets/image/avatar.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 7.1 KiB |
BIN
src/assets/image/home-bg-full.jpg
Normal file
BIN
src/assets/image/home-bg-full.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.9 MiB |
BIN
src/assets/image/home-bg.jpg
Normal file
BIN
src/assets/image/home-bg.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 225 KiB |
43
src/assets/js/utils.js
Normal file
43
src/assets/js/utils.js
Normal file
@@ -0,0 +1,43 @@
|
||||
import { ToastProgrammatic as Toast } from 'buefy';
|
||||
|
||||
/**
|
||||
* @typedef ColorType
|
||||
* @type {'is-white'|'is-black'|'is-light'|'is-dark'|'is-primary'|'is-info'|'is-success'|'is-warning'|'is-danger'}
|
||||
*/
|
||||
|
||||
/**
|
||||
* @description 设置标题
|
||||
* @param {string} [title] 标题内容
|
||||
*/
|
||||
export const setTitle = (title) => {
|
||||
if (title) {
|
||||
document.title = `${title} - Frost-ZX`;
|
||||
} else {
|
||||
document.title = 'Frost-ZX 的主页';
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @description Toast
|
||||
* @param {object} options
|
||||
* @param {number} [options.duration] 时长(毫秒)
|
||||
* @param {boolean} [options.indefinite] 无限时长
|
||||
* @param {string} options.message 消息内容
|
||||
* @param {ColorType} [options.type] 消息类型
|
||||
*/
|
||||
export const toast = (options) => {
|
||||
const {
|
||||
duration = 2000,
|
||||
indefinite = false,
|
||||
message = '',
|
||||
type = 'is-info',
|
||||
} = options;
|
||||
|
||||
return Toast.open({
|
||||
duration,
|
||||
indefinite,
|
||||
message,
|
||||
pauseOnHover: true,
|
||||
type,
|
||||
});
|
||||
};
|
265
src/components/MarkdownParser.vue
Normal file
265
src/components/MarkdownParser.vue
Normal file
@@ -0,0 +1,265 @@
|
||||
<template>
|
||||
<div v-html="mdResult" class="markdown-body"></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// https://www.npmjs.com/package/github-markdown-css
|
||||
// https://www.npmjs.com/package/highlight.js
|
||||
// https://www.npmjs.com/package/markdown-it
|
||||
|
||||
import MarkdownIt from 'markdown-it';
|
||||
import hljs from 'highlight.js/lib/common';
|
||||
|
||||
import 'github-markdown-css/github-markdown-light.css';
|
||||
|
||||
export default {
|
||||
name: 'MarkdownParser',
|
||||
props: {
|
||||
|
||||
/** Markdown 源内容 */
|
||||
mdSrc: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
||||
/** @type {markdownit} */
|
||||
mdInstance: null,
|
||||
|
||||
/** Markdown 转换结果 */
|
||||
mdResult: '',
|
||||
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
|
||||
// 自动渲染
|
||||
mdSrc: {
|
||||
handler(value) {
|
||||
this.render(value);
|
||||
}
|
||||
},
|
||||
|
||||
},
|
||||
mounted() {
|
||||
this.initMD();
|
||||
},
|
||||
methods: {
|
||||
|
||||
/** 初始化实例 */
|
||||
initMD() {
|
||||
|
||||
const vm = this;
|
||||
|
||||
/** @type {markdownit.Options} */
|
||||
const options = {
|
||||
html: true,
|
||||
xhtmlOut: true,
|
||||
breaks: true,
|
||||
langPrefix: 'language-',
|
||||
linkify: false,
|
||||
highlight: (str, lang) => {
|
||||
|
||||
const tagPrefix = `<pre><code class="hljs language-${lang}">`;
|
||||
const tagSuffix = '</code></pre>';
|
||||
|
||||
let content = '';
|
||||
let highlighted = false;
|
||||
|
||||
if (lang && hljs.getLanguage(lang)) {
|
||||
try {
|
||||
// 高亮成功
|
||||
content = hljs.highlight(str, {
|
||||
language: lang,
|
||||
ignoreIllegals: true,
|
||||
}).value;
|
||||
highlighted = true;
|
||||
} catch (error) {
|
||||
// 高亮失败
|
||||
highlighted = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!highlighted) {
|
||||
const md = vm.mdInstance;
|
||||
content = (md ? md.utils.escapeHtml(str) : '');
|
||||
}
|
||||
|
||||
return `${tagPrefix}${content}${tagSuffix}`;
|
||||
|
||||
},
|
||||
}
|
||||
|
||||
/** @type {markdownit} */
|
||||
const md = new MarkdownIt(options);
|
||||
|
||||
this.mdInstance = md;
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* @description 渲染内容
|
||||
* @param {string} [mdSrc]
|
||||
*/
|
||||
render(mdSrc) {
|
||||
|
||||
const { mdInstance: md } = this;
|
||||
|
||||
if (md) {
|
||||
this.mdResult = md.render(mdSrc || this.mdSrc);
|
||||
} else {
|
||||
console.warn('实例不存在,取消渲染。');
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
.markdown-body {
|
||||
margin: 0 auto;
|
||||
padding: 2.5rem;
|
||||
box-sizing: border-box;
|
||||
min-width: 12.5rem;
|
||||
max-width: 60rem;
|
||||
user-select: text;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
img {
|
||||
box-shadow: 0 0 0.75rem rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: disc;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
/*
|
||||
https://highlightjs.org/static/demo/
|
||||
https://highlightjs.org/static/demo/styles/github.css
|
||||
*/
|
||||
|
||||
/*
|
||||
Theme: GitHub
|
||||
Description: Light theme as seen on github.com
|
||||
Author: github.com
|
||||
Maintainer: @Hirse
|
||||
Updated: 2021-05-15
|
||||
|
||||
Outdated base version: https://github.com/primer/github-syntax-light
|
||||
Current colors taken from GitHub's CSS
|
||||
*/
|
||||
|
||||
pre code.hljs {
|
||||
display: block;
|
||||
/* padding: 1em; */
|
||||
max-height: 80vh;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
code.hljs {
|
||||
padding: 3px 5px;
|
||||
}
|
||||
|
||||
.hljs {
|
||||
color: #24292E;
|
||||
background: #FFF;
|
||||
}
|
||||
|
||||
.hljs-doctag,
|
||||
.hljs-keyword,
|
||||
.hljs-meta .hljs-keyword,
|
||||
.hljs-template-tag,
|
||||
.hljs-template-variable,
|
||||
.hljs-type,
|
||||
.hljs-variable.language_ {
|
||||
color: #D73A49;
|
||||
}
|
||||
|
||||
.hljs-title,
|
||||
.hljs-title.class_,
|
||||
.hljs-title.class_.inherited__,
|
||||
.hljs-title.function_ {
|
||||
color: #6F42C1;
|
||||
}
|
||||
|
||||
.hljs-attr,
|
||||
.hljs-attribute,
|
||||
.hljs-literal,
|
||||
.hljs-meta,
|
||||
.hljs-number,
|
||||
.hljs-operator,
|
||||
.hljs-selector-attr,
|
||||
.hljs-selector-class,
|
||||
.hljs-selector-id,
|
||||
.hljs-variable {
|
||||
color: #005CC5;
|
||||
}
|
||||
|
||||
.hljs-meta .hljs-string,
|
||||
.hljs-regexp,
|
||||
.hljs-string {
|
||||
color: #032F62;
|
||||
}
|
||||
|
||||
.hljs-built_in,
|
||||
.hljs-symbol {
|
||||
color: #E36209;
|
||||
}
|
||||
|
||||
.hljs-code,
|
||||
.hljs-comment,
|
||||
.hljs-formula {
|
||||
color: #6A737D;
|
||||
}
|
||||
|
||||
.hljs-name,
|
||||
.hljs-quote,
|
||||
.hljs-selector-pseudo,
|
||||
.hljs-selector-tag {
|
||||
color: #22863A;
|
||||
}
|
||||
|
||||
.hljs-subst {
|
||||
color: #24292E;
|
||||
}
|
||||
|
||||
.hljs-section {
|
||||
color: #005CC5;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.hljs-bullet {
|
||||
color: #735C0F;
|
||||
}
|
||||
|
||||
.hljs-emphasis {
|
||||
color: #24292E;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.hljs-strong {
|
||||
color: #24292E;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.hljs-addition {
|
||||
color: #22863A;
|
||||
background-color: #F0FFF4;
|
||||
}
|
||||
|
||||
.hljs-deletion {
|
||||
color: #B31D28;
|
||||
background-color: #FFEEF0;
|
||||
}
|
||||
</style>
|
25
src/main.js
Normal file
25
src/main.js
Normal file
@@ -0,0 +1,25 @@
|
||||
import Buefy from 'buefy'
|
||||
import Vue from 'vue';
|
||||
|
||||
import App from './App.vue';
|
||||
import router from './router';
|
||||
// import store from './store';
|
||||
|
||||
import 'ress/ress.css';
|
||||
import '@mdi/font/css/materialdesignicons.css';
|
||||
import '@/assets/css/buefy.scss';
|
||||
import '@/assets/css/main.less';
|
||||
|
||||
Vue.config.productionTip = false;
|
||||
|
||||
Vue.use(Buefy, {
|
||||
defaultIconPack: 'mdi',
|
||||
defaultDialogConfirmText: '确定',
|
||||
defaultDialogCancelText: '取消',
|
||||
})
|
||||
|
||||
new Vue({
|
||||
router,
|
||||
// store,
|
||||
render: h => h(App)
|
||||
}).$mount('#app');
|
25
src/request/index.js
Normal file
25
src/request/index.js
Normal file
@@ -0,0 +1,25 @@
|
||||
import request from './request';
|
||||
|
||||
/**
|
||||
* @description 获取正文内容文件
|
||||
* @param {object} options
|
||||
* @param {string} options.category 分类名称
|
||||
* @param {string} options.itemName 内容名称
|
||||
*/
|
||||
export const getContentFile = (options) => {
|
||||
|
||||
const {
|
||||
category,
|
||||
itemName,
|
||||
} = options;
|
||||
|
||||
if (category && itemName) {
|
||||
return request({
|
||||
url: `/contents/${category}/${itemName}.md`,
|
||||
method: 'get',
|
||||
});
|
||||
} else {
|
||||
return Promise.reject();
|
||||
}
|
||||
|
||||
};
|
72
src/request/request.js
Normal file
72
src/request/request.js
Normal file
@@ -0,0 +1,72 @@
|
||||
import axios from 'axios';
|
||||
|
||||
/**
|
||||
* @typedef RequestParams
|
||||
* @type {Object.<string, (number|string|number[]|string[])}
|
||||
*/
|
||||
|
||||
const instance = axios.create({
|
||||
baseURL: '',
|
||||
timeout: 10000,
|
||||
validateStatus: function (status) {
|
||||
return ((status >= 200 && status < 300) || status === 404);
|
||||
},
|
||||
});
|
||||
|
||||
// 拦截请求
|
||||
instance.interceptors.request.use((config) => {
|
||||
return config;
|
||||
}, (error) => {
|
||||
return Promise.reject(error);
|
||||
});
|
||||
|
||||
// 拦截响应
|
||||
instance.interceptors.response.use((response) => {
|
||||
return response;
|
||||
}, (error) => {
|
||||
return Promise.reject(error);
|
||||
});
|
||||
|
||||
/**
|
||||
* @description 处理请求
|
||||
* @param {object} options 配置选项
|
||||
* @param {('get'|'post')} [options.method] 请求方式,默认 GET
|
||||
* @param {string} options.url 请求地址
|
||||
* @param {RequestParams} [options.datas] 请求体(请求方式为 POST 时可用)
|
||||
* @param {RequestParams} [options.query] 查询参数
|
||||
*/
|
||||
const request = function (options = {}) {
|
||||
|
||||
const method = String(options.method || 'get').toLowerCase();
|
||||
const {
|
||||
url = null,
|
||||
datas = null,
|
||||
query = null,
|
||||
} = options;
|
||||
|
||||
if (url === null) {
|
||||
console.error('请求地址为空。');
|
||||
return Promise.reject();
|
||||
}
|
||||
|
||||
if (method === 'get') {
|
||||
return instance({
|
||||
method,
|
||||
url,
|
||||
params: query,
|
||||
});
|
||||
} else if (method === 'post') {
|
||||
return instance({
|
||||
method,
|
||||
url,
|
||||
data: datas,
|
||||
params: query,
|
||||
});
|
||||
} else {
|
||||
console.error('请求方式错误。');
|
||||
return Promise.reject();
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
export default request;
|
39
src/router/index.js
Normal file
39
src/router/index.js
Normal file
@@ -0,0 +1,39 @@
|
||||
import Vue from 'vue';
|
||||
import VueRouter from 'vue-router';
|
||||
|
||||
import routes from './routes';
|
||||
|
||||
import { toast } from '@/assets/js/utils';
|
||||
|
||||
const {
|
||||
push: routerPush,
|
||||
replace: routerReplace,
|
||||
} = VueRouter.prototype;
|
||||
|
||||
const errorHandler = function (error) {
|
||||
console.warn(String(error));
|
||||
};
|
||||
|
||||
VueRouter.prototype.push = function (location) {
|
||||
return routerPush.call(this, location).catch(errorHandler);
|
||||
};
|
||||
|
||||
VueRouter.prototype.replace = function (location) {
|
||||
return routerReplace.call(this, location).catch(errorHandler);
|
||||
};
|
||||
|
||||
Vue.use(VueRouter);
|
||||
|
||||
const router = new VueRouter({
|
||||
routes
|
||||
});
|
||||
|
||||
router.onError(() => {
|
||||
toast({
|
||||
duration: 5000,
|
||||
message: '页面跳转失败,请检查网络连接情况。',
|
||||
type: 'is-danger',
|
||||
});
|
||||
});
|
||||
|
||||
export default router;
|
33
src/router/routes.js
Normal file
33
src/router/routes.js
Normal file
@@ -0,0 +1,33 @@
|
||||
import AboutView from '@/views/AboutView.vue';
|
||||
import ContentView from '@/views/ContentView.vue';
|
||||
import HomeView from '@/views/HomeView.vue';
|
||||
|
||||
/** @type { import('vue-router').RouteConfig[] } */
|
||||
const routes = [
|
||||
{
|
||||
path: '/',
|
||||
name: 'Home',
|
||||
component: HomeView,
|
||||
},
|
||||
{
|
||||
path: '/content',
|
||||
name: 'ContentIndex',
|
||||
component: ContentView,
|
||||
},
|
||||
{
|
||||
path: '/content/:category/:name',
|
||||
name: 'Content',
|
||||
component: ContentView,
|
||||
},
|
||||
{
|
||||
path: '/about',
|
||||
name: 'About',
|
||||
component: AboutView,
|
||||
// component: () => import(
|
||||
// /* webpackChunkName: 'about' */
|
||||
// '@/views/About.vue'
|
||||
// ),
|
||||
},
|
||||
];
|
||||
|
||||
export default routes;
|
17
src/store/index.js
Normal file
17
src/store/index.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import Vue from 'vue';
|
||||
import Vuex from 'vuex';
|
||||
|
||||
Vue.use(Vuex);
|
||||
|
||||
export default new Vuex.Store({
|
||||
state: {
|
||||
},
|
||||
getters: {
|
||||
},
|
||||
mutations: {
|
||||
},
|
||||
actions: {
|
||||
},
|
||||
modules: {
|
||||
}
|
||||
});
|
35
src/views/AboutView.vue
Normal file
35
src/views/AboutView.vue
Normal file
@@ -0,0 +1,35 @@
|
||||
<template>
|
||||
<div class="about-view">
|
||||
<div class="about-wrapper markdown-body">
|
||||
|
||||
<h2>关于本站</h2>
|
||||
|
||||
<ul>
|
||||
<li>使用 Vue CLI 5.0 搭建,基于 Vue 2.X,集成 Vue Router 前端路由。</li>
|
||||
<li>采用了基于 Bulma CSS 框架的 Buefy 组件库。</li>
|
||||
<li>使用了 axios,用于读取 Markdown 文件。</li>
|
||||
<li>使用了 markdown-it,用于将 Markdown 转换为 HTML。</li>
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import 'github-markdown-css/github-markdown-light.css';
|
||||
|
||||
import { setTitle } from '@/assets/js/utils';
|
||||
|
||||
export default {
|
||||
name: 'AboutView',
|
||||
created() {
|
||||
setTitle('关于');
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.about-wrapper {
|
||||
padding: 2.5rem;
|
||||
}
|
||||
</style>
|
387
src/views/ContentView.vue
Normal file
387
src/views/ContentView.vue
Normal file
@@ -0,0 +1,387 @@
|
||||
<template>
|
||||
<div class="content-view" :class="{ 'show-sidebar': isShowSidebar }">
|
||||
|
||||
<!-- 侧边栏 -->
|
||||
<b-sidebar
|
||||
class="content-sidebar"
|
||||
mobile="hide"
|
||||
position="static"
|
||||
type="is-light"
|
||||
:open="true"
|
||||
:reduce="false"
|
||||
fullheight
|
||||
>
|
||||
<b-menu :accordion="false" :activable="false">
|
||||
|
||||
<b-menu-list label="文章信息">
|
||||
<!-- 创建日期 -->
|
||||
<b-menu-item
|
||||
icon="creation"
|
||||
:label="'创建:' + contentInfo.createdAt"
|
||||
></b-menu-item>
|
||||
<!-- 更新日期 -->
|
||||
<b-menu-item
|
||||
icon="update"
|
||||
:label="'更新:' + contentInfo.updatedAt"
|
||||
></b-menu-item>
|
||||
</b-menu-list>
|
||||
|
||||
<b-menu-list label="文章目录">
|
||||
<!-- 分类项 -->
|
||||
<b-menu-item
|
||||
v-for="category in contentList"
|
||||
v-show="!category.isHide"
|
||||
:key="category.name"
|
||||
:active="category.name === contentInfo.category"
|
||||
:expanded="Boolean(category.isExpanded)"
|
||||
:label="category.label"
|
||||
icon="format-list-bulleted-square"
|
||||
>
|
||||
<!-- 内容项 -->
|
||||
<b-menu-item
|
||||
v-for="item in category.items"
|
||||
:key="item.name"
|
||||
:active="(
|
||||
category.name === contentInfo.category && item.name === contentInfo.itemName
|
||||
)"
|
||||
:label="item.title"
|
||||
icon="file-document-outline"
|
||||
@click="changePage(category.name, item.name)"
|
||||
></b-menu-item>
|
||||
</b-menu-item>
|
||||
</b-menu-list>
|
||||
|
||||
</b-menu>
|
||||
</b-sidebar>
|
||||
|
||||
<div class="sidebar-toggle" @click="toggleSidebar()">
|
||||
<b-icon v-if="isShowSidebar" icon="chevron-left" size="is-small" />
|
||||
<b-icon v-else icon="chevron-right" size="is-small" />
|
||||
</div>
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<div class="content-wrapper">
|
||||
<markdown-parser :md-src="contentData" />
|
||||
<b-loading
|
||||
:active="isLoading"
|
||||
:can-cancel="false"
|
||||
:is-full-page="false"
|
||||
></b-loading>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { setTitle, toast } from '@/assets/js/utils';
|
||||
import { getContentFile } from '@/request/index';
|
||||
|
||||
import MarkdownParser from '@/components/MarkdownParser';
|
||||
|
||||
export default {
|
||||
name: 'ContentView',
|
||||
components: {
|
||||
MarkdownParser,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
||||
/** 是否正在载入内容 */
|
||||
isLoading: false,
|
||||
|
||||
/** 是否在移动端显示侧边栏 */
|
||||
isShowSidebar: false,
|
||||
|
||||
/** 内容正文 */
|
||||
contentData: '',
|
||||
|
||||
/** 内容信息 */
|
||||
contentInfo: {
|
||||
category: '',
|
||||
createdAt: '',
|
||||
updatedAt: '',
|
||||
itemName: '',
|
||||
itemtitle: '',
|
||||
},
|
||||
|
||||
/** 内容列表 */
|
||||
contentList: [
|
||||
{
|
||||
name: '_index',
|
||||
label: '索引页面',
|
||||
isHide: true,
|
||||
items: [{
|
||||
name: 'contents',
|
||||
title: '文章页面',
|
||||
createdAt: '',
|
||||
updatedAt: '',
|
||||
}],
|
||||
isExpanded: false,
|
||||
},
|
||||
],
|
||||
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
|
||||
// 切换页面时刷新内容
|
||||
'$route.path': {
|
||||
handler() {
|
||||
this.updateContents();
|
||||
}
|
||||
},
|
||||
|
||||
},
|
||||
created() {
|
||||
this.init();
|
||||
},
|
||||
methods: {
|
||||
|
||||
/** 初始化 */
|
||||
init() {
|
||||
|
||||
const config = window['appConfig'];
|
||||
|
||||
if (config) {
|
||||
|
||||
const listCurr = JSON.parse(JSON.stringify(this.contentList));
|
||||
const listConfig = (config.contentList || []);
|
||||
|
||||
listConfig.forEach((item) => {
|
||||
listCurr.push(item);
|
||||
});
|
||||
|
||||
this.contentList = listCurr;
|
||||
|
||||
} else {
|
||||
toast({
|
||||
indefinite: true,
|
||||
message: '读取配置文件失败!',
|
||||
type: 'is-danger',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.updateContents();
|
||||
|
||||
},
|
||||
|
||||
/** 切换页面 */
|
||||
changePage(category, itemName) {
|
||||
this.$router.push({
|
||||
name: 'Content',
|
||||
params: {
|
||||
category: category,
|
||||
name: itemName,
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* @description 获取正文内容文件
|
||||
* @param {object} options
|
||||
* @param {string} options.category 分类名称
|
||||
* @param {string} options.itemName 内容名称
|
||||
*/
|
||||
getContentFile(options) {
|
||||
|
||||
this.setLoading(true);
|
||||
this.updateContentInfo(options);
|
||||
|
||||
getContentFile(options).then((res) => {
|
||||
|
||||
this.setLoading(false);
|
||||
|
||||
const { status, data } = res;
|
||||
|
||||
if (status === 200) {
|
||||
this.contentData = (data || '');
|
||||
} else if (status === 404) {
|
||||
toast({
|
||||
duration: 3000,
|
||||
message: `内容 /${options.category}/${options.itemName}.md 不存在。`,
|
||||
type: 'is-warning',
|
||||
});
|
||||
this.contentData = '';
|
||||
}
|
||||
|
||||
}).catch((error) => {
|
||||
|
||||
console.error('[请求失败]', error);
|
||||
toast({
|
||||
duration: 3000,
|
||||
message: '请求失败!',
|
||||
type: 'is-danger',
|
||||
});
|
||||
this.setLoading(false);
|
||||
this.contentData = '';
|
||||
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* @description 设置载入状态
|
||||
* @param {boolean} isLoading
|
||||
*/
|
||||
setLoading(isLoading = false) {
|
||||
this.isLoading = isLoading;
|
||||
},
|
||||
|
||||
/** 切换侧边栏显示隐藏 */
|
||||
toggleSidebar() {
|
||||
this.isShowSidebar = !this.isShowSidebar;
|
||||
},
|
||||
|
||||
/** 更新内容 */
|
||||
updateContents() {
|
||||
|
||||
const {
|
||||
name: routeName,
|
||||
params: routeParams,
|
||||
} = this.$route;
|
||||
|
||||
if (routeName === 'Content') {
|
||||
this.getContentFile({
|
||||
category: routeParams.category,
|
||||
itemName: routeParams.name,
|
||||
});
|
||||
} else {
|
||||
this.getContentFile({
|
||||
category: '_index',
|
||||
itemName: 'contents',
|
||||
});
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* @description 更新内容信息
|
||||
* @param {object} options
|
||||
* @param {string} options.category 分类名称
|
||||
* @param {string} options.itemName 内容名称
|
||||
*/
|
||||
updateContentInfo(options) {
|
||||
|
||||
setTitle();
|
||||
|
||||
const {
|
||||
category,
|
||||
itemName,
|
||||
} = options;
|
||||
|
||||
if (!(category && itemName)) {
|
||||
console.error('更新内容信息失败!');
|
||||
return;
|
||||
}
|
||||
|
||||
const { contentInfo, contentList } = this;
|
||||
|
||||
const categoryInfo = contentList.find((item) => {
|
||||
return (item.name === category);
|
||||
});
|
||||
|
||||
if (categoryInfo) {
|
||||
// 标记展开
|
||||
categoryInfo.isExpanded = true;
|
||||
} else {
|
||||
console.error('获取分类信息失败!');
|
||||
return;
|
||||
}
|
||||
|
||||
const itemInfo = categoryInfo.items.find((item) => {
|
||||
return (item.name === itemName);
|
||||
});
|
||||
|
||||
if (itemInfo) {
|
||||
contentInfo.category = category;
|
||||
contentInfo.createdAt = (itemInfo.createdAt || '无');
|
||||
contentInfo.updatedAt = (itemInfo.updatedAt || '无');
|
||||
contentInfo.itemName = (itemInfo.name || '');
|
||||
contentInfo.itemtitle = (itemInfo.title || '无标题');
|
||||
setTitle(itemInfo.title);
|
||||
} else {
|
||||
console.error('获取内容信息失败!');
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.content-view {
|
||||
display: flex;
|
||||
|
||||
> * {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.content-sidebar {
|
||||
flex-shrink: 0;
|
||||
z-index: 12;
|
||||
}
|
||||
|
||||
/deep/ .sidebar-content {
|
||||
width: 320px;
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
width: 0;
|
||||
flex-grow: 1;
|
||||
z-index: 11;
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@sidebarBtnWidth: 1.25rem;
|
||||
@sidebarBtnOffset: 1rem;
|
||||
|
||||
.sidebar-toggle {
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 0%;
|
||||
z-index: 50;
|
||||
width: @sidebarBtnWidth;
|
||||
height: 4em;
|
||||
box-shadow: 0 0 0.5rem rgba(0, 0, 0, 0.2);
|
||||
border-radius: 0 0.25rem 0.25rem 0;
|
||||
background-color: var(--color-primary);
|
||||
color: #FFF;
|
||||
font-size: 1rem;
|
||||
opacity: 0.8;
|
||||
transform: translateY(-50%);
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.show-sidebar {
|
||||
.content-sidebar {
|
||||
width: calc(100% - @sidebarBtnWidth - @sidebarBtnOffset);
|
||||
}
|
||||
|
||||
.sidebar-toggle {
|
||||
left: unset;
|
||||
right: @sidebarBtnOffset;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/deep/ .sidebar-content {
|
||||
display: block !important;
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
overflow-y: auto !important;
|
||||
}
|
||||
}
|
||||
</style>
|
96
src/views/HomeView.vue
Normal file
96
src/views/HomeView.vue
Normal file
@@ -0,0 +1,96 @@
|
||||
<template>
|
||||
<div class="home-view">
|
||||
|
||||
<div class="bg-cover"></div>
|
||||
|
||||
<div class="center-content">
|
||||
<div class="home-title">Frost-ZX 的主页</div>
|
||||
<div class="home-desc">
|
||||
<p>一个使用 Vue + Buefy + markdown-it 开发实现的静态博客。</p>
|
||||
<p>
|
||||
<span>背景图片来源:</span>
|
||||
<a href="https://unsplash.com/photos/myq0GB_AAVU" target="_blank">Unsplash / Niels Cornet</a>
|
||||
</p>
|
||||
</div>
|
||||
<b-button
|
||||
type="is-light"
|
||||
outlined
|
||||
rounded
|
||||
@click="toDetail()"
|
||||
>了解更多</b-button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { setTitle } from '@/assets/js/utils';
|
||||
|
||||
export default {
|
||||
name: 'HomeView',
|
||||
methods: {
|
||||
|
||||
toDetail() {
|
||||
this.$router.push({
|
||||
name: 'ContentIndex'
|
||||
});
|
||||
},
|
||||
|
||||
},
|
||||
created() {
|
||||
setTitle();
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.home-view {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
background-color: #323232;
|
||||
background-image: url("../assets/image/home-bg.jpg");
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
color: #FFF;
|
||||
}
|
||||
|
||||
.bg-cover {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.35);
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
.center-content {
|
||||
position: relative;
|
||||
padding: 0 10%;
|
||||
width: 100%;
|
||||
font-size: 1rem;
|
||||
line-height: 1.5;
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.home-title {
|
||||
font-size: 2.25em;
|
||||
}
|
||||
|
||||
.home-desc {
|
||||
margin: 0.75em 0;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
.button {
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
}
|
||||
</style>
|
23
vue.config.js
Normal file
23
vue.config.js
Normal file
@@ -0,0 +1,23 @@
|
||||
const { defineConfig } = require('@vue/cli-service');
|
||||
|
||||
const { npm_package_name: packageName } = process.env;
|
||||
|
||||
if (packageName) {
|
||||
process.title = packageName;
|
||||
}
|
||||
|
||||
module.exports = defineConfig({
|
||||
|
||||
assetsDir: 'static',
|
||||
publicPath: './',
|
||||
outputDir: 'dist',
|
||||
|
||||
productionSourceMap: false,
|
||||
transpileDependencies: false,
|
||||
|
||||
devServer: {
|
||||
host: '0.0.0.0',
|
||||
port: 9000,
|
||||
},
|
||||
|
||||
});
|
Reference in New Issue
Block a user