添加博客模块,支持Markdown渲染和文章详情页,优化图片懒加载和瀑布流布局

This commit is contained in:
2025-12-30 14:31:04 +08:00
parent 42dcf4195a
commit 79192df508
15 changed files with 3827 additions and 53 deletions

4
extra.d.ts vendored
View File

@ -3,4 +3,6 @@ declare module "vue3-video-play";
declare module "vue3-masonry-plus";
declare module "vite";
declare module "vue-devui/tag";
declare module "vue-infinite-scroll";
declare module "@markdown-next/vue";
declare module "@kangc/v-md-editor/lib/preview";
declare module "@kangc/v-md-editor/lib/theme/github.js";

View File

@ -15,12 +15,19 @@
},
"dependencies": {
"@imengyu/vue3-context-menu": "^1.5.2",
"@kangc/v-md-editor": "^2.3.18",
"@markdown-next/parser": "^0.0.2-alpha",
"@markdown-next/vue": "^0.0.2-alpha",
"@meting/core": "^1.5.13",
"@unocss/reset": "^66.3.3",
"aplayer": "^1.10.1",
"axios": "^1.11.0",
"es-toolkit": "^1.39.8",
"highlight.js": "^11.11.1",
"juejin-markdown-themes": "^1.34.0",
"less": "^4.4.0",
"markdown-exit": "^1.0.0-beta.6",
"md-editor-v3": "^6.3.0",
"pinia": "^3.0.3",
"qweather-icons": "^1.7.0",
"unocss": "^66.3.3",
@ -30,6 +37,7 @@
"v-infinite-scroll": "^1.0.4",
"vite-svg-loader": "^5.1.0",
"vue": "^3.6.0-alpha.2",
"vue-markdown": "^2.2.4",
"vue-router": "^4.5.1",
"vue3-cookies": "^1.0.6",
"vue3-video-play": "^1.3.2"

16
src/api/blog/index.ts Normal file
View File

@ -0,0 +1,16 @@
import request from "@/util/request";
//getBlogList
export function getBlogList() {
return request({
url: "/art",
method: "get",
});
}
//getBlogDetail
export function getBlogDetail(id: string) {
return request({
url: `/art/${id}`,
method: "get",
});
}

View File

@ -24,3 +24,4 @@ export function putShare(data: Record<string, string>) {
data,
});
}

17
src/icon/loading.svg Normal file
View File

