每日一题:组课神器

介绍

在很多教育网站的平台上,课程的章节目录会使用树型组件呈现,为了方便调整菜单,前端工程师会为其赋予拖拽功能。本题需要在已提供的基础项目中,完成可拖拽树型组件的功能。

准备

1
2
3
4
5
6
7
8
├── effect.gif
├── css
│ └── index.css
├── index.html
└── js
├── data.json
├── axios.min.js
└── index.js

其中:

  • index.html 是主页面。
  • js/index.js 是待完善的 js 文件。
  • js/data.json 是存放数据的 json 文件。
  • js/axios.min.js 是 axios 文件。
  • css/index.css 是 css 样式文件。
  • effect.gif 是完成的效果图。

注意:打开环境后发现缺少项目代码,请复制下述命令至命令行进行下载。

1
2
3
cd /home/project
wget -q https://labfile.oss.aliyuncs.com/courses/18213/test8.zip
unzip test8.zip && rm test8.zip

在浏览器中预览 index.html 页面,显示如下所示:

初始效果

目标

请在 js/index.js 文件中补全代码。

最终效果可参考文件夹下面的 gif 图,图片名称为 effect.gif (提示:可以通过 VS Code 或者浏览器预览 gif 图片)。

img

具体需求如下:

  1. 补全 js/index.js 文件中 ajax 函数,功能为根据请求方式 method 不同,拿到树型组件的数据并返回。具体如下:
  • method === "get" 时,判断 localStorage 中是否存在 keydata 的数据,若存在,则从 localStorage 中直接获取后处理为 json 格式并返回;若不存在则从 ./js/data.json(必须使用该路径请求,否则可能会请求不到数据)中使用 ajax 获取并返回。
  • method === "post" 时,将通过参数 data 传递过来的数据转化为 json 格式的字符串,并保存到 localStorage 中,key 命名为 data

最终返回的数据格式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
[
{
"id": 1001,
"label": "第一章 Vue 初体验",
"children": [ ... ]
},
{
"id": 1006,
"label": "第二章 Vue 核心概念",
"children": [
{
"id": 1007,
"label": "2.1 概念理解",
"children": [
{
"id": 1008,
"label": "聊一聊虚拟 DOM",
"tag":"文档 1"
},
...
]
},
{
"id": 1012,
"label": "2.2 Vue 基础入门",
"children": [
{
"id": 1013,
"label": "Vue 的基本语法",
"tag":"实验 6"
},
...
]
}
]
}
]
  1. 补全 js/index.js 文件中的 treeMenusRender 函数,使用所传参数 data 生成指定 DOM 结构的模板字符串(完整的模板字符串的 HTML 样例结构可以在 index.html 中查看),并在包含 .tree-node 的元素节点上加上指定属性如下:
属性名 属性值 描述
data-grade ${grade} 表示菜单的层级,整数,由 treeMenusRender 函数的 grade 参数值计算获得,章节是 1,小节是 2,实验文档是 3。
data-index ${id} 表示菜单的唯一 id,使用每层菜单数据的 id 字段值。
  1. 补全 js/index.js 文件中的 treeDataRefresh 函数,功能为:根据参数列表 { dragGrade, dragElementId }, { dropGrade, dropElementId } 重新生成拖拽后的树型组件数据 treeDatatreeData 为全局变量,直接访问并根据参数处理后重新赋值即可)。

方便规则描述,现将 data.json 中的数据扁平化处理,得到的数据顺序如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[
{ grade: "1", label: "第一章 Vue 初体验", id: "1001" },
{ grade: "2", label: "1.1 Vue 简介", id: "1002" },
{ grade: "3", label: "Vue 的发展历程", id: "1003" },
{ grade: "3", label: "Vue 特点", id: "1004" },
{ grade: "3", label: "一分钟上手 Vue", id: "1005" },
{ grade: "1", label: "第二章 Vue 核心概念", id: "1006" },
{ grade: "2", label: "2.1 概念理解", id: "1007" },
{ grade: "3", label: "聊一聊虚拟 DOM", id: "1008" },
{ grade: "3", label: "感受一下虚拟 DOM", id: "1009" },
{ grade: "3", label: "聊一聊 MVVM 设计模式", id: "1010" },
{ grade: "3", label: "Vue 中的 MVVM 设计模式", id: "1011" }, // 即将被拖拽的元素节点
{ grade: "2", label: "2.2 Vue 基础入门", id: "1012" },
{ grade: "3", label: "Vue 的基本语法", id: "1013" },
{ grade: "3", label: "第一步,创建 Vue 应用实例", id: "1014" },
]

