[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"site-config":3,"article-硅谷甄选项目笔记":22,"comments-硅谷甄选项目笔记":43,"footer-socials":44},{"site_title":4,"site_subtitle":5,"home_intro":6,"avatar":7,"seo_keywords":8,"seo_description":9,"site_subtitle_highlight":10,"home_description":11,"about_name":12,"about_intro":13,"code_comment_2":14,"code_log":15,"code_skills":16,"code_goal":17,"code_comment_1":18,"meteor_density":19,"meteor_max_count":19,"meteor_enabled":20,"meteor_speed":21},"ShineGoldYao","架构代码，","全栈开发者 \u002F 开源爱好者 \u002F 技术探索者","https:\u002F\u002Fforuda.gitee.com\u002Favatar\u002F1762402862010015318\u002F16382196_yaoxingjin_1762402861.png!avatar200","ShiGoldYao,技术博客,全栈开发","专注于前沿技术分享与开源项目展示的个人技术博客","书写未来。","大家好，我是 ShiGoldYao。一名全栈学习与技术爱好者。在这里，我分享关于现代 Web 开发、技术框架学习记录以及极客生活的深度思考。","ShiGoldYao","我是一名全栈技术学习者，对构建高性能、可扩展的现代 Web 应用充满热情。过去一年里，我从初识互联网前后端开发，到逐步沉淀技术体系，始终保持着对前端、后端与工程化的持续探索。\n\n我坚信 “代码如诗”。除了日常学习与项目实践，我也会花大量时间关注开源社区，尝试摸索 WebAssembly、Rust 等前沿技术在浏览器端的更多可能，不断挑战性能与体验的边界。\n\n生活里的我并不只有代码：闲暇时会打打永劫无间，享受博弈与操作的快感；也喜欢打乒乓球，在运动中放松自己；当然，最幸福的时光，还是和女朋友一起慢慢生活、认真恋爱。","\u002F\u002F 🚀 开启学习之旅","","Vue , TypeScript , Nest , Mysql","成为优秀的前端全栈工程师","\u002F\u002F 欢迎来到我的技术世界","3","true","5",{"id":23,"title":24,"slug":25,"coverUrl":15,"summary":15,"content":26,"htmlContent":27,"categoryId":28,"viewCount":29,"likeCount":30,"isTop":30,"isPublish":31,"seoKeywords":15,"seoDescription":15,"publishTime":32,"createTime":33,"updateTime":34,"deleteTime":35,"category":36,"tags":42},"13","一，项目收获及技术","硅谷甄选项目笔记","\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n# 一，项目收获及技术\n\n## 1.1 收获\n\n* 企业级的编码规范\n\n  ![image-20260129230723775](images\u002Fimage-20260129230723775.png)\n\n* 从零开始，封装一个后台管理系统\n\n  ![image-20260129230803256](images\u002Fimage-20260129230803256.png)\n\n* 菜单权限与按钮权限\n\n  ![image-20260129230823689](images\u002Fimage-20260129230823689.png)\n\n* 数据可视化大屏\n\n  ![image-20260129230843202](images\u002Fimage-20260129230843202.png)\n\n* svg 矢量图在项目中的应用\n\n  ![image-20260129230907605](images\u002Fimage-20260129230907605.png)\n\n* 主题颜色切换与暗黑模式的切换\n\n  ![image-20260129230933375](images\u002Fimage-20260129230933375.png)\n\n\n\n## 1.2 技术选选型\n\nVue3 + 组合式API + Vite构建工具 + element-plus + Axios + Echarts + pinia + TypeScript + vue-router\n\n\n# 二，Vue3 组件通信的艺术：构建灵活可复用的UI\n\n在任何复杂的单页应用中，组件之的通信都是一个核心挑战。高效的组件通信能够显著提高代码的复用性，可维护性以及团队的协作效率\n\n## 2.1 props: 父子组件通信的基石\n\n`props（道具）`是Vue中最直接，最常用的父子组件通信方式。在Vue3中，通过 defineProps 宏，我们可以轻松地接受父组件传递的数据，并且无需显示引入，可以在\u003Cscript setup> 中使用，极大的简化了开发流程\n\n**父组件向子组件传递数据案例** ：\n\n子组件可以通过两种方式接受 `props`\n\n父组件传数据的代码\n\n```vue\n\u003Ctemplate>\n\u003Cdiv class=\"box\">\n  \u003Ch1>父组件\u003C\u002Fh1>\n  \u003CChild info=\"我爱祖国\" :money=\"money\">\u003C\u002FChild>\n\u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup lang=\"ts\">\nimport {ref} from \"vue\"\nimport Child from \".\u002FChild.vue\";\n\nlet money = ref(1000)\n\u003C\u002Fscript>\n\n```\n\n**方式一：带类型和默认值的声明**\n\n这种方式提供了更强的类型检查和默认值设置，增加了代码的健壮性。\n\n```typescript\n\nlet props = defineProps({\n  info:{\n    type:String,  \u002F\u002F 接受的数据类型\n    defalut:'默认参数' \u002F\u002F 接受默认数据\n  },\n  money:{\n    type:Number,\n    default:0\n\n  }\n})\n\u002F\u002F 在模板中可以直接使用 props.info,props.money   省略直接 info,money 也可以\n```\n\n**方式二：数组形式的简洁声明**\n\n适用于简单的 `props` 声明，但缺乏类型检查\n\n```typescript\nlet props = defineProps(['info','money'])\n```\n\n**重要提示：** `props` 是单向数据流，子组件只能读取 `props` 数据，不能直接修改它。\n\n\n\n## 2.2 自定义事件：子组件向父组件传递数据\n\n自定义事件是实现子组件向父组件传递数据的关键机制。与原生 DOM 事件不同，自定义事件是 Vue 组件特有的通信方式。\n\n**父组件绑定自定义事件：**\n\n父组件在子组件标签上通过 `@` 符号绑定自定义事件。\n\n```vue\n\u003Ctemplate>\n\u003Cdiv class=\"father\">\n  \u003Ch1>父组件\u003C\u002Fh1>\n  \u003CEvent1 @xxx=\"handler2\">\u003C\u002FEvent1>\n\u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup lang=\"ts\">\nimport Event1 from '.\u002FEvent1.vue'; \n\nconst handler2 = (param1:string,param2:string) =>{\n  console.log('从子组件接收到数据',param1,param2);\n  \n}\n\n\u003C\u002Fscript>\n\n```\n\n**子组件触发自定义事件：**\n\n在子组件内部，使用 `defineEmits` 宏声明需要触发的自定义事件，然后通过 `$emit` 方法触发事件并传递数据。\n\n```vue\n\u003Ctemplate>\n\u003Cdiv class=\"son\">\n  \u003Ch1>我是子组件1\u003C\u002Fh1>\n  \u003Cbutton @click=\"handler\">点我触发事件xxx自定义事件\u003C\u002Fbutton>\n\u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup lang=\"ts\">\n\nlet $emit = defineEmits(['xxx']) \u002F\u002F 声明自定义事件 'xxx',\nconst handler =() =>{\n  $emit('xxx','法拉利','茅台')  \u002F\u002F 第一个参数，自定义事件的名称，父组件通过这个来监听这个事件，剩下的参数是子组件传给父组件的数据\n}\n\u003C\u002Fscript>\n\n```\n\n**Vue3 中原生 DOM 事件的特殊性：**\n\n在 Vue3 中，像 `click`、`dbclick`、`change` 这类原生 DOM 事件，无论是在普通 HTML 标签上还是在自定义组件标签上，默认都视为原生 DOM 事件。这与 Vue2 中需要 `native` 修饰符才能将组件上的事件变为原生 DOM 事件有所不同。但如果子组件内部通过 `defineEmits` 定义了同名的事件，那么它将优先被视为自定义事件。\n\n## 2.3 全局事件总线 (mitt)：实现任意组件通信\n\n在 Vue2 中，我们常利用 `Vue.prototype.$bus` 实现全局事件总线。然而，Vue3 没有 Vue 构造函数，且组合式 API 中没有 `this` 上下文，因此传统的全局事件总线方式不再适用。在 Vue3 中，我们可以借助轻量级的第三方库 `mitt` 来实现全局事件总线功能，从而让任意组件之间进行通信。\n\n**mitt 官网：** https:\u002F\u002Fwww.npmjs.com\u002Fpackage\u002Fmitt\n\n使用：\n\n* 下载安装mitt `$ npm install --save mitt`\n* 在 src 中创建文件夹 bus，index.ts\n* 在 index.ts 中引入 mitt\n\n```vue\n\u002F\u002F引入mitt插件:mitt一个方法,方法执行会返回bus对象\nimport mitt from 'mitt';\nconst $bus = mitt();\nexport default $bus;\n\n```\n\n* 送东西的兄弟\n\n```vue\n\u003Ctemplate>\n  \u003Cdiv class=\"child2\">\n     \u003Ch2>我是子组件2:曹丕\u003C\u002Fh2>\n     \u003Cbutton @click=\"handler\">点击我给兄弟送一台法拉利\u003C\u002Fbutton>\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup lang=\"ts\">\n    \n\u002F\u002F引入$bus对象\nimport $bus from '..\u002F..\u002Fbus';\n\n\u002F\u002F点击按钮回调\nconst handler = ()=>{\n  $bus.emit('car',{car:\"法拉利\"});  \u002F\u002F 第一个参数事件的名称（类型），告诉另一个兄弟是哪个事件，类似于一个表示这个事件的id\n}\n\u003C\u002Fscript>\n\n```\n\n* 收到东西的兄弟\n\n```vue\n\u003Ctemplate>\n  \u003Cdiv class=\"child1\">\n    \u003Ch3>我是子组件1:曹植\u003C\u002Fh3>\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup lang=\"ts\">\n    \nimport $bus from \"..\u002F..\u002Fbus\";\n\u002F\u002F组合式API函数\nimport { onMounted } from \"vue\";\n\u002F\u002F组件挂载完毕的时候,当前组件绑定一个事件,接受将来兄弟组件传递的数据\n\nonMounted(() => {\n  \u002F\u002F第一个参数:即为事件类型  第二个参数:即为事件回调\n  $bus.on(\"car\", (car) => {\n    console.log(car);\n  });\n});\n\u003C\u002Fscript>\n```\n\n\n\n## 2.4 `v-model`：实现父子组件数据的双向绑定(同步数据)\n\n`v-model` 指令不仅用于收集表单数据实现双向绑定，它也是实现父子组件数据同步的强大工具。在底层，`v-model` 实际上是 `props` (`modelValue`) 和自定义事件 (`update:modelValue`) 的语法糖。\n\n`v-model` 实现表单数据收集，数据双向绑定：\n\n```vue\n\u003Ctemplate>\n\u003Cdiv>\n  \u003Ch1>v-model\u003C\u002Fh1>\n  \u003Cinput type=\"text\" v-model=\"info\">\n\u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup lang=\"ts\">\n\u002F\u002F v-model指令，收集表单数据，实现双向绑定\n\nimport {ref} from 'vue'\n\nlet info = ref('')\n\n\u003C\u002Fscript>\n\n```\n\n`v-model` 实现父子组件数据的同步：\n\n父组件：\n\n```vue\n\u003Ctemplate>\n\u003Cdiv class=\"fa\">\n  \u003Ch1>v-model 钱数：{{ money }}\u003C\u002Fh1>\n  \u003Cinput type=\"text\" v-model=\"info\">\n  \u003Chr>\n  \u003C!-- props：父亲给儿子数据 -->\n   \u003C!-- \u003CChild :modelValue=\"money\" @updata:model-value=\"handler\">\u003C\u002FChild> -->\n\n   \u003C!-- v-model组件身上的作用\n    第一：相当于给子组件传递props[modelValue] = 1000\n    第二：相当于给子组件绑定自定义事件 update:modelValue   事件名不能错\n    \n   -->\n   \u003CChild v-model=\"money\">\u003C\u002FChild>\n\u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup lang=\"ts\">\n\u002F\u002F v-model指令，收集表单数据，实现双向绑定\nimport {ref} from 'vue'\n\nlet info = ref('')\n\n\u002F\u002F v-model 实现组件之间的通信，实现父子组件的数据同步\n\u002F\u002F 父亲给子组件数据 Props\n\u002F\u002F 子组件给父组件数据 自定义事件\n\n\u002F\u002F 引入子组件\nimport Child from '.\u002FChild.vue';\n\n\u002F\u002F 父组件的数据钱数\nlet money = ref(10000)\n\n\u002F\u002F 自定义事件的回调\nconst handler = (num) =>{\n  \u002F\u002F 将来接受子组件传递过来的数据\n  \u002F\u002F console.log(num)\n  money.value = num\n}\n\u003C\u002Fscript>\n```\n\n子组件：\n\n```vue\n\u003Ctemplate>\n\u003Cdiv class=\"child\">\n  \u003Ch3>钱数：{{ modelValue }}\u003C\u002Fh3>\n  \u003Cbutton @click=\"handler\">父子组件数据绑定\u003C\u002Fbutton>\n\u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup lang=\"ts\">\n\u002F\u002F 接受 props\nlet props = defineProps(['modelValue'])\nlet $emit = defineEmits(['update:modelValue'])\n\n\u002F\u002F 子组件内部按钮的点击回调\nconst handler = () =>{\n  \u002F\u002F 触发自定义事件\n  $emit('update:modelValue',props.modelValue+1000)\n}\n\u003C\u002Fscript>\n\n```\n\n**多 v-model 绑定：**\n\nVue3 允许一个组件使用多个 `v-model`，从而实现父子组件多个数据的同步。\n\n```vue\n\u003Ctemplate>\n  \u003Cdiv>\n    \u003Ch1>分页示例\u003C\u002Fh1>\n    \u003C!-- 使用 v-model 绑定多个属性 -->\n    \u003CChild1 v-model:pageNo=\"pageNo\" v-model:pageSize=\"pageSize\" \u002F>\n    \u003Cp>当前页码：{{ pageNo }}\u003C\u002Fp>\n    \u003Cp>每页显示条数：{{ pageSize }}\u003C\u002Fp>\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup lang=\"ts\">\nimport { ref } from 'vue';\nimport Child1 from '.\u002FChild1.vue';  \u002F\u002F 引入子组件\n\n\u002F\u002F 定义响应式数据\nlet pageNo = ref(1);   \u002F\u002F 当前页码\nlet pageSize = ref(3); \u002F\u002F 每页显示条数\n\u003C\u002Fscript>\n\n```\n\n子组件\n\n```vue\n\u003Ctemplate>\n  \u003Cdiv class=\"son2\">\n    \u003Ch1>同时绑定多个 v-model\u003C\u002Fh1>\n    \u003Cbutton @click=\"handler\">pageNo {{ pageNo }}\u003C\u002Fbutton>\n    \u003Cbutton @click=\"updatePageSize\">pageSize {{ pageSize }}\u003C\u002Fbutton>\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup lang=\"ts\">\nconst props = defineProps({\n  pageNo: Number,  \u002F\u002F 接收父组件传递的 pageNo\n  pageSize: Number \u002F\u002F 接收父组件传递的 pageSize\n});\n\nconst emit = defineEmits(['update:pageNo', 'update:pageSize']); \u002F\u002F 声明触发的事件\n\n\u002F\u002F 更新 pageNo\nconst handler = () => {\n  emit('update:pageNo', props.pageNo + 1);  \u002F\u002F 每次点击增加页码\n};\n\n\u002F\u002F 更新 pageSize\nconst updatePageSize = () => {\n  emit('update:pageSize', props.pageSize + 2);  \u002F\u002F 每次点击增加每页显示的条数\n};\n\u003C\u002Fscript>\n```\n\n\n\n## 2.5 `useAttrs`：获取组件的非 `props` 属性和事件\n\n`useAttrs` 是 Vue3 提供的一个 Composition API，用于获取组件实例上未被 `defineProps` 声明的属性和事件（包括原生 DOM 事件和自定义事件）。这类似于 Vue2 中的 `$attrs` 和 `$listeners`。\n\n父组件\n\n```vue\n\u003Ctemplate>\n\u003Cdiv>\n  \u003Ch1>useAttrs\u003C\u002Fh1>\n  \u003Cel-button type=\"primary\" size=\"small\" :icon=\"Edit\">\u003C\u002Fel-button>\n  \u003C!-- 自定义组件 加上文字显示 -->\n   \u003CHintButton type=\"primary\" size=\"small\" :icon=\"Edit\" title=\"编辑按钮\" @click=\"handler\">\u003C\u002FHintButton>\n   \n\u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup lang=\"ts\">\nimport { Edit } from '@element-plus\u002Ficons-vue';\nimport HintButton from '.\u002FHintButton.vue';\n\n\u002F\u002F 按钮点击的回调\nconst handler = () =>{\n  alert('12306')\n}\n\u003C\u002Fscript>\n\n```\n\n子组件\n\n```vue\n\n\u003Ctemplate>\n\u003Cdiv :title=\"$attrs.title\">\n  \u003Cel-button :=\"$attrs\">\u003C\u002Fel-button>\n\u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup lang=\"ts\">\n\u002F\u002F 引入useAttrs方法：获取组件标签上属性与事件\nimport {useAttrs} from \"vue\"\n\u002F\u002F 此方法会执行返回一个对象\nlet $attrs = useAttrs()\nconsole.log($attrs)\n\n\u003C\u002Fscript>\n```\n\n需要注意的是，如果 `defineProps` 已经接收了某个属性，那么 `useAttrs` 返回的对象中将不再包含该属性。\n\n\n\n## 2.6 `ref` 与 `$parent`：直接访问子\u002F父组件实例\n\n`ref` 允许我们在父组件中获取子组件的实例（VC），从而直接访问子组件的方法和响应式数据。而 `$parent` 则允许子组件获取其父组件的实例。\n\n**父组件通过 `ref` 访问子组件：**\n\n```vue\n\u003Ctemplate>\n\u003Cdiv class=\"fa\">\n\u003Ch1>我是父亲，我有{{ money }}钱\u003C\u002Fh1>\n\u003Cbutton @click=\"handler\">点我向儿子借10块钱\u003C\u002Fbutton>\n\u003Cbr>\n\u003CSon ref=\"son\">\u003C\u002FSon>\n\u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup lang=\"ts\">\n\u002F\u002F 引入子组件\nimport Son from '.\u002FSon.vue';\nimport {ref} from 'vue'\n\n\u002F\u002F 父组件钱数\nlet money = ref(1000)\n\u002F\u002F 获取子组件钱数\nlet son = ref()  \u002F\u002F 必须同名\n\u002F\u002F 父组件内部按钮点击回调\nconst handler = () =>{\n  money.value += 10\n  \u002F\u002F 让儿子的钱数-10\n  son.value.money -= 10\n  \u002F\u002F 调用到儿子的方法\n  son.value.fly()\n  \n  console.log(son.value)\n}\n\u003C\u002Fscript>\n```\n\n**子组件通过 `defineExpose` 暴露数据和方法：**\n\n在 Vue3 中，组件内部的数据和方法默认不对外暴露。如果希望父组件通过 `ref` 访问，子组件必须使用 `defineExpose` 明确地暴露它们。\n\n```vue\n\u003Ctemplate>\n\u003Cdiv class=\"son\">\n  \u003Ch3>我是儿子，我有{{ money }}钱\u003C\u002Fh3>\n\u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup lang=\"ts\">\n\nimport {ref} from 'vue'\n\n\u002F\u002F 儿子钱数\nlet money = ref(100)\n\nconst fly = () =>{\n  console.log('我可以飞');\n}\n\n\u002F\u002F 组件内部数据对外关闭，别人不能访问\n\u002F\u002F 访问需要通过defineExpose方法对外暴露\ndefineExpose({\n  money,\n  fly\n})\n\u003C\u002Fscript>\n```\n\n**子组件通过 `$parent` 访问父组件：同样父亲的数据也得对外暴露**\n\n```vue\n\u003Ctemplate>\n\u003Cdiv class=\"dau\">\n    \u003Ch3>我是闺女,我有{{ money }}钱\u003C\u002Fh3>\n    \u003Cbutton @click=\"handler($parent)\">点击父亲给我20块钱\u003C\u002Fbutton>\n\u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup lang=\"ts\">\nimport {ref} from 'vue'\n\u002F\u002F 闺女的钱\nlet money = ref(5000)\n\n\u002F\u002F 闺女内部按钮触发的回调\nconst handler = (parent:any) =>{\n    money.value += 20\n    parent.money -= 20   \n}\n\u003C\u002Fscript>\n\n\u003Cstyle scoped>\n.dau{\n    width: 300px;\n    height: 300px;\n    background-color: purple;\n}\n\u003C\u002Fstyle>\n```\n\n\n\n## 2.7 Pinia：新一代集中式状态管理容器\n\nPinia 是 Vue3 推荐的集中式状态管理库，它类似于 Vuex，但更加轻量、直观，并且移除了 `mutations` 和 `modules` 等概念，使得状态管理更加扁平化和易于理解。\n\n使用：\n\n* 安装pinia `npm install pinia`\n\n* 操作新建文件夹 `src\u002Fmain.ts `     \n\n* ```typescript\n  \u002F\u002F 操作main.ts\n  \u002F\u002F创建大仓库\n  import { createPinia } from 'pinia';\n  \u002F\u002FcreatePinia方法可以用于创建大仓库\n  let store = createPinia();\n  \u002F\u002F对外暴露,安装仓库\n  export default store;\n  ```\n\n* ```typescript\n  \u002F\u002F 主文件 main.ts 引入仓库\n  \u002F\u002F引入仓库\n  import store from '.\u002Fstore'\n  app.use(store)\n  ```\n\n* 创建小仓库  `scr\u002Fmodules\u002Finfo.ts`      **(选择式api用法)**\n\n* ```typescript\n  \u002F\u002F定义info小仓库\n  import { defineStore } from \"pinia\";\n  \n  \u002F\u002F第一个仓库:小仓库名字  第二个参数:小仓库配置对象\n  \u002F\u002FdefineStore方法执行会返回一个函数,函数作用就是让组件可以获取到仓库数据\n  let useInfoStore = defineStore(\"info\", {\n      \u002F\u002F存储数据:state\n      state: () => {\n          return {\n              count: 99,\n              arr: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]\n          }\n      },\n      \n      \u002F\u002F 业务逻辑: actions\n      actions: {\n          \u002F\u002F注意:函数没有context上下文对象\n          \u002F\u002F没有commit、没有mutations去修改数据\n          updateNum(a: number, b: number) {\n              this.count += a;\n          }\n      },\n      \n      \u002F\u002F 计算属性:getters\n      getters: {\n          total() {\n              \u002F\u002F 计算和\n              let result:any = this.arr.reduce((prev: number, next: number) => {\n                  return prev + next;\n              }, 0);\n              return result;\n          }\n      }\n  });\n  \u002F\u002F对外暴露方法\n  export default useInfoStore;\n  ```\n\n* 使用仓库数据，方法\n\n* ```vue\n  \u003Ctemplate>\n    \u003Cdiv class=\"child\">\n      \u003Ch1>{{ infoStore.count }}---{{infoStore.total}}\u003C\u002Fh1>\n      \u003Cbutton @click=\"updateCount\">点击我修改仓库数据\u003C\u002Fbutton>\n    \u003C\u002Fdiv>\n  \u003C\u002Ftemplate>\n  \n  \u003Cscript setup lang=\"ts\">\n      \n  import useInfoStore from \"..\u002F..\u002Fstore\u002Fmodules\u002Finfo\";\n  \u002F\u002F获取小仓库对象\n  let infoStore = useInfoStore();\n  console.log(infoStore);\n      \n  \u002F\u002F修改数据方法\n  const updateCount = () => {\n    \u002F\u002F仓库调用自身的方法去修改仓库的数据\n    infoStore.updateNum(66,77);\n  };\n  \u003C\u002Fscript>\n  \n  ```\n\n* 组合式api\n\n* ```typescript\n  \u002F\u002F定义组合式API仓库\n  import { defineStore } from \"pinia\";\n  import { ref, computed,watch} from 'vue';\n  \u002F\u002F创建小仓库\n  let useTodoStore = defineStore('todo', () => {\n      let todos = ref([{ id: 1, title: '吃饭' }, { id: 2, title: '睡觉' }, { id: 3, title: '打豆豆' }]);\n      let arr = ref([1,2,3,4,5]);\n  \n      const total = computed(() => {\n          return arr.value.reduce((prev, next) => {\n              return prev + next;\n          }, 0)\n      })\n      \u002F\u002F务必要返回一个对象:属性与方法可以提供给组件使用\n      return {\n          todos,\n          arr,\n          total,\n          updateTodo() {\n              todos.value.push({ id: 4, title: '组合式API方法' });\n          }\n      }\n  });\n  \n  export default useTodoStore;\n  ```\n\n\n\n## 2.8 插槽 (`slot`)：灵活的内容分发\n\n插槽是 Vue 组件提供内容分发能力的机制，它允许父组件向子组件的指定位置注入内容，从而实现更灵活的组件组合。插槽分为默认插槽、具名插槽和作用域插槽。\n\n**默认插槽：**\n\n子组件定义一个 `\u003Cslot>` 标签，父组件在使用子组件时，在双标签内部书写的内容将填充到这个插槽中。\n\n```vue\n子组件 Test.vue\n\u003Ctemplate>\n  \u003Cdiv class=\"box\">\n    \u003Ch1>我是子组件默认插槽\u003C\u002Fh1>\n    \u003C!-- 默认插槽 -->\n    \u003Cslot>\u003C\u002Fslot>\n    \u003Ch1>我是子组件默认插槽\u003C\u002Fh1>\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n父组件\n    \u003CTest>\n      \u003Cdiv>\n        \u003Cpre>大江东去浪淘尽,千古分流人物\u003C\u002Fpre>\n      \u003C\u002Fdiv>\n    \u003C\u002FTest>\n\n\u003Cscript setup lang=\"ts\">\nimport Test from \".\u002FTest.vue\";\n\u003C\u002Fscript>\n```\n\n**具名插槽：**\n\n子组件定义多个带有 `name` 属性的插槽，父组件通过 `v-slot:` 或 `#` 指令指定内容填充到哪个具名插槽。\n\n```vue\n子组件 Test.vue\n\u003Ch1>具名插槽填充数据\u003C\u002Fh1>\n \u003Cslot name=\"a\">\u003C\u002Fslot>\n \u003Ch1>具名插槽填充数据\u003C\u002Fh1>\n\n \u003Ch1>具名插槽填充数据\u003C\u002Fh1>\n \u003Cslot name=\"b\">\u003C\u002Fslot>\n \u003Ch1>具名插槽填充数据\u003C\u002Fh1>\n\n父组件\n\u003C!-- 具名插槽填充a -->\n      \u003Ctemplate v-slot:a>\n        \u003Cdiv>我是填充具名插槽a位置结构\u003C\u002Fdiv>\n      \u003C\u002Ftemplate>\n      \u003C!-- 具名插槽填充b v-slot指令可以简化为# -->\n      \u003Ctemplate #b>\n        \u003Cdiv>我是填充具名插槽b位置结构\u003C\u002Fdiv>\n      \u003C\u002Ftemplate>\n```\n\n![image-20260129222630502](images\u002Fimage-20260129222630502.png)\n\n\n\n**作用域插槽：**\n\n作用域插槽允许子组件在渲染插槽内容时向父组件传递数据，父组件可以根据这些数据决定插槽内容的结构和样式。\n\n```vue\n父组件\n  \u003CTest1 :todos=\"todos\">    \u003C!-- 将数据传给子组件 -->\n      \u003Ctemplate v-slot=\"{ $row, $index }\">\n \t\t\u003C!-- 父组件决定子组件的结构与外观 -->\n        \u003Cp :style=\"{ color: $row.done ? 'green' : 'red' }\">\n          {{ $row.title }}--{{ $index }}\n        \u003C\u002Fp>\n      \u003C\u002Ftemplate>\n      \n     \u003C!-- 解构\n\t\t\u003Ctemplate v-slot=\"slotProps\">\n \t \t\u003Cp>\n    \t\t {{ slotProps.$row.title }}\n   \t\t\t {{ slotProps.$index }}\n  \t\t\u003C\u002Fp>\n\t\t\u003C\u002Ftemplate>\n\t\t\t\t-->\n    \u003C\u002FTest1>\n\n\u003Cscript setup lang=\"ts\">\n\nimport Test1 from \".\u002FTest1.vue\";\nimport { ref } from \"vue\";\n\u002F\u002F 父组件内部数据\nlet todos = ref([\n  { id: 1, title: \"吃饭\", done: true },\n  { id: 2, title: \"睡觉\", done: false },\n  { id: 3, title: \"打豆豆\", done: true },\n  { id: 4, title: \"打游戏\", done: false },\n]);\n\u003C\u002Fscript>\n\n\n子组件 Test1.vue\n\u003Ctemplate>\n  \u003Cdiv class=\"box\">\n    \u003Ch1>作用域插槽\u003C\u002Fh1>\n    \u003Cul>\n         \u003C!-- 组件内部遍历数组 -->\n      \u003Cli v-for=\"(item, index) in todos\" :key=\"item.id\">\n        \u003C!--作用域插槽:可以将数据回传给父组件-->\n        \u003Cslot :$row=\"item\" :$index=\"index\">\u003C\u002Fslot>\n      \u003C\u002Fli>\n    \u003C\u002Ful>\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup lang=\"ts\">\n\u002F\u002F通过props接受父组件传递数据\ndefineProps([\"todos\"]);\n\u003C\u002Fscript>\n\n```\n\n\n\n# 二、项目初始化与规范化：构建高质量代码的基石\n\n一个高质量的项目离不开严格的代码规范和统一的开发流程。“硅谷甄选运营平台”项目在初始化阶段就集成了多项工具，确保了代码的质量和团队协作的顺畅\n\n## 2.1 环境准备与项目初始化\n\n项目采用 **Vue3** 和 **Vite** 进行构建，并强制使用 **pnpm** 作为包管理器，以确保依赖安装的一致性和高性能。\n\n**环境要求：**\n\n- Node.js v16.14.2+\n\n- pnpm 8.0.0+\n\n- **pnpm 安装：**\n\n  ```css\n  npm i -g pnpm\n  ```\n\n  ![image-20260129233201061](images\u002Fimage-20260129233201061.png)\n\n  **项目初始化命令：**\n\n  ```lua\n  pnpm create vite\n  ```\n\n  * 项目名字\n  * 项目框架\n  * 语言选择\n\n  ![image-20260129233658337](images\u002Fimage-20260129233658337.png)\n\n  * 如果没有自动安装依赖\n\n    ​\t手动安装依赖\n\n    ​\t进入项目根目录后，运行 `pnpm install` 安装所有依赖，然后 `pnpm run dev` 即可启动项目。\n\n  * 项目目录结构\n\n    ![image-20260129234148057](images\u002Fimage-20260129234148057.png)\n\n  ## 注意：\n\n  ### 1️⃣ `node_modules`（**依赖仓库**）\n\n  ```\n  node_modules\u002F\n  ```\n\n  - 所有第三方包都在这\n  - 比如：vue、vite、typescript……\n  - **不要动、不要删、不要提交到 git**\n\n  ### 2️⃣ `src`（你 90% 的代码都在这）\n\n  ```\n  src\u002F\n  ```\n\n  这是**主战场**，之后你会在里面：\n\n  - 写页面\n  - 写组件\n  - 配路由\n  - 写状态管理（pinia）\n\n  👉 admin 项目，`src` 就是“总部”\n\n  ### 3️⃣ `public`（静态资源）\n\n  ```\n  public\u002F\n  ```\n\n  - logo\n  - favicon\n  - 不需要打包处理的资源\n\n  使用方式：\n\n  ```\n  \u003Cimg src=\"\u002Flogo.png\" \u002F>\n  ```\n\n  ### 4️⃣ `index.html`（入口 HTML，非常重要）\n\n  ```\n  index.html\n  ```\n\n  ⚠️ Vite 的一个**大特点**：\n\n  - 这是项目的 **真正入口**\n  - 不是 build 后生成的\n  - **开发阶段就直接用**\n\n  里面会看到：\n\n  ```\n  \u003Cdiv id=\"app\">\u003C\u002Fdiv>\n  \u003Cscript type=\"module\" src=\"\u002Fsrc\u002Fmain.ts\">\u003C\u002Fscript>\n  ```\n\n  ### 5️⃣ `package.json`（项目说明书）\n\n  ```\n  {\n    \"scripts\": {\n      \"dev\": \"vite\",\n      \"build\": \"vite build\"\n    }\n  }\n  ```\n\n  它决定了：\n\n  - 项目叫什么\n  - 用了哪些依赖\n  - `pnpm run dev` 是干嘛的\n\n  ### 6️⃣ `pnpm-lock.yaml`（依赖锁）\n\n  - 锁定依赖版本\n  - **不能随便删**\n  - 团队协作非常重要\n\n  ### 7️⃣ `vite.config.ts`（Vite 配置文件）\n\n  ```\n  export default defineConfig({\n    plugins: [vue()]\n  })\n  ```\n\n  后面你会在这配置：\n\n  - 路径别名 `@`\n  - 代理（解决跨域）\n  - 打包配置\n\n  ### 8️⃣ `tsconfig*.json`（TypeScript 配置）\n\n  ```\n  tsconfig.json\n  tsconfig.app.json\n  tsconfig.node.json\n  ```\n\n  - 给 TS 编译器用的\n  - 初期基本不用碰\n  - 后面你学 TS 深了才会改\n\n  ### 9️⃣ `.vscode`（编辑器配置）\n\n  - VS Code 专用\n  - 代码格式、插件配置\n  - 不影响项目运行\n\n- scr\u002Fstyle.css 删掉\n\n  ![image-20260129234946814](images\u002Fimage-20260129234946814.png)\n\n- APP.vue 代码删除\n\n  ![image-20260129235030953](images\u002Fimage-20260129235030953.png)\n\n- index.html 改标题\n\n  ![image-20260129235601820](images\u002Fimage-20260129235601820.png)\n\n\n\n## 2.2 项目配置：代码质量与开发效率的保障\n\n### 2.2.1 项目自动启动\n\n package.json 中 \"dev\"项目启动 加上 --open 当项目启动的时候自动打开浏览器运行项目\n\n![image-20260130000152141](images\u002Fimage-20260130000152141.png)\n\n### 2.2.2  ESLint 配置：JavaScript\u002FTypeScript 代码质量检测\n\nESLint 是一个可插拔的 JavaScript\u002FTypeScript 代码检测工具，用于发现和报告代码中的问题。\n\n**安装：**\n\n`pnpm create @eslint\u002Fconfig@latest`\n\n![image-20260130134425119](images\u002Fimage-20260130134425119.png)\n\n![image-20260130134442157](images\u002Fimage-20260130134442157.png)\n\n![image-20260130122120694](images\u002Fimage-20260130122120694.png)\n\n\n\n**vue3校验插件安装**\n\n```javascript\npnpm install -D eslint-plugin-import eslint-plugin-vue eslint-plugin-node eslint-plugin-prettier eslint-config-prettier eslint-plugin-node @babel\u002Feslint-parser\n```\n\n重新配置文件\n\n```javascript\nimport js from \"@eslint\u002Fjs\"\nimport globals from \"globals\"\nimport tseslint from \"typescript-eslint\"\nimport pluginVue from \"eslint-plugin-vue\"\nimport importPlugin from \"eslint-plugin-import\"\nimport nPlugin from \"eslint-plugin-n\"\nimport prettierPlugin from \"eslint-plugin-prettier\"\nimport prettierConfig from \"eslint-config-prettier\"\nimport { defineConfig } from \"eslint\u002Fconfig\"\n\nexport default defineConfig([\n   \u002F\u002F 配置需要忽略的文件或目录，例如 dist 和 node_modules\n    {\n    ignores: [\"node_modules\u002F**\", \"dist\u002F**\", \"build\u002F**\"],\n  },\n    \n  \u002F\u002F 基础 JavaScript 规则\n  {\n    files: [\"**\u002F*.{js,mjs,cjs,ts,mts,cts,vue}\"],\n    plugins: {\n      js,\n      import: importPlugin,\n      n: nPlugin,\n      prettier: prettierPlugin,\n    },\n    extends: [\n      \"js\u002Frecommended\",\n      prettierConfig, \u002F\u002F 关闭和 Prettier 冲突的规则\n    ],\n    languageOptions: {\n      globals: {\n        ...globals.browser,\n        ...globals.node,\n      },\n      parserOptions: {\n        ecmaVersion: \"latest\",\n        sourceType: \"module\",\n      },\n    },\n    rules: {\n      \"prettier\u002Fprettier\": \"warn\",\n      \"import\u002Forder\": [\n        \"warn\",\n        {\n          groups: [\"builtin\", \"external\", \"internal\", \"parent\", \"sibling\", \"index\"],\n          \"newlines-between\": \"always\",\n        },\n      ],\n      \"n\u002Fno-process-exit\": \"off\",\n    },\n  },\n\n  \u002F\u002F TypeScript 规则\n  tseslint.configs.recommended,\n\n  \u002F\u002F Vue 规则\n  pluginVue.configs[\"flat\u002Fessential\"],\n\n  \u002F\u002F Vue 文件中 TS 解析\n  {\n    files: [\"**\u002F*.vue\"],\n    languageOptions: {\n      parserOptions: {\n        parser: tseslint.parser,\n      },\n    },\n  },\n])\n\n```\n\n注意：\n\n`在cmd命令行中运行\npnpm remove eslint-plugin-node   移除旧版\npnpm add -D eslint-plugin-n      安装新版`\n\n\n\n在 `package.json` 中添加运行脚本，方便进行代码检查和修复：\n\n```javascript\n\"scripts\": {\n    \"lint\": \"eslint src\",\n    \"fix\": \"eslint src --fix\"\n}\n```\n\n配置其他相关\n\n```javascript\n rules: {\n      \u002F\u002F JavaScript 基础规则\n      'no-var': 'error', \u002F\u002F 禁止使用 var，强制使用 let\u002Fconst\n      'no-multiple-empty-lines': ['warn', { max: 1 }], \u002F\u002F 警告：最多允许一行空行\n      'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off', \u002F\u002F 生产环境禁止使用 console\n      'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off', \u002F\u002F 生产环境禁止使用 debugger\n      'no-unexpected-multiline': 'error', \u002F\u002F 防止由于自动分号插入引发的 bug\n      'no-useless-escape': 'off', \u002F\u002F 允许多余的转义字符（某些正则场景需要）\n\n      \u002F\u002F TypeScript 相关规则\n      '@typescript-eslint\u002Fno-unused-vars': 'error', \u002F\u002F 禁止未使用的变量\n      '@typescript-eslint\u002Fprefer-ts-expect-error': 'error', \u002F\u002F 更推荐使用 ts-expect-error 替代 ts-ignore\n      '@typescript-eslint\u002Fno-explicit-any': 'off', \u002F\u002F 允许使用 any 类型\n      '@typescript-eslint\u002Fno-non-null-assertion': 'off', \u002F\u002F 允许使用非空断言（!）\n      '@typescript-eslint\u002Fno-namespace': 'off', \u002F\u002F 允许使用命名空间（namespace）\n      '@typescript-eslint\u002Fsemi': 'off', \u002F\u002F 关闭对分号的强制检查\n\n      \u002F\u002F Vue 相关规则\n      'vue\u002Fmulti-word-component-names': 'off', \u002F\u002F 允许使用单词组件名（适用于页面组件等）\n      \u002F\u002F 'vue\u002Fscript-setup-uses-vars': 'error', \u002F\u002F 新版插件可能移除了此规则，先注释避免错误\n      'vue\u002Fno-mutating-props': 'off', \u002F\u002F 允许修改 props（某些业务场景可能需要）\n      'vue\u002Fattribute-hyphenation': 'off', \u002F\u002F 关闭模板中 attribute 必须使用连字符的限制\n    },\n  },\n)\n```\n\n安装加载ts格式的配置文件\n\n`pnpm add -D jiti`\n\n检查代码规范\n\n`pnpm run lint`\n\n修复代码规范\n\n`pnpm run fix`\n\n### 2.2.2 Prettier 配置：代码格式化工具\n\nPrettier 是一个强大的代码格式化工具，它专注于代码的美观和一致性，支持多种语言。ESLint 保证代码质量，而 Prettier 保证代码美观。两者结合，相得益彰。\n\n**安装 Prettier 相关依赖：**\n\n```mipsasm\npnpm install -D eslint-plugin-prettier prettier eslint-config-prettier\n```\n\n**新建.prettierrc 文件**\n\n![image-20260130152740128](images\u002Fimage-20260130152740128.png)\n\n\n\n\n\n``semi`: 代码末尾加分号\n\n`singleQuote`: 用单引号替代双引号\n\n`printWidth`: 最大行长度\n\n`tabWidth`: tab缩进宽度\n\n`trailingComma`: 多行时尾逗号风格\n\n`arrowParens`: 箭头函数参数是否加括号\n\n`endOfLine`: 换行符，防止跨平台差异`\n\n\n\n\n\n**新建.prettierignore 忽略文件**\n\n![image-20260130152833257](images\u002Fimage-20260130152833257.png)\n\n**vscode新建settings.json文件**\n\n保存文件时自动格式化代码\n\n统一使用 Prettier 插件格式化 `.vue`、`.ts`、`.js` 文件\n\n```javascript\n{\n  \"editor.formatOnSave\": true,\n  \"[vue]\": {\n    \"editor.defaultFormatter\": \"esbenp.prettier-vscode\"\n  },\n  \"[typescript]\": {\n    \"editor.defaultFormatter\": \"esbenp.prettier-vscode\"\n  },\n  \"[javascript]\": {\n    \"editor.defaultFormatter\": \"esbenp.prettier-vscode\"\n  }\n}\n\n```\n\n\n\n### 2.2.3 Stylelint 配置：CSS\u002FSCSS 代码规范\n\nStylelint 是 CSS 的 Lint 工具，用于格式化 CSS 代码、检查语法错误和不合理的写法，并指定 CSS 书写顺序等。项目中使用 SCSS 作为预处理器。\n\n**安装 Stylelint 及其相关依赖：**\n\n`pnpm add sass sass-loader stylelint postcss postcss-scss postcss-html stylelint-config-prettier stylelint-config-recess-order stylelint-config-recommended-scss stylelint-config-standard stylelint-config-standard-vue stylelint-scss stylelint-order stylelint-config-standard-scss -D`\n\n**创建.stylelintrc.cjs文件，配置**\n\n```javascript\nmodule.exports = {\n  ignoreFiles: ['**\u002F*.min.css', '**\u002Fnode_modules\u002F**', '**\u002Fdist\u002F**', '**\u002Funpackage\u002F**'],\n\n  extends: [\n    'stylelint-config-standard',\n    'stylelint-config-recess-order',\n    'stylelint-config-prettier',\n  ],\n\n  plugins: ['stylelint-scss'],\n\n  overrides: [\n    {\n      files: ['**\u002F*.vue', '**\u002F*.html'],\n      customSyntax: 'postcss-html',\n    },\n    {\n      files: ['**\u002F*.scss'],\n      customSyntax: 'postcss-scss',\n    },\n  ],\n\n  rules: {\n    \u002F* ===== 新手期先别炸 ===== *\u002F\n    'no-empty-source': null,\n    'block-no-empty': null,\n    'declaration-empty-line-before': null,\n\n    \u002F* ===== 基础风格 ===== *\u002F\n    indentation: 2,\n    'string-quotes': 'single',\n    'color-hex-case': 'lower',\n    'number-leading-zero': 'always',\n\n    \u002F* ===== 命名规则放行 ===== *\u002F\n    'selector-class-pattern': null,\n    'custom-property-pattern': null,\n    'keyframes-name-pattern': null,\n\n    \u002F* ===== uniapp \u002F vue 深度选择器 ===== *\u002F\n    'selector-pseudo-element-no-unknown': [\n      true,\n      {\n        ignorePseudoElements: ['v-deep', 'deep'],\n      },\n    ],\n\n    \u002F* ===== scss \u002F uniapp at-rule 全放行 ===== *\u002F\n    'at-rule-no-unknown': null,\n\n    \u002F* ===== :global ===== *\u002F\n    'selector-pseudo-class-no-unknown': [\n      true,\n      {\n        ignorePseudoClasses: ['global'],\n      },\n    ],\n  },\n};\n\n```\n\n**配置package.json文件**\n\n```json\n{\n  \"scripts\": {\n    \"lint\": \"eslint .\",\n    \"lint:fix\": \"eslint . --fix\",\n   \t\"format\": \"prettier --write \\\".\u002F**\u002F*.{html,vue,ts,js,json,md}\\\"\", \u002F\u002F 添加这个\n  }\n}\n```\n\n**运行**\n\n`pnpm run format`\n\n\n\n### 2.2.4 Husky 配置：Git Hook 自动化\n\n为了强制开发人员遵循代码规范，我们引入了 Husky。Husky 可以在 Git 提交（commit）前触发 Git 钩子，自动执行代码格式化和检查。\n\n**安装 Husky：**\n\n```mipsasm\npnpm install -D husky\n```\n\n**先初始化git 仓库**\n\n`git init`\n\n**初始化 Husky：**\n\n```csharp\nnpx husky-init\n```\n\n这会在项目根目录下生成 `.husky` 目录，其中包含 `pre-commit` 文件。\n\n**修改 `.husky\u002Fpre-commit` 文件：**\n\n```perl\npnpm run format # 在提交前自动执行格式化\n```\n\n这样，每次执行 `git commit` 时，代码都会自动格式化。\n\n\n\n### 2.2.5 Commitlint 配置：统一 Git 提交信息规范\n\n为了保持 Git 提交信息的清晰和一致性，我们使用 Commitlint 来强制执行提交规范。\n\n**安装 Commitlint：**\n\n```sql\npnpm add @commitlint\u002Fconfig-conventional @commitlint\u002Fcli -D\n```\n\n**创建 `commitlint.config.cjs` 配置文件：**\n\n```java\n\u002F\u002F commitlint.config.cjs\nmodule.exports = {\n  \u002F\u002F 继承社区推荐的 'config-conventional' 规范\n  extends: ['@commitlint\u002Fconfig-conventional'],\n\n  \u002F\u002F 自定义规则 (0=禁用, 1=警告, 2=报错)\n  rules: {\n    \u002F\u002F type 类型必须是以下之一\n    'type-enum': [\n      2,\n      'always',\n      [\n        'feat',     \u002F\u002F 新功能\n        'fix',      \u002F\u002F 修复bug\n        'docs',     \u002F\u002F 文档\n        'style',    \u002F\u002F 代码格式\n        'refactor', \u002F\u002F 重构\n        'perf',     \u002F\u002F 性能优化\n        'test',     \u002F\u002F 测试\n        'chore',    \u002F\u002F 构建或辅助工具变动\n        'revert',   \u002F\u002F 回退\n        'build',    \u002F\u002F 打包\n      ],\n    ],\n\n    \u002F\u002F type 大小写: 不作限制\n    'type-case': [0],\n\n    \u002F\u002F type 是否为空: 不作限制\n    'type-empty': [0],\n\n    \u002F\u002F scope 是否为空: 不作限制\n    'scope-empty': [0],\n\n    \u002F\u002F scope 大小写: 不作限制\n    'scope-case': [0],\n\n    \u002F\u002F subject 结尾标点: 不作限制\n    'subject-full-stop': [0, 'never'],\n\n    \u002F\u002F subject 大小写: 不作限制\n    'subject-case': [0, 'never'],\n\n    \u002F\u002F header 最大长度: 不作限制\n    'header-max-length': [0, 'always', 72],\n  },\n};\n```\n\n在 `package.json` 中添加 Commitlint 脚本：\n\n```json\n\"scripts\": {\n    \"commitlint\": \"commitlint --config commitlint.config.cjs -e -V\"\n}\n```\n\n配置 Husky，在 `commit-msg` 钩子中执行 Commitlint 检查：(cmd命令行运行)\n\n```sql\nnpx husky add .husky\u002Fcommit-msg\n```\n\n在生成的 `.husky\u002Fcommit-msg` 文件中添加：(删除里面的undefined),添加下面这个\n\n```undefined\npnpm commitlint\n```\n\n**git提交规范**\n\n`git commit -m \"feat: 增加用户登录功能\"\ngit commit -m \"fix: 修复登录接口错误\"`\n\n\n\n# 三、项目集成：提升开发效率与用户体验\n\n在项目规范化之后，我们将核心的第三方库和工具集成到项目中，进一步提升开发效率和用户体验。\n\n## 3.1 Element Plus 集成：美观高效的 UI 组件库\n\n硅谷甄选运营平台采用 Element Plus 作为 UI 组件库，它提供了丰富的组件和友好的开发体验。\n\n**安装 Element Plus：**\n\n```bash\npnpm install element-plus @element-plus\u002Ficons-vue\n```\n\n```typescript\nimport { createApp } from 'vue';\nimport App from '.\u002FApp.vue';\n\n\u002F\u002F 引入element-plus插件与样式\nimport ElementPlus from 'element-plus';\nimport 'element-plus\u002Fdist\u002Findex.css';\n\u002F\u002F 配置element-plus国际化\nimport { zhCn } from 'element-plus\u002Fes\u002Flocales.mjs';\n\n\u002F\u002F 获取应用实例对象\nconst app = createApp(App);\n\u002F\u002F 安装element-plus插件\napp.use(ElementPlus,{\n    locale:zhCn \u002F\u002F element-plus国际化\n});\n\u002F\u002F 将应用挂在到挂载点上\napp.mount('#app');\n\n```\n\n## 3.2 `src` 别名配置：简化模块导入\n\n为了简化文件路径，提高代码可读性，我们为 `src` 目录配置了 `@` 别名。\n\n**`vite.config.ts` 配置：**\n\n```typescript\nimport { defineConfig } from 'vite';\nimport vue from '@vitejs\u002Fplugin-vue';\nimport path from 'path';\n\nexport default defineConfig({\n  plugins: [vue()],\n  resolve: {\n    alias: {\n      '@': path path.resolve(__dirname, '.\u002Fsrc'),\n    },\n  },\n});\n```\n\n**`tsconfig.json` TypeScript 编译配置：**\n\n```typescript\n{\n  \"compilerOptions\": {\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"@\u002F*\": [\"src\u002F*\"]\n    }\n  }\n}\n```\n\n`eslint.config.ts` **ESLint 导入解析器配置**：\n\n```typescript\nsettings: {\n  'import\u002Fresolver': {\n    typescript: {\n      alwaysTryTypes: true,\n      project: '.\u002Ftsconfig.app.json',\n    },\n  },\n}\n```\n\n**创建**`.eslint-import-resolver-typescript.json `**ESLint 导入解析器配置**\n\n```json\n{\n  \"alwaysTryTypes\": true,\n  \"project\": \".\u002Ftsconfig.app.json\"\n}\n```\n\n**安装依赖**\n\n```bash\npnpm add -D @types\u002Fnode\npnpm add -D eslint-import-resolver-typescript\n```\n\n\n\n## 3.3 环境变量配置：灵活适应多环境部署\n\n项目开发通常会经历开发、测试和生产等不同环境，每个环境的接口地址等配置可能不同。通过环境变量配置，我们可以轻松地在不同环境之间切换。\n\n**项目根目录创建环境变量文件：**\n\n- `.env.development` (开发环境)\n- `.env.production` (生产环境)\n- `.env.test` (测试环境)\n\n**`.env.development` 示例：**\n\n```ini\n✅ Node 环境变量\n❌ 前端代码里不能直接读（Vite 不会暴露它）\n✔ 常用于：\n后端 Node\n构建工具判断环境\nNODE_ENV = 'development' \n\n✅ 可以在前端读取\n用途：\n页面标题\n系统名称\nlogo 旁边文字\nVITE_APP_TITLE = '硅谷甄选运营平台'\n\n✅ 前端可读\n✅ 常配合 代理 使用\nVITE_APP_BASE_API = '\u002Fdev-api' # 变量必须以 VITE_ 为前缀才能暴露给外部读取\n```\n\n**`.env.test` 示例：**\n\n```ini\nNODE_ENV = 'test'\nVITE_APP_TITLE = '硅谷甄选运营平台'\nVITE_APP_BASE_API = '\u002Ftest-api' # 变量必须以 VITE_ 为前缀才能暴露给外部读取\n\n```\n\n**`.env.production` 示例：**\n\n```ini\nNODE_ENV = 'production'\nVITE_APP_TITLE = '硅谷甄选运营平台'\nVITE_APP_BASE_API = '\u002Fprod-api' # 变量必须以 VITE_ 为前缀才能暴露给外部读取\nVITE_SERVE=\"HTTP:\u002F\u002Fyyy.com\"\n```\n\n\n\n**`package.json` 配置运行命令：**\n\n```json\n\"scripts\": {\n    \"dev\": \"vite --open\",\n    \"build:test\": \"vue-tsc && vite build --mode test\",\n    \"build:pro\": \"vue-tsc && vite build --mode production\",\n    \"preview\": \"vite preview\"\n}\n```\n\n在代码中通过 `import.meta.env` 对象获取环境变量。\n\n![image-20260131131902927](images\u002Fimage-20260131131902927.png)\n\n## 3.4 SVG 图标配置与封装：轻量级矢量图方案\n\nSVG 矢量图相比传统图片资源具有体积小、不失真、性能优越等优点。项目中通过 `vite-plugin-svg-icons` 插件集成 SVG 图标。\n\n**安装插件：**\n\n```mipsasm\npnpm install vite-plugin-svg-icons -D\n```\n\n**`vite.config.ts` 配置插件：**\n\n```javascript\nimport { defineConfig } from 'vite';\nimport vue from '@vitejs\u002Fplugin-vue';\nimport { createSvgIconsPlugin } from 'vite-plugin-svg-icons';\nimport path from 'path';\n\nexport default defineConfig(({ command }) => {\n  return {\n    plugins: [\n      vue(),\n      createSvgIconsPlugin({\n        \u002F\u002F 指定需要缓存的图标文件夹\n        iconDirs: [path.resolve(process.cwd(), 'src\u002Fassets\u002Ficons')],\n        \u002F\u002F 指定 symbolId 的格式\n        symbolId: 'icon-[dir]-[name]',\n      }),\n    ],\n  };\n});\n```\n\n**入口文件 `main.ts` 导入：**\n\n```go\nimport 'virtual:svg-icons-register'; \u002F\u002F注册 SVG 图标 ，让 Vite 的 SVG 插件生效。\n```\n\n**注意：在src\u002Fassets下创建icons**\n\n**SVG图标使用**\n\n```vue\n\u003Ctemplate>\n  \u003Cdiv class=\"\">\n    \u003Ch1>测试\u003C\u002Fh1>\n    \u003C!-- svg图标使用 -->\n    \u003Csvg style=\"width: 30px;height: 30px;\">\n      \u003Cuse xlink:href=\"#icon-suo\" fill=\"\">\u003C\u002Fuse>\n    \u003C\u002Fsvg>\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n```\n\n**SVG 图标全局组件封装 (`src\u002Fcomponents\u002FSvgIcon\u002Findex.vue`)：**\n\n为了方便使用，我们将 SVG 图标封装成一个全局组件 `SvgIcon`。\n\n```vue\n\u003Ctemplate>\n    \u003Csvg  class=\"svg-icon\" :style=\"{ width: width, height: height }\">\n      \u003Cuse :href=\"prefix + name\" :fill=\"color\">\u003C\u002Fuse>\n    \u003C\u002Fsvg>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup lang=\"ts\">\ndefineProps({\n  \u002F\u002F xlink:href 属性值的前缀\n  prefix: {\n    type: String,\n    default: '#icon-'    \u002F\u002F default 是 Vue 官方提供的 props 选项，用来给“父组件没传的 prop”一个默认值。\n\t\t\t\n  },\n  \u002F\u002F svg 矢量图的名字\n  name: String,\n  \u002F\u002F svg 图标的颜色\n  color: {\n    type: String,\n    default: \"\"\n  },\n  \u002F\u002F svg 宽度\n  width: {\n    type: String,\n    default: '16px'\n  },\n  \u002F\u002F svg 高度\n  height: {\n    type: String,\n    default: '16px'\n  }\n});\n\u003C\u002Fscript>\n\u003Cstyle scoped>\n   .svg-icon {\n  fill: currentColor;        \u002F\u002F 让 SVG 图形的填充颜色跟随当前元素的字体颜色，保持一致。\n  vertical-align: middle;    \u002F\u002F 是让行内或行内块元素在行高里垂直居中，更美观对齐文本和其他元素。\n}\n\u003C\u002Fstyle>\n\n\n```\n\n**全局组件注册 (`src\u002Fcomponents\u002Findex.ts`)：**\n\n```typescript\n\u002F\u002F 引入项目中的全部组件\nimport SvgIcon from '.\u002FSvgIcon\u002Findex.vue';\nimport type { App, Component } from 'vue';\n\u002F\u002F 全局对象\nconst components: Record\u003Cstring, Component> = { SvgIcon };\n\u002F\u002F 对外暴露插件对象\nexport default {\n    install(app: App) {\n        \u002F\u002F 注册项目全部的全局组件\n        Object.keys(components).forEach((key: string) => {\n            \u002F\u002F 注册为全局组件\n            app.component(key, components[key]!); \u002F\u002F !告诉ts不可能为空\n        });\n    }\n};\n```\n\n注意：\n\n`类型注解：`Record\u003Cstring, Component>`\n\n这是TypeScript的**工具类型**（Utility Type），用于约束`components`对象的结构：\n\n- `Record\u003CK, V>`表示：**键的类型为`K`，值的类型为`V`的对象**。\n- 这里`K`是`string`（键必须是字符串，即组件的名称），`V`是`Component`（值必须是Vue组件，来自`vue`的类型定义）。\n\n**作用**：确保`components`对象的键只能是字符串（组件名），值只能是Vue组件，避免类型错误（比如不小心放入非组件的值）。`\n\n\n\n**在 `main.ts` 中安装全局组件：引入自定义插件**\n\n```javascript\n\u002F\u002F 引入自定义插件\nimport gloablComponent from '.\u002Fcomponents\u002Findex';\n\u002F\u002F 安装自定义插件\napp.use(gloablComponent);\n```\n\n**使用**\n\n```vue\n\u003Ctemplate>\n  \u003Cdiv class=\"\">\n    \u003Ch1>测试\u003C\u002Fh1>\n    \u003C!-- svg图标使用 -->\n    \u003Csvg-icon name=\"suo\" color=\"red\" width=\"100px\" height=\"100px\">\u003C\u002Fsvg-icon>\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup lang=\"ts\">\nimport SvgIcon from '@\u002Fcomponents\u002FSvgIcon\u002Findex.vue'\n\u003C\u002Fscript>\n```\n\n`注意`：\n\n为什么使用组件是svg-icon呢，而不是SvgIcon\n\nVue官网推荐组件名（大驼峰）\n\nVue 会自动将 PascalCase （大驼峰）转换为 kebab-case （短横线）：\n\n自定义标签名：\n\n```typescript\nconst components = {\n  'my-custom-icon': SvgIcon,  \u002F\u002F ← 自定义标签名\n};\n```\n\n## 3.5 集成 Sass：增强样式开发能力\n\n项目已通过 Stylelint 配置安装了 `sass` 和 `sass-loader`，可以直接在 Vue 组件中使用 SCSS 语法，只需在 `\u003Cstyle>` 标签上添加 `lang=\"scss\"`。\n\n**引入全局样式和变量：**\n\n在 `src\u002Fstyles` 目录下创建 `index.scss` (用于引入全局重置样式) 和 `variable.scss` (用于定义全局 SCSS 变量)。\n\n**`src\u002Fstyles\u002Findex.scss`：清楚默认样式**\n\n`npm官网`\n\n```scss\nhtml, body, div, span, applet, object, iframe,\nh1, h2, h3, h4, h5, h6, p, blockquote, pre,\na, abbr, acronym, address, big, cite, code,\ndel, dfn, em, img, ins, kbd, q, s, samp,\nsmall, strike, strong, sub, sup, tt, var,\nb, u, i, center,\ndl, dt, dd, ol, ul, li,\nfieldset, form, label, legend,\ntable, caption, tbody, tfoot, thead, tr, th, td,\narticle, aside, canvas, details, embed, \nfigure, figcaption, footer, header, hgroup, \nmenu, nav, output, ruby, section, summary,\ntime, mark, audio, video {\n\tmargin: 0;\n\tpadding: 0;\n\tborder: 0;\n\tfont-size: 100%;\n\tfont: inherit;\n\tvertical-align: baseline;\n}\n\u002F* HTML5 display-role reset for older browsers *\u002F\narticle, aside, details, figcaption, figure, \nfooter, header, hgroup, menu, nav, section {\n\tdisplay: block;\n}\nbody {\n\tline-height: 1;\n}\nol, ul {\n\tlist-style: none;\n}\nblockquote, q {\n\tquotes: none;\n}\nblockquote:before, blockquote:after,\nq:before, q:after {\n\tcontent: '';\n\tcontent: none;\n}\ntable {\n\tborder-collapse: collapse;\n\tborder-spacing: 0;\n}\n```\n\n**`main.ts` 引入全局样式：**\n\n```go\nimport '@\u002Fstyles\u002Findex.scss';\n```\n\n**`src\u002Fstyles\u002Findex.scss`：**\n\n```scss\n@use '.\u002Freset.scss'; \u002F\u002F 引入重置样式,npm官网复制\n\t\t\t\t\t\u002F\u002F 其他全局样式\n```\n\n**`vite.config.ts` 配置全局 SCSS 变量：**\n\n为了在所有组件中都能使用全局 SCSS 变量，需要在 Vite 配置中进行设置。\n\n```typescript\nimport { defineConfig } from 'vite';\nimport vue from '@vitejs\u002Fplugin-vue';\nimport path from 'path';\n\n\u002F\u002F 引入svg需要用到的插件\nimport { createSvgIconsPlugin } from 'vite-plugin-svg-icons';\n\nexport default defineConfig({\n  plugins: [\n    vue(),\n    createSvgIconsPlugin({\n      \u002F\u002F 指定需要缓存的图标文件夹\n      iconDirs: [path.resolve(process.cwd(), 'src\u002Fassets\u002Ficons')],\n      \u002F\u002F 指定 symbolId 的格式\n      symbolId: 'icon-[dir]-[name]',\n    }),\n  ],\n  resolve: {\n    alias: {\n      '@': path.resolve(__dirname, '.\u002Fsrc'),\n    },\n  },\n  css: {\n    preprocessorOptions: {\n      scss: {\n        javascriptEnabled: true,\n        additionalData: `@use \"@\u002Fstyles\u002Fvariable.scss\" as *;`, \u002F\u002F 引入全局变量\n      },\n    },\n  },\n});\n\n```\n\n## 3.6 Mock 数据：前端独立开发利器\n\n在后端接口尚未完成时，Mock 数据能够让前端独立进行开发和测试，提高开发效率。项目使用了 `vite-plugin-mock` 和 `mockjs`。\n\n**安装依赖：**\n\n```mipsasm\npnpm install -D vite-plugin-mock mockjs\n```\n\n```typescript\nexport default defineConfig(({ mode }) => {\n  const isDev = mode === 'development';\n    \u002F\u002F mode 的值：\n  \t\u002F\u002F - 'development'  开发环境\n  \t\u002F\u002F - 'production' 生产环境\n    \u002F\u002F - 'test'       测试环境\n\n  return {\n    plugins: [\n      vue(),\n      createSvgIconsPlugin({\n        \u002F\u002F 指定需要缓存的图标文件夹\n        iconDirs: [path.resolve(process.cwd(), 'src\u002Fassets\u002Ficons')],\n        \u002F\u002F 指定 symbolId 的格式\n        symbolId: 'icon-[dir]-[name]',\n      }),\n        \u002F\u002F 条件加载插件\n      isDev &&\n        viteMockServe({\n          mockPath: 'mock',\n          enable: true,\n        }),\n    ],\n```\n\n在项目根目录下创建\n\n**`mock\u002Fuser.ts` 示例：**\n\n在 `mock` 文件夹下创建 `user.ts` 文件，定义模拟的用户登录和信息接口。\n\n```typescript\n\u002F\u002F mock\u002Fuser.ts\n\u002F\u002F 用户信息数据\n\u002F\u002F 该函数返回一个数组，包含用户的两个信息\nfunction createUserList() {\n    return [\n        {\n            userId: 1,\n            avatar:\n                'https:\u002F\u002Fwpimg.wallstcn.com\u002Ff778738c-e4f8-4870-b634-56703b4acafe.gif',\n            username: 'admin',\n            password: '111111',\n            desc: '平台管理员',\n            roles: ['平台管理员'],\n            buttons: ['cuser.detail'],\n            routes: ['home'],\n            token: 'Admin Token',\n        },\n        {\n            userId: 2,\n            avatar:\n                'https:\u002F\u002Fwpimg.wallstcn.com\u002Ff778738c-e4f8-4870-b634-56703b4acafe.gif',\n            username: 'system',\n            password: '111111',\n            desc: '系统管理员',\n            roles: ['系统管理员'],\n            buttons: ['cuser.detail', 'cuser.user'],\n            routes: ['home'],\n            token: 'System Token',\n        },\n    ];\n}\n\u002F\u002F 对外暴露一个数组：数组里面包含两个接口\n\u002F\u002F 登录假的接口，获取用户信息的假的接口\nexport default [\n    \u002F\u002F 用户登录接口\n    {\n        url: '\u002Fapi\u002Fuser\u002Flogin', \u002F\u002F 请求地址\n        method: 'post', \u002F\u002F 请求方式\n        response: ({ body }) => {\n            \u002F\u002F 获取请求体携带过来的用户名与密码\n            const { username, password } = body;\n            \u002F\u002F 调用获取用户信息函数,用于判断是否有此用户\n            const checkUser = createUserList().find(\n                (item) => item.username === username && item.password === password,\n            );\n            \u002F\u002F 没有用户返回失败信息\n            if (!checkUser) {\n                return { code: 201, data: { message: '账号或者密码不正确' } };\n            }\n            \u002F\u002F 如果有返回成功信息\n            const { token } = checkUser;\n            return { code: 200, data: { token } };\n        },\n    },\n    \u002F\u002F 获取用户信息\n    {\n        url: '\u002Fapi\u002Fuser\u002Finfo',\n        method: 'get',\n        response: (request) => {\n            \u002F\u002F 获取请求头携带token\n            const token = request.headers.token;\n            \u002F\u002F 查看用户信息是否包含有次token用户\n            const checkUser = createUserList().find((item) => item.token === token);\n            \u002F\u002F 没有返回失败的信息\n            if (!checkUser) {\n                return { code: 201, data: { message: '获取用户信息失败' } };\n            }\n            \u002F\u002F 如果有返回成功信息\n            return { code: 200, data: { checkUser } };\n        },\n    },\n];\n\n```\n\n## 3.7 Axios 二次封装：统一网络请求与错误处理\n\n在前端项目中，Axios 是常用的 HTTP 客户端。对其进行二次封装，可以实现请求拦截、响应拦截、统一错误处理等功能，极大地提升开发效率和代码健壮性。\n\n**安装 Axios：**\n\n```mipsasm\npnpm install axios\n```\n\n**`src\u002Futils\u002Frequest.ts` Axios 二次封装：**\n\n```typescript\n\u002F\u002F 进行axios二次封装:使用请求与相应拦截器\nimport axios from 'axios';\nimport { ElMessage } from 'element-plus';\n\n\u002F\u002F 第一步：利用axios对象的create方法，去创建axios实例（其他的配置：基础路径，超时的时间）\nconst request = axios.create({\n  baseURL: import.meta.env.VITE_APP_BASE_API, \u002F\u002F 基础路径会携带上\u002Fapi\n  timeout: 5000, \u002F\u002F超时的时间\n});\n\u002F\u002F 第二步：request实例添加请求与响应拦截器\nrequest.interceptors.request.use((config) => {\n  \u002F\u002F config配置对象，headers属性请求头，经常给服务器端携带公共参数\n  return config;\n});\n\n\u002F\u002F 第三步：响应拦截器\nrequest.interceptors.response.use(\n  (response) => {\n    \u002F\u002F 成功回调\n    \u002F\u002F 简化数据\n\n    return response.data;\n  },\n  (error) => {\n    \u002F\u002F 失败回调，处理http网络错误\n    \u002F\u002F 定义一个变量：存储网络错误信息\n    let message = '';\n    \u002F\u002F http状态码\n    const status = error.response.status;\n    switch (status) {\n      case 401:\n        message = 'token过期';\n        break;\n      case 403:\n        message = '无权访问';\n        break;\n      case 404:\n        message = '请求地址错误';\n        break;\n      case 500:\n        message = '服务器出现问题';\n        break;\n      default:\n        message = '网络出现问题';\n        break;\n    }\n    \u002F\u002F 使用 Element Plus 的 ElMessage 显示错误信息\n    ElMessage({\n      type: 'error',\n      message: message,\n    });\n    return Promise.reject(error); \u002F\u002F继续向下传递错误\n  }\n);\n\u002F\u002F 对外暴露\nexport default request;\n\n```\n\n## 3.8 API 接口统一管理：清晰的接口结构\n\n为了避免接口地址硬编码、提高代码可读性和维护性，项目采用了统一的 API 接口管理方式。\n\n**在 `src` 目录下创建 `api` 文件夹，并按模块（如 `user`、`product`、`acl`）进行分类管理。**\n\n**`src\u002Fapi\u002Fuser\u002Findex.ts` 示例：**\n\n```typescript\n\u002F\u002F 统一管理项目用户相关的接口\nimport request from '@\u002Futils\u002Frequest';\n\nimport type { loginForm, loginResponse, userResponseData } from '.\u002Ftype';\n\u002F\u002F 统一管理接口\nenum API {\n  LOGIN_URL = '\u002Fuser\u002Flogin',\n  USERINFO_URL = '\u002Fuser\u002Finfo',\n}\n\u002F\u002F 暴露请求函数\n\u002F\u002F 登录接口方法\nexport const reqLogin = (data: loginForm) => request.post\u003Cany, loginResponse>(API.LOGIN_URL, data);\n\u002F\u002F 获取用户信息接口方法\nexport const reqUserInfo = () => request.get\u003Cany, userResponseData>(API.USERINFO_URL);\n\n```\n\n**`src\u002Fapi\u002Fuser\u002Ftype.ts` 示例 (定义数据类型)：**\n\n```typescript\n\u002F\u002F 登录接口需要携带参数ts类型\nexport interface loginForm {\n  username: string;\n  password: string;\n}\n\ninterface dataType {\n  token: string;\n}\n\u002F\u002F 登录接口返回的数据类型\nexport interface loginResponse {\n  code: number;\n  data: dataType;\n}\n\ninterface userInfo {\n  userId: number;\n  avater: string;\n  username: string;\n  password: string;\n  desc: string;\n  roles: string[];\n  buttons: string[];\n  routes: string[];\n  token: string;\n}\n\u002F\u002F 定义服务器返回用户信息相关的数据类型\ninterface user {\n  checkUser: userInfo;\n}\nexport interface userResponseData {\n  code: number;\n  data: user;\n}\n\n```\n\n这种模块化的接口管理方式，使得接口定义清晰、易于查找和维护，并且结合 TypeScript 提供了强大的类型检查，进一步提升了代码质量。\n\n\n\n# 四，项目的实现\n\n## 4.1 路由配置\n\n用 vue-router@4 配置路由\n\n安装\n\n`pnpm install vue-router`\n\n`src\u002Fviews`新建三个页面组件文件\u002Flogin\u002Findex.vue，\u002Fhome\u002Findex.vue，\u002F404\u002Findex.vue\n\n`src\u002Frouter\u002Findex.ts`创建路由器(路由器配置)\n\n```typescript\n\u002F\u002F 通过vue-router插件实现模板路由配置\nimport { createRouter, createWebHashHistory } from 'vue-router';\n\nimport { constantRoute } from '.\u002Frouters';\n\u002F\u002F创建路由器\nconst router = createRouter({\n  \u002F\u002F 使用 hash 模式（URL 带 #）\n  history: createWebHashHistory(),\n  \u002F\u002F 路由规则数组  \n  routes: constantRoute,\n  \u002F\u002F 页面切换时的滚动行为\n  scrollBehavior() {\n    return {\n      left: 0,\n      top: 0,\n    };\n  },\n});\n\nexport default router;\n\n```\n\n`src\u002Frouter\u002Frouters.ts`配置路由（路由规则的定义）\n\n```typescript\n\u002F\u002F 对外暴漏配置路由\nexport const constantRoute = [\n  {\n    \u002F\u002F 登录\n     \u002F\u002FURL 路径\n    path: '\u002Flogin',\n      \u002F\u002F 对应的页面组件\n    component: () => import('@\u002Fviews\u002Flogin\u002Findex.vue'),\n      \u002F\u002F 路由名称（用于编程式导航）\n    name: 'login',\n  },\n  {\n    \u002F\u002F 登录成功以后展示数据的路由\n    path: '\u002F',\n    component: () => import('@\u002Fviews\u002Fhome\u002Findex.vue'),\n    name: 'layout',\n      children: [\n  {\n    path: 'home',  \u002F\u002F 子路由写相对路径\n    component: () => import('@\u002Fviews\u002Fhome\u002Findex.vue'),\n    meta: {\n      title: '首页',\n      hidden: false,\n    },\n  },\n  },\n  {\n    \u002F\u002F404\n    path: '\u002F404',\n    component: () => import('@\u002Fviews\u002F404\u002Findex.vue'),\n    name: '404',\n  },\n  \u002F\u002F 重定向到其他路由\n  {\n    path: '\u002F:pathMatch(.*)*',\n    redirect: '\u002F404',\n    name: 'Any',\n  },\n];\n\n```\n\n`注意：懒加载特性`\n\n语法 () => import('组件路径') \n\n作用 按需加载页面组件 \n\n优势 减少首次加载时间、提升用户体验 \n\n打包 每个页面单独打包 \n\n推荐 所有路由都使用懒加载\n\n**子路由路径**\n\n子路由一律写相对路径\n\n\n\n`Vue Router 的两种模式`\n\n**Hash 模式 （当前使用）**\n\n```typescript\nimport { createWebHashHistory } from 'vue-router';\n\nconst router = createRouter({\n  history: createWebHashHistory(),\n  routes: constantRoute,\n});\n```\n\n```text\nurl风格\t带 # 号\nhttp:\u002F\u002Flocalhost:5173\u002F#\u002Flogin\nhttp:\u002F\u002Flocalhost:5173\u002F#\u002Fhome\nhttp:\u002F\u002Flocalhost:5173\u002F#\u002Fabout\n```\n\n**History 模式**\n\n```typescript\nimport { createWebHistory } from 'vue-router';\n\nconst router = createRouter({\n  history: createWebHistory(),\n  routes: constantRoute,\n});\n```\n\n```reStructuredText\nurl风格\nhttp:\u002F\u002Flocalhost:5173\u002Flogin\nhttp:\u002F\u002Flocalhost:5173\u002Fhome\nhttp:\u002F\u002Flocalhost:5173\u002Fabout\n```\n\n## 4.2 登录静态页面\n\n静态页面搭建\n\n```vue\n\u003Ctemplate>\n    \u003Cdiv class=\"login_container\">\n        \u003Cel-row>\n            \u003Cel-col :span=\"12\" :xs=\"0\">\u003C\u002Fel-col>\n            \u003Cel-col :span=\"12\" :xs=\"24\">\n                \u003Cel-form class=\"login_form\" :model=\"loginForm\">\n                    \u003Ch1>Hello\u003C\u002Fh1>\n                    \u003Ch2>欢迎来来到硅谷甄选\u003C\u002Fh2>\n                    \u003Cel-form-item>\n                        \u003Cel-input v-model=\"loginForm.username\" placeholder=\"请输入用户名\" :prefix-icon=\"User\">\u003C\u002Fel-input>\n                    \u003C\u002Fel-form-item>\n                    \u003Cel-form-item>\n                        \u003Cel-input v-model=\"loginForm.password\" placeholder=\"请输入密码\" :prefix-icon=\"Lock\" type=\"password\"\n                            show-password>\u003C\u002Fel-input>\n                    \u003C\u002Fel-form-item>\n                    \u003Cel-form-item>\n                        \u003Cel-button class=\"login_btn\" type=\"primary\" size=\"default\">登录\u003C\u002Fel-button>\n                    \u003C\u002Fel-form-item>\n                \u003C\u002Fel-form>\n            \u003C\u002Fel-col>\n        \u003C\u002Fel-row>\n    \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup lang=\"ts\">\nimport { Lock, User } from '@element-plus\u002Ficons-vue'\nimport { reactive } from 'vue';\n\n\u002F\u002F手机账号和密码的数据\nconst loginForm = reactive({\n    username: 'admin',\n    password: '111111'\n})\n\u003C\u002Fscript>\n\u003Cstyle scoped lang=\"scss\">\n.login_container {\n    width: 100%;\n    height: 100vh;\n    \u002F\u002F background-color: skyblue;\n    background: url('@\u002Fassets\u002Fimages\u002Fbackground.jpg') no-repeat;\n    background-size: cover;\n}\n\n.login_form {\n    position: relative;\n    width: 70%;\n    top: 30vh;\n    background: url('@\u002Fassets\u002Fimages\u002Flogin_form.png');\n    padding: 40px;\n}\n\nh1 {\n    color: white;\n    font-size: 40px;\n}\n\nh2 {\n    font-size: 20px;\n    color: white;\n    margin: 20px 0px;\n}\n\n.login_btn {\n    width: 100%;\n}\n\u003C\u002Fstyle>\n\n```\n\n![image-20260203161820084](images\u002Fimage-20260203161820084.png)\n\n### 4.2.1 什么是栅格布局（先把“感觉”建立起来）\n\n 1️⃣栅格布局 = 把一行切成很多等宽的小格子\n\n想象一条横线：\n\n```\n|--------------------------------|\n```\n\nElement Plus 规定：\n\n> **一行 = 24 个小格子**\n\n```\n|1|2|3|4|5|6|7|8|9|10|11|12|13|14|15|16|17|18|19|20|21|22|23|24|\n```\n\n2️⃣ 页面里的任何一块内容\n\n本质上就是：\n\n> **“我占这一行的第几格到第几格”**\n\n### 4.2.2 el-row \u002F el-col 在干什么（核心）\n\nel-row 是什么？\n\n👉 **一条“横着的参考线”**\n\n```\n\u003Cel-row>\n  ...\n\u003C\u002Fel-row>\n```\n\n它只干一件事：\n\n> 告诉浏览器：\n>  **现在开始按 24 栅格来分配宽度**\n\nel-col 是什么？\n\n👉 **在这一行里占多少格**\n\n```\n\u003Cel-col :span=\"6\">\u003C\u002Fel-col>\n```\n\n意思是：\n\n```\n我占 6 \u002F 24 = 25% 宽度\n```\n\n### 4.2.3 span 是怎么工作的（一定要吃透）\n\n一行总宽 = 24\n\n| span | 占比  |\n| ---- | ----- |\n| 24   | 100%  |\n| 12   | 50%   |\n| 8    | 33%   |\n| 6    | 25%   |\n| 4    | 16.6% |\n\n例子 1：左右两栏\n\n```\n\u003Cel-row>\n  \u003Cel-col :span=\"12\">左\u003C\u002Fel-col>\n  \u003Cel-col :span=\"12\">右\u003C\u002Fel-col>\n\u003C\u002Fel-row>\n```\n\n视觉效果：\n\n```\n| 左 左 左 左 左 左 | 右 右 右 右 右 右 |\n```\n\n### 4.2.4 响应式是怎么回事（重点）\n\n屏幕不是只有一种大小\n\n- 手机 📱\n- 平板 💻\n- 电脑 🖥️\n\n👉 所以 **同一列，在不同屏幕占的宽度可以不一样**\n\n------\n\nElement Plus 的断点（你只要记 4 个）\n\n| 属性 | 含义     |\n| ---- | -------- |\n| xs   | 手机     |\n| sm   | 平板     |\n| md   | 普通电脑 |\n| lg   | 大屏     |\n| xl   | 超大屏   |\n\n### 4.2.5 例子：你现在的登录页（拆解）\n\n```\n\u003Cel-col :span=\"12\" :xs=\"0\">\u003C\u002Fel-col>\n\u003Cel-col :span=\"12\" :xs=\"24\">\u003C\u002Fel-col>\n```\n\n在电脑上：\n\n```\n| 左空白 12 | 登录表单 12 |\n```\n\n在手机上：\n\n```\n| 登录表单 24 |\n```\n\n左边那一列直接消失（`xs=0`）\n\n\n\n## 4.3 模板封装登录业务\n\n`安装pinia管理数据`\n\n`npm i pinia`\n\n**src下新建store文件**\n\n**创建大仓库store\u002Findex.ts**\n\n```typescript\n\u002F\u002F 大仓库\nimport { createPinia } from 'pinia';\n\u002F\u002F 创建大仓库\nconst pinia = createPinia();\n\u002F\u002F对外暴露，入口文件需要安装仓库\nexport default pinia;\n\n```\n\n**创建小仓库store\u002Fmodules\u002Fuser.ts**\n\n```typescript\n\u002F\u002F 选项式api\n\u002F\u002F 创建用户相关的小仓库\nimport { defineStore } from 'pinia';\n\u002F\u002F创建用户小仓库\nconst useUserStore = defineStore('User', {\n  \u002F\u002F 小仓库存储数据的地方\n  state: () => {\n    return {};\n  },\n  \u002F\u002F 异步|逻辑的地方\n  actions: {},\n  getters: {},\n});\n\n\u002F\u002F 对外暴露获取小仓库方法\nexport default useUserStore;\n\n\n\u002F\u002F 组合式api\nimport { defineStore } from 'pinia';\nimport { ref, computed } from 'vue';\n\nconst useUserStore = defineStore('User', () => {\n  \u002F\u002Fstate\n  const user = ref(null);\n\n  \u002F\u002Fgetters\n\n  \u002F\u002Factions\n\n\u002F\u002F 对外暴露state,getters,actions\n})\nexport default useUserStore;\n\n\n```\n\n**src\u002Fmodules\u002Fuser.ts选项式api写法**\n\n```typescript\n\u002F\u002F 创建用户相关的小仓库\nimport { el } from 'element-plus\u002Fes\u002Flocales.mjs';\nimport { defineStore } from 'pinia';\n\n\u002F\u002F 引入接口\nimport { reqLogin } from '@\u002Fapi\u002Fuser';\n\u002F\u002F 引入数据类型\nimport type { loginForm } from '@\u002Fapi\u002Fuser\u002Ftype';\n\u002F\u002F创建用户小仓库\nconst useUserStore = defineStore('User', {\n  \u002F\u002F 小仓库存储数据的地方\n  state: () => {\n    return {\n      token: localStorage.getItem('TOKEN'), \u002F\u002F用户唯一标识token\n    };\n  },\n  \u002F\u002F 异步|逻辑的地方\n  actions: {\n    \u002F\u002F用户登录的方法\n    async userLogin(data: loginForm) {\n      \u002F\u002F登录请求\n      const result = await reqLogin(data);\n      \u002F\u002F 登录请求:成功200->token\n      \u002F\u002F 登录请求:失败201->登录失败错误的信息\n      if (result.code === 200) {\n        \u002F\u002F pinia仓库存储一下token\n        this.token = result.data.token;\n        \u002F\u002F由于pinia\u002Fvuex存储数据其实利用js对象\n        \u002F\u002F本地存储持久化存储一份\n        localStorage.setItem('TOKEN', result.data.token);\n        \u002F\u002F 能保证当前async函数返回一个成功的promise\n        return 'ok';\n      } else {\n        return Promise.reject(new Error(result.data.message));\n      }\n    },\n  },\n  getters: {},\n});\n\n\u002F\u002F 对外暴露获取小仓库方法\nexport default useUserStore;\n\n```\n\n\n\n**views\u002Flogin\u002Findex.vue 登录页面代码**\n\n```vue\n\u003Ctemplate>\n    \u003Cdiv class=\"login_container\">\n        \u003Cel-row>\n            \u003Cel-col :span=\"12\" :xs=\"0\">\u003C\u002Fel-col>\n            \u003Cel-col :span=\"12\" :xs=\"24\">\n                \u003Cel-form class=\"login_form\" :model=\"loginForm\">\n                    \u003Ch1>Hello\u003C\u002Fh1>\n                    \u003Ch2>欢迎来来到硅谷甄选\u003C\u002Fh2>\n                    \u003Cel-form-item>\n                        \u003Cel-input v-model=\"loginForm.username\" placeholder=\"请输入用户名\" :prefix-icon=\"User\">\u003C\u002Fel-input>\n                    \u003C\u002Fel-form-item>\n                    \u003Cel-form-item>\n                        \u003Cel-input v-model=\"loginForm.password\" placeholder=\"请输入密码\" :prefix-icon=\"Lock\" type=\"password\"\n                            show-password>\u003C\u002Fel-input>\n                    \u003C\u002Fel-form-item>\n                    \u003Cel-form-item>\n                        \u003Cel-button class=\"login_btn\" type=\"primary\" size=\"default\" @click=\"login\"\n                            :loading=\"loading\">登录\u003C\u002Fel-button>\n                    \u003C\u002Fel-form-item>\n                \u003C\u002Fel-form>\n            \u003C\u002Fel-col>\n        \u003C\u002Fel-row>\n    \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup lang=\"ts\">\nimport { Lock, User } from '@element-plus\u002Ficons-vue';\nimport { ElNotification } from 'element-plus';\nimport { reactive, ref } from 'vue';\nimport { useRouter } from 'vue-router';\n\n\u002F\u002F 引入用户相关的小仓库\nimport useUserStore from '@\u002Fstore\u002Fmodules\u002Fuser';\nimport { fa } from 'element-plus\u002Fes\u002Flocales.mjs';\n\nlet useStore = useUserStore();\n\u002F\u002F 获取路由器\nlet $router = useRouter();\n\u002F\u002F 定义加载效果开始为false\nlet loading = ref(false)\n\u002F\u002F收集账号和密码的数据\nconst loginForm = reactive({\n    username: 'admin',\n    password: '111111',\n});\n\u002F\u002F 登录按钮的回调\nconst login = async () => {\n    \u002F\u002F 点击登录出现加载效果\n    loading.value = true\n    \n    \u002F\u002F 点击登录按你以后能干什么？\n    \u002F\u002F 通知仓库发登录请求\n    \u002F\u002F 请求成功->首页展示数据的地方\n    \u002F\u002F 请求失败->弹出登录失败信息\n    \n    try {\n        \u002F\u002F调用用户仓库的登录方法，发送登录请求，等待返回\n        await useStore.userLogin(loginForm);\n        \u002F\u002F登录成功提示信息\n        ElNotification({\n            type: 'success',\n            message: '登录成功',\n        });\n        \u002F\u002F编程式导航跳转到展示数据首页\n        $router.push('\u002F');\n        \u002F\u002F登录成功，加载按钮也消失\n        loading.value = false\n\n    } catch (error) {\n        \u002F\u002F 如果登录请求失败（如账号密码错误或网络错误），执行这里的代码\n        \u002F\u002F 关闭加载状态，隐藏“加载中”\n        loading.value = false\n        ElNotification({\n            type: 'error',\n            message: (error as Error).message, \u002F\u002F 告诉 TS error 是 Error 类型(断言)\n        });\n\n    }\n};\n\u003C\u002Fscript>\n\u003Cstyle scoped lang=\"scss\">\n.login_container {\n    width: 100%;\n    height: 100vh;\n    \u002F\u002F background-color: skyblue;\n    background: url('@\u002Fassets\u002Fimages\u002Fbackground.jpg') no-repeat;\n    background-size: cover;\n}\n\n.login_form {\n    position: relative;\n    width: 70%;\n    top: 30vh;\n    background: url('@\u002Fassets\u002Fimages\u002Flogin_form.png');\n    padding: 40px;\n}\n\nh1 {\n    color: white;\n    font-size: 40px;\n}\n\nh2 {\n    font-size: 20px;\n    color: white;\n    margin: 20px 0px;\n}\n\n.login_btn {\n    width: 100%;\n}\n\u003C\u002Fstyle>\n\n```\n\n\n\n## 4.4 用户仓库数据ts类型定义与封装token\n\n**定义用户仓库state数据类型store\u002Fmodules\u002Ftypes\u002Ftype.ts**\n\n```typescript\nexport interface UserState {\n  token: string | null;\n}\n\n```\n\n**修改store\u002Fmodules\u002Fuser.ts中store数据类型**\n\n```typescript\n\u002F\u002F 创建用户相关的小仓库\nimport { defineStore } from 'pinia';\n\n\u002F\u002F 引入接口\n\nimport { reqLogin } from '@\u002Fapi\u002Fuser';\n\n\u002F\u002F 引入操作存储的工具方法\nimport { SET_TOKEN, GET_TOKEN } from '@\u002Futils\u002Ftoken';\n\n\u002F\u002F 引入数据类型\nimport type { loginForm, loginResponseData } from '@\u002Fapi\u002Fuser\u002Ftype';\n\nimport type { UserState } from '.\u002Ftypes\u002Ftype';\n\n\u002F\u002F创建用户小仓库\nconst useUserStore = defineStore('User', {\n    \n  \u002F\u002F 小仓库存储数据的地方 使用UserState规范state数据类型\n  state: (): UserState => {\n    return {\n      token: GET_TOKEN(), \u002F\u002F用户唯一标识token\n    };\n  }\n```\n\n**封装本地存储，存储数据与读取数据方法**\n\n用于用户登录后的身份验证\n\n优势：\n\n- 代码简洁\n- 统一管理\n- 易于维护\n- 类型安全\n\n**新建utils\u002Ftoken.ts**\n\n```typescript\n\u002F\u002F 封装本地存储存储数据与读取数据方法\n\n\u002F\u002F 存储数据\nexport const SET_TOKEN = (token: string) => {\n  localStorage.setItem('TOKEN', token);\n};\n\u002F\u002F 本地存储获取数据\nexport const GET_TOKEN = () =>{\n    return localStorage.getItem('TOKEN'),\n}\n```\n\n**修改store\u002Fmodules\u002Fuser.ts中存储和获取token方法**\n\n```typescript\n\u002F\u002F 创建用户相关的小仓库\nimport { defineStore } from 'pinia';\n\n\u002F\u002F 引入接口\n\nimport { reqLogin } from '@\u002Fapi\u002Fuser';\n\n\u002F\u002F 引入操作存储的工具方法\nimport { SET_TOKEN, GET_TOKEN } from '@\u002Futils\u002Ftoken';\n\n\u002F\u002F 引入数据类型\nimport type { loginForm, loginResponseData } from '@\u002Fapi\u002Fuser\u002Ftype';\n\nimport type { UserState } from '.\u002Ftypes\u002Ftype';\n\n\u002F\u002F创建用户小仓库\nconst useUserStore = defineStore('User', {\n  \u002F\u002F 小仓库存储数据的地方\n  state: (): UserState => {\n    return {\n      token: GET_TOKEN(), \u002F\u002F用户唯一标识token\n    };\n  },\n  \u002F\u002F 异步|逻辑的地方\n  actions: {\n    \u002F\u002F用户登录的方法\n    async userLogin(data: loginForm) {\n      \u002F\u002F登录请求\n      const result: loginResponseData = await reqLogin(data);\n      \u002F\u002F 登录请求:成功200->token\n      \u002F\u002F 登录请求:失败201->登录失败错误的信息\n      if (result.code === 200) {\n        \u002F\u002F pinia仓库存储一下token\n        this.token = result.data.token as string;\n        \u002F\u002F由于pinia\u002Fvuex存储数据其实利用js对象\n        \u002F\u002F本地存储持久化存储一份\n        SET_TOKEN(result.data.token as string);\n        \u002F\u002F 能保证当前async函数返回一个成功的promise\n        return 'ok';\n      } else {\n        return Promise.reject(new Error(result.data.message));\n      }\n    },\n  },\n  getters: {},\n});\n\n\u002F\u002F 对外暴露获取小仓库方法\nexport default useUserStore;\n\n```\n\n## 4.5 登录模板表单校验\n\n登录表单校验的作用，是在前端提前拦截不合法输入，提升用户体验、减少无效请求、让系统更安全、更专业。\n\n**表单绑定model**\n\n```vue\n\u003Cel-form class=\"login_form\" :model=\"loginForm\" :rules=\"rules\" ref=\"loginForms\">\u003C\u002Fel-form>\n\u003Cscript setup lang=\"ts\">\n\u002F\u002F收集账号和密码的数据\nconst loginForm = reactive({\n    username: 'admin',\n    password: '111111',\n});\n\u003C\u002Fscript>\n```\n\n**定义校验规则（rules），并且绑定rules**\n\n```vue\n\u003Cel-form class=\"login_form\" :model=\"loginForm\" :rules=\"rules\" ref=\"loginForms\">\u003C\u002Fel-form>\n\n\u003Cscript setup lang=\"ts\">\n \t\u002F\u002F 定义表单校验需要配置对象\nconst rules = {\n    \u002F\u002F规则对象属性\n    \u002F\u002F required,代表这个字段务必要校验的\n    \u002F\u002F min:文本长度至少多少位\n    \u002F\u002F max:文本长度最多多少位\n    \u002F\u002F message:错误的提示信息\n    \u002F\u002F trigger:触发校验表单的时机，change->文本发生变化触发校验，blur->失去焦点校验\n    username: [\n        { required: true, min: 6, max: 10, message: '账号长度至少六位，最多十位', trigger: 'change' }\n    ],\n    password: [\n        { required: true, min: 6, message: '密码至少六位', trigger: 'change' }\n    ]\n}\n\u003C\u002Fscript>\n```\n\n**在 el-form-item 上写 `prop`**\n\n```vue\n\u003Cel-form-item prop=\"username\">\n                        \u003Cel-input v-model=\"loginForm.username\" placeholder=\"请输入用户名\" :prefix-icon=\"User\">\u003C\u002Fel-input>\n                    \u003C\u002Fel-form-item>\n\u003Cel-form-item prop=\"password\">\n                        \u003Cel-input v-model=\"loginForm.password\" placeholder=\"请输入密码\" :prefix-icon=\"Lock\" type=\"password\"\n                            show-password>\u003C\u002Fel-input>\n                    \u003C\u002Fel-form-item>\n```\n\n**给 el-form 设置 ref（为了手动触发校验，validate的使用）**\n\n```vue\n\u003Cel-form class=\"login_form\" :model=\"loginForm\" :rules=\"rules\" ref=\"loginForms\">\u003C\u002Fel-form>\n\n\u003Cscript setup lang=\"ts\">\n\u002F\u002F获取el-form组件\nlet loginForms = ref()\n\n\u002F\u002F 登录按钮的回调\nconst login = async () => {\n    \u002F\u002F保证全部表单相校验通过在发请求\n    await loginForms.value.validate()    \n    \u002F\u002F 点击登录出现加载效果\n    loading.value = true\n    \u002F\u002F 点击登录按你以后能干什么？\n    \u002F\u002F 通知仓库发登录请求\n    \u002F\u002F 请求成功->首页展示数据的地方\n    \u002F\u002F 请求失败->弹出登录失败信息\n    try {\n        \u002F\u002F 保证登录成功\n        useStore.userLogin(loginForm);\n        \u002F\u002F编程式导航跳转到展示数据首页\n        \u002F\u002F登录成功提示信息\n        ElNotification({\n            type: 'success',\n            message: '登录成功',\n        });\n        \u002F\u002F编程式导航跳转到展示数据首页\n        $router.push('\u002F');\n        \u002F\u002F登录成功，加载按钮也消失\n        loading.value = false\n\n    } catch (error) {\n        \u002F\u002F 登录失败 加载效果消失\n        loading.value = false\n        ElNotification({\n            type: 'error',\n            message: (error as Error).message,\n        });\n\n    }\n};\n\u003C\u002Fscript>\n\n\nvalidate() 是一个 Promise\n成功时（表单校验通过）：Promise 变成 resolved，await validate() 后代码继续执行\n失败时（有校验不通过）：Promise 变成 rejected，await validate() 抛出异常，可以用 catch 捕获\n\nVue 3 的 ref + template ref 机制\n你在模板里给某个组件或元素加上 ref=\"loginForms\"，Vue 会把对应的组件实例或 DOM 元素，自动赋值给你定义的 loginForms 变量。\nVue 编译模板时，发现 ref=\"loginForms\"\n运行时，会把 \u003Cel-form> 的实例赋给 loginForms.value\n```\n\n### 4.5.1 自定义校验规则使用（可写正则）\n\n**给 `\u003Cel-form-item>` 设置 `prop`**\n\n这个 `prop` 对应表单绑定数据的字段名（`loginForm.username`）\n\n**在rules对象中自定义校验规则**\n\n```typescript\nconst rules = {\n  username: [\n    { trigger: 'change', validator: validatorUserName }\n  ],\n  password: [\n    { trigger: 'change', validator: validatorPassword }\n  ]\n}\n\n```\n\n**写自定义校验函数**\n\n函数签名固定，接受三个参数\n\n```typescript\nconst validatorUserName = (rule, value, callback) => {\n  \u002F\u002F rule：校验规则对象\n  \u002F\u002F value：当前表单字段的值\n  \u002F\u002F callback：校验结果的回调\n};\n\u002F\u002F 自定义校验规则\nconst validatorUserName = (rule: any, value: any, callback: any) => {\n    \u002F\u002F rule:即为校验规则对象\n    \u002F\u002F value：即为表单元素文本内容\n    \u002F\u002F 函数：如果符合条件callback放行通过\n    \u002F\u002F 如果不符合条件callback方法，注入错误提示信息\n    \u002F\u002F 可以写正则\n    if (value.length >= 5) {\n        callback()\n    } else {\n        callback(new Error('账号长度至少五位'))\n    }\n}\n```\n\n**`把rules` 传给 `\u003Cel-form>` 组件**\n\n\n\n## 4.6 _layout组件的静态搭建\n\n**创建src\u002Flayout\u002Findex.vue**\n\n```vue\n\u003Ctemplate>\n    \u003Cdiv class=\"layout_container\">\n        \u003C!-- 左侧菜单 -->\n        \u003Cdiv class=\"layout_slider\">123\u003C\u002Fdiv>\n        \u003C!-- 顶部导航 -->\n        \u003Cdiv class=\"layout_tabbar\">456\u003C\u002Fdiv>\n        \u003C!-- 内容展示区域 -->\n        \u003Cdiv class=\"layout_main\">\n\n        \u003C\u002Fdiv>\n    \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup lang=\"ts\">\n\n\u003C\u002Fscript>\n\u003Cstyle scoped lang=\"scss\">\n.layout_container {\n    width: 100%;\n    height: 100vh;\n    background-color: skyblue;\n    \n.layout_slider {\n    width: $base-menu-width;\n    height: 100vh;\n    background-color: $base-menu-background;\n}\n\n.layout_tabbar {\n    position: fixed;\n    left: $base-menu-width ;\n    top: 0;\n    width: calc(100% - $base-menu-width);\n    height: $base-tabbar-height;\n    background-color: springgreen;\n}\n\n.layout_main {\n    position: absolute;\n    width: calc(100% - $base-menu-width);\n    height: calc(100vh - $base-tabbar-height );\n    background-color: red;\n    left: $base-menu-width ;\n    top: $base-tabbar-height;\n    padding: 20px;\n    overflow: auto;\n}\n    }\n    \n\u003C\u002Fstyle>\n```\n\n**配置全局SCSS样式 src\u002Fstyles\u002Fvariable.scss**\n\n```scss\n\u002F\u002F 项目提供scss全局变量\n\u002F\u002F 左侧的菜单的宽度\n$base-menu-width:260px;\n\u002F\u002F 左侧菜单的背景颜色\n$base-menu-background:#00152b;\n\u002F\u002F 顶部导航的高度\n$base-tabbar-height:50px;\n\n```\n\n**滚动条设置src\u002Fstyles\u002Findex.scss**\n\n```scss\n\u002F\u002F 滚动条外观设置\n::-webkit-scrollbar {\n    width: 10px;\n}\n\n::-webkit-scrollbar-track{\n    background-color: $base-menu-background;\n}\n\n::-webkit-scrollbar-thumb{\n    width: 10px;\n    background-color: yellow;\n    border-radius: 10px;\n}\n\n```\n\n**修改src\u002Frouter\u002Frouters.ts**\n\n将登录成功后看到的页面\n\n```typescript\n \u002F\u002F 登录成功以后展示数据的路由\n    path: '\u002F',\n    component: () => import('@\u002Flayout\u002Findex.vue'),\n    name: 'layout',\n  },\n```\n\n\n\n## 4.7 logo组件的封装\n\n封装 Logo 是为了用配置统一控制显示与样式，做到一处修改、全局生效，提升代码可维护性。\n\n**新建src\u002Fsetting.ts用来管理配置logo和标题**\n\n```typescript\n\u002F\u002F 用于项目logo,标题的配置\nexport default {\n  title: '硅谷甄选运营平台', \u002F\u002F 项目的标题\n  logo: 'public\u002Fimage.png', \u002F\u002F 项目的logo\n  logoHidden: true, \u002F\u002F logo组件是否隐藏设置\n};\n\n```\n\n**新建src\u002Flayout\u002Flogo\u002Findex.vue**\n\n```vue\n\u003Ctemplate>\n    \u003Cdiv class=\"logo\" v-if=\"setting.logoHidden\">\n        \u003Cimg :src='setting.logo' alt=\"\">\n        \u003Cp>{{ setting.title }}\u003C\u002Fp>\n    \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup lang=\"ts\">\n\u002F\u002F 引入设置标题与logo这配置文件\nimport setting from '@\u002Fsetting';\n\u003C\u002Fscript>\n\u003Cstyle scoped lang=\"scss\">\n    \n.logo {\n    width: 100%;\n    height: $base-menu-logo-height;\n    color: white;\n    display: flex;\n    align-items: center;\n    font-size: $base-logo-tittle-fontsize;\n    padding: 10px;\n\nimg {\n    width: 40px;\n    height: 40px;\n}\n\np {\n    margin-left: 10px;\n}\n    }\n\u003C\u002Fstyle>\n```\n\n注意：样式收敛到 logo 内，这样以后你在这个组件里再加别的 `img \u002F p` 也不会乱。\n\n**src\u002Flayout\u002Findex.vue引入logo组件**\n\n```vue\n\u003Ctemplate>\n    \u003Cdiv class=\"layout_container\">\n        \u003C!-- 左侧菜单 -->\n        \u003Cdiv class=\"layout_slider\">\n            \u003CLogo>\u003C\u002FLogo>\n        \u003C\u002Fdiv>\n        \u003C!-- 顶部导航 -->\n        \u003Cdiv class=\"layout_tabbar\">456\u003C\u002Fdiv>\n        \u003C!-- 内容展示区域 -->\n        \u003Cdiv class=\"layout_main\">\n\n        \u003C\u002Fdiv>\n    \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\u003Cscript setup lang=\"ts\">\nimport Logo from '.\u002Flogo\u002Findex.vue'\n\u003C\u002Fscript>\n```\n\n**全局variable.scss样式设置**\n\n```scss\n\u002F\u002F左侧菜单logo高度设置\n$base-menu-logo-height:50px;\n\u002F\u002F左侧菜单logo右侧文字大小\n$base-logo-tittle-fontsize:20px;\n\n```\n\n\n\n## 4.8 左侧菜单静态搭建\n\n**src\u002Flayout\u002Findex.vue**\n\n```vue\n\u003Ctemplate>\n    \u003Cdiv class=\"layout_container\">\n        \u003C!-- 左侧菜单 -->\n        \u003Cdiv class=\"layout_slider\">\n            \u003CLogo>\u003C\u002FLogo>\n            \u003C!-- 展示菜单 -->\n            \u003C!-- 滚动菜单 -->\n            \u003Cel-scrollbar class=\"scrollbar\">\n                \u003C!-- 菜单组件 -->\n                \u003Cel-menu class=\"el_menu\">\n                    \u003Cel-menu-item index=\"1\">首页\u003C\u002Fel-menu-item>\n                    \u003Cel-menu-item index=\"2\">数据大屏\u003C\u002Fel-menu-item>\n                    \u003C!-- 折叠菜单 -->\n                    \u003Cel-sub-menu index=\"3\">\n                        \u003Ctemplate #title>\n                            \u003Cspan>权限管理\u003C\u002Fspan>\n                        \u003C\u002Ftemplate>\n                        \u003Cel-menu-item index=\"3-1\">用户管理\u003C\u002Fel-menu-item>\n                        \u003Cel-menu-item index=\"3-2\">角色管理\u003C\u002Fel-menu-item>\n                        \u003Cel-menu-item index=\"3-3\">菜单管理\u003C\u002Fel-menu-item>\n                    \u003C\u002Fel-sub-menu>\n                \u003C\u002Fel-menu>\n            \u003C\u002Fel-scrollbar>\n        \u003C\u002Fdiv>\n        \u003C!-- 顶部导航 -->\n        \u003Cdiv class=\"layout_tabbar\">456\u003C\u002Fdiv>\n        \u003C!-- 内容展示区域 -->\n        \u003Cdiv class=\"layout_main\">\n\n        \u003C\u002Fdiv>\n    \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup lang=\"ts\">\nimport Logo from '.\u002Flogo\u002Findex.vue'\n\u003C\u002Fscript>\n\u003Cstyle scoped lang=\"scss\">\n.layout_container {\n    width: 100%;\n    height: 100vh;\n    color: white;\n\n    .layout_slider {\n        width: $base-menu-width;\n        height: 100vh;\n        background-color: $base-menu-background;\n\n        .scrollbar {\n            width: 100%;\n            height: calc(100vh - $base-menu-logo-height);\n\n            .el_menu {\n                --el-menu-bg-color: #00152b;\n                --el-menu-text-color: #fff;\n            }\n        }\n    }\n\n    .layout_tabbar {\n        position: fixed;\n        left: $base-menu-width ;\n        top: 0;\n        width: calc(100% - $base-menu-width);\n        height: $base-tabbar-height;\n        background-color: springgreen;\n    }\n\n    .layout_main {\n        position: absolute;\n        width: calc(100% - $base-menu-width);\n        height: calc(100vh - $base-tabbar-height );\n        background-color: red;\n        left: $base-menu-width ;\n        top: $base-tabbar-height;\n        padding: 20px;\n        overflow: auto;\n    }\n}\n\u003C\u002Fstyle>\n```\n\n### 4.8.1 滚动菜单的使用（`el-scrollbar`）\n\n当侧边菜单内容过多，超出固定高度时，使用 `el-scrollbar` 给菜单区域增加自定义滚动条，避免页面整体滚动。\n\n```vue\n典型用法结构\n\n\u003Cel-scrollbar class=\"scrollbar\">\n  \u003Cel-menu class=\"menu\">\n    \u003Cel-menu-item index=\"1\">首页\u003C\u002Fel-menu-item>\n    \u003Cel-sub-menu index=\"2\">\n      \u003Ctemplate #title>\n        \u003Cspan>权限管理\u003C\u002Fspan>\n      \u003C\u002Ftemplate>\n      \u003Cel-menu-item index=\"2-1\">用户管理\u003C\u002Fel-menu-item>\n    \u003C\u002Fel-sub-menu>\n  \u003C\u002Fel-menu>\n\u003C\u002Fel-scrollbar>\n\n样式配合（关键点）\nel-scrollbar 高度固定或计算，保证菜单超出时出现滚动条\n\u003Cstyle scoped lang=\"scss\">\n    .scrollbar {\n \t\t height: calc(100vh - $base-menu-logo-height);\n \t\t \u002F* 高度 = 全屏高度 - logo 高度 *\u002F\n}\n\u003C\u002Fstyle>\n```\n\n**注意：**\n\n`el-scrollbar` 包裹的内容区域高度要超过自身高度，滚动条才会出现\n\n`el-scrollbar` 只能有一个直接子元素（通常是菜单容器）\n\n保证菜单组件的高度不要超出 `el-scrollbar` 的高度\n\n### 4.8.2 el-menu组件的使用\n\n* 普通菜单（`el-menu-item`）\n\n  没有下级菜单的入口\n\n  ```vue\n  \u003Cel-menu>\n  \t\u003Cel-menu-item index=\"1\">首页\u003C\u002Fel-menu-item>\n  \u003C\u002Fel-menu>\n  ```\n\n* 展开菜单 \u002F 子菜单（`el-sub-menu`）\n\n  有下级菜单，需要展开\u002F收起\n\n  ```vue\n  \u003Cel-menu>\n  \t\u003Cel-sub-menu index=\"2\">\n      具名插槽写子菜单的标题\n    \t\t\u003Ctemplate #title>\n      \t\t\u003Cspan>权限管理\u003C\u002Fspan>\n    \t\t\u003C\u002Ftemplate>\n  \n    \t\t\u003Cel-menu-item index=\"2-1\">用户管理\u003C\u002Fel-menu-item>\n    \t\t\u003Cel-menu-item index=\"2-2\">角色管理\u003C\u002Fel-menu-item>\n  \t\u003C\u002Fel-sub-menu>\n  \u003C\u002Fel-menu>\n  ```\n\n* 样式设置\n\n  ```scss\n  .menu {\n    border-right: none;  \u002F\u002F 去掉右侧边框\n    --el-menu-bg-color: #00152b;  \u002F\u002F 背景颜色\n    --el-menu-text-color: #fff;\t\u002F\u002F 文本颜色\n    --el-menu-hover-bg-color: #409eff;  \u002F\u002F 鼠标悬停颜色\n    --el-menu-active-color: #409eff;    \u002F\u002F 鼠标点击颜色\n  }\n  ```\n\n**注意：**\n\n`el-menu`：菜单容器\n\n`el-menu-item`：普通菜单项\n\n`el-sub-menu`：子菜单\n\n`index`：**唯一标识（必须）**\n\n### 4.8.3 递归组件生成动态菜单\n\n通过递归组件实现菜单的无限层级和结构复用，简化代码维护，提高扩展性和可读性\n\n\n\n**src\u002Frouter\u002Frouters.ts**\n\n在路由中起每个菜单的名字，并配置展示逻辑，写入子路由\n\n```typescript\n\u002F\u002F 对外暴漏配置路由\nexport const constantRoute = [\n  {\n    \u002F\u002F 登录\n    path: '\u002Flogin',\n    component: () => import('@\u002Fviews\u002Flogin\u002Findex.vue'),\n    name: 'login',\n    meta: {\n      title: '登录',\n      hidden: true, \u002F\u002F 代表路由标题在菜单中是否隐藏，true隐藏。false不隐藏\n    },\n  },\n  {\n    \u002F\u002F 登录成功以后展示数据的路由\n    path: '\u002F',\n    component: () => import('@\u002Flayout\u002Findex.vue'),\n    name: 'layout',\n    meta: {\n      title: 'layout',\n      hidden: false,\n    },\n    children: [\n      {\n        path: 'home',\n        component: () => import('@\u002Fviews\u002Fhome\u002Findex.vue'),\n        meta: {\n          title: '首页',\n          hidden: false,\n        },\n      },\n      {\n        path: 'text',\n        component: () => import('@\u002Fviews\u002Fhome\u002Findex.vue'),\n        meta: {\n          title: '测试',\n          hidden: false,\n        },\n      },\n    ],\n  },\n  {\n    \u002F\u002F404\n    path: '\u002F404',\n    component: () => import('@\u002Fviews\u002F404\u002Findex.vue'),\n    name: '404',\n    meta: {\n      title: '404',\n      hidden: true,\n    },\n  },\n  \u002F\u002F 重定向\n  {\n    path: '\u002F:pathMatch(.*)*',\n    redirect: '\u002F404',\n    name: 'Any',\n    meta: {\n      title: 'Any',\n      hidden: true,\n    },\n  },\n];\n\n\u002F\u002F 子路由一律写相对路径\n\n```\n\n\n\n**创建src\u002Flayout\u002Fmenu\u002Findex.vue**\n\n```vue\n\u003Ctemplate>\n  \u003Ctemplate v-for=\"(item, index) in menuList\" :key=\"item.path\">\n    \u003C!-- 没有子路由 -->\n    \u003Ctemplate v-if=\"!item.children\">\n      \u003Cel-menu-item :index=\"item.path\" v-if=\"!item.meta.hidden\">\n        \u003Cspan>标\u003C\u002Fspan>\n        \u003Cspan>{{ item.meta.title }}\u003C\u002Fspan>\n      \u003C\u002Fel-menu-item>\n    \u003C\u002Ftemplate>\n    \u003C!-- 有子路由但是只有一个子路由 -->\n    \u003Ctemplate v-if=\"item.children && item.children.length == 1\">\n      \u003Cel-menu-item :index=\"item.children[0].path\" v-if=\"!item.children[0].meta.hidden\">\n        \u003Cspan>{{ item.children[0].meta.title }}\u003C\u002Fspan>\n      \u003C\u002Fel-menu-item>\n    \u003C\u002Ftemplate>\n    \u003C!-- 有子路由且个数大于1 -->\n    \u003Cel-sub-menu :index=\"item.path\" v-if=\"item.children && item.children.length > 1\">\n      \u003Ctemplate #title>\n        \u003Cspan>{{ item.meta.title }}\u003C\u002Fspan>\n      \u003C\u002Ftemplate>\n      \u003C!-- 使用递归 -->\n      \u003C!-- 组件调用自己  处理无限层级、代码复用、易于维护 -->\n      \u003CSideMenu :menuList=\"item.children\">\u003C\u002FSideMenu>\n    \u003C\u002Fel-sub-menu>\n  \u003C\u002Ftemplate>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup lang=\"ts\">\n\u002F\u002F 给组件起名字\n\u002F\u002F 告诉 Vue：\n\u002F\u002F “递归时，请用 组件名注册表 去找自己”    \ndefineOptions({\n  name: 'SideMenu'\n})\n\u002F\u002F 有一个名为 menuList 的 props，来自父组件传递的数据\ndefineProps(['menuList']);\n\n\n\u003C\u002Fscript>\n\n\u003Cstyle scoped>\u003C\u002Fstyle>\n\n```\n\n**注意：**\n\n在 Vue 3 + `\u003Cscript setup>` 里：\n `defineOptions({ name: 'Menu' })`\n 就是用来“给组件起名字”的\n\n它等价于你以前写的：\n\n```vue\nexport default {\n  name: 'Menu'\n}\n```\n\n只是 **`script setup` 专用写法**。\n\n```vue\nv-if=\"!item.meta.hidden\" 写入是否展示逻辑，有的一级路由不需要展示,login,404,Any\n```\n\n**v-if 是 Vue 的指令，用来根据表达式的真假决定是否渲染这个节点。**\n\n\n\n**写入动态生成路由组件src\u002Flayout\u002Findex.vue**\n\n```vue\n\u003Ctemplate>\n    \u003Cdiv class=\"layout_container\">\n        \u003C!-- 左侧菜单 -->\n        \u003Cdiv class=\"layout_slider\">\n            \u003CLogo>\u003C\u002FLogo>\n            \u003C!-- 展示菜单 -->\n            \u003C!-- 滚动菜单 -->\n            \u003Cel-scrollbar class=\"scrollbar\">\n                \u003C!-- 菜单组件 -->\n                \u003Cel-menu class=\"el_menu\">\n                    \u003C!-- 根据路由动态生成菜单 -->\n                    \u003C!-- 给子组件传路由数组 -->\n                    \u003CSideMenu :menuList=\"useStore.menuRouters\">\u003C\u002FSideMenu> \n                \u003C\u002Fel-menu>\n            \u003C\u002Fel-scrollbar>\n        \u003C\u002Fdiv>\n        \u003C!-- 顶部导航 -->\n        \u003Cdiv class=\"layout_tabbar\">456\u003C\u002Fdiv>\n        \u003C!-- 内容展示区域 -->\n        \u003Cdiv class=\"layout_main\">\n\n        \u003C\u002Fdiv>\n    \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup lang=\"ts\">\n\u002F\u002F 引入左侧菜单logo子组件\nimport Logo from '.\u002Flogo\u002Findex.vue'\n\u002F\u002F 引入菜单组件\nimport SideMenu from '.\u002Fmenu\u002Findex.vue'\n\n\u002F\u002F 获取用户相关的小仓库\nimport useUserStore from '@\u002Fstore\u002Fmodules\u002Fuser';\nlet useStore = useUserStore()\n\n\n\n\u003C\u002Fscript>\n\u003Cstyle scoped lang=\"scss\">\n.layout_container {\n    width: 100%;\n    height: 100vh;\n    color: white;\n\n    .layout_slider {\n        width: $base-menu-width;\n        height: 100vh;\n        background-color: $base-menu-background;\n\n        .scrollbar {\n            width: 100%;\n            height: calc(100vh - $base-menu-logo-height);\n\n            .el_menu {\n                --el-menu-bg-color: #00152b;\n                --el-menu-text-color: #fff;\n                --el-menu-hover-bg-color: #00152b;\n                border-right: none; \u002F\u002F 去掉右侧边框\n            }\n        }\n    }\n\n    .layout_tabbar {\n        position: fixed;\n        left: $base-menu-width ;\n        top: 0;\n        width: calc(100% - $base-menu-width);\n        height: $base-tabbar-height;\n        background-color: springgreen;\n    }\n\n    .layout_main {\n        position: absolute;\n        width: calc(100% - $base-menu-width);\n        height: calc(100vh - $base-tabbar-height );\n        background-color: red;\n        left: $base-menu-width ;\n        top: $base-tabbar-height;\n        padding: 20px;\n        overflow: auto;\n    }\n}\n\u003C\u002Fstyle>\n```\n\n",null,"1",99,0,1,"2026-04-07T16:22:29.810Z","2026-04-07T16:22:29.815Z","2026-05-25T22:05:44.500Z","0",{"id":28,"categoryName":37,"slug":38,"description":39,"sort":30,"isEnable":31,"createTime":40,"updateTime":41,"deleteTime":30},"前端开发","frontend-engineering","2222","2026-04-03T02:36:11.945Z","2026-04-07T16:38:46.496Z",[],[],[]]