1. useRouter()拿到路由器可以查看路由以及使用路由器的方法们2. el-menu-item v-foritem inrouter.options.routes[0].children :indexitem.pathrouter.options.routes[0].children 这个是路由表里的第一个路由的孩子路由们:indexitem.path index是el-menu-item 组件自带的属性配合v-for用来标记 “当前哪个路由被选中”indexel-plus组件中SubMenu的属性充当二级菜单项的身份证起到选择、标记的作用可作为参数传递给组件的点击事件等。3.el-menu-item v-foritem in router.options.routes[0].children :keyitem.path :indexitem.pathel-iconicon-menu //el-icon//这样写就是被写死了采用 component :isitem.meta.icon /实现动态变化spanNavigator Two/span/el-menu-item4.el-menu-itemclickselectMenuv-foritem in router.options.routes[0].children :indexitem.path//子菜单el-menu-item点击时会把$event作为默认参数传给selectMenu//这个$event里包含了当前点击菜单项的完整信息index就在其中const selectMenu (key){console.log(key)};5. 利用组件切换路由的两种方式el-menu :routertrue !-- 这一句就是开启“点击菜单自动跳转路由” -- 通过给菜单绑定点击事件......../divel-menu-item clickselectMenu v-foritem in router.options.routes[0].children :keyitem.path :indexitem.path//这里的key是给VUE看的身份证有了它删除更新结点不会错乱//这里index相当于表单的学号是给el-menu看的有了它可以标记选中的el-menu-itemel-iconcomponent :isitem.meta.icon //el-iconspan{{ item.meta.title }}/span/el-menu-item/el-menu/el-aside/templatescript setupimport { useRouter } from vue-router;const iconUrl new URL(/assets/images/机器人.png,import.meta.url).hrefconst router useRouter();//使用点击事件实现菜单的路由跳转//keyel-menu-item的click事件默认会自带一个参数就是当前菜单的事件信息对象。const selectMenu (key){console.log(key);const currentRouterouter.options.routes[0];router.push(${currentRoute.path}/${key.index});};/script6. 在组件TableSearch子组件中完善输入框父组件knowledge传递搜索框数据给子组件搜索框类型input/select、prop、placeholder、label子组件接收后渲染搜索框 通过:model和v-model实现表单的双向绑定7. el-col v-binditem.col搭配const formItemAttr computed(() {const {formItem} propsformItem.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:68. 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 代理被替换掉了codeJavaScriptlet list reactive([]); // ❌ 错误做法响应式会丢失视图不会更新 list [{ id: 1, name: A }]; // ✅ 必须保留原引用来修改内部属性 list.length 0; list.push({ id: 1, name: A }); // 或者必须将数组包在一个对象的属性里 reactive({ data: [] })如果是ref直接通过 .value 重新赋值即可完美保留响应式操作极其简单codeJavaScriptconst list ref([]); // ✅ 正确直接赋新值响应式依然存在 list.value [{ id: 1, name: A }];② 解决解构Destructuring丢失响应式的问题如果解构一个 reactive 对象解构出来的变量会变成普通的非响应式变量。codeJavaScriptconst 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.clicksubmitForm(ruleFormRef)//要把菜单实例传给这个点击函数才能在函数内部调用菜单实例的 validate 方法来验证表单数据11.怎么理解res.data.data1. 在响应拦截器中res 具体包含哪些内容在一个基于 Axios 的响应拦截器里axios.interceptors.response.use(res {...})这个 res 对象是由 Axios 封装好并返回给你的。它的结构是固定的无论你请求什么接口res 对象在 Axios 层面一定包含以下几个核心字段data由后端服务器实际返回的响应体数据这就是你写代码时最关心的东西。statusHTTP 状态码例如 200 代表成功404 代表找不到500 代表服务器错误。statusTextHTTP 状态信息例如状态码 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。示例总结对比resAxios 的响应对象包含请求头、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 :datatableDatael-table :datatableData stylewidth:100%; margin-top: 25px; el-table-column label文章标题 width200 template #defaultscope div styledisplay: flex; align-items: center; el-icontimer //el-icon span{{ scope.row.title }}/span /div /template /el-table-column el-table-column propauthorName label作者 width150 / /el-tableel-table中prop 的使用。prop 属性就是一把“对暗号的钥匙”当 Element UI 的底层代码运行时它是由 el-table 替你执行了那个看不见的 v-for 循环。每次遍历一行对应数组里的一个对象el-table-column 就会拿着你写的propauthorName去问当前这个对象第一行喂把你里面叫 authorName 的值交出来于是拿到了“张三”填在格子里。第二行喂把你里面叫 authorName 的值交出来于是拿到了“李四”填在格子里。第三行...拿到了“王五”。当我们使用自定义插槽时情况有所不同潜台词不要你帮我打印文本了我不仅要显示 title我还要在前面放一个绿色的 el-icon 图标还要给文字加个 span 控制大小。如何取值既然你不要 Element 帮你自动去取数据填入那你肯定得知道当前这一行的数据是什么对吧这就是scope.row的作用scope是 Element UI 递给你的一份“当前行的数据大礼包”。scope.row就代表当前循环到的这一行的那一整个数据对象类似于刚才说的 item。所以你要自己手动写 scope.row.title 把标题掏出来。14. 关于knowledge组件及articleDialog组件的双向传递父组件knowledge中ArticleDialogv-model:modelValuedialogVisible/// 这是一句语法糖 首先:modelValuedialogVisible 利用modelValue这个桥梁将dialogVisible传递给子组件 子组件利用prop接收其次父组件在子组件身上偷偷监听了一个叫 update:modelValue 的事件一旦子组件触发了这个事件父组件就会自动把传回来的新值重新赋给 dialogVisible.......const dialogVisible ref(false) //初始化子组件articleDialog中el-dialogtitle文章详情v-modelprops.modelValuewidth50%closehandelClose/el-dialog...const props defineProps({modelValue:{type:Boolean,default:false}})template el-dialog title文章详情 v-modeldialogVisible width50% closehandelClose /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 () { } /scripttemplate div PageHead title知识文章 template #buttons el-button clickdialogVisible true typeprimary新增/el-button /template /PageHead TabelSearch :formItem formItem searchhandelSearch/ el-table :datatableData stylewidth:100%; margin-top: 25px; el-table-column label文章标题 fixedleft width400 template #defaultscope div styledisplay: flex; align-items: center; el-icontimer //el-icon span{{ scope.row.title }}/span /div /template /el-table-column el-table-column label分类 width200 template #defaultscope div styledisplay: flex; align-items: center; el-icontimer //el-icon span{{ categoryMap[scope.row.categoryId] }}/span /div /template /el-table-column el-table-column propauthorName label作者 width150 / el-table-column propreadCount label阅读量 width150 / el-table-column proppublishedAt label发布时间 width150 / el-table-column fixedright label操作 width240 template #defaultscope el-button text typeprimary 编辑/el-button el-button v-ifscope.row.status 0||scope.row.status 2 text typesuccess 发布/el-button el-button v-ifscope.row.status 1 text typewarning 下线/el-button el-button text typedanger 删除/el-button /template /el-table-column /el-table el-pagination stylemargin-top: 25px; :page-sizepagenation.size layoutprev,pager,next :totalpagenation.total changehandelChange / ArticleDialog v-model:modelValuedialogVisible/ /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 clickdialogVisible true typeprimary新增/el-button ArticleDialog v-model:modelValuedialogVisible/起点父组件自己兜里掏出了一个变量叫 dialogVisible初始值是 false。动作用户在父组件点“新增”按钮时父组件直接把这个变量改成了 true。传递因为写了 v-model这个 true 就顺着隐形的管道作为 modelValue 这个快递寄给了子组件。此时到了子组件的代码codeJavaScript// 子组件 const dialogVisible computed({ get() { return props.modelValue } // 发现父亲寄来的是 true // ... })子组件开门子组件里的 el-dialog v-modeldialogVisible 的 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-modelVue 的底层机制一直在监听这个喇叭就会乖乖地把传下来的 dialogVisible 改成了 false。紧接着父组件的数据变了重新流向子组件子组件的 get 读到 false弹窗就乖乖关上了。环节 3为什么要双向传递图个啥你可能会问太绕了吧既然点那个大绿按钮能开为什么不直接在子组件内部定一个 visible 这个变量自己管自己开门关门这就是组件化解耦分离思想的最完美的体现为什么要父传子如果子组件自己管变量父组件怎么告诉他“嘿兄弟你赶紧跳出来”父组件可能需要调用子组件的方法像 ref.value.open()那太麻烦了。数据驱动的方式就是父组件甩一个 true 过去子组件立刻开启。为什么要子传父既然控制权交给了父组件如果用户点了“叉号”或者点弹窗里的“取消”如果子组件不通知父组件把数据改回去下次父组件想再打开这扇门发现自己的变量依然是 true明明上一次就没改掉就不会再产生反应了。总结一句话双向绑定的作用就是让父组件和子组件始终保持同步。父想开门就传 true 给子子想关门就 emit 发假传真给父让父心甘情愿改成 false。在这个过程中谁也没有破坏“Props 只读单向传递”的规矩。这就是这套写法被推为行业标杆的原因