拖拽前后的规则说明如下:

  • 情况一:若拖拽的节点和放置的节点为同级,即 treeDataRefresh 函数参数列表中 dragGrade == dropGrade,则将 id == dragElementId(例如:1011)的节点移动到 id==dropElementId(例如:1008)的节点后,作为其后第一个邻近的兄弟节点。最终生成的数据顺序如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[
{ grade: "1", label: "第一章 Vue 初体验", id: "1001" },
{ grade: "2", label: "1.1 Vue 简介", id: "1002" },
{ grade: "3", label: "Vue 的发展历程", id: "1003" },
{ grade: "3", label: "Vue 特点", id: "1004" },
{ grade: "3", label: "一分钟上手 Vue", id: "1005" },
{ grade: "1", label: "第二章 Vue 核心概念", id: "1006" },
{ grade: "2", label: "2.1 概念理解", id: "1007" },
{ grade: "3", label: "聊一聊虚拟 DOM", id: "1008" },
// 在目标元素节点下方插入
{ grade: "3", label: "Vue 中的 MVVM 设计模式", id: "1011" },
{ grade: "3", label: "感受一下虚拟 DOM", id: "1009" },
{ grade: "3", label: "聊一聊 MVVM 设计模式", id: "1010" },
// 移除被拖拽的元素节点
{ grade: "2", label: "2.2 Vue 基础入门", id: "1012" },
{ grade: "3", label: "Vue 的基本语法", id: "1013" },
{ grade: "3", label: "第一步,创建 Vue 应用实例", id: "1014" }
]
  • 情况二:若拖拽的节点和放置的节点为上下级,即 treeDataRefresh 函数参数列表中 dragGrade - dropGrade == 1,则将 id == dragElementId(例如:1011)的节点移动到 id==dropElementId(例如:1002)的节点下,并作为其第一个子节点。最终生成的数据顺序如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[
{ grade: "1", label: "第一章 Vue 初体验", id: "1001" },
{ grade: "2", label: "1.1 Vue 简介", id: "1002" },
// 在目标元素节点下方插入
{ grade: "3", label: "Vue 中的 MVVM 设计模式", id: "1011" },
{ grade: "3", label: "Vue 的发展历程", id: "1003" },
{ grade: "3", label: "Vue 特点", id: "1004" },
{ grade: "3", label: "一分钟上手 Vue", id: "1005" },
{ grade: "1", label: "第二章 Vue 核心概念", id: "1006" },
{ grade: "2", label: "2.1 概念理解", id: "1007" },
{ grade: "3", label: "聊一聊虚拟 DOM", id: "1008" },
{ grade: "3", label: "感受一下虚拟 DOM", id: "1009" },
{ grade: "3", label: "聊一聊 MVVM 设计模式", id: "1010" },
// 移除被拖拽的元素节点
{ grade: "2", label: "2.2 Vue 基础入门", id: "1012" },
{ grade: "3", label: "Vue 的基本语法", id: "1013" },
{ grade: "3", label: "第一步,创建 Vue 应用实例", id: "1014" }
];

规定

  • 请勿修改 js/index.js 文件外的任何内容。
  • 请严格按照考试步骤操作,切勿修改考试默认提供项目中的文件名称、文件夹路径、class 名、id 名、图片名等,以免造成无法判题通过。

判分标准

  • 完成目标 1,得 5 分。
  • 完成目标 2,得 10 分。
  • 完成目标 3,得 10 分。

总通过次数: 80 | 总提交次数: 227 | 通过率: 35.2%

难度: 困难 标签: 蓝桥杯, 2023, 省赛, Web 前端, JavaScript, 异步

tijie

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
/**
* @description 模拟 ajax 请求,拿到树型组件的数据 treeData
* @param {string} url 请求地址
* @param {string} method 请求方式,必填,默认为 get
* @param {string} data 请求体数据,可选参数
* @return {Array}
* */
async function ajax({ url, method = "get", data }) {
let result;
// TODO:根据请求方式 method 不同,拿到树型组件的数据
// 当method === "get" 时,localStorage 存在数据从 localStorage 中获取,不存在则从 /js/data.json 中获取
// 当method === "post" 时,将数据保存到localStorage 中,key 命名为 data
if (method === "get") {
let result = localStorage.getItem("data");
if (result === undefined) {
console.log(result);
} else {
result = (await axios({ url, method })).data.data;
console.log(result);
}
} else if (method === "post") {
localStorage.setItem("data", data);
}

return result;
}

