feat: 优化文章页面加载表现

This commit is contained in:
539943419 committed 2025-03-25 13:24:17 +08:00
1 parent 370ec61f3d
commit c1be93739d
7 files changed
+157 -148

No files matched your search

+92 -63
View File
@@ -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>
@@ -35,7 +39,7 @@ const cmRootRef = ref<HTMLElement>()
const isLoading = ref(true) const isLoading = ref(true)
const wrapperClass = computed(() => { const wrapperClass = computed(() => {
return ['cm-rander-wrapper', props.inline === true ? 'cm-rander-wrapper-inline' : ''] return ['cm-rander-wrapper', props.inline === true ? 'cm-rander-wrapper-inline' : '']
}) })
const rows = computed(() => { const rows = computed(() => {
@@ -50,60 +54,65 @@ const rows = computed(() => {
}) })
const useLangExtensions = async () => { const useLangExtensions = async () => {
const ext = props.type const ext = props.type
switch (ext) { switch (ext) {
case 'js': case 'js':
case 'jsx': case 'jsx':
case 'ts': case 'ts':
case 'tsx': case 'tsx':
const { javascript } = await import('@codemirror/lang-javascript') const { javascript } = await import('@codemirror/lang-javascript')
return [ return [
javascript({ javascript({
jsx: ext.substring(2, 3) === 'x', jsx: ext.substring(2, 3) === 'x',
typescript: ext.substring(0, 2) === 'js' typescript: ext.substring(0, 2) === 'js'
}) })
] ]
break break
case 'css': case 'css':
case 'scss': case 'scss':
const { sass } = await import('@codemirror/lang-sass') const { sass } = await import('@codemirror/lang-sass')
return [sass()] return [sass()]
break break
case 'html': case 'html':
const { html } = await import('@codemirror/lang-html') const { html } = await import('@codemirror/lang-html')
return [html()] return [html()]
break break
case 'py': case 'py':
const { python } = await import('@codemirror/lang-python') const { python } = await import('@codemirror/lang-python')
return [python()] return [python()]
break break
case 'go': case 'go':
const { go } = await import('@codemirror/lang-go') const { go } = await import('@codemirror/lang-go')
return [go()] return [go()]
break break
case 'json': case 'json':
const { json } = await import('@codemirror/lang-json') const { json } = await import('@codemirror/lang-json')
return [json()] return [json()]
break break
case 'md': case 'md':
const { markdown } = await import('@codemirror/lang-markdown') const { markdown } = await import('@codemirror/lang-markdown')
return [markdown()] return [markdown()]
break break
case 'yaml': case 'yaml':
const { yaml } = await import('@codemirror/lang-yaml') const { yaml } = await import('@codemirror/lang-yaml')
return [yaml()] return [yaml()]
break break
case 'vue': case 'vue':
const { vue } = await import('@codemirror/lang-vue') const { vue } = await import('@codemirror/lang-vue')
return [vue()] return [vue()]
break break
case 'xml': case 'xml':
const { xml } = await import('@codemirror/lang-xml') const { xml } = await import('@codemirror/lang-xml')
return [xml()] return [xml()]
break break
default: default:
return [] return []
} }
}
const copyCode = () => {
navigator.clipboard.writeText(props.data)
ElMessage.success('复制成功')
} }
let cm: EditorView let cm: EditorView
@@ -141,14 +150,17 @@ onBeforeUnmount(() => {
<style lang="scss"> <style lang="scss">
.cm-rander-wrapper { .cm-rander-wrapper {
&-inline { &-inline {
display: inline-block; display: inline-block;
.cm-header {
display: none !important; .cm-header {
} display: none !important;
.cm-gutters { }
display: none !important;
} .cm-gutters {
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;
+40 -33
View File
@@ -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,38 +90,43 @@ 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;
line-height: 1.25; line-height: 1.25;
color: #111; color: #111;
} }
h1 { h1 {
font-size: 2em; font-size: 2em;
border-bottom: 1px solid #eaecef; border-bottom: 1px solid #eaecef;
padding-bottom: 0.3em; padding-bottom: 0.3em;
} }
h2 { h2 {
font-size: 1.5em; font-size: 1.5em;
border-bottom: 1px solid #eaecef; border-bottom: 1px solid #eaecef;
padding-bottom: 0.3em; padding-bottom: 0.3em;
} }
h3 { h3 {
font-size: 1.25em; font-size: 1.25em;
} }
h4 { h4 {
font-size: 1em; font-size: 1em;
} }
h5 { h5 {
font-size: 0.875em; font-size: 0.875em;
} }
h6 { h6 {
font-size: 0.85em; font-size: 0.85em;
color: #6a737d; color: #6a737d;
@@ -135,25 +140,27 @@ 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;
} }
ul { ul {
list-style-type: disc; list-style-type: disc;
} }
ol { ol {
list-style-type: decimal; list-style-type: decimal;
} }
li { li {
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;
} }
@@ -162,7 +169,7 @@ const VNodeArr = computed(() => {
a { a {
color: #0366d6; color: #0366d6;
text-decoration: none; text-decoration: none;
&:hover { &:hover {
text-decoration: underline; text-decoration: underline;
} }
@@ -174,11 +181,11 @@ const VNodeArr = computed(() => {
padding: 0 1em; padding: 0 1em;
color: #6a737d; color: #6a737d;
border-left: 0.25em solid #dfe2e5; border-left: 0.25em solid #dfe2e5;
> :first-child { > :first-child {
margin-top: 0; margin-top: 0;
} }
> :last-child { > :last-child {
margin-bottom: 0; margin-bottom: 0;
} }
@@ -194,22 +201,22 @@ const VNodeArr = computed(() => {
border-spacing: 0; border-spacing: 0;
border-collapse: collapse; border-collapse: collapse;
} }
th { th {
font-weight: 600; font-weight: 600;
padding: 6px 13px; padding: 6px 13px;
border: 1px solid #dfe2e5; border: 1px solid #dfe2e5;
} }
td { td {
padding: 6px 13px; padding: 6px 13px;
border: 1px solid #dfe2e5; border: 1px solid #dfe2e5;
} }
tr { tr {
background-color: #fff; background-color: #fff;
border-top: 1px solid #c6cbd1; border-top: 1px solid #c6cbd1;
&:nth-child(2n) { &:nth-child(2n) {
background-color: #f6f8fa; background-color: #f6f8fa;
} }
+3 -1
View File
@@ -1,6 +1,8 @@
<template> <template>
<div class="user-wrapper"> <div class="user-wrapper">
<NuxtPage /> <Suspense>
<NuxtPage />
</Suspense>
</div> </div>
</template> </template>
+1 -3
View File
@@ -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[]>
+12 -39
View File
@@ -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 markdownIt = await useMarkdownit()
return {
title: response.title,
content: response.data,
rendered: markdownIt.render(response.data)
}
},
// 使用缓存
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 = ref<postContent>()
const postData = computed(() => fetchedPost.value)
onBeforeMount(async () => {
const postDataPromise = getPostData(Number(id))
markdownRender.value = (await import('~/components/markdownRender.vue')).default
postData.value = await postDataPromise
loading.value = false
})
</script> </script>
<style scoped> <style scoped>
+1 -1
View File
@@ -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
View File
@@ -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>
} }