Skip to content

Commit

Permalink
feat: support docTree and outline
Browse files Browse the repository at this point in the history
  • Loading branch information
terwer committed Nov 11, 2024
1 parent 129e1c8 commit d66a7af
Show file tree
Hide file tree
Showing 6 changed files with 248 additions and 490 deletions.
326 changes: 108 additions & 218 deletions components/static/Detail.vue
Original file line number Diff line number Diff line change
@@ -1,239 +1,129 @@
<!--
- Copyright (c) 2023, Terwer . All rights reserved.
- DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
-
- This code is free software; you can redistribute it and/or modify it
- under the terms of the GNU General Public License version 2 only, as
- published by the Free Software Foundation. Terwer designates this
- particular file as subject to the "Classpath" exception as provided
- by Terwer in the LICENSE file that accompanied this code.
-
- This code is distributed in the hope that it will be useful, but WITHOUT
- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
- FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
- version 2 for more details (a copy is included in the LICENSE file that
- accompanied this code).
-
- You should have received a copy of the GNU General Public License version
- 2 along with this work; if not, write to the Free Software Foundation,
- Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
-
- Please contact Terwer, Shenzhen, Guangdong, China, youweics@163.com
- or visit www.terwer.space if you need additional information or have any
- questions.
-->
<template>
<div class="app-layout">
<aside class="sidebar">
<button class="expand-collapse-btn" @click="toggleAll">
{{ allExpanded ? "Collapse All" : "Expand All" }}
</button>
<SidebarItem
v-for="(item, index) in nestedTreeData"
:key="index"
:item="item"
:expanded-ids="expandedIds"
:all-expanded="allExpanded"
:max-depth="maxDepth"
@select="handleSelect"
/>
</aside>
<main class="main">main</main>
<aside class="outline">
<Outline :items="outlineItems" />
</aside>
</div>
</template>

<script setup lang="ts">
import { JsonUtil, ObjectUtil } from "zhi-common"
import { Post } from "zhi-blog-api"
import { createAppLogger } from "~/common/appLogger"
import { checkExpires, getSummery } from "~/utils/utils"
import { useServerAssets } from "~/plugins/renderer/useServerAssets"
import { useAuthModeFetch } from "~/composables/useAuthModeFetch"
import { useProviderMode } from "~/composables/useProviderMode"
import Sidebar from "~/components/static/Sidebar.vue"
<script setup>
import SidebarItem from "~/components/static/SidebarItem.vue"
import Outline from "~/components/static/Outline.vue"
import { type TreeNode, TreeUtils } from "~/utils/TreeUtils"