/**
* @description 找到元素节点的父亲元素中类选择器中含有 tree-node 的元素节点
* @param {Element} node 传入的元素节点
* @return {Element} 得到的元素节点
*/
const getTreeNode = (node) => {
let curElement = node;
while (!curElement.classList.contains("tree-node")) {
if (curElement.classList.contains("tree")) {
break;
}
curElement = curElement.parentNode;
}
return curElement;
};

/**
* @description 根据 dragElementId, dropElementId 重新生成拖拽完成后的树型组件的数据 treeData
* @param {number} dragGrade 被拖拽的元素的等级,值为 dragElement data-grade属性对应的值
* @param {number} dragElementId 被拖拽的元素的id,值为当前数据对应在 treeData 中的id
* @param {number} dropGrade 放入的目标元素的等级,值为 dropElement data-grade属性对应的值
* @param {number} dropElementId 放入的目标元素的id,值为当前数据对应在 treeData 中的id
*/
function treeDataRefresh(
{ dragGrade, dragElementId },
{ dropGrade, dropElementId }
) {
if (dragElementId === dropElementId) return;
// TODO:根据 `dragElementId, dropElementId` 重新生成拖拽完成后的树型组件的数据 `treeData`
}

/**
* @description 根据 treeData 的数据生成树型组件的模板字符串,在包含 .tree-node 的元素节点需要加上 data-grade=${index}表示菜单的层级 data-index="${id}" 表示菜单的唯一id
* @param {array} data treeData 数据
* @param {number} grade 菜单的层级
* @return 树型组件的模板字符串
*
* */
function treeMenusRender(data, grade = 0) {
let treeTemplate = "";
// TODO:根据传入的 treeData 的数据生成树型组件的模板字符串
console.log(data);
grade = 1;
for (let first of data) {
// 遍历章
// 结构是章container > 章标题 ....
treeTemplate += `
<!-- 每一章的 DOM 结构 start -->
<div class="tree-node" data-index="${first.id}" data-grade="${grade}">
<!-- 每一章标题的 DOM 结构 start -->
<div class="tree-node-content" style="margin-left: 0px">
<div class="tree-node-content-left">
<img
src="./images/dragger.svg"
alt=""
class="point-svg"
/>
<span class="tree-node-label">${first.label}</span>
<img
class="config-svg"
src="./images/config.svg"
alt=""
/>
</div>
</div>
<!-- 每一章标题的 DOM 结构 end -->
`;

for(let second of first) {
treeTemplate += `
<!-- 每一章下所有小节的 DOM 结构 start -->
<div class="tree-node-children">
<!-- 第一个小节的 DOM 结构 start -->
<div class="tree-node" data-index="${second.id}" data-grade="2">
<!-- 第一个小节标题的 DOM 结构 start -->
<div class="tree-node-content" style="margin-left: 15px">
<div class="tree-node-content-left">
<img
src="./images/dragger.svg"
alt=""
class="point-svg"
/>
<span class="tree-node-label">${second.label}</span>
<img
class="config-svg"
src="./images/config.svg"
alt=""
/>
</div>
</div>
<!-- 第一个小节标题的 DOM 结构 end -->
</div>
<!-- 第一个小节的 DOM 结构 end -->
`
for(let third of second) {
treeTemplate += `
<!-- 每小节下所有文档/实验的 DOM 结构 start -->
<div class="tree-node-children">
<!-- 第一个文档/实验的 DOM 结构 start -->
<div class="tree-node" data-index="1003" data-grade="3">
<div class="tree-node-content" style="margin-left: 30px">
<div class="tree-node-content-left">
<img
src="./images/dragger.svg"
alt=""
class="point-svg"
/>
<span class="tree-node-tag">实验1</span>
<span class="tree-node-label">Vue 的发展历程</span>
</div>
<div class="tree-node-content-right">
<div class="students-count">
<span class="number"> 0人完成</span>
<span class="line">|</span>
<span class="number">0人提交报告</span>
</div>
<div class="config">
<img
class="config-svg"
src="./images/config.svg"
alt=""
/>
<button class="doc-link">编辑文档</button>
</div>
</div>
</div>
</div>
<!-- 第一个文档/实验的 DOM 结构 end -->
<!-- 第二个文档/实验的 DOM 结构 start -->
<div class="tree-node" data-index="1003" data-grade="3">
<div class="tree-node-content" style="margin-left: 30px">
<div class="tree-node-content-left">
<img
src="./images/dragger.svg"
alt=""
class="point-svg"
/>
<span class="tree-node-tag">实验1</span>
<span class="tree-node-label">Vue 的发展历程</span>
</div>
<div class="tree-node-content-right">
<div class="students-count">
<span class="number"> 0人完成</span>
<span class="line">|</span>
<span class="number">0人提交报告</span>
</div>
<div class="config">
<img
class="config-svg"
src="./images/config.svg"
alt=""
/>
<button class="doc-link">编辑文档</button>
</div>
</div>
</div>
</div>
<!-- 第一个文档/实验的 DOM 结构 end -->
</div>
<!-- 每小节下所有文档/实验的 DOM 结构 end -->`
}
}

treeTemplate += `
</div>
<!-- 每一章下所有小节的 DOM 结构 end -->`

treeTemplate += `
</div>
<!-- 每一章的 DOM 结构 end -->`;
}
return treeTemplate;
}

