feat: add recursive structure tree
This commit is contained in:
@@ -74,10 +74,10 @@
|
||||
| 052 | 显示/隐藏标签 toggle 开关 | 前端 | 0.25d | ✅ 已完成 |
|
||||
| 053 | 缩放控制(70%/100%/150% scale 切换) | 前端 | 0.25d | ✅ 已完成 |
|
||||
| | **结构树** | | | |
|
||||
| 054 | 递归树组件 + 数据绑定 | 前端 | 0.5d | ⏳ 未完成 |
|
||||
| 055 | 展开/折叠交互 + 图标 | 前端 | 0.25d | ⏳ 未完成 |
|
||||
| 056 | 状态圆点(已配置绿/待审核黄/已禁用红/未配置灰) | 前端 | 0.25d | ⏳ 未完成 |
|
||||
| 057 | 点击树节点 → 预览区 scrollIntoView + 闪烁高亮 | 前端 | 0.5d | ⏳ 未完成 |
|
||||
| 054 | 递归树组件 + 数据绑定 | 前端 | 0.5d | ✅ 已完成 |
|
||||
| 055 | 展开/折叠交互 + 图标 | 前端 | 0.25d | ✅ 已完成 |
|
||||
| 056 | 状态圆点(已配置绿/待审核黄/已禁用红/未配置灰) | 前端 | 0.25d | ✅ 已完成 |
|
||||
| 057 | 点击树节点 → 预览区 scrollIntoView + 闪烁高亮 | 前端 | 0.5d | ✅ 已完成 |
|
||||
| | **配置面板** | | | |
|
||||
| 058 | 区域名称 input(默认取 text_preview) | 前端 | 0.25d | ⏳ 未完成 |
|
||||
| 059 | 区域类型 select | 前端 | 0.25d | ⏳ 未完成 |
|
||||
|
||||
@@ -108,3 +108,16 @@
|
||||
7. 增加 70%、100%、150% 缩放控制。
|
||||
8. 执行前端构建和浏览器 DOM/点击选中检查。
|
||||
- **执行结果**: 完成任务 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">
|
||||
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>
|
||||
@@ -21,3 +21,19 @@ textarea,
|
||||
select {
|
||||
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