feat: add recursive structure tree
This commit is contained in:
@@ -74,10 +74,10 @@
|
|||||||
| 052 | 显示/隐藏标签 toggle 开关 | 前端 | 0.25d | ✅ 已完成 |
|
| 052 | 显示/隐藏标签 toggle 开关 | 前端 | 0.25d | ✅ 已完成 |
|
||||||
| 053 | 缩放控制(70%/100%/150% scale 切换) | 前端 | 0.25d | ✅ 已完成 |
|
| 053 | 缩放控制(70%/100%/150% scale 切换) | 前端 | 0.25d | ✅ 已完成 |
|
||||||
| | **结构树** | | | |
|
| | **结构树** | | | |
|
||||||
| 054 | 递归树组件 + 数据绑定 | 前端 | 0.5d | ⏳ 未完成 |
|
| 054 | 递归树组件 + 数据绑定 | 前端 | 0.5d | ✅ 已完成 |
|
||||||
| 055 | 展开/折叠交互 + 图标 | 前端 | 0.25d | ⏳ 未完成 |
|
| 055 | 展开/折叠交互 + 图标 | 前端 | 0.25d | ✅ 已完成 |
|
||||||
| 056 | 状态圆点(已配置绿/待审核黄/已禁用红/未配置灰) | 前端 | 0.25d | ⏳ 未完成 |
|
| 056 | 状态圆点(已配置绿/待审核黄/已禁用红/未配置灰) | 前端 | 0.25d | ✅ 已完成 |
|
||||||
| 057 | 点击树节点 → 预览区 scrollIntoView + 闪烁高亮 | 前端 | 0.5d | ⏳ 未完成 |
|
| 057 | 点击树节点 → 预览区 scrollIntoView + 闪烁高亮 | 前端 | 0.5d | ✅ 已完成 |
|
||||||
| | **配置面板** | | | |
|
| | **配置面板** | | | |
|
||||||
| 058 | 区域名称 input(默认取 text_preview) | 前端 | 0.25d | ⏳ 未完成 |
|
| 058 | 区域名称 input(默认取 text_preview) | 前端 | 0.25d | ⏳ 未完成 |
|
||||||
| 059 | 区域类型 select | 前端 | 0.25d | ⏳ 未完成 |
|
| 059 | 区域类型 select | 前端 | 0.25d | ⏳ 未完成 |
|
||||||
|
|||||||
@@ -108,3 +108,16 @@
|
|||||||
7. 增加 70%、100%、150% 缩放控制。
|
7. 增加 70%、100%、150% 缩放控制。
|
||||||
8. 执行前端构建和浏览器 DOM/点击选中检查。
|
8. 执行前端构建和浏览器 DOM/点击选中检查。
|
||||||
- **执行结果**: 完成任务 047-053,Word 预览区支持渲染、选中、标签、颜色和缩放。
|
- **执行结果**: 完成任务 047-053,Word 预览区支持渲染、选中、标签、颜色和缩放。
|
||||||
|
|
||||||
|
## 会话 ID: 20260701-structure-tree
|
||||||
|
- [2026-07-01 20:41:24]
|
||||||
|
- **执行原因**: 按任务清单继续完成结构树 054-057。
|
||||||
|
- **执行过程**:
|
||||||
|
1. 新增 `StructureTreeNode.vue` 递归树节点组件。
|
||||||
|
2. 将结构树改为层级数据绑定,支持 children 递归渲染。
|
||||||
|
3. 增加展开/折叠按钮和箭头旋转状态。
|
||||||
|
4. 增加已配置、待审核、已禁用、未配置四类状态圆点。
|
||||||
|
5. 点击树节点后更新高亮,并滚动到预览区对应 `data-block-id` 元素。
|
||||||
|
6. 增加全局 `tree-flash` 动画用于预览区闪烁提示。
|
||||||
|
7. 执行前端构建和浏览器 DOM 检查。
|
||||||
|
- **执行结果**: 完成任务 054-057,结构树支持递归展示、状态、折叠和预览定位。
|
||||||
|
|||||||
@@ -1,9 +1,47 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const nodes = [
|
import { reactive, ref } from "vue";
|
||||||
{ id: "block_001", name: "企业经营分析报告", status: "done" },
|
|
||||||
{ id: "block_003", name: "一、经营概况", status: "pending" },
|
import StructureTreeNode, { type StructureNode } from "./StructureTreeNode.vue";
|
||||||
{ id: "block_005", name: "经营指标表", status: "empty" },
|
|
||||||
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -12,14 +50,16 @@ const nodes = [
|
|||||||
<span>文档结构</span>
|
<span>文档结构</span>
|
||||||
<span class="count">{{ nodes.length }}</span>
|
<span class="count">{{ nodes.length }}</span>
|
||||||
</header>
|
</header>
|
||||||
<ul>
|
<ul class="tree-list">
|
||||||
<li v-for="node in nodes" :key="node.id">
|
<StructureTreeNode
|
||||||
<button type="button">
|
v-for="node in nodes"
|
||||||
<span class="dot" :class="node.status"></span>
|
:key="node.id"
|
||||||
<span class="node-name">{{ node.name }}</span>
|
:node="node"
|
||||||
<span class="node-id">{{ node.id }}</span>
|
:active-id="activeId"
|
||||||
</button>
|
:expanded-keys="expandedKeys"
|
||||||
</li>
|
@select="selectNode"
|
||||||
|
@toggle="toggleNode"
|
||||||
|
/>
|
||||||
</ul>
|
</ul>
|
||||||
</aside>
|
</aside>
|
||||||
</template>
|
</template>
|
||||||
@@ -53,7 +93,7 @@ header {
|
|||||||
line-height: 18px;
|
line-height: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
ul {
|
.tree-list {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@@ -61,48 +101,4 @@ ul {
|
|||||||
overflow: auto;
|
overflow: auto;
|
||||||
list-style: none;
|
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>
|
</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>
|
||||||
@@ -21,3 +21,19 @@ textarea,
|
|||||||
select {
|
select {
|
||||||
font: inherit;
|
font: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.doc-block.tree-flash {
|
||||||
|
animation: tree-flash 1.6s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes tree-flash {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
outline-color: var(--c-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
background: #fff7d6;
|
||||||
|
outline-color: var(--c-warn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user