@ -0,0 +1,17 @@
<svg class="mx-1 size-5 animate-spin text-[#FFC0CB] duration-10000" viewBox="0 0 512 512" fill="currentColor"><g><path fill="currentColor" d="M330.555,101.873c0-34.164-13.767-65.108-36.047-87.622c-2.432-2.465-9.63-4.349-14.212,3.466
c-4.589,7.808-17.205,27.431-17.205,27.431c-1.534,2.438-4.219,3.918-7.089,3.918c-2.883,0-5.555-1.48-7.089-3.918
c0,0-12.623-19.623-17.205-27.431c-4.582-7.815-11.781-5.931-14.219-3.466c-22.274,22.514-36.04,53.458-36.04,87.622
c0,51.02,30.663,94.847,74.553,114.142C299.892,196.72,330.555,152.892,330.555,101.873z"></path><path fill="currentColor" d="M204.51,253.425c-4.78-47.705-36.998-90.409-85.518-106.176c-32.492-10.554-66.177-7.027-94.47,7.205
c-3.096,1.555-7.117,7.822-1.096,14.589c6.007,6.774,20.766,24.842,20.766,24.842c1.849,2.212,2.432,5.22,1.542,7.952
c-0.891,2.74-3.13,4.829-5.918,5.528c0,0-22.562,5.945-31.404,7.89c-8.849,1.945-9.281,9.37-7.692,12.445
c14.527,28.144,39.698,50.801,72.197,61.362C121.436,304.828,172.6,289.212,204.51,253.425z"></path><path fill="currentColor" d="M96.772,362.484c-20.082,27.643-27.136,60.766-22.342,92.074c0.527,3.424,5.233,9.178,13.527,5.554
c8.308-3.63,30.054-12.088,30.054-12.088c2.672-1.062,5.698-0.692,8.041,1c2.329,1.692,3.623,4.466,3.418,7.335
c0,0-1.308,23.294-2.199,32.315c-0.883,9.006,6.048,11.712,9.466,11.15c31.252-5.116,60.581-22.054,80.662-49.704
c29.993-41.266,30.944-94.758,6.781-136.155C177.332,303.773,126.758,321.204,96.772,362.484z"></path><path fill="currentColor" d="M287.824,313.964c-24.165,41.397-23.212,94.888,6.78,136.155c20.082,27.65,49.41,44.588,80.663,49.704
c3.418,0.562,10.349-2.144,9.466-11.15c-0.89-9.021-2.205-32.315-2.205-32.315c-0.198-2.869,1.102-5.643,3.431-7.335
c2.322-1.692,5.363-2.062,8.034-1c0,0,21.746,8.458,30.047,12.088c8.294,3.623,13.007-2.13,13.534-5.554
c4.787-31.308-2.254-64.43-22.342-92.074C385.246,321.204,334.672,303.766,287.824,313.964z"></path><path fill="currentColor" d="M503.6,215.254c-8.849-1.945-31.41-7.89-31.41-7.89c-2.795-0.706-5.027-2.788-5.918-5.528
c-0.89-2.732-0.308-5.74,1.534-7.952c0,0,14.76-18.068,20.78-24.842c6.014-6.766,1.993-13.034-1.096-14.589
c-28.301-14.232-61.978-17.76-94.485-7.205c-48.519,15.767-80.731,58.472-85.512,106.176
c31.91,35.787,83.067,51.403,131.593,35.636c32.492-10.561,57.67-33.218,72.191-61.362
C512.867,224.624,512.449,217.199,503.6,215.254z"></path></g></svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -1,8 +1,8 @@
<template>
<div class="image-container flex justify-center items-center">
<div class="image-container flex w-full">
<n-image ref="lazyRef" class="lazy__img" :src="url" @load="handleLoad" @error="handleError">
<template #placeholder>
<img width="400" :src="loading" alt="loading" />
<img :src="loading" alt="loading" />
</template>
<template #error>
<img :src="errorImg" alt="error" />
@ -60,12 +60,18 @@ onMounted(() => {
/* object-fit: cover; */
}
.lazy__img {
width: 100%;
display: flex;
justify-content: center;
align-items: center;
}
.lazy__img img[alt="loading"],
.lazy__img img[alt="error"] {
width: 80px;
height: 80px;
width: 100%;
height: auto;
padding: 1em;
margin: 0 auto;
display: block;
}
</style>

View File

@ -1,7 +1,7 @@
<template>
<div ref="waterfallWrapper" class="waterfall-list" :style="{ height: `${wrapperHeight}px` }">
<div v-for="(item, index) in list" :key="getKey(item, index)" class="waterfall-item">
<div class="waterfall-card">
<div v-for="(item, index) in list" :key="getKey(item, index)" :style="{height:`${colWidth * item.height / item.width}px`}" class="waterfall-item">
<div class="waterfall-card h-full">
<slot name="item" :item="item" :index="index" :url="getRenderURL(item)" />
</div>
</div>

View File

@ -7,14 +7,28 @@ import 'vue-devui/tag/style.css';
import App from "./App.vue";
import router from "./router";
// 自定义主题配置 - 设置主色和二级色\
import 'md-editor-v3/lib/style.css';
import "vfonts/FiraCode.css";
import Tag from 'vue-devui/tag';
// import VMdPreview from '@kangc/v-md-editor/lib/preview';
// import '@kangc/v-md-editor/lib/style/preview.css';
// import githubTheme from '@kangc/v-md-editor/lib/theme/github.js';
// import '@kangc/v-md-editor/lib/theme/style/github.css';
// // highlightjs
// // import hljs from 'highlight.js';
// VMdPreview.use(githubTheme, {
// // Hljs: hljs,
// });
const app = createApp(App);
app.use(Tag)
app.use(createPinia());
app.use(router);
// app.use(VMdPreview)
useConfig();
for (const key in icon) {
// console.log(key, icon[key]);

View File

@ -1,18 +0,0 @@
<template>
<div class="article-page">
<h1>文章页</h1>
</div>
</template>
<script setup lang="ts">
definePage({
name: 'blog',
meta: {
title: '文章',
}
})
</script>
<style scoped>
/* 文章页样式 */
</style>

View File

@ -14,9 +14,9 @@
backgroundColor="transparent"> >
<template #item="{ item }">
<div
class="card rounded-md shadow-lg overflow-hidden group transition-transform duration-300 box-border hover:-translate-y-1.5">
class="card h-full flex items-center justify-center rounded-md shadow-lg overflow-hidden group transition-transform duration-300 box-border hover:-translate-y-1.5">
<!-- <div class="image-wrapper"> -->
<LazyImg class="rounded-md shadow overflow-hidden" :url="item.filepath" />
<LazyImg class="rounded-md shadow overflow-hidden" :Pwidth="item.width" :Pheight="item.height" :url="item.filepath" />
<!-- </div> -->
<div
class="hidden truncate group-hover:block absolute rounded-md z-10 truncate top-0 text-center w-full bg-[#00000070] text-white">
@ -81,6 +81,7 @@ async function getFileList() {
page_num: pn.value,
page_size: ps.value,
});
// console.log('>>> --> getFileList --> res:', res);
if (res.data.length < ps.value) {
isLoadAll.value = true;
@ -102,6 +103,8 @@ async function getFileList() {
}
// 监听滚动事件
const handleScroll: any = throttle((e: any) => {
// console.log('>>> --> handleScroll --> loading:', e)

View File

@ -2,6 +2,7 @@
<div class="plink-page">
<h1>友链页</h1>
<!-- 友链内容 -->
<icon-loading class="zhuan text-primary w-12 h-12" />
</div>
</template>
@ -16,5 +17,24 @@ definePage({
</script>
<style scoped>
/* 友链页样式 */
/* 无限旋转动画 */
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.zhuan {
animation: spin 1.5s linear infinite;
}
</style>

51
src/views/blog/[bid].vue Normal file
View File

@ -0,0 +1,51 @@
<template>
<div class="article-page w-full">
<div class="title">{{ blogData.title }}</div>
<div class="tags">
<div v-for="item in blogData.tags" :key="item.tid" class="tag">{{ item.name }}</div>
</div>
<div class="auther">{{ blogData.nickname }}</div>
<MdPreview class="!w-[80%] ml-1/10" :editorId="id" previewTheme="github" :modelValue="markdown" />
<MdCatalog class="absolute top-4 right-4" :editorId="id" :scrollElement="scrollElement" />
<div class="time">{{ blogData.updated_at }}</div>
</div>
</template>
<script setup lang="ts">
import type { ExposeParam } from 'md-editor-v3';
import { MdCatalog, MdPreview } from 'md-editor-v3';
import 'md-editor-v3/lib/preview.css';
const editorRef = ref<ExposeParam>();
const id = 'preview-only';
const scrollElement = document.documentElement;
const route: any = useRoute()
definePage({
name: 'blog/:bid',
path: '/blog/:bid',
meta: {
title: '文章详情',
}
})
const markdown = ref('# Hello World\n\nThis is **markdown**!');
const blogData = ref<any>({})
async function getBlogDetail() {
const bid: any = route.params?.bid || 0
// console.log('>>> --> getBlogDetail --> bid:', route, bid)
const res = await $http.blog.getBlogDetail(bid)
console.log('>>> --> getBlogDetail --> res:', res.data)
blogData.value = res.data
markdown.value = res.data.cont
editorRef.value?.toggleCatalog(true)
}
onMounted(() => {
getBlogDetail()
})
</script>
<style scoped>
/* 文章页样式 */
</style>

34
src/views/blog/index.vue Normal file
View File

@ -0,0 +1,34 @@
<template>
<div class="article-page w-full">
<h1>文章页</h1>
<div class="blog-list">
<div v-for="item in blogList" :key="item.aid" class="blog-item">
<div @click="$router.push(`/blog/${item.aid}`)" class="blog-title">{{ item.title }}</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
definePage({
name: 'blog',
meta: {
title: '文章',
}
})
const blogList = ref<any[]>([])
async function getBlogList() {
const res = await $http.blog.getBlogList()
console.log('>>> --> getBlogList --> res:', res.data)
blogList.value = res.data
}
onMounted(() => {
getBlogList()
})
</script>
<style scoped>
/* 文章页样式 */
</style>

9
typed-router.d.ts vendored
View File

@ -19,7 +19,8 @@ declare module 'vue-router/auto-routes' {
*/
export interface RouteNamedMap {
'apps': RouteRecordInfo<'apps', '/Apps', Record<never, never>, Record<never, never>>,
'blog': RouteRecordInfo<'blog', '/Blog', Record<never, never>, Record<never, never>>,
'blog': RouteRecordInfo<'blog', '/blog', Record<never, never>, Record<never, never>>,
'blog/:bid': RouteRecordInfo<'blog/:bid', '/blog/:bid', { bid: ParamValue<true> }, { bid: ParamValue<false> }>,
'gallery': RouteRecordInfo<'gallery', '/Gallery', Record<never, never>, Record<never, never>>,
'home': RouteRecordInfo<'home', '/Home', Record<never, never>, Record<never, never>>,
'404': RouteRecordInfo<'404', '/NotFound', Record<never, never>, Record<never, never>>,
@ -42,10 +43,14 @@ declare module 'vue-router/auto-routes' {
routes: 'apps'
views: never
}
'src/views/Blog.vue': {
'src/views/blog/index.vue': {
routes: 'blog'
views: never
}
'src/views/blog/[bid].vue': {
routes: 'blog/:bid'
views: never
}
'src/views/Gallery.vue': {
routes: 'gallery'
views: never

3655
yarn.lock

File diff suppressed because it is too large Load Diff