1. useRouter():拿到路由器,可以查看路由以及使用路由器的方法们
2. <el-menu-item v-for="item inrouter.options.routes[0].children" :index="item.path">
- router.options.routes[0].children 这个是路由表里的第一个路由的孩子路由们
- :index="item.path" (index是el-menu-item 组件自带的属性,配合v-for,用来标记 “当前哪个路由被选中”)
- index:el-plus组件中SubMenu的属性,充当二级菜单项的身份证,起到选择、标记的作用,可作为参数传递给组件的点击事件等。
3.
<el-menu-item v-for="item in router.options.routes[0].children" :key="item.path" :index="item.path">
<el-icon><icon-menu /></el-icon>
//这样写就是被写死了,采用 <component :is="item.meta.icon" />实现动态变化
<span>Navigator Two</span>
</el-menu-item>
4.
<el-menu-item@click="selectMenu"v-for="item in router.options.routes[0].children" :index="item.path">
//子菜单(<el-menu-item>)点击时,会把
$event作为默认参数传给selectMenu
//这个$event里包含了当前点击菜单项的完整信息,index就在其中const selectMenu = (key)=>{
console.log(key)
};
5. 利用组件切换路由的两种方式
- <el-menu :router="true" <!-- 🔥 这一句就是开启“点击菜单自动跳转路由” --> >
- 通过给菜单绑定点击事件
........
</div>
<el-menu-item @click="selectMenu" v-for="item in router.options.routes[0].children" :key="item.path" :index="item.path">//这里的key是给VUE看的身份证,有了它,删除更新结点不会错乱
//这里index相当于表单的学号,是给el-menu看的,有了它可以标记选中的el-menu-item
<el-icon><component :is="item.meta.icon" /></el-icon>
<span>{{ item.meta.title }}</span>
</el-menu-item>
</el-menu>
</el-aside>
</template><script setup>
import { useRouter } from 'vue-router';
const iconUrl = new URL('@/assets/images/机器人.png',import.meta.url).href
const router = useRouter();//使用点击事件实现菜单的路由跳转
//key:
el-menu-item的@click事件,默认会自带一个参数,就是当前菜单的事件信息对象。
const selectMenu = (key)=>{
console.log(key);
const currentRoute=router.options.routes[0];
router.push(`${currentRoute.path}/${key.index}`);
};</script>
6. 在组件TableSearch(子组件)中完善输入框
父组件(knowledge)传递搜索框数据给子组件:搜索框类型(input/select)、prop、placeholder、label
子组件接收后渲染搜索框 通过:model和v-model实现表单的双向绑定
7. <el-col v-bind="item.col">搭配
const formItemAttr = computed(() =>{
const {formItem} = props
formItem.forEach(item =>{
item.col = { xs:24, sm:12, md:8, lg:6 , xl:6 }
})
return formItem;
})
如何理解v-bind : 这里v-bind就把变量item.col的实际内容搬到了这个标签内,等价于:
<el-col xs:24, sm:12, md:8, lg:6 , xl:6>
8. form标签上的ref是用来获取组件实例,调用组件的方法的(检验等)
9.定义响应式数组或对象的时候是常使用reactive还是ref?为什么?这两种定义都是深层响应式数据吗
在 Vue 3 中定义响应式的数组或对象时,推荐优先使用 ref(尽管 reactive 也是一种选择,且专门用于对象等复杂类型)。
同时回答你的第二个问题:是的,ref 和 reactive 默认情况下都是深层响应式(Deep Reactive)的数据。也就是说,无论你嵌套了多少层对象或数组,修改最深层的属性依然会触发视图更新。
下面为你详细解释为什么推荐使用 ref,以及它们的具体区别:
1. 为什么两者都是深层响应式?
reactive:底层利用 ES6 的 Proxy 对整个对象进行了深层的代理拦截。这使得任何层级的属性读取或修改都能被追踪。
ref:当我们把一个对象或数组传给 ref 时(例如 ref({ a: 1 })),ref 底层其实会自动调用 reactive()来处理这个对象。所以它的深层响应式能力本质上是 reactive 赋予的。
2. 既然都能用,为什么更推荐 ref?
官方文档在最新的指南中更倾向于建议开发者统一使用 ref,主要原因包括:
① 解决“重新赋值”导致响应式丢失的问题(最痛点的区别)
在实际开发中,我们经常需要把后端请求返回的全新数组或对象直接赋值给变量。
如果是reactive,直接赋值整个对象/数组会彻底丢失响应式,因为你改变了它的内存指针,原有的 Proxy 代理被替换掉了:
codeJavaScript
let list = reactive([]); // ❌ 错误做法:响应式会丢失,视图不会更新 list = [{ id: 1, name: 'A' }]; // ✅ 必须保留原引用来修改内部属性 list.length = 0; list.push({ id: 1, name: 'A' }); // 或者必须将数组包在一个对象的属性里: reactive({ data: [] })如果是ref,直接通过 .value 重新赋值即可,完美保留响应式,操作极其简单:
codeJavaScript
const list = ref([]); // ✅ 正确:直接赋新值,响应式依然存在 list.value = [{ id: 1, name: 'A' }];② 解决解构(Destructuring)丢失响应式的问题
如果解构一个 reactive 对象,解构出来的变量会变成普通的非响应式变量。
codeJavaScript
const state = reactive({ count: 0, title: 'Hello' }); let { count } = state; count++; // ❌ 这里的 count 已经是一个局部的普通数字变量,视图不会更新如果是 ref 包含的对象,我们通常不直接解构它自身,即使要在解构时保留响应式,也能通过解构 .value 再配合其他方式,或者统一心智:所有的响应式变量都是带 .value 的盒子。
③ 统一团队的心智模型
reactive 只能用于对象、数组、Map、Set 等引用类型。
ref 既可以用于基础类型(数字、字符串、布尔值),也可以用于引用类型。
如果项目中混用两者,开发者在写代码时总要停下来思考:“这个变量是 ref 还是 reactive?我在这里需不需要写 .value?”
因此,很多团队选择一律使用 ref。这样在 <script> 或者纯 JS/TS 逻辑中,凡是响应式数据就一律点 .value,而在 <template> 模板中 Vue 会自动帮我们解包不需要写 .value,逻辑极其统一。
总结建议
闭眼首选 ref:无论定义基础数据、还是数组、甚至是对象,全都用 ref。除了要在 JS 代码里多写 .value 之外,几乎没有其他缺点,且完美避开了重新赋值丢失响应式的坑。
什么时候可以用 reactive:如果你碰巧需要定义一个类似于表单数据的聚合对象 form,且明确知道这个对象永远不会被整体重新赋值(只会修改 form.username、form.password),那么用 reactive(form) 会让你的代码看起来更清爽(少写很多 .value)。
10.@click="submitForm(ruleFormRef)"
//要把菜单实例传给这个点击函数,才能在函数内部调用菜单实例的 validate 方法来验证表单数据
11.怎么理解res.data.data
1. 在响应拦截器中,res 具体包含哪些内容?
在一个基于 Axios 的响应拦截器里(axios.interceptors.response.use(res => {...})),这个 res 对象是由 Axios 封装好并返回给你的。它的结构是固定的,无论你请求什么接口,res 对象在 Axios 层面一定包含以下几个核心字段:
data:由后端服务器实际返回的响应体数据(这就是你写代码时最关心的东西)。
status:HTTP 状态码(例如 200 代表成功,404 代表找不到,500 代表服务器错误)。
statusText:HTTP 状态信息(例如状态码 200 对应的文本通常是 "OK")。
headers:服务器响应的 HTTP 头信息(包含了比如 Token、内容类型 Content-Type 等)。
config:你当初发起这个请求时,提供给 Axios 的配置信息(请求的 URL、基础路径、请求方式 GET/POST、超时时间等)。
request:原生的 XMLHttpRequest 对象(浏览器环境下),它是底层发出请求的真正载体。
2. 对于每个程序来说,res 中都是有固定包含的内容的吗?
答案是:外层结构是固定的,但内层(res.data)是完全不固定的。
固定的部分(Axios 决定的):只要你用的是 Axios,那么 res 对象里永远都有上面提到的 data、status、headers 等字段。这是库的标准。
不固定的部分(后端决定的):res.data 里面的内容是由你们公司的后端程序员或者第三方接口决定的。不同的公司、不同的接口,返回的内容格式千差万别。
3. res.data 和 res.data.data 都是什么?为什么会叠词?
这是初学者最容易懵的地方,其实这是一个**“套娃”**现象。
第一层:res.data(来源于 Axios)
正如前面所说,res.data 中的 data 是Axios 提供的包裹字段。无论后端给你返回一个字符串 "Hello",还是一个数字 123,还是一个 JSON 对象,Axios 都会把它塞进 res.data 里。
第二层:.data(来源于后端团队的规范)
在现代的后端开发中,为了让前端更好地处理错误,后端通常不会直接把单纯的业务数据丢给你,而是会在外面再包裹一层“标准的数据结构”(这叫:全局统一响应格式)。
后端通常会统一返回类似这样一个 JSON 对象:
codeJSON
{ "code": 200, // 业务状态码(200代表成功,50001代表密码错误等) "message": "登录成功", // 提示信息,用于给前端弹窗展示 "data": { // 真正的业务数据!!!后端恰好也把它命名为了 "data" "userId": 1001, "username": "张三", "token": "xxxxx" } }组合起来:res.data.data 诞生了
现在我们来看看“套娃”是怎么发生的:
前端 Axios 接收到请求,把上面那一整个 JSON 塞进了 res.data。
此时,res.data 的值等于:
codeJSON{ "code": 200, "message": "登录成功", "data": { "username": "张三" } }你想获取真正的用户信息(张三),你需要先访问 Axios 的包 (res.data),然后再访问后端的包 (.data)。
于是你写出了:res.data.data。
示例总结对比:
res:Axios 的响应对象(包含请求头、HTTP 状态码 200 等)。
res.data:后端写在代码里,真正扔给浏览器的全套数据(包含 code, message, data)。
res.data.data:全套数据里,专门用来装核心业务数据的那个字段。
💡 进阶小贴士:如何在代码里消灭一层 .data?
天天写 res.data.data 很难受,所以在实际开发中,我们通常会在响应拦截器里把它扒掉一层:
codeJavaScript
// 在项目的 axios 配置文件中: axios.interceptors.response.use( (res) => { // 拦截器在数据交到你手里之前,偷偷帮你扒掉 Axios 的那层 data // 如果后端的业务状态码是 200,直接返回后端的数据结构 if (res.data.code === 200) { return res.data; // 注意这里!我们直接返回了 res.data } else { // 弹出后端的错误信息 alert(res.data.message); return Promise.reject(new Error(res.data.message)); } }, (error) => { return Promise.reject(error); } );经过这样配置后,你在具体的页面里发请求时,拿到的 res 就直接是后端给的那堆东西了。
你就可以直接写 res.data 来获取真实的业务数据,而不需要再写 res.data.data 了:
// 页面中的使用: const res = await loginApi(); console.log(res.message); // "登录成功" console.log(res.data.username); // "张三"12 数组的map方法
map实际上就是循环处理数组元素,并将返回值保存在新数组中。
13. <el-table :data="tableData">
<el-table :data="tableData" style="width:100%; margin-top: 25px;"> <el-table-column label="文章标题" width="200"> <template #default="scope"> <div style="display: flex; align-items: center;"> <el-icon><timer /></el-icon> <span>{{ scope.row.title }}</span> </div> </template> </el-table-column> <el-table-column prop="authorName" label="作者" width="150" /> </el-table>el-table中prop 的使用。
prop 属性就是一把“对暗号的钥匙”
当 Element UI 的底层代码运行时,它是由 <el-table> 替你执行了那个看不见的 v-for 循环。
每次遍历一行(对应数组里的一个对象),<el-table-column> 就会拿着你写的prop="authorName"去问当前这个对象:
第一行:喂!把你里面叫 authorName 的值交出来!于是拿到了“张三”填在格子里。
第二行:喂!把你里面叫 authorName 的值交出来!于是拿到了“李四”填在格子里。
第三行:...拿到了“王五”。
当我们使用自定义插槽时,情况有所不同:
潜台词:不要你帮我打印文本了!我不仅要显示 title,我还要在前面放一个绿色的 <el-icon> 图标,还要给文字加个 <span> 控制大小。
如何取值:既然你不要 Element 帮你自动去取数据填入,那你肯定得知道当前这一行的数据是什么对吧?这就是scope.row的作用!
scope:是 Element UI 递给你的一份“当前行的数据大礼包”。
scope.row:就代表当前循环到的这一行的那一整个数据对象(类似于刚才说的 item)。
所以你要自己手动写 scope.row.title 把标题掏出来。
14. 关于knowledge组件及articleDialog组件的双向传递
父组件(knowledge)中:
<ArticleDialogv-model:modelValue="dialogVisible"/>
// 这是一句语法糖 首先:modelValue="dialogVisible" 利用modelValue这个桥梁将dialogVisible传递给子组件 子组件利用prop接收
其次:父组件在子组件身上偷偷监听了一个叫 update:modelValue 的事件,一旦子组件触发了这个事件,父组件就会自动把传回来的新值重新赋给 dialogVisible!
.......
const dialogVisible = ref(false) //初始化
子组件(articleDialog)中:
<el-dialog
title="文章详情"
v-model="props.modelValue"
width="50%"
@close="handelClose"
></el-dialog>
...
const props = defineProps({
modelValue:{
type:Boolean,
default:false
}
})
<template> <el-dialog title="文章详情" v-model="dialogVisible" width="50%" @close="handelClose" ></el-dialog> </template> <script setup> import { ref,computed } from 'vue'; const props = defineProps({ modelValue:{ type:Boolean, default:false } }) const emit = defineEmits(['update:modelValue']) const dialogVisible = computed({ get(){ return props.modelValue }, set(val){ emit('update:modelValue',val) } }) const handelClose = () =>{ } </script><template> <div> <PageHead title="知识文章"> <template #buttons> <el-button @click="dialogVisible = true" type="primary">新增</el-button> </template> </PageHead> <TabelSearch :formItem = 'formItem' @search="handelSearch"/> <el-table :data="tableData" style="width:100%; margin-top: 25px;"> <el-table-column label="文章标题" fixed="left" width="400"> <template #default="scope"> <div style="display: flex; align-items: center;"> <el-icon><timer /></el-icon> <span>{{ scope.row.title }}</span> </div> </template> </el-table-column> <el-table-column label="分类" width="200"> <template #default="scope"> <div style="display: flex; align-items: center;"> <el-icon><timer /></el-icon> <span>{{ categoryMap[scope.row.categoryId] }}</span> </div> </template> </el-table-column> <el-table-column prop="authorName" label="作者" width="150" /> <el-table-column prop="readCount" label="阅读量" width="150" /> <el-table-column prop="publishedAt" label="发布时间" width="150" /> <el-table-column fixed="right" label="操作" width="240"> <template #default="scope"> <el-button text type="primary" >编辑</el-button> <el-button v-if="scope.row.status === 0||scope.row.status === 2" text type="success" >发布</el-button> <el-button v-if="scope.row.status === 1" text type="warning" >下线</el-button> <el-button text type="danger" >删除</el-button> </template> </el-table-column> </el-table> <el-pagination style="margin-top: 25px;" :page-size="pagenation.size" layout="prev,pager,next" :total="pagenation.total" @change="handelChange" /> <ArticleDialog v-model:modelValue="dialogVisible"/> </div> </template> <script setup> import { onMounted , ref ,reactive} from 'vue'; import PageHead from '../components/PageHead.vue'; import TabelSearch from '../components/TabelSearch.vue'; import { categoryTree , articlePage} from '../api/admin'; import ArticleDialog from '../components/ArticleDialog.vue'; //父组件中的数据来源,根据提供的表单数据,帮助子组件确认应该渲染出怎样的组件 const formItem = [ {comp:'input', prop:'title', label:'文章标题', placeholder:'请输入文章标题'}, {comp:'select', prop:'categoryId', label:'分类', placeholder:'请选择分类'}, {comp:'select', prop:'status', label:'状态', placeholder:'选择状态',options:[ {label:'草稿', value:'0'}, {label:'已发布', value:'1'}, {label:'已下线', value:'2'}, ]} ] const pagenation = reactive({ currentPage:1, size:10, total:0 }) const handelSearch = async (formData) =>{ console.log(formData) const params = { //从后端接口得知参数有分页参数和表单数据,所以这里把它们合并成一个对象 ...pagenation, ...formData }; const {records,total} = await articlePage(params) tableData.value = records } const handelChange = (page) =>{ pagenation.currentPage = page handelSearch() //页码改变时重新获取数据 } const categoryMap = reactive({}) //用来存储分类数据的映射关系 const categories = ref([]) //用来存储分类数据的列表 const tableData = ref([]) const dialogVisible = ref(false) //组件挂载时获取分类数据,并构建映射关系 onMounted( async ()=>{ const data = await categoryTree() console.log(data,'分类数据') categories.value = data.map(item =>{ categoryMap[item.id] = item.categoryName //构建id到名称的映射关系 return { label:item.categoryName, value:item.id } }) formItem[1].options = categories.value } ) handelSearch() console.log(tableData,'表格数据') </script>环节 1:是谁打开了新增框?(父组件的权力)
让我们先看父组件的代码:
codeHtml
<!-- 父组件 --> <el-button @click="dialogVisible = true" type="primary">新增</el-button> <ArticleDialog v-model:modelValue="dialogVisible"/>起点:父组件自己兜里掏出了一个变量叫 dialogVisible(初始值是 false)。
动作:用户在父组件点“新增”按钮时,父组件直接把这个变量改成了 true。
传递:因为写了 v-model,这个 true 就顺着隐形的管道,作为 modelValue 这个快递寄给了子组件。
此时到了子组件的代码:
codeJavaScript
// 子组件 const dialogVisible = computed({ get() { return props.modelValue } // 发现父亲寄来的是 true // ... })子组件开门:子组件里的 <el-dialog v-model="dialogVisible"> 的 dialogVisible 通过计算属性的 get 读到了父组件传来的 true,于是乎,当当当当!弹窗漂亮地弹出来了!
环节 2:新增框是怎么关闭的?emit 起到了什么作用?
这是最核心的部分。当我们在弹窗上点击右上角的 "X" 或者点击空白遮罩层想要关掉弹窗时,会发生什么?
在基于 Element Plus 的体系里,当用户试图关弹窗时,<el-dialog> 内部会尝试去修改它绑定的变量变成 false。
在你的代码里,<el-dialog> 绑定的是谁?是我们自己造的中介机构:计算属性 dialogVisible。
当 <el-dialog> 试图塞给 dialogVisible 一个 false 的时候,就会触发我们写的set 拦截器:
codeJavaScript
// 子组件 set(val) { // 这里的 val 就是弹窗尝试传来的 false emit('update:modelValue', val) // 拿到喇叭,朝天大喊! }emit 的作用就在这里!
子组件是个打工仔,他没权力决定自己关不关门(因为门票在父组件手里)。所以他用 emit('update:modelValue', false) 这个大喇叭向父组件喊话:“老板(父组件)!用户点叉号了!快把那个绑定的变量变回 false 吧!”
此时,父组件因为写了 v-model(Vue 的底层机制一直在监听这个喇叭),就会乖乖地把传下来的 dialogVisible 改成了 false。
紧接着,父组件的数据变了,重新流向子组件,子组件的 get 读到 false,弹窗就乖乖关上了。
环节 3:为什么要双向传递?图个啥?
你可能会问:太绕了吧!既然点那个大绿按钮能开,为什么不直接在子组件内部定一个 visible 这个变量,自己管自己开门关门?
这就是组件化解耦(分离)思想的最完美的体现!
为什么要父传子?
如果子组件自己管变量,父组件怎么告诉他“嘿兄弟,你赶紧跳出来”?父组件可能需要调用子组件的方法(像 ref.value.open()),那太麻烦了。数据驱动的方式就是父组件甩一个 true 过去,子组件立刻开启。为什么要子传父?
既然控制权交给了父组件,如果用户点了“叉号”,或者点弹窗里的“取消”,如果子组件不通知父组件把数据改回去,下次父组件想再打开这扇门,发现自己的变量依然是 true(明明上一次就没改掉),就不会再产生反应了。
总结一句话:
双向绑定的作用就是让父组件和子组件始终保持同步。父想开门,就传 true 给子;子想关门,就 emit 发假传真给父,让父心甘情愿改成 false。在这个过程中,谁也没有破坏“Props 只读单向传递”的规矩。这就是这套写法被推为行业标杆的原因!