添加博客模块,支持Markdown渲染和文章详情页,优化图片懒加载和瀑布流布局
This commit is contained in:
4
extra.d.ts
vendored
4
extra.d.ts
vendored
@ -3,4 +3,6 @@ declare module "vue3-video-play";
|
|||||||
declare module "vue3-masonry-plus";
|
declare module "vue3-masonry-plus";
|
||||||
declare module "vite";
|
declare module "vite";
|
||||||
declare module "vue-devui/tag";
|
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";
|
||||||
@ -15,12 +15,19 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@imengyu/vue3-context-menu": "^1.5.2",
|
"@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",
|
"@meting/core": "^1.5.13",
|
||||||
"@unocss/reset": "^66.3.3",
|
"@unocss/reset": "^66.3.3",
|
||||||
"aplayer": "^1.10.1",
|
"aplayer": "^1.10.1",
|
||||||
"axios": "^1.11.0",
|
"axios": "^1.11.0",
|
||||||
"es-toolkit": "^1.39.8",
|
"es-toolkit": "^1.39.8",
|
||||||
|
"highlight.js": "^11.11.1",
|
||||||
|
"juejin-markdown-themes": "^1.34.0",
|
||||||
"less": "^4.4.0",
|
"less": "^4.4.0",
|
||||||
|
"markdown-exit": "^1.0.0-beta.6",
|
||||||
|
"md-editor-v3": "^6.3.0",
|
||||||
"pinia": "^3.0.3",
|
"pinia": "^3.0.3",
|
||||||
"qweather-icons": "^1.7.0",
|
"qweather-icons": "^1.7.0",
|
||||||
"unocss": "^66.3.3",
|
"unocss": "^66.3.3",
|
||||||
@ -30,6 +37,7 @@
|
|||||||
"v-infinite-scroll": "^1.0.4",
|
"v-infinite-scroll": "^1.0.4",
|
||||||
"vite-svg-loader": "^5.1.0",
|
"vite-svg-loader": "^5.1.0",
|
||||||
"vue": "^3.6.0-alpha.2",
|
"vue": "^3.6.0-alpha.2",
|
||||||
|
"vue-markdown": "^2.2.4",
|
||||||
"vue-router": "^4.5.1",
|
"vue-router": "^4.5.1",
|
||||||
"vue3-cookies": "^1.0.6",
|
"vue3-cookies": "^1.0.6",
|
||||||
"vue3-video-play": "^1.3.2"
|
"vue3-video-play": "^1.3.2"
|
||||||
|
|||||||
16
src/api/blog/index.ts
Normal file
16
src/api/blog/index.ts
Normal 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",
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -23,4 +23,5 @@ export function putShare(data: Record<string, string>) {
|
|||||||
method: "put",
|
method: "put",
|
||||||
data,
|
data,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
17
src/icon/loading.svg
Normal file
17
src/icon/loading.svg
Normal 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 |
@ -1,8 +1,8 @@
|
|||||||
<template>
|
<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">
|
<n-image ref="lazyRef" class="lazy__img" :src="url" @load="handleLoad" @error="handleError">
|
||||||
<template #placeholder>
|
<template #placeholder>
|
||||||
<img width="400" :src="loading" alt="loading" />
|
<img :src="loading" alt="loading" />
|
||||||
</template>
|
</template>
|
||||||
<template #error>
|
<template #error>
|
||||||
<img :src="errorImg" alt="error" />
|
<img :src="errorImg" alt="error" />
|
||||||
@ -60,12 +60,18 @@ onMounted(() => {
|
|||||||
/* object-fit: cover; */
|
/* object-fit: cover; */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.lazy__img {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
.lazy__img img[alt="loading"],
|
.lazy__img img[alt="loading"],
|
||||||
.lazy__img img[alt="error"] {
|
.lazy__img img[alt="error"] {
|
||||||
width: 80px;
|
width: 100%;
|
||||||
height: 80px;
|
height: auto;
|
||||||
padding: 1em;
|
padding: 1em;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
display: block;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div ref="waterfallWrapper" class="waterfall-list" :style="{ height: `${wrapperHeight}px` }">
|
<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 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">
|
<div class="waterfall-card h-full">
|
||||||
<slot name="item" :item="item" :index="index" :url="getRenderURL(item)" />
|
<slot name="item" :item="item" :index="index" :url="getRenderURL(item)" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
16
src/main.ts
16
src/main.ts
@ -7,14 +7,28 @@ import 'vue-devui/tag/style.css';
|
|||||||
import App from "./App.vue";
|
import App from "./App.vue";
|
||||||
import router from "./router";
|
import router from "./router";
|
||||||
// 自定义主题配置 - 设置主色和二级色\
|
// 自定义主题配置 - 设置主色和二级色\
|
||||||
|
import 'md-editor-v3/lib/style.css';
|
||||||
import "vfonts/FiraCode.css";
|
import "vfonts/FiraCode.css";
|
||||||
import Tag from 'vue-devui/tag';
|
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);
|
const app = createApp(App);
|
||||||
|
|
||||||
app.use(Tag)
|
app.use(Tag)
|
||||||
app.use(createPinia());
|
app.use(createPinia());
|
||||||
app.use(router);
|
app.use(router);
|
||||||
|
// app.use(VMdPreview)
|
||||||
useConfig();
|
useConfig();
|
||||||
for (const key in icon) {
|
for (const key in icon) {
|
||||||
// console.log(key, icon[key]);
|
// console.log(key, icon[key]);
|
||||||
|
|||||||
@ -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>
|
|
||||||
@ -14,9 +14,9 @@
|
|||||||
backgroundColor="transparent"> >
|
backgroundColor="transparent"> >
|
||||||
<template #item="{ item }">
|
<template #item="{ item }">
|
||||||
<div
|
<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"> -->
|
<!-- <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> -->
|
||||||
<div
|
<div
|
||||||
class="hidden truncate group-hover:block absolute rounded-md z-10 truncate top-0 text-center w-full bg-[#00000070] text-white">
|
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_num: pn.value,
|
||||||
page_size: ps.value,
|
page_size: ps.value,
|
||||||
});
|
});
|
||||||
|
|
||||||
// console.log('>>> --> getFileList --> res:', res);
|
// console.log('>>> --> getFileList --> res:', res);
|
||||||
if (res.data.length < ps.value) {
|
if (res.data.length < ps.value) {
|
||||||
isLoadAll.value = true;
|
isLoadAll.value = true;
|
||||||
@ -102,6 +103,8 @@ async function getFileList() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 监听滚动事件
|
// 监听滚动事件
|
||||||
const handleScroll: any = throttle((e: any) => {
|
const handleScroll: any = throttle((e: any) => {
|
||||||
// console.log('>>> --> handleScroll --> loading:', e)
|
// console.log('>>> --> handleScroll --> loading:', e)
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
<div class="plink-page">
|
<div class="plink-page">
|
||||||
<h1>友链页</h1>
|
<h1>友链页</h1>
|
||||||
<!-- 友链内容 -->
|
<!-- 友链内容 -->
|
||||||
|
<icon-loading class="zhuan text-primary w-12 h-12" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -16,5 +17,24 @@ definePage({
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
/* 友链页样式 */
|
/* 无限旋转动画 */
|
||||||
|
@keyframes spin {
|
||||||
|
0% {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.zhuan {
|
||||||
|
animation: spin 1.5s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
51
src/views/blog/[bid].vue
Normal file
51
src/views/blog/[bid].vue
Normal 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
34
src/views/blog/index.vue
Normal 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
9
typed-router.d.ts
vendored
@ -19,7 +19,8 @@ declare module 'vue-router/auto-routes' {
|
|||||||
*/
|
*/
|
||||||
export interface RouteNamedMap {
|
export interface RouteNamedMap {
|
||||||
'apps': RouteRecordInfo<'apps', '/Apps', Record<never, never>, Record<never, never>>,
|
'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>>,
|
'gallery': RouteRecordInfo<'gallery', '/Gallery', Record<never, never>, Record<never, never>>,
|
||||||
'home': RouteRecordInfo<'home', '/Home', 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>>,
|
'404': RouteRecordInfo<'404', '/NotFound', Record<never, never>, Record<never, never>>,
|
||||||
@ -42,10 +43,14 @@ declare module 'vue-router/auto-routes' {
|
|||||||
routes: 'apps'
|
routes: 'apps'
|
||||||
views: never
|
views: never
|
||||||
}
|
}
|
||||||
'src/views/Blog.vue': {
|
'src/views/blog/index.vue': {
|
||||||
routes: 'blog'
|
routes: 'blog'
|
||||||
views: never
|
views: never
|
||||||
}
|
}
|
||||||
|
'src/views/blog/[bid].vue': {
|
||||||
|
routes: 'blog/:bid'
|
||||||
|
views: never
|
||||||
|
}
|
||||||
'src/views/Gallery.vue': {
|
'src/views/Gallery.vue': {
|
||||||
routes: 'gallery'
|
routes: 'gallery'
|
||||||
views: never
|
views: never
|
||||||
|
|||||||
Reference in New Issue
Block a user