目录
介绍
准备
目标
规定
思路
参考解法
介绍
在很多教育网站的平台上,课程的章节目录会使用树型组件呈现,为了方便调整菜单,前端工程师会为其赋予拖拽功能。本题需要在已提供的基础项目中,完成可拖拽树型组件的功能。
准备
├── 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
是完成的效果图。
注意:打开环境后发现缺少项目代码,请复制下述命令至命令行进行下载。
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 图片)。
具体需求如下:
1.补全 js/index.js
文件中 ajax
函数,功能为根据请求方式 method
不同,拿到树型组件的数据并返回。具体如下:
-
当
method === "get"
时,判断localStorage
中是否存在key
为data
的数据,若存在,则从localStorage
中直接获取后处理为 json 格式并返回;若不存在则从./js/data.json
(必须使用该路径请求,否则可能会请求不到数据)中使用 ajax 获取并返回。 -
当
method === "post"
时,将通过参数data
传递过来的数据转化为 json 格式的字符串,并保存到localStorage
中,key
命名为data
。
最终返回的数据格式如下:
[
{
"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"
},
...
]
}
]
}
]
2.补全 js/index.js
文件中的 treeMenusRender
函数,使用所传参数 data
生成指定 DOM 结构的模板字符串(完整的模板字符串的 HTML
样例结构可以在 index.html
中查看),并在包含 .tree-node
的元素节点上加上指定属性如下:
属性名 | 属性值 | 描述 |
---|---|---|
data-grade | ${grade} | 表示菜单的层级,整数,由 treeMenusRender 函数的 grade 参数值计算获得,章节是 1,小节是 2,实验文档是 3。 |
data-index | ${id} | 表示菜单的唯一 id,使用每层菜单数据的 id 字段值。 |
3.补全 js/index.js
文件中的 treeDataRefresh
函数,功能为:根据参数列表 { dragGrade, dragElementId }, { dropGrade, dropElementId }
重新生成拖拽后的树型组件数据 treeData
(treeData
为全局变量,直接访问并根据参数处理后重新赋值即可)。
方便规则描述,现将 data.json
中的数据扁平化处理,得到的数据顺序如下:
[
{ 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
)的节点后,作为其后第一个邻近的兄弟节点。最终生成的数据顺序如下:
[
{ 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
)的节点下,并作为其第一个子节点。最终生成的数据顺序如下:
[
{ 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 名、图片名等,以免造成无法判题通过。先自己做一下吧:传送门
思路
这道题目时第14届蓝桥杯省赛真题的最后一题。难度还是挺大的。第一问主要是进行数据的读取,使用到了axios来进行数据的请求,同时使用到了LocalStorage的数据读取已经数据设置。第二个功能主要是进行数据的模板渲染。最后主要是考察对DOM的操作。
参考解法
第一个功能相对来说比较简单,主要考察axios发送请求以及LocalStorage本地数据的读取以及存储的知识点。获取数据主要是使用到localStorage中的getItem方法,存储数据主要是用到了了其setItem的方法。进行本地数据的存储可以减少不必要的ajax请求,因此可以进行性能的优化。同时还需要知道JSON.parse以及JSON.stringify的使用方法,第一个是用于将 JSON 字符串解析成对应的 JavaScript 对象,而后者是则是相反。
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"){
const localData = localStorage.getItem("data")
if(localData){
result = JSON.parse(localData)
}else{
const res = await axios.get(url)
result = res.data.data
}
}
if(method === "post"){
localStorage.setItem("data",JSON.stringify(data))
}
return result;
}
第二个功能的实现使用到了递归的方式处理了树形数据的多层级结构,确保了生成的 HTML 结构符合树型组件的预期。
function treeMenusRender(data, grade = 0) {
let treeTemplate = "";
// TODO:根据传入的 treeData 的数据生成树型组件的模板字符串
function generateTree(nodes, depth = 0) {
let template = '';
nodes.forEach(node => {
template += `<div class="tree-node" data-index="${node.id}" data-grade="${depth + 1}">
<div class="tree-node-content" style="margin-left: ${depth * 15}px">
<div class="tree-node-content-left">
<img src="./images/dragger.svg" alt="" class="point-svg" />
<span class="tree-node-tag">${node.tag || ''}</span>
<span class="tree-node-label">${node.label}</span>
<img class="config-svg" src="./images/config.svg" alt="" />
</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>`;
if (node.children && node.children.length > 0) {
template += `<div class="tree-node-children">${generateTree(node.children, depth + 1)}</div>`;
}
template += `</div>`;
});
return template;
}
// 生成树型组件的模板字符串
treeTemplate = generateTree(data, grade);
return treeTemplate;
}
treeMenusRender 函数:这个函数接收两个参数:data 是树形数据的数组,grade 是可选的用来表示树的层级。在函数内部定义了 generateTree 函数,用来递归生成树的结构。generateTree 函数接收一个节点数组和深度参数,然后对每个节点进行处理。
generateTree 函数:这个函数是用来构建树结构的核心部分。它使用了递归的方式处理树形结构的不同层级。对于传入的节点数组,它遍历每个节点,并根据节点的数据生成相应的 HTML 结构。对于每个节点,它创建一个包含节点信息的 <div class="tree-node"> 元素。使用 data-index 和 data-grade 属性存储节点的索引和等级信息。对于每个节点的子节点,递归调用 generateTree 函数,直到遍历完所有层级的节点。
生成的 HTML 结构包含了节点的内容,例如标签、标签属性、图标等。每个节点都包含一个 <div class="tree-node">,里面嵌套着节点的内容和可能存在的子节点。生成的 HTML 结构存储在 treeTemplate 变量中,最终作为函数的返回值。
最后一个功能的实现,先找到需要移动的数据并保存到变量当中,将需要移动的数据删除,寻找需要移动要的位置,最后插入数据。
function treeDataRefresh(
{ dragGrade, dragElementId },
{ dropGrade, dropElementId }
) {
if (dragElementId === dropElementId) return;
// TODO:根据 `dragElementId, dropElementId` 重新生成拖拽完成后的树型组件的数据 `treeData`
if( dragGrade == dropGrade || dragGrade - dropGrade == 1){
let drapItem = {}
for(let i = 0;i < treeData.length;i++){
if(treeData[i].id === Number(dragElementId)){
drapItem = treeData[i]
treeData.splice(i,1)
break
}
for(let j = 0;j < treeData[i].children.length;j++){
if(treeData[i].children[j].id === Number(dragElementId)){
drapItem = treeData[i].children[j]
treeData[i].children.splice(j,1)
break
}
for(let k = 0;k < treeData[i].children[j].children.length;k++){
if(treeData[i].children[j].children[k].id === Number(dragElementId)){
drapItem = treeData[i].children[j].children[k]
treeData[i].children[j].children.splice(k,1)
break
}
}
}
}
if(dragGrade == dropGrade){
for(let i = 0;i < treeData.length;i++){
if(treeData[i].id === Number(dropElementId)){
treeData.splice(i+1,0,drapItem)
return
}
for(let j = 0;j < treeData[i].children.length;j++){
if(treeData[i].children[j].id === Number(dropElementId)){
treeData[i].children.splice(j+1,0,drapItem)
return
}
for(let k = 0;k < treeData[i].children[j].children.length;k++){
if(treeData[i].children[j].children[k].id === Number(dropElementId)){
treeData[i].children[j].children.splice(k+1,0,drapItem)
return
}
}
}
}
}else if(dragGrade - dropGrade == 1){
for(let i = 0;i < treeData.length;i++){
if(treeData[i].id === Number(dropElementId)){
console.log( treeData[i])
treeData[i].children.unshift(drapItem)
return
}
for(let j = 0;j < treeData[i].children.length;j++){
if(treeData[i].children[j].id === Number(dropElementId)){
treeData[i].children[j].children.unshift(drapItem)
return
}
}
}
}
}
}
首先,我们找到拖拽的节点(根据 dragElementId
),然后将其从原始位置中移除,并将移除的节点保存在 drapItem
中。它首先遍历整个 treeData
数组,查找具有匹配 id
的节点。第一个循环:遍历主节点(第一级)。第二个循环:遍历每个主节点下的子节点(第二级)。第三个循环:遍历每个子节点下的子节点(第三级)。
如果找到匹配的 id
,则会将该节点保存在 drapItem
中,并从数组中移除。这个过程确保了我们可以正确找到拖拽的节点,并将其从原始位置中取出,以备后续的插入或添加到新位置。
拖拽到同一级别时,查找被拖拽的节点: 首先在树形数据 treeData 中查找被拖拽的节点(根据 dragElementId)。移除被拖拽的节点: 找到后,从原来的位置中移除该节点,并保存在 drapItem 中。插入到目标节点后: 接着,在 treeData 中找到放置节点的位置(根据 dropElementId),然后将被拖拽的节点 drapItem 插入到放置节点后面。
拖拽到下一级时,查找被拖拽的节点: 同样地,在树形数据 treeData 中查找被拖拽的节点(根据 dragElementId)。移除被拖拽的节点: 找到后,从原来的位置中移除该节点,并保存在 drapItem 中。添加为子节点: 接着,在 treeData 中找到放置节点的位置(根据 dropElementId),将被拖拽的节点 drapItem 添加为放置节点的子节点的第一个节点。
(第三部分的代码可以考虑更加简化的方法,因此试了一直弄不出来,累了,就直接暴力解决了。优化考虑就交给各位了!)