let treeData; // 树型组件的数据 treeData

// 拖拽到目标元素放下后执行的函数
const dropHandler = (dragElement, dropElement) => {
let dragElementId = dragElement.dataset.index;
let dragGrade = dragElement.dataset.grade;
if (dropElement) {
let dropElementId = dropElement.dataset.index;
let dropGrade = dropElement.dataset.grade;

treeDataRefresh({ dragGrade, dragElementId }, { dropGrade, dropElementId });
document.querySelector(".tree").innerHTML = treeMenusRender(treeData);
document.querySelector("#test").innerText = treeData
? JSON.stringify(treeData)
: "";
ajax({ url: "./js/data.json", method: "post", data: treeData });
}
};
// 初始化
ajax({ url: "./js/data.json" }).then((res) => {
treeData = res;
document.querySelector("#test").innerText = treeData
? JSON.stringify(treeData)
: "";
let treeEle = document.querySelector(".tree");
treeEle.dataset.grade = 0;
let treeTemplate = treeMenusRender(treeData);
treeTemplate && (treeEle.innerHTML = treeTemplate);
const mDrag = new MDrag(".tree-node", dropHandler);
// 事件委托,按下小图标记录得到被拖拽的元素,该元素 class 包含 .tree-node
document.querySelector(".tree").addEventListener("mousedown", (e) => {
e.preventDefault();
if (
e.target.nodeName.toLowerCase() === "img" &&
e.target.classList.contains("point-svg")
) {
let dragElement = getTreeNode(e.target);
// MDrag类的drag方法实现拖拽效果
mDrag.drag(e, dragElement);
}
});
});

