feat: add recursive structure tree
This commit is contained in:
@@ -1,9 +1,47 @@
|
||||
<script setup lang="ts">
|
||||
const nodes = [
|
||||
{ id: "block_001", name: "企业经营分析报告", status: "done" },
|
||||
{ id: "block_003", name: "一、经营概况", status: "pending" },
|
||||
{ id: "block_005", name: "经营指标表", status: "empty" },
|
||||
import { reactive, ref } from "vue";
|
||||
|
||||
import StructureTreeNode, { type StructureNode } from "./StructureTreeNode.vue";
|
||||
|
||||
const nodes: StructureNode[] = [
|
||||
{
|
||||
id: "block_001",
|
||||
name: "企业经营分析报告",
|
||||
status: "done",
|
||||
children: [
|
||||
{ id: "block_002", name: "报告说明", status: "review" },
|
||||
{
|
||||
id: "block_003",
|
||||
name: "一、经营概况",
|
||||
status: "done",
|
||||
children: [
|
||||
{ id: "block_004", name: "经营概况段落", status: "empty" },
|
||||
{ id: "block_005", name: "经营指标表", status: "disabled" },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const activeId = ref("block_003");
|
||||
const expandedKeys = reactive<Record<string, boolean>>({
|
||||
block_001: true,
|
||||
block_003: true,
|
||||
});
|
||||
|
||||
function toggleNode(id: string) {
|
||||
expandedKeys[id] = !expandedKeys[id];
|
||||
}
|
||||
|
||||
function selectNode(node: StructureNode) {
|
||||
activeId.value = node.id;
|
||||
const target = document.querySelector<HTMLElement>(`[data-block-id="${node.id}"]`);
|
||||
if (!target) return;
|
||||
|
||||
target.scrollIntoView({ behavior: "smooth", block: "center" });
|
||||
target.classList.add("tree-flash");
|
||||
window.setTimeout(() => target.classList.remove("tree-flash"), 1600);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -12,14 +50,16 @@ const nodes = [
|
||||
<span>文档结构</span>
|
||||
<span class="count">{{ nodes.length }}</span>
|
||||
</header>
|
||||
<ul>
|
||||
<li v-for="node in nodes" :key="node.id">
|
||||
<button type="button">
|
||||
<span class="dot" :class="node.status"></span>
|
||||
<span class="node-name">{{ node.name }}</span>
|
||||
<span class="node-id">{{ node.id }}</span>
|
||||
</button>
|
||||
</li>
|
||||
<ul class="tree-list">
|
||||
<StructureTreeNode
|
||||
v-for="node in nodes"
|
||||
:key="node.id"
|
||||
:node="node"
|
||||
:active-id="activeId"
|
||||
:expanded-keys="expandedKeys"
|
||||
@select="selectNode"
|
||||
@toggle="toggleNode"
|
||||
/>
|
||||
</ul>
|
||||
</aside>
|
||||
</template>
|
||||
@@ -53,7 +93,7 @@ header {
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
ul {
|
||||
.tree-list {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
margin: 0;
|
||||
@@ -61,48 +101,4 @@ ul {
|
||||
overflow: auto;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
button {
|
||||
display: grid;
|
||||
width: 100%;
|
||||
grid-template-columns: 8px minmax(0, 1fr);
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
padding: 7px 12px;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background: var(--c-surface-soft);
|
||||
}
|
||||
|
||||
.dot {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 50%;
|
||||
background: var(--c-text-muted);
|
||||
}
|
||||
|
||||
.dot.done {
|
||||
background: var(--c-success);
|
||||
}
|
||||
|
||||
.dot.pending {
|
||||
background: var(--c-warn);
|
||||
}
|
||||
|
||||
.node-name {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.node-id {
|
||||
grid-column: 2;
|
||||
color: var(--c-text-muted);
|
||||
font-size: 11px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
<script setup lang="ts">
|
||||
import { RightOutlined } from "@ant-design/icons-vue";
|
||||
|
||||
export type StructureNode = {
|
||||
id: string;
|
||||
name: string;
|
||||
status: "done" | "review" | "disabled" | "empty";
|
||||
children?: StructureNode[];
|
||||
};
|
||||
|
||||
defineProps<{
|
||||
node: StructureNode;
|
||||
activeId: string;
|
||||
expandedKeys: Record<string, boolean>;
|
||||
level?: number;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
select: [node: StructureNode];
|
||||
toggle: [id: string];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<li>
|
||||
<div
|
||||
class="tree-node"
|
||||
:class="{ active: activeId === node.id }"
|
||||
:style="{ paddingLeft: `${10 + (level ?? 0) * 16}px` }"
|
||||
>
|
||||
<button
|
||||
v-if="node.children?.length"
|
||||
class="toggle-btn"
|
||||
:class="{ expanded: expandedKeys[node.id] }"
|
||||
type="button"
|
||||
:aria-label="expandedKeys[node.id] ? '折叠' : '展开'"
|
||||
@click.stop="emit('toggle', node.id)"
|
||||
>
|
||||
<RightOutlined />
|
||||
</button>
|
||||
<span v-else class="toggle-spacer"></span>
|
||||
<button class="node-main" type="button" @click="emit('select', node)">
|
||||
<span class="dot" :class="node.status"></span>
|
||||
<span class="node-name">{{ node.name }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<ul v-if="node.children?.length" v-show="expandedKeys[node.id]" class="tree-children">
|
||||
<StructureTreeNode
|
||||
v-for="child in node.children"
|
||||
:key="child.id"
|
||||
:node="child"
|
||||
:active-id="activeId"
|
||||
:expanded-keys="expandedKeys"
|
||||
:level="(level ?? 0) + 1"
|
||||
@select="emit('select', $event)"
|
||||
@toggle="emit('toggle', $event)"
|
||||
/>
|
||||
</ul>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
li,
|
||||
ul {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.tree-node {
|
||||
display: grid;
|
||||
grid-template-columns: 18px minmax(0, 1fr);
|
||||
align-items: center;
|
||||
padding: 3px 10px;
|
||||
}
|
||||
|
||||
.tree-node.active {
|
||||
background: var(--c-primary-soft);
|
||||
color: var(--c-primary);
|
||||
}
|
||||
|
||||
.toggle-btn,
|
||||
.node-main {
|
||||
border: 0;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.toggle-btn {
|
||||
display: grid;
|
||||
width: 18px;
|
||||
height: 22px;
|
||||
place-items: center;
|
||||
color: var(--c-text-muted);
|
||||
font-size: 10px;
|
||||
transition: transform 0.15s ease;
|
||||
}
|
||||
|
||||
.toggle-btn.expanded {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.toggle-spacer {
|
||||
width: 18px;
|
||||
}
|
||||
|
||||
.node-main {
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
grid-template-columns: 7px minmax(0, 1fr);
|
||||
gap: 7px;
|
||||
align-items: center;
|
||||
padding: 3px 2px;
|
||||
color: inherit;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.dot {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 50%;
|
||||
background: var(--c-text-muted);
|
||||
}
|
||||
|
||||
.dot.done {
|
||||
background: var(--c-success);
|
||||
}
|
||||
|
||||
.dot.review {
|
||||
background: var(--c-warn);
|
||||
}
|
||||
|
||||
.dot.disabled {
|
||||
background: #d33;
|
||||
}
|
||||
|
||||
.dot.empty {
|
||||
background: var(--c-border);
|
||||
}
|
||||
|
||||
.node-name {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user