// https://github.com/nuxt/nuxt/issues/15346
// 由于布局是个宏,静态构建情况下,不能动态设置,只能在前面的页面写死
// props
const props = defineProps({
showTitleSign: Boolean,
overrideSeo: Boolean,
pageId: {
type: String,
default: undefined,
const treeData = ref([
{ id: "1", parentId: null, name: "Section 1" },
{ id: "2", parentId: "1", name: "Subsection 1.1" },
{ id: "3", parentId: "1", name: "Subsection 1.2" },
{ id: "4", parentId: null, name: "Section 2" },
{ id: "5", parentId: "4", name: "Subsection 2.1" },
{ id: "6", parentId: "3", name: "Subsection 1.2.1" },
{ id: "7", parentId: "3", name: "Subsection 1.2.2" },
])
const expandedIds = ref([])
const maxDepth = ref(3)
const allExpanded = ref(false)
const currentItem = ref(null)
const outlineItems = ref([
{ id: "section-1", title: "Introduction", level: 1 },
{
id: "section-1-1",
title: "What is Vue",
level: 2,
children: [
{ id: "section-1-1-1", title: "Vue Basics", level: 3 },
{
id: "section-1-1-2",
title: "Vue Lifecycle",
level: 3,
children: [{ id: "section-1-1-2-1", title: "Lifecycle Hooks", level: 4 }],
},
],
},
})

const logger = createAppLogger("static-share-page")
const { t } = useI18n()
const route = useRoute()
const id = props.pageId ?? ((route.params.id ?? "") as string)
const { getFirstImageSrc } = useServerAssets()
const { fetchPostMeta } = useAuthModeFetch()
const { providerMode } = useProviderMode()

// datas
const formData = reactive({
post: {} as Post,
shareEnabled: true,
isExpires: false,

// 文档树
items: <TreeNode[]>[],
defaultOpen: id,
maxDepth: 3,
openItems: <string[]>[],
selectedItem: {},

outlineItems: <any[]>[],
})

const onItemSelected = (item: any) => {
formData.selectedItem = item
{
id: "section-2",
title: "Advanced Topics",
level: 1,
children: [
{ id: "section-2-1", title: "Reactivity", level: 2 },
{
id: "section-2-2",
title: "Composition API",
level: 2,
children: [{ id: "section-2-2-1", title: "Setup Function", level: 3 }],
},
],
},
])
const buildTree = (list, parentId = null, depth = 1) => {
return list
.filter((item) => item.parentId === parentId)
.map((item) => ({
...item,
depth,
children: depth < maxDepth.value ? buildTree(list, item.id, depth + 1) : [],
}))
}
const getPostData = async () => {
const resText = await fetchPostMeta(id, providerMode)
const currentPost = JsonUtil.safeParse<Post>(resText, {} as Post)
logger.info("currentPost=>", currentPost)
formData.post = currentPost
formData.shareEnabled = !ObjectUtil.isEmptyObject(formData.post)
// logger.info("post=>", formData.post)
// logger.info(`shareEnabled=>${formData.shareEnabled}`)
const nestedTreeData = computed(() => buildTree(treeData.value))
const attrs = JsonUtil.safeParse<any>(formData.post?.attrs ?? "{}", {})
formData.isExpires = checkExpires(attrs)
const handleSelect = (item) => {
currentItem.value = item
outlineItems.value = generateOutline(item)
}
await getPostData()
if (!props.overrideSeo) {
const titleSign = " - " + t("blog.share")
const title = `${formData?.post?.title ?? "404 Not Found"}${props.showTitleSign ? titleSign : ""}`
const desc = getSummery(formData?.post?.description ?? "")
const headImage = getFirstImageSrc(formData?.post?.description ?? "")
const seoMeta = {
title: title,
ogTitle: title,
description: desc,
ogDescription: desc,
} as any
if (headImage) {
logger.info("get a head image from doc=>", headImage)
seoMeta.ogImage = headImage
}
useSeoMeta(seoMeta)
const generateOutline = (item) => {
return item.children || []
}
const editorDom = formData.post.editorDom?.replaceAll('contenteditable="true"', 'contenteditable="false"') ?? ""

const parseOutline = (content: string, depth = 1): any[] => {
const headings = <any[]>[]

const items: any[] = []
headings.forEach((heading) => {
const id = heading.id || heading.textContent?.trim().toLowerCase().replace(/\s+/g, "-") || ""
items.push({ id, title: heading.textContent || "", depth })
})

return items
}

const contentRef = ref<HTMLElement | null>(null)
const scrollToSection = (id: string) => {
if (contentRef.value) {
const section = contentRef.value.querySelector(`#${id}`)
if (section) {
section.scrollIntoView({ behavior: "smooth" })
}
}
// 控制所有项的展开/收起
const toggleAll = () => {
allExpanded.value = !allExpanded.value
expandedIds.value = allExpanded.value ? treeData.value.map((item) => item.id) : []
}

onMounted(async () => {
formData.items = TreeUtils.buildTree(formData.post.docTree ?? [])
formData.openItems.push(formData.defaultOpen)
// formData.outlineItems = parseOutline(editorDom)

console.log("formData.items", formData.items)
console.log("formData.openItems", formData.openItems)
})

const VNode = () =>
h("div", {
class: "",
innerHTML: editorDom,
})
</script>

<template>
<div v-if="!formData.shareEnabled || formData.isExpires">
<el-empty :description="formData.isExpires ? t('blog.index.no.expires') : t('blog.index.no.permission')">
</el-empty>
</div>
<div v-else class="container">
<!-- 文档树 -->
<Sidebar
v-model="formData.openItems"
:items="formData.items"
:default-open="formData.defaultOpen"
:max-depth="formData.maxDepth"
@item-selected="onItemSelected"
/>
<!-- 分享正文 -->
<div class="content-container">
<div class="outline">
<Outline
:outline-items="formData.outlineItems"
:max-depth="formData.maxDepth"
@scroll-to-section="scrollToSection"
/>
</div>
<div ref="contentRef" class="content">
<div class="fn__flex-1 protyle" data-loading="finished">
<div class="protyle-content protyle-content--transition" data-fullwidth="true">
<div class="protyle-title protyle-wysiwyg--attr">
<div
contenteditable="false"
data-position="center"
spellcheck="false"
class="protyle-title__input"
data-render="true"
>
{{ formData.post.title }}
</div>
</div>
<div
v-highlight
v-sbeauty
v-sdomparser
class="protyle-wysiwyg protyle-wysiwyg--attr"
spellcheck="false"
contenteditable="false"
data-doc-type="NodeDocument"
:data-page-id="id"
>
<VNode />
</div>
</div>
</div>
</div>
</div>
</div>
</template>

<style scoped lang="stylus">
.container
<style lang="stylus" scoped>
.app-layout
display flex
height 100vh
.sidebar
width 250px
background-color #f4f4f9
padding 15px
box-shadow 2px 0 5px rgba(0, 0, 0, 0.1)

ul
list-style-type none
padding 0

li
cursor pointer
padding 8px 16px
border-bottom 1px solid #ddd
transition background-color 0.3s

&.open
background-color #e0e0e0

&:hover
background-color #e0e0e0

.content
background-color #fafafa
border-right 1px solid #f0f0f0
overflow-y auto
.expand-collapse-btn
width 100%
padding 10px
background-color #1890ff
color white
border none
cursor pointer
text-align center
.main
flex 1
padding 20px
background-color #fff
box-shadow -2px 0 5px rgba(0, 0, 0, 0.1)
overflow-y auto
.outline
width 200px
overflow-y auto
</style>
Loading

0 comments on commit d66a7af

Please sign in to comment.