feat: 优化文章页面加载表现
This commit is contained in:
1 parent
370ec61f3d
commit
c1be93739d
7 files changed
+75
-66
No files matched your search
@@ -2,9 +2,13 @@
|
|||||||
<div :class="wrapperClass">
|
<div :class="wrapperClass">
|
||||||
<div class="cm-header">
|
<div class="cm-header">
|
||||||
<div class="cm-header-title">
|
<div class="cm-header-title">
|
||||||
<span>CodeMirror</span>
|
<span>{{ props.type ?? 'CodeMirror' }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="cm-header-controls">
|
<div class="cm-header-controls">
|
||||||
|
<div class="cm-header-controls-copy" :class="{ 'cm-header-controls-copy-hide': !show }"
|
||||||
|
@click="copyCode">
|
||||||
|
复制
|
||||||
|
</div>
|
||||||
<div class="cm-switch-show-hide" @click="show = !show">
|
<div class="cm-switch-show-hide" @click="show = !show">
|
||||||
{{ show ? '收起' : '展开' }}
|
{{ show ? '收起' : '展开' }}
|
||||||
</div>
|
</div>
|
||||||
@@ -106,6 +110,11 @@ const useLangExtensions = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const copyCode = () => {
|
||||||
|
navigator.clipboard.writeText(props.data)
|
||||||
|
ElMessage.success('复制成功')
|
||||||
|
}
|
||||||
|
|
||||||
let cm: EditorView
|
let cm: EditorView
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
@@ -142,13 +151,16 @@ onBeforeUnmount(() => {
|
|||||||
.cm-rander-wrapper {
|
.cm-rander-wrapper {
|
||||||
&-inline {
|
&-inline {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
|
||||||
.cm-header {
|
.cm-header {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cm-gutters {
|
.cm-gutters {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.cm-header {
|
.cm-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -161,6 +173,23 @@ onBeforeUnmount(() => {
|
|||||||
user-select: none;
|
user-select: none;
|
||||||
|
|
||||||
.cm-header-controls {
|
.cm-header-controls {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
|
||||||
|
.cm-header-controls-copy {
|
||||||
|
cursor: pointer;
|
||||||
|
color: #fff;
|
||||||
|
fill: #fff;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-header-controls-copy-hide {
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.cm-switch-show-hide {
|
.cm-switch-show-hide {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
|
|||||||
@@ -2,42 +2,40 @@
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="md-render-wrapper markdown-body">
|
<div class="md-render-wrapper markdown-body">
|
||||||
<!-- <div v-html="renderedData" :key="_key"></div> -->
|
<VNodeMD v-if="VNodeMD && props.data"></VNodeMD>
|
||||||
<VNodeArr></VNodeArr>
|
<div v-else>
|
||||||
|
<el-skeleton :rows="12" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import useMarkdownit from '~/hooks/useMarkdownit';
|
import useMarkdownit from '~/hooks/useMarkdownit';
|
||||||
import Html2VNode from '~/utils/html2VNode';
|
import Html2VNode from '~/utils/html2VNode';
|
||||||
import codeMirror from './codeMirror.vue';
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
data: string
|
data: string
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const _key = ref(Math.random())
|
const VNodeMD = shallowRef<any>()
|
||||||
|
|
||||||
const markdownIt = await useMarkdownit()
|
watch(() => props.data, async () => {
|
||||||
|
|
||||||
const VNodeArr = computed(() => {
|
|
||||||
if (!props.data) {
|
if (!props.data) {
|
||||||
return () => []
|
return () => []
|
||||||
}
|
}
|
||||||
|
const markdownIt = await useMarkdownit()
|
||||||
const renderedData = markdownIt.render(props.data)
|
const renderedData = markdownIt.render(props.data)
|
||||||
console.log({
|
|
||||||
renderedData
|
|
||||||
})
|
|
||||||
const h2v = new Html2VNode()
|
const h2v = new Html2VNode()
|
||||||
h2v.use(
|
h2v.use(
|
||||||
{
|
{
|
||||||
filter(tagName) {
|
filter(tagName) {
|
||||||
return tagName === 'code'
|
return tagName === 'code'
|
||||||
},
|
},
|
||||||
render({
|
async render({
|
||||||
item,
|
item,
|
||||||
tagAttrs,
|
tagAttrs,
|
||||||
}) {
|
}) {
|
||||||
|
const codeMirror = (await import('./codeMirror.vue')).default
|
||||||
const codeType = tagAttrs?.class?.slice(1, -1)?.split('-')?.[1] ?? ''
|
const codeType = tagAttrs?.class?.slice(1, -1)?.split('-')?.[1] ?? ''
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const inline = (item.children[0].children.trim() as string).includes('\n') ? false : true
|
const inline = (item.children[0].children.trim() as string).includes('\n') ? false : true
|
||||||
@@ -53,12 +51,14 @@ const VNodeArr = computed(() => {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
const res = h2v.render(renderedData)
|
const res = await h2v.render(renderedData)
|
||||||
return {
|
VNodeMD.value = {
|
||||||
setup: () => {
|
setup: () => {
|
||||||
return () => res
|
return () => res
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
}, {
|
||||||
|
immediate: true
|
||||||
})
|
})
|
||||||
|
|
||||||
// watch(() => renderedData.value, () => {
|
// watch(() => renderedData.value, () => {
|
||||||
@@ -90,7 +90,12 @@ const VNodeArr = computed(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* 恢复标题样式 */
|
/* 恢复标题样式 */
|
||||||
h1, h2, h3, h4, h5, h6 {
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6 {
|
||||||
margin-top: 1em;
|
margin-top: 1em;
|
||||||
margin-bottom: 0.5em;
|
margin-bottom: 0.5em;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
@@ -135,7 +140,8 @@ const VNodeArr = computed(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* 恢复列表样式 */
|
/* 恢复列表样式 */
|
||||||
ul, ol {
|
ul,
|
||||||
|
ol {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
margin-bottom: 1em;
|
margin-bottom: 1em;
|
||||||
padding-left: 2em;
|
padding-left: 2em;
|
||||||
@@ -153,7 +159,8 @@ const VNodeArr = computed(() => {
|
|||||||
margin-bottom: 0.25em;
|
margin-bottom: 0.25em;
|
||||||
}
|
}
|
||||||
|
|
||||||
li > ul, li > ol {
|
li>ul,
|
||||||
|
li>ol {
|
||||||
margin-top: 0.25em;
|
margin-top: 0.25em;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="user-wrapper">
|
<div class="user-wrapper">
|
||||||
|
<Suspense>
|
||||||
<NuxtPage />
|
<NuxtPage />
|
||||||
|
</Suspense>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
</article>
|
</article>
|
||||||
<div class="posts-skeleton" v-if="pending">
|
<div class="posts-skeleton" v-if="pending && 0">
|
||||||
<div class="posts-skeleton-item" v-for="_ in range(5)">
|
<div class="posts-skeleton-item" v-for="_ in range(5)">
|
||||||
<el-skeleton :rows="3" />
|
<el-skeleton :rows="3" />
|
||||||
</div>
|
</div>
|
||||||
@@ -48,8 +48,6 @@ const { data, pending } = await useFetch<{ code: number, data: postItem[] }>('/a
|
|||||||
// 添加以下选项
|
// 添加以下选项
|
||||||
lazy: true, // 避免客户端自动重新获取
|
lazy: true, // 避免客户端自动重新获取
|
||||||
server: true, // 仅在服务器端获取数据
|
server: true, // 仅在服务器端获取数据
|
||||||
// cache: 'force-cache', // 强制使用缓存
|
|
||||||
// dedupe: 'force', // 强制使用缓存的数据
|
|
||||||
})
|
})
|
||||||
// 从响应中提取文章列表
|
// 从响应中提取文章列表
|
||||||
const posts = computed(() => data.value?.data || []) as ComputedRef<postItem[]>
|
const posts = computed(() => data.value?.data || []) as ComputedRef<postItem[]>
|
||||||
|
|||||||
+10
-37
@@ -1,9 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="md-wrapper">
|
<div class="md-wrapper">
|
||||||
<div class="md" v-if="postData">
|
<div class="md" v-if="!loading && postData">
|
||||||
<div class="md-title">{{ postData.title }}</div>
|
<div class="md-title">{{ postData.title }}</div>
|
||||||
<div class="md-divider"></div>
|
<div class="md-divider"></div>
|
||||||
<markdown-render :data="postData.content"></markdown-render>
|
<markdown-render :data="postData.data"></markdown-render>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="md-skeleton-item">
|
<div v-else class="md-skeleton-item">
|
||||||
<el-skeleton :rows="12" />
|
<el-skeleton :rows="12" />
|
||||||
@@ -12,49 +12,22 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import useMarkdownit from '~/hooks/useMarkdownit';
|
|
||||||
import usePostStore from '~/hooks/pinia/usePostStore';
|
|
||||||
import markdownRender from '~/components/markdownRender.vue';
|
|
||||||
import type { postContent } from '~/types/post';
|
import type { postContent } from '~/types/post';
|
||||||
|
|
||||||
const postStore = usePostStore()
|
const markdownRender = shallowRef<any>()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const id = route.params.id as string
|
const id = route.params.id as string
|
||||||
|
|
||||||
// 使用useFetch来获取文章内容,支持SSR
|
const loading = ref(true)
|
||||||
const { data: fetchedPost } = await useFetch('/api/posts/getPostContent', {
|
|
||||||
method: 'POST',
|
|
||||||
body: {
|
|
||||||
id
|
|
||||||
},
|
|
||||||
transform: async (response: postContent) => {
|
|
||||||
// 缓存数据
|
|
||||||
postStore.setPostCache(id, response)
|
|
||||||
|
|
||||||
// 渲染Markdown
|
const postData = ref<postContent>()
|
||||||
const markdownIt = await useMarkdownit()
|
|
||||||
|
|
||||||
return {
|
onBeforeMount(async () => {
|
||||||
title: response.title,
|
const postDataPromise = getPostData(Number(id))
|
||||||
content: response.data,
|
markdownRender.value = (await import('~/components/markdownRender.vue')).default
|
||||||
rendered: markdownIt.render(response.data)
|
postData.value = await postDataPromise
|
||||||
}
|
loading.value = false
|
||||||
},
|
|
||||||
// 使用缓存
|
|
||||||
async onRequest({ options }) {
|
|
||||||
const cache = postStore.getPostCache(id)
|
|
||||||
if (cache) {
|
|
||||||
return {
|
|
||||||
title: cache.title,
|
|
||||||
content: cache.data,
|
|
||||||
rendered: (await useMarkdownit()).render(cache.data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// 响应式数据
|
|
||||||
const postData = computed(() => fetchedPost.value)
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
import useDefaultStore from "~/hooks/pinia/useDefaultStore"
|
import useDefaultStore from "~/hooks/pinia/useDefaultStore"
|
||||||
import usePostStore from "~/hooks/pinia/usePostStore"
|
import usePostStore from "~/hooks/pinia/usePostStore"
|
||||||
import useMiaoFetch from "~/hooks/useMiaoFetch"
|
// import useMiaoFetch from "~/hooks/useMiaoFetch"
|
||||||
// import usePostStore from "~/hooks/pinia/usePostStore"
|
// import usePostStore from "~/hooks/pinia/usePostStore"
|
||||||
import type { postContent, postItem } from "~/types/post"
|
import type { postContent, postItem } from "~/types/post"
|
||||||
|
|
||||||
|
|||||||
+8
-8
@@ -41,11 +41,11 @@ export default class Html2VNode {
|
|||||||
return middlewareMap;
|
return middlewareMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
private render2VNode = (
|
private render2VNode = async (
|
||||||
wrinklesResult: (miaoVNodeType | string)[],
|
wrinklesResult: (miaoVNodeType | string)[],
|
||||||
middlewareMap: Map<string, middlewareType[]>
|
middlewareMap: Map<string, middlewareType[]>
|
||||||
): (VNode | string)[] => {
|
): Promise<(VNode | string)[]> => {
|
||||||
return wrinklesResult.map((item: miaoVNodeType | string): VNode | string => {
|
return Promise.all(wrinklesResult.map((async (item: miaoVNodeType | string): Promise<VNode | string> => {
|
||||||
if (typeof item === 'string') {
|
if (typeof item === 'string') {
|
||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
@@ -59,18 +59,18 @@ export default class Html2VNode {
|
|||||||
middlewareMap
|
middlewareMap
|
||||||
})
|
})
|
||||||
if(_res) {
|
if(_res) {
|
||||||
return _res
|
return await _res
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return ''
|
return ''
|
||||||
})
|
})))
|
||||||
}
|
}
|
||||||
|
|
||||||
private defaultMiddleware: middlewareType = {
|
private defaultMiddleware: middlewareType = {
|
||||||
filter: (_: string) => {
|
filter: (_: string) => {
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
render: ({
|
render: async ({
|
||||||
item, tagName, tagAttrs, middlewareMap
|
item, tagName, tagAttrs, middlewareMap
|
||||||
}) => {
|
}) => {
|
||||||
if (!tagName) {
|
if (!tagName) {
|
||||||
@@ -78,7 +78,7 @@ export default class Html2VNode {
|
|||||||
}
|
}
|
||||||
let child: string | (string | VNode)[] = item.children as string
|
let child: string | (string | VNode)[] = item.children as string
|
||||||
if (typeof item.children !== 'string') {
|
if (typeof item.children !== 'string') {
|
||||||
child = this.render2VNode(item.children, middlewareMap)
|
child = await this.render2VNode(item.children, middlewareMap)
|
||||||
}
|
}
|
||||||
return h(tagName ?? 'span', tagAttrs, child)
|
return h(tagName ?? 'span', tagAttrs, child)
|
||||||
}
|
}
|
||||||
@@ -221,5 +221,5 @@ export type middlewareType = {
|
|||||||
tagAttrs: { [key: string]: string },
|
tagAttrs: { [key: string]: string },
|
||||||
middlewareMap: Map<string, middlewareType[]>
|
middlewareMap: Map<string, middlewareType[]>
|
||||||
}
|
}
|
||||||
) => VNode | string
|
) => Promise<VNode | string>
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user