Skip to content

Commit

Permalink
feat: support docTree
Browse files Browse the repository at this point in the history
  • Loading branch information
terwer committed Nov 11, 2024
1 parent 36c6749 commit 4767264
Show file tree
Hide file tree
Showing 4 changed files with 260 additions and 21 deletions.
3 changes: 3 additions & 0 deletions app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,7 @@ export default defineAppConfig<AppConfig>({
content: "body { background-color: #f5f5f5; }",
},
],

outline: [],
docTree: [],
})
92 changes: 71 additions & 21 deletions components/static/Detail.vue
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ 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"

// https://github.com/nuxt/nuxt/issues/15346
// 由于布局是个宏,静态构建情况下,不能动态设置,只能在前面的页面写死
Expand Down Expand Up @@ -58,8 +59,44 @@ const formData = reactive({
post: {} as Post,
shareEnabled: true,
isExpires: false,

// 文档树
items: [
{ id: "1", name: "首页", children: [] },
{
id: "2",
name: "产品",
children: [
{ id: "3", name: "产品A", content: "这是产品A的内容" },
{
id: "4",
name: "产品B",
children: [
{ id: "5", name: "产品B1", content: "这是产品B1的内容" },
{ id: "6", name: "产品B2", content: "这是产品B2的内容" },
],
},
],
},
{
id: "7",
name: "关于我们",
children: [
{ id: "8", name: "公司简介", content: "这是公司的简介" },
{ id: "9", name: "联系方式", content: "这是联系方式" },
],
},
],
defaultOpen: "8",
maxDepth: 3,
openItems: <string[]>[],
selectedItem: {},
})

const onItemSelected = (item: any) => {
formData.selectedItem = item
}

const getPostData = async () => {
const resText = await fetchPostMeta(id, providerMode)
const currentPost = JsonUtil.safeParse<Post>(resText, {} as Post)
Expand Down Expand Up @@ -92,7 +129,9 @@ if (!props.overrideSeo) {
useSeoMeta(seoMeta)
}

onMounted(async () => {})
onMounted(async () => {
formData.openItems.push(formData.defaultOpen)
})

const VNode = () =>
h("div", {
Expand All @@ -106,31 +145,42 @@ const VNode = () =>
<el-empty :description="formData.isExpires ? t('blog.index.no.expires') : t('blog.index.no.permission')">
</el-empty>
</div>
<div v-else 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 v-else>
<!-- 文档树 -->
<Sidebar
v-model="formData.openItems"
:items="formData.items"
:default-open="formData.defaultOpen"
:max-depth="formData.maxDepth"
@item-selected="onItemSelected"
/>
<!-- 分享正文 -->
<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
contenteditable="false"
data-position="center"
v-highlight
v-sbeauty
v-sdomparser
class="protyle-wysiwyg protyle-wysiwyg--attr"
spellcheck="false"
class="protyle-title__input"
data-render="true"
contenteditable="false"
data-doc-type="NodeDocument"
:data-page-id="id"
>
{{ formData.post.title }}
<VNode />
</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>
</template>
Expand Down
127 changes: 127 additions & 0 deletions components/static/Sidebar.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
<template>
<div class="sidebar">
<ul>
<li v-for="item in items" :key="item.id" @click="toggleItem(item.id)">
{{ item.name }}
<ul v-if="isOpen(item.id) && (depth < maxDepth || maxDepth === -1)">
<SidebarItem
:item="item"
:open-items="openItems"
:depth="depth + 1"
:max-depth="maxDepth"
@item-selected="selectItem"
/>
</ul>
</li>
</ul>
</div>
</template>

<script setup lang="ts">
import { ref, computed, watch } from "vue"
import { PropType } from "vue"
import SidebarItem from "./SidebarItem.vue"
const props = defineProps({
modelValue: {
type: Array as PropType<string[]>,
default: () => [],
},
items: {
type: Array as PropType<{ id: string; name: string; children?: { id: string; name: string; children?: any }[] }[]>,
default: () => [],
},
defaultOpen: {
type: String,
default: undefined,
},
maxDepth: {
type: Number,
default: -1, // -1 表示不限制深度
},
})
const emit = defineEmits(["update:modelValue", "itemSelected"])
const openItems = ref(props.modelValue)
const depth = 0 // 初始化 depth 为 0
const isOpen = (id: string) => openItems.value.includes(id)
const toggleItem = (id: string) => {
if (isOpen(id)) {
openItems.value = openItems.value.filter((i) => i !== id)
} else {
openItems.value.push(id)
}
}
const selectItem = (item: { name: string; content: string }) => {
emit("itemSelected", item)
}
const openAllParents = (id: string, path: string[]) => {
for (let i = path.length - 1; i >= 0; i--) {
const parentId = path[i]
if (!openItems.value.includes(parentId)) {
openItems.value.push(parentId)
}
}
if (!openItems.value.includes(id)) {
openItems.value.push(id)
}
}
const findPathToId = (id: string, items: any[], path: string[]): string[] | null => {
for (let i = 0; i < items.length; i++) {
const item = items[i]
if (item.id === id) {
return [...path, item.id]
}
if (item.children && item.children.length > 0) {
const childPath = findPathToId(id, item.children, [...path, item.id])
if (childPath) {
return childPath
}
}
}
return null
}
watch(
() => props.defaultOpen,
(newVal) => {
if (newVal !== undefined) {
const path = findPathToId(newVal, props.items, [])
if (path) {
openAllParents(newVal, path)
}
}
},
{ immediate: true }
)
watch(openItems, (newVal) => {
emit("update:modelValue", newVal)
})
</script>

<style lang="stylus" scoped>
.sidebar
width 250px
background-color #f4f4f9
padding 15px
ul
list-style-type none
padding 0
li
cursor pointer
padding 8px 0
border-bottom 1px solid #ddd
&:hover
background-color #e0e0e0
</style>
59 changes: 59 additions & 0 deletions components/static/SidebarItem.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<template>
<ul>
<li v-for="subItem in item.children" :key="subItem.id" @click="selectItem(subItem)">
{{ subItem.name }}
<ul v-if="subItem.children && subItem.children.length > 0 && (depth < maxDepth || maxDepth === -1)">
<SidebarItem
:item="subItem"
:open-items="openItems"
:depth="depth + 1"
:max-depth="maxDepth"
@item-selected="selectItem"
/>
</ul>
</li>
</ul>
</template>

<script setup lang="ts">
import { type PropType } from "vue"
const props = defineProps({
item: {
type: Object as PropType<{ id: string; name: string; children?: { id: string; name: string; children?: any }[] }>,
required: true,
},
openItems: {
type: Array as PropType<string[]>,
required: true,
},
depth: {
type: Number,
required: true,
},
maxDepth: {
type: Number,
default: -1, // -1 表示不限制深度
},
})
const emit = defineEmits(["itemSelected"])
const selectItem = (item: any) => {
emit("itemSelected", item)
}
</script>

<style lang="stylus" scoped>
ul
list-style-type none
padding 0
li
cursor pointer
padding 8px 0
border-bottom 1px solid #ddd
&:hover
background-color #e0e0e0
</style>

0 comments on commit 4767264

Please sign in to comment.