H3m-Blog
一、项目介绍
1.1 项目介绍
一个基于SpringBoot和Vue3的博客系统,博客名称来源于陈奕迅于2009年发布的粤语专辑《H3M》
1.2 技术架构
主要技术栈: SpringBoot2 + Vue3 + MySQL8.0
1.3 主要功能
内容丰富,尽情体验~
二、快速开始
-
运行h3m-blog.sql文件,创建数据库
-
修改blog/src/main/resources/application.yml 中的配置内容,主要包括:
( 1 ) 数据库账号密码: username,password
( 2 )redis 相关配置,如果有密码自行添加password
-
在idea中打开H3m-Blog-back项目文件,等待Maven加载好依赖项,启动BlogAplication.
-
VsCode中打开H3m-Blog-front文件夹,安装前端依赖并启动前端项目
npm install npm run dev
-
修改图床请求头,这里我使用的是https://imglt.com/ 图床,大家可以自行注册这个网站然后获取自己的tokens,然后填入即可. 在src\utils\axios.config.js中。
-
打开 http://127.0.0.1:5174/ 开启博客之旅~
FAQs:
- 默认的账号与密码
所有现有的用户的密码都是1234,管理员账户为 EasonChan
-
接口文档
https://apifox.com/apidoc/shared-b5658688-b374-4473-a046-a902bfc6b711
三、数据库设计
四、前端
1. 前端主要使用到的一些依赖插件:
"dependencies": {
"@DatatracCorporation/markdown-it-mermaid": "npm:@datatraccorporation/markdown-it-mermaid@^0.5.0",
"@element-plus/icons-vue": "^2.3.1",
"axios": "^1.7.7",
"clipboard": "^2.0.11",
"element-plus": "^2.8.4",
"highlight.js": "^11.10.0",
"jquery": "^3.7.1",
"js-md5": "^0.7.3",
"markdown-it": "^14.1.0",
"markdown-it-abbr": "^1.0.4",
"markdown-it-container": "^3.0.0",
"markdown-it-deflist": "^2.1.0",
"markdown-it-emoji": "^2.0.0",
"markdown-it-footnote": "^3.0.3",
"markdown-it-ins": "^3.0.1",
"markdown-it-mark": "^3.0.1",
"markdown-it-sub": "^1.0.0",
"markdown-it-sup": "^1.0.0",
"markdown-it-task-lists": "^2.1.1",
"markdown-it-toc": "^1.1.0",
"markdown-it-toc-done-right": "^4.2.0",
"mavon-editor": "^3.0.0-beta",
"pinia": "^2.2.4",
"pinia-plugin-persistedstate": "^4.1.1",
"vue": "^3.4.37",
"vue-easy-lightbox": "^1.19.0",
"vue-router": "^4.4.5"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.1.2",
"less": "^4.2.0",
"sass": "^1.80.3",
"sass-loader": "^16.0.2",
"vite": "^5.4.1"
}
2. mavonEditor
这是一个富文本编辑器,github网站:https://github.com/hinesboy/mavonEditor
3. markdown-it
GitHub 官网:https://github.com/markdown-it/markdown-it?tab=readme-ov-file#markdown-it-
配置如下:
import MarkdownIt from "markdown-it";
import emoji from "markdown-it-emoji";
import deflist from "markdown-it-deflist";
import abbr from "markdown-it-abbr";
import footnote from "markdown-it-footnote";
import ins from "markdown-it-ins";
import mark from "markdown-it-mark";
import taskLists from "markdown-it-task-lists";
import container from "markdown-it-container";
import toc from "markdown-it-toc-done-right";
import mermaid from "@DatatracCorporation/markdown-it-mermaid";
var config = {
html: true,
xhtmlOut: true,
breaks: true,
langPrefix: "lang-",
linkify: false,
typographer: true,
quotes: "“”‘’",
};
let markdownIt = new MarkdownIt(config);
markdownIt
.use(emoji)
.use(deflist)
.use(abbr)
.use(footnote)
.use(ins)
.use(mark)
.use(taskLists)
.use(container)
.use(container, "hljs-left")
.use(container, "hljs-center")
.use(container, "hljs-right")
.use(toc)
.use(mermaid);
export default markdownIt;
4. mathjax
这是一个用于美化数学公式的脚本
/**
* 异步载入 MathJax 脚本
*/
export function injectMathJax() {
if (!window.MathJax) {
const script = document.createElement("script");
script.src =
"https://cdn.bootcdn.net/ajax/libs/mathjax/3.2.0/es5/tex-chtml.js";
script.async = true;
document.head.appendChild(script);
}
}
/**
* 初始化 MathJax
* @param options 自定义 MathJax 配置
* @param callback 当 MathJax 脚本载入完毕时
*/
export function initMathJax(options = {}, callback) {
injectMathJax();
const defaultConfig = {
tex: {
inlineMath: [["$", "$"]],
displayMath: [["$$", "$$"]],
processEnvironments: true,
processRefs: true,
},
options: {
skipHtmlTags: ["noscript", "style", "textarea", "pre", "code"],
ignoreHtmlClass: "tex2jax_ignore",
},
startup: {
pageReady: () => {
callback && callback();
},
},
svg: {
fontCache: "global",
},
};
const mergeConfig = Object.assign({}, defaultConfig, options);
window.MathJax = mergeConfig;
}
/**
* 渲染指定容器中的数学公式
* @param {string} el 需要被渲染的容器
* @returns Promise
*/
export function renderByMathjax(el) {
if (!window.MathJax || !window.MathJax.version) {
return;
}
el = [...document.querySelectorAll(el)];
return new Promise((resolve, reject) => {
window.MathJax.typesetPromise(el)
.then(() => {
resolve(void 0);
})
.catch((err) => reject(err));
});
}
用法:
import { initMathJax, renderByMathjax } from "@/utils/mathjax";
initMathJax({}, () => {
renderByMathjax(".article-content");
renderByMathjax(".comment-item-content");
});
-
代码块美化
主要用到的就是highlight.js
index.js:
import "./index.less";
import "highlight.js/styles/atom-one-dark.css";
import $ from "jquery";
import hljs from "highlight.js/lib/core";
import javascript from "highlight.js/lib/languages/javascript";
import vbscript from "highlight.js/lib/languages/vbscript";
import python from "highlight.js/lib/languages/python";
import matlab from "highlight.js/lib/languages/matlab";
import csharp from "highlight.js/lib/languages/csharp";
import shell from "highlight.js/lib/languages/shell";
import vhdl from "highlight.js/lib/languages/vhdl";
import java from "highlight.js/lib/languages/java";
import css from "highlight.js/lib/languages/css";
import xml from "highlight.js/lib/languages/xml";
import sql from "highlight.js/lib/languages/sql";
import cpp from "highlight.js/lib/languages/cpp";
import c from "highlight.js/lib/languages/c";
import ClipboardJS from "clipboard";
hljs.registerLanguage("javascript", javascript);
hljs.registerLanguage("vbscript", vbscript);
hljs.registerLanguage("python", python);
hljs.registerLanguage("matlab", matlab);
hljs.registerLanguage("csharp", csharp);
hljs.registerLanguage("shell", shell);
hljs.registerLanguage("vhdl", vhdl);
hljs.registerLanguage("java", java);
hljs.registerLanguage("html", xml);
hljs.registerLanguage("xml", xml);
hljs.registerLanguage("css", css);
hljs.registerLanguage("sql", sql);
hljs.registerLanguage("cpp", cpp);
hljs.registerLanguage("c", c);
hljs.configure({ ignoreUnescapedHTML: true });
/**
* 高亮代码块
* @param {Element} element 包含 pre code 代码块的元素
*/
function highlightCode(element) {
const codeEls = element.querySelectorAll("pre code");
codeEls.forEach((el) => {
hljs.highlightElement(el);
});
}
/**
* 给代码块添加行号
* @param {Element} element 包含 pre code 代码块的元素
*/
function buildLineNumber(element) {
let $codes = $(element).find("pre code");
if (!$codes.length) {
return false;
}
$.each($codes, (_, code) => {
if (!$(code).hasClass("hljsln")) {
$(code).addClass("hljsln");
$(code).html(addLineNumbersFor($(code).html()));
var $lastNum = $("span[data-num]:last");
if (!$lastNum.html()) {
$lastNum.remove();
}
}
});
}
function addLineNumbersFor(html) {
var text = html.replace(/<span[^>]*>|<\/span>/g, "");
if (/\r|\n$/.test(text)) {
html += '<span class="ln-eof"></span>';
}
var num = 1;
html = html.replace(/\r\n|\r|\n/g, function (a) {
num++;
return a + '<span class="ln-num" data-num="' + num + '"></span>';
});
html = '<span class="ln-num" data-num="1"></span>' + html;
html = '<span class="ln-bg"></span>' + html;
return html;
}
/**
* 给代码块添加复制按钮
* @param {Element} element 包含 pre code 代码块的元素
*/
function buildCopyButton(element) {
let $pres = $(element).find("pre");
if (!$pres.length) return;
$pres.each(function () {
var t = $(this).children("code").text();
// 创建按钮
var btn = $('<span class="copy">复制</span>').attr(
"data-clipboard-text",
t
);
$(this).prepend(btn);
var c = new ClipboardJS(btn[0]);
c.on("success", function () {
btn.addClass("copyed").text("复制成功");
setTimeout(function () {
btn.text("复制").removeClass("copyed");
}, 1000);
});
c.on("error", function () {
btn.text("复制失败");
});
});
}
/**
* 创建代码块
* @param {string} selector 包含 pre code 的元素选择器
*/
function buildCodeBlock(selector) {
let elements = document.querySelectorAll(selector);
for (let element of elements) {
highlightCode(element);
buildLineNumber(element);
buildCopyButton(element);
}
}
export default buildCodeBlock;
index.less:
.hljs,
.hljsln {
display: block;
overflow-x: auto;
padding: 0.7em 1em 0.7em 3em !important;
background: #282c34 !important;
border: 1px solid #282c34 !important;
color: #bababa;
font-family: Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace !important;
font-size: 14px !important;
position: relative;
/* 代码块不换行 */
white-space: pre;
word-break: normal;
&::-webkit-scrollbar {
height: 4px;
}
}
.hljs.ln-hide {
padding: 0.7em 1em 0.7em 1em !important;
}
.ln-bg {
position: absolute;
z-index: 1;
top: 0;
left: 0;
width: 37px;
height: 100%;
background: #282c34;
border-radius: 7px 0px 0px 7px;
}
.ln-num {
position: absolute;
z-index: 2;
left: 0;
width: 37px;
text-align: center;
display: inline-block;
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
&::before {
color: #999;
font-style: normal;
font-weight: normal;
text-align: center;
content: attr(data-num);
}
}
.hljs-comment,
.hljs-quote {
color: #5c6370;
font-style: italic;
}
.hljs-doctag,
.hljs-keyword,
.hljs-formula {
color: #c678dd;
}
.hljs-section,
.hljs-name,
.hljs-selector-tag,
.hljs-deletion,
.hljs-subst {
color: #e06c75;
}
.hljs-literal {
color: #56b6c2;
}
.hljs-string,
.hljs-regexp,
.hljs-addition,
.hljs-attribute,
.hljs-meta-string {
color: #98c379;
}
.class_ {
color: #e5c07b !important;
}
.hljs-attr {
color: #d19a66 !important;
}
.hljs-attr,
.hljs-variable,
.hljs-template-variable,
.hljs-type,
.hljs-selector-class,
.hljs-selector-attr,
.hljs-selector-pseudo,
.hljs-number {
color: #d19a66;
}
.hljs-symbol,
.hljs-bullet,
.hljs-link,
.hljs-meta,
.hljs-selector-id,
.hljs-title {
color: #61afef;
}
.hljs-emphasis {
font-style: italic;
}
.hljs-strong {
font-weight: bold;
}
.hljs-link {
text-decoration: underline;
}
.hljs-built_in {
color: #56b6c2;
}
.hljs-tag .hljs-name {
color: #e06c75
}
.hljs-tag {
color: #bababa;
}
.hljs-attribute,
.hljs-doctag,
.hljs-keyword,
.hljs-meta .hljs-keyword,
.hljs-name,
.hljs-selector-tag,
.hljs-title {
font-weight: normal !important;
}
.hljs-punctuation {
color: #bababa;
}
.hljs::-webkit-scrollbar,
.hljsln::-webkit-scrollbar {
height: 3px;
}
.article-content pre,
.comment-item-content pre {
position: relative;
&>span {
position: absolute;
top: 0;
right: 0;
border-radius: 3px;
padding: 3px 10px;
font-size: 12px;
font-family: Lato, PingFang SC, Microsoft YaHei, sans-serif;
font-weight: normal;
background: #fff;
color: #000;
cursor: pointer;
opacity: 0;
margin: 8px 10px;
transition: all 0.3s;
z-index: 5;
&:hover {
background: #ddd;
}
}
&:hover>span {
opacity: 1;
transition: opacity 0.25s;
}
}
使用示例:
import buildCodeBlock from "@/utils/code-block"; // 导入
// 直接应用于元素类上即可
buildCodeBlock(".article-content");
5. text-effect
一个文字动画效果特效
index.css
.particletext {
text-align: center;
font-size: 48px;
position: relative;
}
.particletext.bubbles>.particle {
opacity: 0;
position: absolute;
background-color: rgba(241, 240, 154, 0.849);
/* background-color: rgba(33, 150, 243, 0.5); */
animation: bubbles 3s ease-in infinite;
border-radius: 100%;
}
.particletext.hearts>.particle {
opacity: 0;
position: absolute;
background-color: #cc2a5d;
animation: hearts 3s ease-in infinite;
}
.particletext.hearts>.particle:before,
.particletext.hearts>.particle:after {
position: absolute;
content: '';
border-radius: 100px;
top: 0px;
left: 0px;
width: 100%;
height: 100%;
background-color: #cc2a5d;
}
.particletext.hearts>.particle:before {
transform: translateX(-50%);
}
.particletext.hearts>.particle:after {
transform: translateY(-50%);
}
.particletext.lines>.particle {
position: absolute;
background-color: rgba(244, 67, 54, 0.5);
animation: lines 3s linear infinite;
}
.particletext.confetti>.particle {
opacity: 0;
position: absolute;
animation: confetti 3s ease-in infinite;
}
.particletext.confetti>.particle.c1 {
background-color: rgba(76, 175, 80, 0.5);
}
.particletext.confetti>.particle.c2 {
background-color: rgba(156, 39, 176, 0.5);
}
.particletext.sunbeams>.particle {
position: absolute;
background-color: rgba(253, 216, 53, 0.5);
animation: sunbeams 3s linear infinite;
}
@keyframes bubbles {
0% {
opacity: 0;
}
20% {
opacity: 1;
transform: translate(0, -20%);
}
100% {
opacity: 0;
transform: translate(0, -1000%);
}
}
@keyframes hearts {
0% {
opacity: 0;
transform: translate(0, 0%) rotate(45deg);
}
20% {
opacity: 0.8;
transform: translate(0, -20%) rotate(45deg);
}
100% {
opacity: 0;
transform: translate(0, -1000%) rotate(45deg);
}
}
@keyframes lines {
0%,
50%,
100% {
transform: translateY(0%);
}
25% {
transform: translateY(100%);
}
75% {
transform: translateY(-100%);
}
}
@keyframes confetti {
0% {
opacity: 0;
transform: translateY(0%) rotate(0deg);
}
10% {
opacity: 1;
}
35% {
transform: translateY(-800%) rotate(270deg);
}
80% {
opacity: 1;
}
100% {
opacity: 0;
transform: translateY(2000%) rotate(1440deg);
}
}
@keyframes sunbeams {
0% {
transform: translateY(40%) rotate(0deg);
}
50% {
transform: translateY(-40%) rotate(180deg);
}
100% {
transform: translateY(40%) rotate(360deg);
}
0%,
14%,
17%,
43%,
53%,
71%,
80%,
94%,
100% {
opacity: 0;
}
6%,
15%,
24%,
28%,
48%,
55%,
78%,
82%,
99% {
opacity: 1;
}
}
index.js
import "./index.css"
import $ from 'jquery'
/**
* 生成粒子效果
*/
function createParticles() {
bubbles();
hearts();
lines();
confetti();
sunbeams();
}
function bubbles() {
$.each($(".particletext.bubbles"), function () {
var bubblecount = ($(this).width() / 50) * 10;
for (var i = 0; i <= bubblecount; i++) {
var size = ($.rnd(20, 50) / 10);
$(this).append('<span class="particle" style="top:' + $.rnd(20, 80) + '%; left:' + $.rnd(0, 95) + '%;width:' + size + 'px; height:' + size + 'px;animation-delay: ' + ($.rnd(0, 30) / 10) + 's;"></span>');
}
});
}
function hearts() {
$.each($(".particletext.hearts"), function () {
var heartcount = ($(this).width() / 50) * 5;
for (var i = 0; i <= heartcount; i++) {
var size = ($.rnd(60, 120) / 10);
$(this).append('<span class="particle" style="top:' + $.rnd(20, 80) + '%; left:' + $.rnd(0, 95) + '%;width:' + size + 'px; height:' + size + 'px;animation-delay: ' + ($.rnd(0, 30) / 10) + 's;"></span>');
}
});
}
function lines() {
$.each($(".particletext.lines"), function () {
var linecount = ($(this).width() / 50) * 10;
for (var i = 0; i <= linecount; i++) {
$(this).append('<span class="particle" style="top:' + $.rnd(-30, 30) + '%; left:' + $.rnd(-10, 110) + '%;width:' + $.rnd(1, 3) + 'px; height:' + $.rnd(20, 80) + '%;animation-delay: -' + ($.rnd(0, 30) / 10) + 's;"></span>');
}
});
}
function confetti() {
$.each($(".particletext.confetti"), function () {
var confetticount = ($(this).width() / 50) * 10;
for (var i = 0; i <= confetticount; i++) {
$(this).append('<span class="particle c' + $.rnd(1, 2) + '" style="top:' + $.rnd(10, 50) + '%; left:' + $.rnd(0, 100) + '%;width:' + $.rnd(6, 8) + 'px; height:' + $.rnd(3, 4) + 'px;animation-delay: ' + ($.rnd(0, 30) / 10) + 's;"></span>');
}
});
}
function sunbeams() {
$.each($(".particletext.sunbeams"), function () {
var linecount = ($(this).width() / 50) * 10;
for (var i = 0; i <= linecount; i++) {
$(this).append('<span class="particle" style="top:' + $.rnd(-50, 0) + '%; left:' + $.rnd(0, 100) + '%;width:' + $.rnd(1, 3) + 'px; height:' + $.rnd(80, 160) + '%;animation-delay: -' + ($.rnd(0, 30) / 10) + 's;"></span>');
}
});
}
$.rnd = function (m, n) {
m = parseInt(m);
n = parseInt(n);
return Math.floor(Math.random() * (n - m + 1)) + m;
}
export default createParticles
使用示例:
<template>
<div class="cover">
<div class="title particletext bubbles" v-if="title">
{{ title }}
</div>
<div class="slot particletext bubbles" v-else> // 使用即可
<slot></slot>
</div>
<div class="text">{{ text }}</div>
</div>
</template>
<script setup>
import createParticles from "@/utils/text-effect"; // 导入
import { onMounted, defineProps } from "vue";
const props = defineProps({
title: {
type: String,
},
text: {
type: String,
},
});
onMounted(() => {
createParticles(); // 加载
});
</script>
五、后端
主要技术:Jwt+SpringSecurity+redis+MyBatis-plus 项目遵守三层架构设计规范。
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>8.0.31</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.33</version>
</dependency>
<!---jwt依赖 -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.19.2</version>
</dependency>
<!-- 如果jdk>8-->
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.1</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.1</version>
</dependency>
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>3.10.2</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>3.0.5</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>com.sun.xml.bind</groupId>
<artifactId>jaxb-impl</artifactId>
<version>2.3.0</version>
</dependency>
<dependency>
<groupId>com.sun.xml.bind</groupId>
<artifactId>jaxb-core</artifactId>
<version>2.3.0</version>
</dependency>
<dependency>
<groupId>javax.activation</groupId>
<artifactId>activation</artifactId>
<version>1.1.1</version>
</dependency>
六、页面展示