feat: init commit
This commit is contained in:
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
3
.vscode/extensions.json
vendored
Normal file
3
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"recommendations": ["Vue.volar"]
|
||||||
|
}
|
||||||
5
README.md
Normal file
5
README.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# Vue 3 + Vite
|
||||||
|
|
||||||
|
This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
|
||||||
|
|
||||||
|
Learn more about IDE Support for Vue in the [Vue Docs Scaling up Guide](https://vuejs.org/guide/scaling-up/tooling.html#ide-support).
|
||||||
13
index.html
Normal file
13
index.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/public/Merlin.ico" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Merlin's Myblog</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
8
jsconfig.json
Normal file
8
jsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["src/*"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2863
package-lock.json
generated
Normal file
2863
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
29
package.json
Normal file
29
package.json
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"name": "myblog",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "^1.8.4",
|
||||||
|
"crypto-js": "^4.2.0",
|
||||||
|
"element-plus": "^2.9.7",
|
||||||
|
"github-markdown-css": "^5.8.1",
|
||||||
|
"marked": "^15.0.11",
|
||||||
|
"path": "^0.12.7",
|
||||||
|
"pinia": "^3.0.2",
|
||||||
|
"pinia-plugin-persistedstate": "^4.2.0",
|
||||||
|
"vditor": "^3.11.0",
|
||||||
|
"vue": "^3.5.13",
|
||||||
|
"vue-axios": "^3.5.2",
|
||||||
|
"vue-router": "^4.5.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@vitejs/plugin-vue": "^5.2.1",
|
||||||
|
"vite": "^6.2.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
public/Merlin.ico
Normal file
BIN
public/Merlin.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 264 KiB |
11
src/App.vue
Normal file
11
src/App.vue
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<script setup>
|
||||||
|
import { RouterView } from 'vue-router'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<RouterView />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
BIN
src/assets/Merlin.ico
Normal file
BIN
src/assets/Merlin.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 264 KiB |
95
src/components/MarkdownEditor.vue
Normal file
95
src/components/MarkdownEditor.vue
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
<template>
|
||||||
|
<div ref="vditorRef" class="vditor"></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted, defineExpose } from 'vue';
|
||||||
|
import Vditor from 'vditor';
|
||||||
|
import 'vditor/dist/index.css';
|
||||||
|
import { userInfoStore } from '@/store/store';
|
||||||
|
|
||||||
|
const userInfo = userInfoStore();
|
||||||
|
|
||||||
|
|
||||||
|
const vditorRef = ref(null);
|
||||||
|
let vditor = null;
|
||||||
|
|
||||||
|
// 定义 localStorage 的 key
|
||||||
|
const STORAGE_KEY = 'publishArticle';
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
getContent,
|
||||||
|
setContent,
|
||||||
|
clearContent,
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
vditor = new Vditor(vditorRef.value, {
|
||||||
|
height: 400,
|
||||||
|
toolbar: [
|
||||||
|
'headings', 'bold', 'italic', 'strike', '|',
|
||||||
|
'list', 'ordered-list', 'check', '|',
|
||||||
|
'quote', 'line', 'code', 'inline-code', '|',
|
||||||
|
'upload', 'link', 'table', '|',
|
||||||
|
'preview', 'fullscreen'
|
||||||
|
],
|
||||||
|
placeholder: '请输入你的文章内容...',
|
||||||
|
mode: 'sv',
|
||||||
|
cache: {
|
||||||
|
enable: false, // 禁用vditor内置缓存
|
||||||
|
},
|
||||||
|
upload: {
|
||||||
|
url: '/api/admin/upload/img',
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${userInfo.token}`
|
||||||
|
},
|
||||||
|
fieldName: 'image',
|
||||||
|
success(_, response) {
|
||||||
|
response = JSON.parse(response);
|
||||||
|
console.log(response);
|
||||||
|
if (response.code === 200) {
|
||||||
|
console.log('上传成功');
|
||||||
|
vditor.insertValue('')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
after() {
|
||||||
|
console.log('Vditor 初始化完成');
|
||||||
|
// 初始化后,加载本地缓存内容
|
||||||
|
const cachedContent = localStorage.getItem(STORAGE_KEY);
|
||||||
|
if (cachedContent) {
|
||||||
|
vditor.setValue(cachedContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
// 监听输入变化,实时保存
|
||||||
|
input(value) {
|
||||||
|
localStorage.setItem(STORAGE_KEY, value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 获取内容
|
||||||
|
function getContent() {
|
||||||
|
return vditor.getValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置内容
|
||||||
|
|
||||||
|
function setContent(markdown) {
|
||||||
|
vditor.setValue(markdown);
|
||||||
|
localStorage.setItem(STORAGE_KEY, markdown);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清空内容(比如发布成功后)
|
||||||
|
function clearContent() {
|
||||||
|
vditor.setValue('');
|
||||||
|
localStorage.removeItem(STORAGE_KEY);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.vditor {
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
24
src/function/user.js
Normal file
24
src/function/user.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
import { userInfoStore } from "@/store/store";
|
||||||
|
|
||||||
|
const userinfo = userInfoStore();
|
||||||
|
|
||||||
|
export const getUserInfo = async () => {
|
||||||
|
console.log("getUserInfojs was used!");
|
||||||
|
try {
|
||||||
|
const response = await axios.get("/api/blog/get/userinfo", {
|
||||||
|
headers: {
|
||||||
|
Authorization: "Bearer " + userinfo.token,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (response.data.code === 200) {
|
||||||
|
userinfo.user.u_id = response.data.data.u_id;
|
||||||
|
userinfo.user.u_name = response.data.data.u_name;
|
||||||
|
userinfo.user.u_avatar = response.data.data.u_avatar;
|
||||||
|
userinfo.user.role = response.data.data.role;
|
||||||
|
console.log(userinfo.user);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
39
src/main.js
Normal file
39
src/main.js
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import "@/styles/main.css";
|
||||||
|
|
||||||
|
import { createApp } from "vue";
|
||||||
|
import App from "./App.vue";
|
||||||
|
import router from "./router";
|
||||||
|
import { createPinia } from "pinia";
|
||||||
|
import piniaPluginPersistedstate from "pinia-plugin-persistedstate";
|
||||||
|
|
||||||
|
// const setVh = () => {
|
||||||
|
// const vh = window.innerHeight * 0.01;
|
||||||
|
// document.documentElement.style.setProperty("--vh", `${vh}px`);
|
||||||
|
// };
|
||||||
|
|
||||||
|
// window.addEventListener("resize", setVh);
|
||||||
|
// window.addEventListener("load", setVh);
|
||||||
|
|
||||||
|
// setVh();
|
||||||
|
|
||||||
|
const pinia = createPinia();
|
||||||
|
pinia.use(piniaPluginPersistedstate);
|
||||||
|
|
||||||
|
const app = createApp(App);
|
||||||
|
|
||||||
|
app.use(router);
|
||||||
|
app.use(pinia);
|
||||||
|
|
||||||
|
// imoort element-plus
|
||||||
|
import ElementPlus from "element-plus";
|
||||||
|
import "element-plus/dist/index.css";
|
||||||
|
app.use(ElementPlus);
|
||||||
|
|
||||||
|
//import axios
|
||||||
|
import axios from "axios";
|
||||||
|
import VueAxios from "vue-axios";
|
||||||
|
app.use(VueAxios, axios);
|
||||||
|
|
||||||
|
app.provide("axios", app.config.globalProperties.axios);
|
||||||
|
|
||||||
|
app.mount("#app");
|
||||||
98
src/router.js
Normal file
98
src/router.js
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
import { createRouter, createWebHistory } from "vue-router";
|
||||||
|
|
||||||
|
const router = createRouter({
|
||||||
|
history: createWebHistory(import.meta.env.BASE_URL),
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
path: "/",
|
||||||
|
name: "blog",
|
||||||
|
component: () => import("@/views/blog/blog.vue"),
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: "/",
|
||||||
|
name: "show",
|
||||||
|
component: () => import("@/views/blog/show.vue"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/viewDetails",
|
||||||
|
name: "viewDetails",
|
||||||
|
component: () => import("@/views/blog/viewDetails.vue"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/myplayer",
|
||||||
|
name: "myplayer",
|
||||||
|
component: () => import("@/views/blog/myplayer.vue"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/me",
|
||||||
|
name: "me",
|
||||||
|
component: () => import("@/views/blog/me.vue"),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/admin",
|
||||||
|
name: "admin",
|
||||||
|
component: () => import("@/views/admin/admin.vue"),
|
||||||
|
|
||||||
|
children: [
|
||||||
|
// {
|
||||||
|
// path: "/",
|
||||||
|
// name: "dashboard",
|
||||||
|
// component: () => import("@/views/admin/dashboard.vue"),
|
||||||
|
// },
|
||||||
|
{
|
||||||
|
path: "posts",
|
||||||
|
name: "posts",
|
||||||
|
meta: {
|
||||||
|
title: "文章管理",
|
||||||
|
},
|
||||||
|
component: () => import("@/views/admin/article.vue"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "news",
|
||||||
|
name: "news",
|
||||||
|
component: () => import("@/views/admin/news.vue"),
|
||||||
|
meta: {
|
||||||
|
title: "新闻管理",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "projects",
|
||||||
|
name: "projects",
|
||||||
|
component: () => import("@/views/admin/projects.vue"),
|
||||||
|
meta: {
|
||||||
|
title: "项目管理",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "details",
|
||||||
|
name: "details",
|
||||||
|
component: () => import("@/views/admin/details.vue"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "comments",
|
||||||
|
name: "comments",
|
||||||
|
component: () => import("@/views/admin/comments.vue"),
|
||||||
|
},
|
||||||
|
// {
|
||||||
|
// path: "tags",
|
||||||
|
// name: "tags",
|
||||||
|
// component: () => import("@/views/admin/tags.vue"),
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// path: "users",
|
||||||
|
// name: "users",
|
||||||
|
// component: () => import("@/views/admin/users.vue"),
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// path: "settings",
|
||||||
|
// name: "settings",
|
||||||
|
// component: () => import("@/views/admin/settings.vue"),
|
||||||
|
// },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
||||||
94
src/store/details.js
Normal file
94
src/store/details.js
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
import { defineStore } from "pinia";
|
||||||
|
|
||||||
|
export const detailsStore = defineStore("details", {
|
||||||
|
state: () => ({
|
||||||
|
type: "",
|
||||||
|
}),
|
||||||
|
actions: {
|
||||||
|
setType(type) {
|
||||||
|
this.type = type;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const articleStore = defineStore("article", {
|
||||||
|
state: () => ({
|
||||||
|
detail: {
|
||||||
|
a_id: "",
|
||||||
|
title: "",
|
||||||
|
content: "",
|
||||||
|
created: "",
|
||||||
|
updated: "",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
actions: {
|
||||||
|
setArticle(data) {
|
||||||
|
this.detail.a_id = data.a_id;
|
||||||
|
this.detail.title = data.title;
|
||||||
|
this.detail.content = data.content;
|
||||||
|
this.detail.created = data.created;
|
||||||
|
this.detail.updated = data.updated;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const newsStore = defineStore("news", {
|
||||||
|
state: () => ({
|
||||||
|
detail: {
|
||||||
|
a_id: "",
|
||||||
|
title: "",
|
||||||
|
content: "",
|
||||||
|
published: "",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
actions: {
|
||||||
|
setNews(data) {
|
||||||
|
this.detail.a_id = data.a_id;
|
||||||
|
this.detail.title = data.n_title;
|
||||||
|
this.detail.content = data.synopsis;
|
||||||
|
this.detail.published = data.published;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const projectStore = defineStore("project", {
|
||||||
|
state: () => ({
|
||||||
|
detail: {
|
||||||
|
p_id: "",
|
||||||
|
p_name: "",
|
||||||
|
techstack: "",
|
||||||
|
content: "",
|
||||||
|
p_status: "",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
actions: {
|
||||||
|
setProject(data) {
|
||||||
|
this.detail.p_id = data.p_id;
|
||||||
|
this.detail.p_name = data.p_name;
|
||||||
|
this.detail.techstack = data.techstack;
|
||||||
|
this.detail.content = data.details;
|
||||||
|
this.detail.p_status = data.p_status;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const viewDetailsStore = defineStore("viewDetails", {
|
||||||
|
state: () => ({
|
||||||
|
detail: {
|
||||||
|
a_id: "",
|
||||||
|
title: "",
|
||||||
|
content: "",
|
||||||
|
created: "",
|
||||||
|
updated: "",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
actions: {
|
||||||
|
setViewDetails(data) {
|
||||||
|
this.detail.a_id = data.a_id;
|
||||||
|
this.detail.title = data.title;
|
||||||
|
this.detail.content = data.content;
|
||||||
|
this.detail.created = data.created;
|
||||||
|
this.detail.updated = data.updated;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
49
src/store/store.js
Normal file
49
src/store/store.js
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import { ref, reactive } from "vue";
|
||||||
|
import { defineStore } from "pinia";
|
||||||
|
|
||||||
|
export const userInfoStore = defineStore(
|
||||||
|
"userInfo",
|
||||||
|
() => {
|
||||||
|
const token = ref(null);
|
||||||
|
|
||||||
|
const user = reactive({
|
||||||
|
u_id: "",
|
||||||
|
u_name: "",
|
||||||
|
u_avatar: "@/assets/defaultavatar.jpg",
|
||||||
|
u_account: "",
|
||||||
|
role: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const clearUserInfo = () => {
|
||||||
|
this.token.value = null;
|
||||||
|
this.user = {
|
||||||
|
u_id: "",
|
||||||
|
u_name: "",
|
||||||
|
u_avatar: "@/assets/defaultavatar.jpg",
|
||||||
|
u_account: "",
|
||||||
|
role: "",
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
token,
|
||||||
|
user,
|
||||||
|
clearUserInfo,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{
|
||||||
|
persist: {
|
||||||
|
key: "userInfo",
|
||||||
|
storage: localStorage,
|
||||||
|
paths: ["token", "user"],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const messagePointStore = defineStore("message", () => {
|
||||||
|
const hasNewMessage = ref(false);
|
||||||
|
|
||||||
|
return {
|
||||||
|
hasNewMessage,
|
||||||
|
};
|
||||||
|
});
|
||||||
11
src/styles/admin.css
Normal file
11
src/styles/admin.css
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
h1 {
|
||||||
|
font-size: 35px;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-menu-box {
|
||||||
|
width: 20%;
|
||||||
|
}
|
||||||
|
.left-menu {
|
||||||
|
padding: 5%;
|
||||||
|
}
|
||||||
86
src/styles/base.css
Normal file
86
src/styles/base.css
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
/* color palette from <https://github.com/vuejs/theme> */
|
||||||
|
:root {
|
||||||
|
--vt-c-white: #ffffff;
|
||||||
|
--vt-c-white-soft: #f8f8f8;
|
||||||
|
--vt-c-white-mute: #f2f2f2;
|
||||||
|
|
||||||
|
--vt-c-black: #181818;
|
||||||
|
--vt-c-black-soft: #222222;
|
||||||
|
--vt-c-black-mute: #282828;
|
||||||
|
|
||||||
|
--vt-c-indigo: #2c3e50;
|
||||||
|
|
||||||
|
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
|
||||||
|
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
|
||||||
|
--vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
|
||||||
|
--vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
|
||||||
|
|
||||||
|
--vt-c-text-light-1: var(--vt-c-indigo);
|
||||||
|
--vt-c-text-light-2: rgba(60, 60, 60, 0.66);
|
||||||
|
--vt-c-text-dark-1: var(--vt-c-white);
|
||||||
|
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* semantic color variables for this project */
|
||||||
|
:root {
|
||||||
|
--color-background: var(--vt-c-white);
|
||||||
|
--color-background-soft: var(--vt-c-white-soft);
|
||||||
|
--color-background-mute: var(--vt-c-white-mute);
|
||||||
|
|
||||||
|
--color-border: var(--vt-c-divider-light-2);
|
||||||
|
--color-border-hover: var(--vt-c-divider-light-1);
|
||||||
|
|
||||||
|
--color-heading: var(--vt-c-text-light-1);
|
||||||
|
--color-text: var(--vt-c-text-light-1);
|
||||||
|
|
||||||
|
--section-gap: 160px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
--color-background: var(--vt-c-black);
|
||||||
|
--color-background-soft: var(--vt-c-black-soft);
|
||||||
|
--color-background-mute: var(--vt-c-black-mute);
|
||||||
|
|
||||||
|
--color-border: var(--vt-c-divider-dark-2);
|
||||||
|
--color-border-hover: var(--vt-c-divider-dark-1);
|
||||||
|
|
||||||
|
--color-heading: var(--vt-c-text-dark-1);
|
||||||
|
--color-text: var(--vt-c-text-dark-2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
min-height: 100vh;
|
||||||
|
color: var(--color-text);
|
||||||
|
background: var(--color-background);
|
||||||
|
transition:
|
||||||
|
color 0.5s,
|
||||||
|
background-color 0.5s;
|
||||||
|
line-height: 1.6;
|
||||||
|
font-family:
|
||||||
|
Inter,
|
||||||
|
-apple-system,
|
||||||
|
BlinkMacSystemFont,
|
||||||
|
'Segoe UI',
|
||||||
|
Roboto,
|
||||||
|
Oxygen,
|
||||||
|
Ubuntu,
|
||||||
|
Cantarell,
|
||||||
|
'Fira Sans',
|
||||||
|
'Droid Sans',
|
||||||
|
'Helvetica Neue',
|
||||||
|
sans-serif;
|
||||||
|
font-size: 15px;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
35
src/styles/main.css
Normal file
35
src/styles/main.css
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
@import "./base.css";
|
||||||
|
|
||||||
|
#app {
|
||||||
|
/* max-width: 1280px; */
|
||||||
|
/* margin: 0 auto; */
|
||||||
|
/* padding: 2rem; */
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
a,
|
||||||
|
.green {
|
||||||
|
text-decoration: none;
|
||||||
|
/* color: hsla(160, 100%, 37%, 1); */
|
||||||
|
transition: 0.4s;
|
||||||
|
/* padding: 3px; */
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (hover: hover) {
|
||||||
|
a:hover {
|
||||||
|
/* background-color: hsla(160, 100%, 37%, 0.2); */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
body {
|
||||||
|
/* display: flex; */
|
||||||
|
place-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#app {
|
||||||
|
/* display: grid; */
|
||||||
|
/* grid-template-columns: 1fr 1fr;
|
||||||
|
padding: 0 2rem; */
|
||||||
|
}
|
||||||
|
}
|
||||||
41
src/styles/show.css
Normal file
41
src/styles/show.css
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
.container {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
padding: 10px;
|
||||||
|
font-size: 30px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: auto;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
.welcome {
|
||||||
|
padding: 20px;
|
||||||
|
font-style: italic;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
.newsbox {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.news {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin: auto;
|
||||||
|
padding: 10px;
|
||||||
|
flex: 2;
|
||||||
|
}
|
||||||
|
.newspic {
|
||||||
|
display: inline-block;
|
||||||
|
background-color: #5b5a5a;
|
||||||
|
width: 170px;
|
||||||
|
height: 110px;
|
||||||
|
}
|
||||||
|
.newscontent {
|
||||||
|
margin-right: auto;
|
||||||
|
display: inline-block;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
82
src/views/admin/admin.vue
Normal file
82
src/views/admin/admin.vue
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
<template>
|
||||||
|
<el-container style="height: 100vh;width: 100vw;">
|
||||||
|
<!-- 左侧导航栏 -->
|
||||||
|
<el-aside class="left-menu-box">
|
||||||
|
<router-link to="/admin">
|
||||||
|
<h1>博客后台</h1>
|
||||||
|
</router-link>
|
||||||
|
<el-menu default-active="1" class="left-menu" @select="handleSelect" router>
|
||||||
|
<el-menu-item index="/admin/news">
|
||||||
|
<el-icon>
|
||||||
|
<document />
|
||||||
|
</el-icon>
|
||||||
|
<span>新闻管理</span>
|
||||||
|
</el-menu-item>
|
||||||
|
<el-menu-item index="/admin/posts">
|
||||||
|
<el-icon>
|
||||||
|
<document />
|
||||||
|
</el-icon>
|
||||||
|
<span>文章管理</span>
|
||||||
|
</el-menu-item>
|
||||||
|
<el-menu-item index="/admin/projects">
|
||||||
|
<el-icon>
|
||||||
|
<document />
|
||||||
|
</el-icon>
|
||||||
|
<span>项目管理</span>
|
||||||
|
</el-menu-item>
|
||||||
|
<el-menu-item index="/admin/comments">
|
||||||
|
<el-icon><chat-line-round /></el-icon>
|
||||||
|
<span>评论管理</span>
|
||||||
|
</el-menu-item>
|
||||||
|
<el-menu-item index="/admin/users">
|
||||||
|
<el-icon>
|
||||||
|
<user />
|
||||||
|
</el-icon>
|
||||||
|
<span>用户管理</span>
|
||||||
|
</el-menu-item>
|
||||||
|
<el-menu-item index="/admin/settings">
|
||||||
|
<el-icon>
|
||||||
|
<setting />
|
||||||
|
</el-icon>
|
||||||
|
<span>系统设置</span>
|
||||||
|
</el-menu-item>
|
||||||
|
</el-menu>
|
||||||
|
</el-aside>
|
||||||
|
|
||||||
|
<!-- 右侧主体 -->
|
||||||
|
<el-container>
|
||||||
|
<el-header class="">
|
||||||
|
{{ currentTitle }}
|
||||||
|
</el-header>
|
||||||
|
|
||||||
|
<el-main class="p-4 bg-gray-50">
|
||||||
|
<!-- 这里是主内容区域,使用 <router-view> 插入子路由组件 -->
|
||||||
|
<router-view />
|
||||||
|
</el-main>
|
||||||
|
</el-container>
|
||||||
|
</el-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { Document, User, ChatLineRound, Setting } from '@element-plus/icons-vue'
|
||||||
|
import '@/styles/admin.css'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
|
const handleSelect = (index) => {
|
||||||
|
console.log("导航选择:", index)
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentTitle = computed(() => {
|
||||||
|
return route.meta.title || '博客后台管理系统'
|
||||||
|
})
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- <style scoped>
|
||||||
|
.el-menu-vertical-demo {
|
||||||
|
border-right: none;
|
||||||
|
}
|
||||||
|
</style> -->
|
||||||
129
src/views/admin/article.vue
Normal file
129
src/views/admin/article.vue
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
<template>
|
||||||
|
<el-card>
|
||||||
|
<template #header>
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<span>文章列表</span>
|
||||||
|
<el-button type="primary" @click="openAddArticleModal = true">新增文章</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-table :data="tableData" style="width: 100%">
|
||||||
|
<el-table-column prop="title" label="标题" />
|
||||||
|
<el-table-column prop="created" label="创建时间" />
|
||||||
|
<el-table-column prop="updated" label="更新时间" />
|
||||||
|
<el-table-column label="操作">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-button size="small" @click="openDetails(scope.row)">查看</el-button>
|
||||||
|
<el-button size="small">编辑</el-button>
|
||||||
|
<el-button size="small" type="danger">删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<el-dialog v-model="openAddArticleModal" title="新增文章" style="height: 70%; width: 80%;">
|
||||||
|
<el-input size="large" v-model="title" prefix-icon="el-icon-edit" placeholder="请输入文章标题"></el-input>
|
||||||
|
<MarkdownEditor ref="editor" />
|
||||||
|
<el-button type="primary" @click="submitArticle">发布</el-button>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { userInfoStore } from '@/store/store'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import axios from 'axios'
|
||||||
|
import MarkdownEditor from '@/components/MarkdownEditor.vue'
|
||||||
|
import { articleStore, detailsStore } from '@/store/details'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { Edit } from '@element-plus/icons-vue'
|
||||||
|
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const detail = detailsStore()
|
||||||
|
const articleinfo = articleStore()
|
||||||
|
const userinfo = userInfoStore()
|
||||||
|
|
||||||
|
const openAddArticleModal = ref(false)
|
||||||
|
|
||||||
|
const editor = ref(null)
|
||||||
|
|
||||||
|
const page = ref(1)
|
||||||
|
const pageSize = ref(5)
|
||||||
|
const total = ref()
|
||||||
|
|
||||||
|
const title = ref("")
|
||||||
|
const tableData = ref([
|
||||||
|
])
|
||||||
|
|
||||||
|
const getArticle = async () => {
|
||||||
|
try {
|
||||||
|
const response = await axios.get("/api/admin/get/article", {
|
||||||
|
headers: {
|
||||||
|
Authorization: "Bearer " + userinfo.token,
|
||||||
|
},
|
||||||
|
params: {
|
||||||
|
current: page.value,
|
||||||
|
size: pageSize.value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (!response.data.code === 200) {
|
||||||
|
ElMessage.error(response.data.message)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
tableData.value = response.data.data.records
|
||||||
|
total.value = response.data.data.total
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
ElMessage.error("获取文章失败")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const submitArticle = async () => {
|
||||||
|
const markdownContent = editor.value.getContent()
|
||||||
|
console.log("提交的内容:", markdownContent)
|
||||||
|
try {
|
||||||
|
const response = await axios.post("/api/admin/publish/article", {
|
||||||
|
title: title.value,
|
||||||
|
content: markdownContent,
|
||||||
|
}, {
|
||||||
|
headers: {
|
||||||
|
Authorization: "Bearer " + userinfo.token,
|
||||||
|
contentType: "application/json"
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if (!response.data.code === 200) {
|
||||||
|
ElMessage.error(response.data.message)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ElMessage.success("发布成功")
|
||||||
|
openAddArticleModal.value = false
|
||||||
|
getArticle()
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
ElMessage.error("发布失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
editor.value.clearContent()
|
||||||
|
title.value = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
const openDetails = (row) => {
|
||||||
|
console.log(row)
|
||||||
|
// 打开文章详情页面
|
||||||
|
articleinfo.setArticle(row) // 设置文章详情
|
||||||
|
detail.setType("article")
|
||||||
|
// 跳转到文章详情页面
|
||||||
|
router.push('/admin/details')
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
page.value = 1
|
||||||
|
getArticle()
|
||||||
|
})
|
||||||
|
|
||||||
|
</script>
|
||||||
66
src/views/admin/comments.vue
Normal file
66
src/views/admin/comments.vue
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
<template>
|
||||||
|
<el-card>
|
||||||
|
<template #header>
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<span>评论列表</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-table :data="tableData" style="width: 100%">
|
||||||
|
<el-table-column prop="a_id" label="文章ID" />
|
||||||
|
<el-table-column prop="sender" label="发送人" />
|
||||||
|
<el-table-column prop="profile" label="" />
|
||||||
|
<el-table-column prop="comment" label="评论内容" />
|
||||||
|
<el-table-column prop="sent" label="发送时间" />
|
||||||
|
<el-table-column label="操作">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-button size="small" type="danger" @click="handleDelete(scope.row)">删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</el-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { userInfoStore } from '@/store/store'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
const userinfo = userInfoStore()
|
||||||
|
|
||||||
|
const page = ref(1)
|
||||||
|
const pageSize = ref(5)
|
||||||
|
const total = ref()
|
||||||
|
|
||||||
|
const tableData = ref([])
|
||||||
|
|
||||||
|
const getComment = async () => {
|
||||||
|
try {
|
||||||
|
const response = await axios.get("/api/admin/get/comments", {
|
||||||
|
headers: {
|
||||||
|
Authorization: "Bearer " + userinfo.token,
|
||||||
|
},
|
||||||
|
params: {
|
||||||
|
current: page.value,
|
||||||
|
size: pageSize.value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (!response.data.code === 200) {
|
||||||
|
ElMessage.error(response.data.message)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
tableData.value = response.data.data.records
|
||||||
|
total.value = response.data.data.total
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
ElMessage.error("获取文章失败")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
page.value = 1
|
||||||
|
getComment()
|
||||||
|
})
|
||||||
|
|
||||||
|
</script>
|
||||||
105
src/views/admin/details.vue
Normal file
105
src/views/admin/details.vue
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
<template>
|
||||||
|
<el-row>
|
||||||
|
<el-col :span="24">
|
||||||
|
<el-button @click="goBack()">
|
||||||
|
< 返回列表</el-button>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-row>
|
||||||
|
<el-col :span="24">
|
||||||
|
<h1 v-if="detail.type === 'article'">{{ articleinfo.detail.title }}</h1>
|
||||||
|
<p v-if="detail.type === 'article'" style="padding: 20px;">发布时间:{{ articleinfo.detail.created }}---更新时间:{{
|
||||||
|
articleinfo.detail.updated }}</p>
|
||||||
|
<h1 v-if="detail.type === 'news'">{{ newsinfo.detail.title }}</h1>
|
||||||
|
<p v-if="detail.type === 'news'" style="padding: 20px;">发布时间:{{ newsinfo.detail.published }}</p>
|
||||||
|
<h1 v-if="detail.type === 'project'">{{ projectinfo.detail.title }}</h1>
|
||||||
|
<p v-if="detail.type === 'project'" style="padding: 20px;">发布时间:{{ projectinfo.detail.created }}---更新时间:{{
|
||||||
|
projectinfo.detail.updated }}</p>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-row>
|
||||||
|
<el-col :span="24">
|
||||||
|
<div style="width: 100%;">
|
||||||
|
<div v-html="Content" class="markdown-body"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { articleStore, detailsStore, newsStore, projectStore } from '@/store/details'
|
||||||
|
import { onMounted, ref } from 'vue'
|
||||||
|
import { marked } from 'marked'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const detail = detailsStore()
|
||||||
|
|
||||||
|
const articleinfo = articleStore()
|
||||||
|
const newsinfo = newsStore()
|
||||||
|
const projectinfo = projectStore()
|
||||||
|
|
||||||
|
const Content = ref('')
|
||||||
|
|
||||||
|
const show = ref(null)
|
||||||
|
|
||||||
|
|
||||||
|
const setShow = () => {
|
||||||
|
if (detail.type === 'article')
|
||||||
|
show.value = articleinfo.detail
|
||||||
|
else if (detail.type === 'news')
|
||||||
|
show.value = newsinfo.detail
|
||||||
|
else if (detail.type === 'project')
|
||||||
|
show.value = projectinfo.detail
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const goBack = () => {
|
||||||
|
router.back()
|
||||||
|
}
|
||||||
|
|
||||||
|
const convertContent = () => {
|
||||||
|
const markdownContent = show.value.content
|
||||||
|
Content.value = marked.parse(markdownContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
setShow()
|
||||||
|
convertContent()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
@import 'github-markdown-css/github-markdown.css';
|
||||||
|
|
||||||
|
::v-deep(.markdown-body) {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep(.markdown-body table) {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep(.markdown-body th),
|
||||||
|
::v-deep(.markdown-body td) {
|
||||||
|
border: 1px solid #d0d7de;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep(.markdown-body th) {
|
||||||
|
background-color: #f6f8fa;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep(.markdown-body img) {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
display: block;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
95
src/views/admin/news.vue
Normal file
95
src/views/admin/news.vue
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
<template>
|
||||||
|
<el-card>
|
||||||
|
<template #header>
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<span>新闻列表</span>
|
||||||
|
<el-button type="primary" @click="openAddNewsModal = true">新增新闻</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-table :data="tableData" style="width: 100%">
|
||||||
|
<el-table-column prop="n_title" label="标题" />
|
||||||
|
<el-table-column prop="published" label="发布时间" />
|
||||||
|
<el-table-column label="操作">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-button size="small" @click="openDetails(scope.row)">查看</el-button>
|
||||||
|
<el-button size="small">编辑</el-button>
|
||||||
|
<el-button size="small" type="danger">删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<el-dialog v-model="openAddNewsModal" title="新增新闻" style="height: 70%; width: 80%;">
|
||||||
|
<el-input size="large" v-model="title" prefix-icon="el-icon-edit" placeholder="请输入文章标题"></el-input>
|
||||||
|
<MarkdownEditor ref="editor" />
|
||||||
|
<el-button type="primary" @click="submit()">发布</el-button>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { userInfoStore } from '@/store/store'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import axios from 'axios'
|
||||||
|
import { newsStore, detailsStore } from '@/store/details'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import MarkdownEditor from '@/components/MarkdownEditor.vue'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const newsinfo = newsStore()
|
||||||
|
const detailtype = detailsStore()
|
||||||
|
const userinfo = userInfoStore()
|
||||||
|
|
||||||
|
const openAddNewsModal = ref(false)
|
||||||
|
|
||||||
|
const page = ref(1)
|
||||||
|
const pageSize = ref(5)
|
||||||
|
const total = ref()
|
||||||
|
|
||||||
|
const tableData = ref([])
|
||||||
|
|
||||||
|
const getNews = async () => {
|
||||||
|
try {
|
||||||
|
const response = await axios.get("/api/admin/get/news", {
|
||||||
|
headers: {
|
||||||
|
Authorization: "Bearer " + userinfo.token,
|
||||||
|
},
|
||||||
|
params: {
|
||||||
|
current: page.value,
|
||||||
|
size: pageSize.value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (!response.data.code === 200) {
|
||||||
|
ElMessage.error(response.data.message)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
tableData.value = response.data.data.records
|
||||||
|
total.value = response.data.data.total
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
ElMessage.error("获取文章失败")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const openDetails = (row) => {
|
||||||
|
console.log(row)
|
||||||
|
// 打开文章详情页面
|
||||||
|
newsinfo.setNews(row) // 设置文章详情
|
||||||
|
detailtype.setType("news")
|
||||||
|
// 跳转到文章详情页面
|
||||||
|
router.push('/admin/details')
|
||||||
|
}
|
||||||
|
|
||||||
|
const submit = async () => {
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
page.value = 1
|
||||||
|
getNews()
|
||||||
|
})
|
||||||
|
|
||||||
|
</script>
|
||||||
67
src/views/admin/projects.vue
Normal file
67
src/views/admin/projects.vue
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
<template>
|
||||||
|
<el-card>
|
||||||
|
<template #header>
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<span>文章列表</span>
|
||||||
|
<el-button type="primary">新增文章</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-table :data="tableData" style="width: 100%">
|
||||||
|
<el-table-column prop="p_name" label="项目名称" />
|
||||||
|
<el-table-column prop="techstack" label="技术栈" />
|
||||||
|
<el-table-column prop="p_status" label="项目状态" />
|
||||||
|
<el-table-column label="操作">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-button size="small">查看</el-button>
|
||||||
|
<el-button size="small">编辑</el-button>
|
||||||
|
<el-button size="small" type="danger">删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</el-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { userInfoStore } from '@/store/store'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
const userinfo = userInfoStore()
|
||||||
|
|
||||||
|
const page = ref(1)
|
||||||
|
const pageSize = ref(5)
|
||||||
|
const total = ref()
|
||||||
|
|
||||||
|
const tableData = ref([])
|
||||||
|
|
||||||
|
const getProject = async () => {
|
||||||
|
try {
|
||||||
|
const response = await axios.get("/api/admin/get/projects", {
|
||||||
|
headers: {
|
||||||
|
Authorization: "Bearer " + userinfo.token,
|
||||||
|
},
|
||||||
|
params: {
|
||||||
|
current: page.value,
|
||||||
|
size: pageSize.value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (!response.data.code === 200) {
|
||||||
|
ElMessage.error(response.data.message)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
tableData.value = response.data.data.records
|
||||||
|
total.value = response.data.data.total
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
ElMessage.error("获取文章失败")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
page.value = 1
|
||||||
|
getProject()
|
||||||
|
})
|
||||||
|
|
||||||
|
</script>
|
||||||
351
src/views/blog/blog.vue
Normal file
351
src/views/blog/blog.vue
Normal file
@@ -0,0 +1,351 @@
|
|||||||
|
<template>
|
||||||
|
<div class="blog">
|
||||||
|
<el-container>
|
||||||
|
<el-header>
|
||||||
|
<el-menu :default-active="activeMenu" mode="horizontal" :ellipsis="false" class="menu">
|
||||||
|
<router-link to="/"><el-menu-item index="1">首页</el-menu-item></router-link>
|
||||||
|
<router-link><el-menu-item index="2">参与项目</el-menu-item></router-link>
|
||||||
|
<router-link><el-menu-item index="3">联系站长</el-menu-item></router-link>
|
||||||
|
<el-sub-menu index="4" class="sub-menu">
|
||||||
|
<template #title>
|
||||||
|
在线应用
|
||||||
|
</template>
|
||||||
|
<router-link to="/myplayer">
|
||||||
|
<el-menu-item index="4-1">MyPlayer</el-menu-item>
|
||||||
|
</router-link>
|
||||||
|
</el-sub-menu>
|
||||||
|
|
||||||
|
<div class="login-btn">
|
||||||
|
<div class="login" @click="blogLogin()">{{ u_name }}</div>
|
||||||
|
</div>
|
||||||
|
</el-menu>
|
||||||
|
</el-header>
|
||||||
|
<el-main>
|
||||||
|
<div class="blog-content">
|
||||||
|
<router-view />
|
||||||
|
</div>
|
||||||
|
</el-main>
|
||||||
|
<el-footer style="text-align: right;">
|
||||||
|
<!-- <p>© 2025 烛火创意科技 保留所有权利</p> -->
|
||||||
|
<p>粤ICP备2025400740-1号</p>
|
||||||
|
</el-footer>
|
||||||
|
</el-container>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 用户登录、注册弹窗 -->
|
||||||
|
<el-dialog title="用户登录" v-model="loginDialog" style="min-width: 400px; max-width: 500px;">
|
||||||
|
<el-form :model="loginInfo" label-width="auto" style="width: 100%;" :rules="isadmin ? null : rules">
|
||||||
|
<el-form-item label="账号or邮箱" prop="u_account">
|
||||||
|
<el-input v-model="loginInfo.u_account"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="密码" prop="u_password">
|
||||||
|
<el-input v-model="loginInfo.u_password" type="password"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-checkbox v-model="isadmin">管理员登录</el-checkbox>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button @click="login()">登录</el-button>
|
||||||
|
<el-button @Click="goToRegister()" type="primary">注册</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<el-dialog title="用户注册" v-model="registDialog" style="min-width: 400px; max-width: 500px;">
|
||||||
|
<el-form ref="formRef" :model="loginInfo" label-width="auto" style="width: 100%;" :rules="rules">
|
||||||
|
<el-form-item label="邮箱" prop="u_account">
|
||||||
|
<el-input v-model="loginInfo.u_account"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="密码" prop="u_password">
|
||||||
|
<el-input v-model="loginInfo.u_password" type="password"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="确认密码" prop="u_password_confirm">
|
||||||
|
<el-input v-model="loginInfo.u_password_confirm" type="password"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="验证码" prop="code">
|
||||||
|
<el-input v-model="loginInfo.code" style="width: 50%; margin-right: 10px" />
|
||||||
|
<el-button :disabled="countdown > 0" @click="getCode()">
|
||||||
|
{{ countdown > 0 ? `${countdown}s 后重试` : '获取验证码' }}
|
||||||
|
</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button style="margin-left: auto;" type="primary" @click="submitForm">注册</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { reactive, ref, watch } from 'vue';
|
||||||
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
|
import SHA256 from 'crypto-js/sha256';
|
||||||
|
import { userInfoStore } from '@/store/store';
|
||||||
|
import { getUserInfo } from '@/function/user';
|
||||||
|
import { ElMessage } from 'element-plus';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
const userinfo = userInfoStore();
|
||||||
|
const activeMenu = ref('1');
|
||||||
|
const u_name = ref('登录')
|
||||||
|
|
||||||
|
// 控制登录弹窗的显示与隐藏
|
||||||
|
const loginDialog = ref(false);
|
||||||
|
const registDialog = ref(false);
|
||||||
|
|
||||||
|
const isadmin = ref(false);
|
||||||
|
|
||||||
|
const route = useRoute(); // 获取当前路由信息
|
||||||
|
const router = useRouter(); // 获取路由实例
|
||||||
|
|
||||||
|
const formRef = ref(null); // 用于表单验证的引用
|
||||||
|
|
||||||
|
const loginInfo = reactive({
|
||||||
|
u_account: '',
|
||||||
|
u_password: '',
|
||||||
|
u_password_confirm: '',
|
||||||
|
code_id: '',
|
||||||
|
code: ''
|
||||||
|
})
|
||||||
|
const hashpassword = ref('')
|
||||||
|
|
||||||
|
const countdown = ref(0);
|
||||||
|
let timer = null;
|
||||||
|
|
||||||
|
const rules = reactive({
|
||||||
|
u_account: [
|
||||||
|
{ required: true, message: '请输入邮箱', trigger: 'blur' },
|
||||||
|
{ type: 'email', message: '请输入有效的邮箱地址', trigger: ['blur', 'change'] }
|
||||||
|
],
|
||||||
|
u_password: [
|
||||||
|
{ required: true, message: '请输入密码', trigger: 'blur' },
|
||||||
|
{ min: 6, max: 20, message: '密码长度必须在6到20个字符之间', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
u_password_confirm: [
|
||||||
|
{ required: true, message: '请确认密码', trigger: 'blur' },
|
||||||
|
{
|
||||||
|
validator: (rule, value, callback) => {
|
||||||
|
if (value !== loginInfo.u_password) {
|
||||||
|
callback(new Error('两次输入的密码不一致'));
|
||||||
|
} else {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
trigger: 'blur'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
code: [
|
||||||
|
{ required: true, message: '请输入验证码', trigger: 'blur' }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
// 根据当前路由设置 activeMenu
|
||||||
|
const updateActiveMenu = () => {
|
||||||
|
switch (route.path) {
|
||||||
|
case '/':
|
||||||
|
activeMenu.value = '1';
|
||||||
|
break;
|
||||||
|
case '/about':
|
||||||
|
activeMenu.value = '2';
|
||||||
|
break;
|
||||||
|
case '/contact':
|
||||||
|
activeMenu.value = '3';
|
||||||
|
break;
|
||||||
|
case '/myplayer':
|
||||||
|
activeMenu.value = '4';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
activeMenu.value = '1'; // 默认选中首页
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 初始化时设置 activeMenu
|
||||||
|
updateActiveMenu();
|
||||||
|
|
||||||
|
// 监听路由变化,动态更新 activeMenu
|
||||||
|
watch(route, () => {
|
||||||
|
updateActiveMenu();
|
||||||
|
});
|
||||||
|
|
||||||
|
const blogLogin = () => {
|
||||||
|
if (u_name.value == 'admin') {
|
||||||
|
const origin = window.location.origin
|
||||||
|
const redirect_uri = `${origin}/admin`
|
||||||
|
window.open(redirect_uri, '_blank')
|
||||||
|
}
|
||||||
|
if (u_name.value == '登录') {
|
||||||
|
loginDialog.value = true;
|
||||||
|
} else {
|
||||||
|
router.push('/me')
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const goToRegister = () => {
|
||||||
|
loginDialog.value = false;
|
||||||
|
registDialog.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const submitForm = () => {
|
||||||
|
formRef.value.validate((valid) => {
|
||||||
|
if (valid) {
|
||||||
|
register()
|
||||||
|
} else {
|
||||||
|
console.log('error submit!!')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const login = async () => {
|
||||||
|
hashpassword.value = SHA256(loginInfo.u_password).toString();
|
||||||
|
// console.log(hashpassword.value)
|
||||||
|
if (isadmin.value) {
|
||||||
|
const response = await axios.post('/api/admin/login', {
|
||||||
|
u_account: loginInfo.u_account,
|
||||||
|
u_password: hashpassword.value
|
||||||
|
})
|
||||||
|
if (!response.data.code == 200) {
|
||||||
|
ElMessage.error(response.data.message)
|
||||||
|
return
|
||||||
|
} else if (response.data.data.role !== 'Admin') {
|
||||||
|
ElMessage.error('不是管理员,请联系站长')
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
userinfo.token = response.data.data.token
|
||||||
|
userinfo.user.u_account = loginInfo.u_account
|
||||||
|
loginDialog.value = false;
|
||||||
|
getUserInfo();
|
||||||
|
u_name.value = userinfo.user.u_name
|
||||||
|
const origin = window.location.origin
|
||||||
|
const redirect_uri = `${origin}/admin`
|
||||||
|
window.open(redirect_uri, '_blank')
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const response = await axios.post('/api/login', {
|
||||||
|
u_account: loginInfo.u_account,
|
||||||
|
u_password: hashpassword.value
|
||||||
|
}).then((response) => {
|
||||||
|
console.log(response.data);
|
||||||
|
if (response.data.code == 200) {
|
||||||
|
userinfo.token = response.data.data.token;
|
||||||
|
getUserInfo();
|
||||||
|
u_name.value = response.data.u_name;
|
||||||
|
loginDialog.value = false;
|
||||||
|
ElMessage.success('登录成功');
|
||||||
|
} else {
|
||||||
|
ElMessage.error(response.data.msg)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const register = async () => {
|
||||||
|
ElMessage.success('注册成功');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const startCountdown = () => {
|
||||||
|
countdown.value = 60
|
||||||
|
timer = setInterval(() => {
|
||||||
|
countdown.value--
|
||||||
|
if (countdown.value <= 0 && timer) {
|
||||||
|
clearInterval(timer)
|
||||||
|
}
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
const getCode = async () => {
|
||||||
|
if (countdown.value > 0) return
|
||||||
|
|
||||||
|
// 此处调用你的发送验证码接口逻辑
|
||||||
|
// 例如 axios.post('/api/code', { email: loginInfo.value.u_account })
|
||||||
|
|
||||||
|
// 成功后开始倒计时
|
||||||
|
startCountdown()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.blog {
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-header {
|
||||||
|
margin: auto;
|
||||||
|
padding: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-content {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.menu {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
background-color: #727272;
|
||||||
|
border-radius: 13px;
|
||||||
|
height: 62px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-sub-menu__title {
|
||||||
|
border-radius: 15px;
|
||||||
|
color: #ffffff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-sub-menu__title:hover {
|
||||||
|
border-radius: 15px;
|
||||||
|
color: #000000 !important;
|
||||||
|
background-color: #ececec;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu .el-menu-item {
|
||||||
|
border-radius: 15px;
|
||||||
|
color: #ffffff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu .el-menu-item:hover {
|
||||||
|
background-color: #ececec !important;
|
||||||
|
color: #000000 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu .el-menu-item.is-active {
|
||||||
|
text-decoration: underline !important;
|
||||||
|
background-color: #444444 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.login-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-left: auto;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login {
|
||||||
|
color: #ffffff;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 5px 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login:hover {
|
||||||
|
background-color: #ececec;
|
||||||
|
color: #000000;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
7
src/views/blog/me.vue
Normal file
7
src/views/blog/me.vue
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<template>
|
||||||
|
<div>me</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
</style>
|
||||||
16
src/views/blog/myplayer.vue
Normal file
16
src/views/blog/myplayer.vue
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<h2>MyPlayer</h2>
|
||||||
|
<p>Myplayer是一款基于Vue+springboot的在线聊天、在线影音播放的web应用</p>
|
||||||
|
<p>基于webSocket、webRTC等实时交互技术,为用户带来流畅的在线体验</p>
|
||||||
|
<p>目前还处于开发阶段,功能还在不断完善中,敬请期待</p>
|
||||||
|
<button @click="gotoMyPlayer()">立即前往MyPlayer</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
|
||||||
|
const gotoMyPlayer = () => {
|
||||||
|
window.open('https://myplayer.merlin.xin', '_blank');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style></style>
|
||||||
107
src/views/blog/show.vue
Normal file
107
src/views/blog/show.vue
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
<template>
|
||||||
|
<div class="container">
|
||||||
|
<el-row :span="24" style="height: 100%;">
|
||||||
|
<el-col :span="5">
|
||||||
|
<el-row>
|
||||||
|
<el-col :span="24" style="text-align: right;">
|
||||||
|
<h1>欢迎来到我的博客</h1>
|
||||||
|
<p class="welcome">Welcome to Merlin's blog</p>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="19" style="height: 100%; border-left: 1px solid #eee;">
|
||||||
|
<el-row style="height: 10%;">
|
||||||
|
<el-col :span="24">
|
||||||
|
<h1 style="font-size: 35px;padding-left: 15px;">最新消息:</h1>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-row style="height: 90%;">
|
||||||
|
<el-col :span="24" style="height: 100%;">
|
||||||
|
<el-scrollbar class="newsbox">
|
||||||
|
<div class="news" v-for="item in news" @click="openDetails(item.a_id)">
|
||||||
|
<div class="newspic">
|
||||||
|
<img :src="item.pic" alt="芜湖" />
|
||||||
|
</div>
|
||||||
|
<div class="newscontent">
|
||||||
|
<h2>{{ item.n_title }}</h2>
|
||||||
|
<p>{{ item.synopsis }}</p>
|
||||||
|
<p class="date">{{ item.published }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</el-scrollbar>
|
||||||
|
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import '@/styles/show.css'
|
||||||
|
import axios from 'axios'
|
||||||
|
import { onMounted, ref } from 'vue'
|
||||||
|
import { userInfoStore } from '@/store/store'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { viewDetailsStore } from '@/store/details'
|
||||||
|
|
||||||
|
const userinfo = userInfoStore()
|
||||||
|
const details = viewDetailsStore()
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const pageSize = 10
|
||||||
|
const page = 1
|
||||||
|
const news = ref([
|
||||||
|
])
|
||||||
|
|
||||||
|
const getNews = async () => {
|
||||||
|
try {
|
||||||
|
const response = await axios.get('/api/blog/get/news', {
|
||||||
|
headers: {
|
||||||
|
Authorization: 'Bearer ' + userinfo.token
|
||||||
|
},
|
||||||
|
params: {
|
||||||
|
current: page,
|
||||||
|
size: pageSize
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (response.data.code === 200) {
|
||||||
|
news.value = response.data.data.records
|
||||||
|
} else {
|
||||||
|
console.log(response.data.message)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const openDetails = async (a_id) => {
|
||||||
|
const response = await axios.get('/api/blog/get/article/' + a_id, {
|
||||||
|
headers: {
|
||||||
|
Authorization: 'Bearer ' + userinfo.token
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (!response.data.code === 200) {
|
||||||
|
console.log(response.data.message)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
details.setViewDetails(response.data.data)
|
||||||
|
router.push('viewDetails')
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
getNews()
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
</script>
|
||||||
105
src/views/blog/viewDetails.vue
Normal file
105
src/views/blog/viewDetails.vue
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
<template>
|
||||||
|
<el-row>
|
||||||
|
<el-col :span="24">
|
||||||
|
<el-button @click="goBack()">
|
||||||
|
< 返回列表</el-button>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-row>
|
||||||
|
<el-col :span="24">
|
||||||
|
<h1 v-if="detail.type === 'article'">{{ articleinfo.detail.title }}</h1>
|
||||||
|
<p v-if="detail.type === 'article'" style="padding: 20px;">发布时间:{{ articleinfo.detail.created }}---更新时间:{{
|
||||||
|
articleinfo.detail.updated }}</p>
|
||||||
|
<h1 v-if="detail.type === 'news'">{{ newsinfo.detail.title }}</h1>
|
||||||
|
<p v-if="detail.type === 'news'" style="padding: 20px;">发布时间:{{ newsinfo.detail.published }}</p>
|
||||||
|
<h1 v-if="detail.type === 'project'">{{ projectinfo.detail.title }}</h1>
|
||||||
|
<p v-if="detail.type === 'project'" style="padding: 20px;">发布时间:{{ projectinfo.detail.created }}---更新时间:{{
|
||||||
|
projectinfo.detail.updated }}</p>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-row>
|
||||||
|
<el-col :span="24">
|
||||||
|
<div style="width: 100%;">
|
||||||
|
<div v-html="Content" class="markdown-body"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { articleStore, detailsStore, newsStore, projectStore } from '@/store/details'
|
||||||
|
import { onMounted, ref } from 'vue'
|
||||||
|
import { marked } from 'marked'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const detail = detailsStore()
|
||||||
|
|
||||||
|
const articleinfo = articleStore()
|
||||||
|
const newsinfo = newsStore()
|
||||||
|
const projectinfo = projectStore()
|
||||||
|
|
||||||
|
const Content = ref('')
|
||||||
|
|
||||||
|
const show = ref(null)
|
||||||
|
|
||||||
|
|
||||||
|
const setShow = () => {
|
||||||
|
if (detail.type === 'article')
|
||||||
|
show.value = articleinfo.detail
|
||||||
|
else if (detail.type === 'news')
|
||||||
|
show.value = newsinfo.detail
|
||||||
|
else if (detail.type === 'project')
|
||||||
|
show.value = projectinfo.detail
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const goBack = () => {
|
||||||
|
router.back()
|
||||||
|
}
|
||||||
|
|
||||||
|
const convertContent = () => {
|
||||||
|
const markdownContent = show.value.content
|
||||||
|
Content.value = marked.parse(markdownContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
setShow()
|
||||||
|
convertContent()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
@import 'github-markdown-css/github-markdown.css';
|
||||||
|
|
||||||
|
::v-deep(.markdown-body) {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep(.markdown-body table) {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep(.markdown-body th),
|
||||||
|
::v-deep(.markdown-body td) {
|
||||||
|
border: 1px solid #d0d7de;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep(.markdown-body th) {
|
||||||
|
background-color: #f6f8fa;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep(.markdown-body img) {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
display: block;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
59
vite.config.js
Normal file
59
vite.config.js
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import { defineConfig } from "vite";
|
||||||
|
import vue from "@vitejs/plugin-vue";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [vue()],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
"@": path.resolve(__dirname, "src"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
proxy: {
|
||||||
|
"/api": {
|
||||||
|
target: "http://localhost:8080", // 后端服务器地址
|
||||||
|
changeOrigin: true, // 允许跨域
|
||||||
|
rewrite: (path) => path.replace(/^\/api/, ""), // 重写路径,去掉 /api 前缀
|
||||||
|
},
|
||||||
|
"/proxy": {
|
||||||
|
target: "http://localhost:3000", // 代理服务器的地址
|
||||||
|
changeOrigin: true, // 必须设置为 true,才能避免跨域问题
|
||||||
|
// rewrite: (path) => path.replace(/^\/proxy/, ''), // 重写路径,去掉 /api 前缀
|
||||||
|
},
|
||||||
|
"/online": {
|
||||||
|
target: "ws://localhost:8080",
|
||||||
|
changeOrigin: true,
|
||||||
|
ws: true,
|
||||||
|
},
|
||||||
|
"/voice": {
|
||||||
|
target: "ws://localhost:8080",
|
||||||
|
changeOrigin: true,
|
||||||
|
ws: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// server: {
|
||||||
|
// https:{
|
||||||
|
// key: fs.readFileSync('./cert/merlin.xin.key'),
|
||||||
|
// cert: fs.readFileSync('./cert/merlin.xin.pem'),
|
||||||
|
// },
|
||||||
|
// proxy: {
|
||||||
|
// '/api': {
|
||||||
|
// target: 'https://localhost:8443', // 后端服务器地址
|
||||||
|
// changeOrigin: true, // 允许跨域
|
||||||
|
// rewrite: (path) => path.replace(/^\/api/, ''), // 重写路径,去掉 /api 前缀
|
||||||
|
// },
|
||||||
|
// '/online':{
|
||||||
|
// target:'wss://localhost:8443/online',
|
||||||
|
// changeOrigin:true,
|
||||||
|
// ws:true,
|
||||||
|
// },
|
||||||
|
// '/voice':{
|
||||||
|
// target:'wss://localhost:8443/voice',
|
||||||
|
// changeOrigin:true,
|
||||||
|
// ws:true,
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user