/**
* @description 实现拖拽功能的类,该类的功能为模拟 HTML5 drag 的功能
* 鼠标按下后,监听 document 的 mousemove 和 mouseup 事件
* 当开始拖拽一个元素后会在 body 内插入对应的克隆元素,并随着鼠标的移动而移动
* 鼠标抬起后,移除克隆元素和 mousemove 事件,如果到达目标触发传入的 dropHandler 方法
*/
class MDrag {
constructor(dropElementSelector, dropHandler) {
// 目标元素的选择器
this.dropElementSelector = dropElementSelector;
// 拖拽到目标元素放下后执行的函数
this.dropHandler = dropHandler;

// 保存所有的目标元素
this.dropBoundingClientRectArr = [];
// 被拖拽的元素
this._dragElement = null;
// 拖拽中移动的元素
this._dragElementClone = null;
// 目标元素
this._dropElement = null;
// 拖拽移动事件
this._dragMoveBind = null;
// 拖拽鼠标抬起事件
this._dragUpBind = null;

this.init();
}
init() {
const dropElements = document.querySelectorAll(this.dropElementSelector);
this.dropBoundingClientRectArr = Array.from(dropElements).map((el) => {
return { boundingClientRect: el.getBoundingClientRect(), el };
});
}
dragMove(e) {
const { pageX, pageY } = e;
this._dragElementClone.style.left = `${e.pageX}px`;
this._dragElementClone.style.top = `${e.pageY}px`;
this.setMouseOverElementStyle(pageX, pageY);
}
dragend(e) {
// 移动到目标元素后mouseup事件触发,删除 this._dragElementClone 元素和解除mousemove/mouseup事件
const { pageX, pageY } = e;
document.removeEventListener("mousemove", this._dragMoveBind);
document.removeEventListener("mouseup", this._dragUpBind);
if (
Array.from(document.body.children).indexOf(this._dragElementClone) != -1
) {
document.body.removeChild(this._dragElementClone);
}
this._dropElement = this.getActualDropElement(pageX, pageY);
this.drop();
}
drag(e, dragElement) {
this._dragElement = dragElement;
this._dragElementClone = dragElement.cloneNode(true);
this._dragElementClone.style.position = "absolute";
this._dragElementClone.style.left = `${e.pageX - 20}px`;
this._dragElementClone.style.top = `${e.pageY - 20}px`;
this._dragElementClone.style.opacity = 0.5;
this._dragElementClone.style.width = "800px";
document.body.appendChild(this._dragElementClone);
// 绑定mousemove和mouseup事件
this._dragMoveBind = this.dragMove.bind(this);
this._dragUpBind = this.dragend.bind(this);
document.addEventListener("mousemove", this._dragMoveBind);
document.addEventListener("mouseup", this._dragUpBind);
return this;
}
getActualDropElement(pageX, pageY) {
const dropAttributeArr = this.dropBoundingClientRectArr.filter(
(obj) =>
pageY >= obj.boundingClientRect.top &&
pageY <= obj.boundingClientRect.top + obj.boundingClientRect.height
);
if (dropAttributeArr.length == 1) {
return dropAttributeArr[0].el;
} else if (dropAttributeArr.length > 1) {
let temp = dropAttributeArr.reduce((prev, next) => {
if (
Math.abs(pageY - prev.boundingClientRect.top) <=
Math.abs(pageY - next.boundingClientRect.top)
) {
return prev;
} else {
return next;
}
});
return temp.el;
} else {
return null;
}
}
setMouseOverElementStyle(pageX, pageY) {
let mousemoveEle = this.getActualDropElement(pageX, pageY);
if (mousemoveEle) {
this.dropBoundingClientRectArr.forEach((obj) => {
obj.el.classList.contains("mouseover-active") &&
obj.el.classList.remove("mouseover-active");
});
mousemoveEle.classList.add("mouseover-active");
}
}
drop() {
this.dropHandler && this.dropHandler(this._dragElement, this._dropElement);
this.init();
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
function treeMenusRender(data, grade = 0) {
let treeTemplate = "";
// TODO:根据传入的 treeData 的数据生成树型组件的模板字符串
console.log(data);
grade = 1;
for (let first of data) {
// 遍历章
// 结构是章container > 章标题 ....
treeTemplate += `
<!-- 每一章的 DOM 结构 start -->
<div class="tree-node" data-index="${first.id}" data-grade="${grade}">
<!-- 每一章标题的 DOM 结构 start -->
<div class="tree-node-content" style="margin-left: 0px">
<div class="tree-node-content-left">
<img
src="./images/dragger.svg"
alt=""
class="point-svg"
/>
<span class="tree-node-label">${first.label}</span>
<img
class="config-svg"
src="./images/config.svg"
alt=""
/>
</div>
</div>
<!-- 每一章标题的 DOM 结构 end -->
<!-- 每一章下所有小节的 DOM 结构 start -->
<div class="tree-node-children">
`;

for (let second of first.children) {
treeTemplate += `

<!-- 第一个小节的 DOM 结构 start -->
<div class="tree-node" data-index="${second.id}" data-grade="2">
<!-- 第一个小节标题的 DOM 结构 start -->
<div class="tree-node-content" style="margin-left: 15px">
<div class="tree-node-content-left">
<img
src="./images/dragger.svg"
alt=""
class="point-svg"
/>
<span class="tree-node-label">${second.label}</span>
<img
class="config-svg"
src="./images/config.svg"
alt=""
/>
</div>
</div>
<!-- 第一个小节标题的 DOM 结构 end -->

<!-- 每小节下所有文档/实验的 DOM 结构 start -->
<div class="tree-node-children">
`;
for (let third of second.children) {
treeTemplate += `

<!-- 第一个文档/实验的 DOM 结构 start -->
<div class="tree-node" data-index="${third.id}" data-grade="3">
<div class="tree-node-content" style="margin-left: 30px">
<div class="tree-node-content-left">
<img
src="./images/dragger.svg"
alt=""
class="point-svg"
/>
<span class="tree-node-tag">${third.tag}</span>
<span class="tree-node-label">${third.label}</span>
</div>
<div class="tree-node-content-right">
<div class="students-count">
<span class="number"> 0人完成</span>
<span class="line">|</span>
<span class="number">0人提交报告</span>
</div>
<div class="config">
<img
class="config-svg"
src="./images/config.svg"
alt=""
/>
<button class="doc-link">编辑文档</button>
</div>
</div>
</div>
</div>
<!-- 第一个文档/实验的 DOM 结构 end -->
`;
}
treeTemplate += `
</div>
<!-- 第一个小节的 DOM 结构 end -->
</div>
<!-- 每小节下所有文档/实验的 DOM 结构 end -->`
}

treeTemplate += `
</div>
<!-- 每一章下所有小节的 DOM 结构 end -->`;

treeTemplate += `
</div>
<!-- 每一章的 DOM 结构 end -->`;
}
return treeTemplate;
}