Vue3
vue3.3.4 + vite4.4.9
1.路由
1.1 安装和配置
npm install vue-router@4
- router/index.js
import {
createRouter,
createWebHashHistory,
} from 'vue-router'
import ItProjectIndex from '../views/itproject/Index.vue';
const routes = [
// 路由守卫 vue3动态路由问题导致刷新完页面会爆出No match found for location with path
{
path: "/:pathMatch(.*)*", // 必备
component: () => import("@/views/404.vue"),
},
{
path: '/login',
component: Login,
name: 'Login'
},
{
path: '/',
component: Frame,
redirect: '/homePage',
name: '首页',
children: [
{
path: 'homePage',
component: HomePage,
name: '欢迎页',
},
// 菜谱
{
path: '/menu',
name: '菜谱管理',
children: [
{
path: '/menu/index',
component: MenuIndex,
name: '菜谱列表'
},
]
},
]
},
moduleRouter
]
// 创建路由对象
const router = createRouter({
history: createWebHashHistory(), //url有#
// history: createWebHistory(), //url没有#
routes
})
export default router
- 模块化路由 router/module/index.js
const moduleRouter = {
path: '/module',
component: PageFrame,
redirect: '/module/index',
children: [
{
path: '/module/index',
component: moduleIndex,
name: 'moduleIndex'
},
]
}
export default moduleRouter;
- main.js
import router from "./router/index";
const app = createApp(App);
app.use(router).mount('#app');
1.2 路由变化监听
- onBeforeRouteUpdate钩子
import { onBeforeRouteUpdate } from "vue-router";
onBeforeRouteUpdate((val, oldVal) => {
const query = val.query
this.isHeader = !query.hasOwnProperty('isHeader') ? '1' : query.isHeader
});
1.3 路由传参
query和params传参的时候,每一个参数应该以字符串形式传入
如果传一个对象必须JSON.stringify()序列化
传map需要先转化为object再转化为json传入
- index.js
{
path: '/Consumer/shopDetail',
component: ShopDetail,
name: 'ShopDetail',
query: {
shopInfo: {}
}
},
- 父组件
import { useRouter, useRoute } from 'vue-router'
const router = useRouter();
router.push({
// map -> json 需要先转object
let mapObj = Object.create(null);
for (let[k,v] of data.order) {
mapObj[k] = v;
}
const mapJson = JSON.stringify(mapObj)
path: '/Consumer/shopItemDetail',
query: {
shopItemInfo: JSON.stringify(data.shopItemList[key]),
order: mapJson,
}
})
- 子组件
接受参数用route
import { useRouter, useRoute } from 'vue-router'
const route = useRoute();
data.shopItemInfo = JSON.parse(route.query.shopItemInfo)
// json -> map
let mapJson = route.query.order;
const obj = JSON.parse(<string>mapJson)
let map = new Map();
for (let k of Object.keys(obj)) {
map.set(k,obj[k]);
}
data.order = map
2.ElementPlus
2.1 安装和配置
npm install element-plus --save
- main.js
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
const app = createApp(App);
app.use(ElementPlus).mount('#app');
2.2 表单验证
const data = reactive({
rules: {
name: [
{required: true, message: '分类名不能为空', trigger: 'blur'}
],
parentId: [
{required: true, message: '大类ID不能为空', trigger: 'blur'}
]
},
})
// 表单ref
const itemForm = ref();
const submitForm = (formName) => {
// 表单验证
itemForm.value.validate((valid, fields) => {
if (valid) {
saveOrUpdate();
} else {
ElMessage({
message: '请校验',
type: 'warning',
})
}
})
}
3. Axios
3.1 安装和配置
npm install axios
- main.js
import axios from 'axios'
const app = createApp(App);
app.config.globalProperties.$axios = axios;
3.2 封装
- 封装为request.js
/**
* ajax请求配置
*/
import axios from 'axios'
import {ElMessage} from 'element-plus'
import qs from "qs";
import {getToken, removeToken} from '@/utils/auth/auth'
// axios默认配置
axios.defaults.timeout = 10000 // 超时时间
// 网关地址
axios.defaults.baseURL = "http://localhost:8921";
// 整理数据
axios.defaults.transformRequest = function(data) {
return data
}
// 路由请求拦截
axios.interceptors.request.use(
config => {
// 权限系统适配网关
if (config.url.indexOf('/manager') !== -1) {
config.baseURL = 'http://localhost:8921';
}
if (getToken()) {
config.headers['Authorization'] = getToken()
}
console.log(axios.defaults.baseURL)
if (config.type === 'form'){
// 后端@RequestParams注解接收
config.headers['Content-Type'] = 'application/x-www-form-urlencoded'
config.data = qs.stringify(config.data)
} else if (config.type === 'file') {
// 后端@RequestParams注解接收
config.headers['Content-Type'] = 'multipart/form-data'
config.data = qs.stringify(config.data)
} else {
// 后端@RequestBody注解接收
config.headers['Content-Type'] = 'application/json;charset=UTF-8'
config.data = JSON.stringify(config.data)
}
return config
},
error => {
return Promise.reject(error.response)
}
)
// 路由响应拦截
axios.interceptors.response.use(
response => {
if (response.headers && (response.headers['content-type'] === 'application/x-msdownload' ||
response.headers['content-type'].indexOf('application/vnd.ms-excel') !== -1 ||
response.headers['content-type'].indexOf('application/octet-stream') !== -1)) {
return response;
} else {
// if (res.code !== '20000') {
// Message({
// message: res.message || '操作失败,请联系管理员',
// type: 'error',
// duration: 5 * 1000
// })
// return Promise.reject(new Error(JSON.stringify(res) || '操作失败,请联系管理员'))
// } else {
// return res
// }
return response.data
}
},
error => {
console.log('err' + error) // for debug
if (error && error.response) {
const {status} = error.response;
if (status === 401) {
ElMessage.error('Token值无效,请重新登录');
removeToken();
router.replace('/login');
} else {
// Message({
// message: error.message,
// type: 'error',
// duration: 5 * 1000
// });
}
} else {
ElMessage.error('网络出现问题,请稍后再试');
}
return Promise.reject(error)
}
// response => {
// if (response.data.success === false) {
// return ElMessage.error(response.data.errDesc)
// } else {
// return response.data
// }
// },
// error => {
// return Promise.reject(error.response) // 返回接口返回的错误信息
// }
)
export default axios
3.3 Api使用
- api.js
import request from '@/utils/request'
export default {
testLogin(data) {
return request({
url: 'shop/test/testLogin',
method: 'post',
type: 'form',
data: data
})
},
testLogin1(data) {
return request({
url: 'shop/test/testLogin1',
method: 'post',
data: data
})
},
}
- vue界面使用
mounted() {
// 原生
this.$axios.get("http://localhost:9000/order/order/1").then(res => {
this.obj= res.data[0];
console.log(res.data);
})
// 封装
Api.test().then(res => {
console.log(res);
})
}
4. Vuex/Store
store是用来存储组件状态的,而不是用来做本地数据存储的。所以,对于不希望页面刷新之后被重置的数据,使用本地存储来进行存储。
4.1 安装和配置
npm install vuex@next --save
- store/index.js
import {createStore } from 'vuex'
export default createStore({
// state是用来定义数据,这里可以设置数据的默认数据,但是为了数据的安全性,通常不直接修改state数据,而是通过mutations修改
state: {
count: 0,
},
// getters用来处理数据,对state中的数据进行处理,得到自己想要的效果,当需要在多处组件使用这种数据时,gettters是你最好的选择
getters:{
getCount(state){
return state.count;
},
},
// 不能直接改变 store 中的状态。改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation
// mutations通常为修改state数据而使用,这用就可以避免直接修改state的数据
mutations: {
increment (state) {
state.count++
},
},
// actions当你的数据是需要发送请求获取时,这是非常完美的选择
actions: {
async setGoods(content,id){
let res = await api.getGoods(id);
content.commit('setGoods',res.data);
}
// 各部分使用
// console.log(store.state.goods); //state
// console.log(store.getters['goodsData']); //getters
// store.commit('changeGoods',123); //mutations
// actions
// async function abc() {
// await store.dispatch("setGoods", {
// id: 123,
// });
// }
},
// 模块化引入
modules: {
// tagsView,
// side,
// app,
// permission
},
})
- main.js
import store from './store/index'
const app = createApp(App);
app.use(store).mount('#app');
5.全局变量
设置一个JSON格式的配置文件
- public/config.json
{
"BASE_URL": "/api",
"obj":{
"NAME":"",
},
}
- main.js
app.config.globalProperties = {
BASE_URL: dataJson.BASE_URL,
obj : dataJson.obj,
};
5.1 getCurrentInstance
不推荐使用
- script 使用
直接用this.***的方式使用全局属性 - script lang="ts" setup 使用
import { getCurrentInstance } from 'vue'
// 使用getCurrentInstanceAPI获取全局对象方法一,从globalProperties中可以获取到所有的全局变量
const currentInstance = getCurrentInstance();
const globalProperties = currentInstance?.appContext.config.globalProperties
console.log(globalProperties);
5.2 使用 Provide / Inject
- main.js
// 变量
app.provide('$test', '666')
// 函数
import dayjs from 'dayjs'
const formatTime = (time, format) => (time ? dayjs(time).format(format || 'YYYY-MM-DD') : '-')
app.provide('dayjs', formatTime)
- 组件
// 变量
import { inject } from 'vue'
const test = inject('$test')
console.log('inject的$test', test)
// 函数
const dayjs = inject('dayjs')
const formatResult = dayjs(1639014286)
console.log('dayFormat', formatResult)
5.3 俩个script标签
- main.js
app.config.globalProperties.myOption = 'myOption'
- 组件
<script>
import HelloWorld from './components/HelloWorld.vue'
import { onBeforeMount, defineComponent } from 'vue'
let that = this
export default defineComponent({
beforeCreate() {
that = this
},
})
</script>
<script setup>
onBeforeMount(() => {
console.log('that', that.myOption)
})
</script>
5.4 单js文件
import dataJson from '../../public/config.json'
const key= dataJson.key;
6.SCSS
6.1 安装和配置
npm install --save-dev sass-loader
- style目录
放入.scss文件 - vite.config.js
export default defineConfig({
plugins: [
vue(),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
},
css: {
// css预处理器
preprocessorOptions: {
scss: {
// 引入 mixin.scss 这样就可以在全局中使用 mixin.scss中预定义的变量了
// 给导入的路径最后加上 ;
additionalData: '@import "@/style/name1.scss";' +
'@import "@/style/name2.scss";' +
'@import "@/style/name3.scss";'
}
}
}
})
6.2 组件使用
<style lang="scss" scoped>
</style>
- 深度选择器
/deep/ 改为 v-deep
7.生命周期
- 使用
<script lang="ts" setup>
// 引入生命周期钩子
import { onMounted } from "vue";
onMounted(()=>{
console.log(' DOM被挂载完成时执行');
});
</script>
7.1 组件比数据更早完成渲染
有时候子组件需要的数据还没请求完成,但界面已经渲染完成
- 加个判断即可
<template v-if="canteenList.length!=0">
<canteen-card :list='canteenList' @goTo='goInCanteen'></canteen-card>
</template>
8.data,method,watch,computed 使用
- data
// 挂载使用
<template>
v-model="data.activeName"
</template>
<script lang="ts" setup>
import { reactive, ref, toRefs} from 'vue'
// Data
const data = reactive({
// formList:搜索条件对象 分页控制对象
formList: {
name: '',
parentId: ''
},
// 分页配置
pageConfig: {
currentPage: 1,
pageSize: 10,
total: 1000
},
// dialog
type: '',
})
// 解构抛出 直接使用
// const { type} = toRefs(data)
// 函数使用
const getData = () => {
// 查询方法
// 查询参数
const params = {
name : data.formList.name,
parentId : data.formList.parentId,
pageIndex : data.pageConfig.currentPage,
pageSize : data.pageConfig.pageSize
}
}
</script>
- method
<script lang="ts" setup>
// 函数使用
const getData = () => {
// 查询方法
// 查询参数
const params = {
name : data.formList.name,
parentId : data.formList.parentId,
pageIndex : data.pageConfig.currentPage,
pageSize : data.pageConfig.pageSize
}
}
</script>
- computed
```js
<script lang="ts" setup>
import { ref, computed} from 'vue'
// computed计算属性传入一个回调函数
const areYouSureYouAreABaby = computed(() => {
return `I'm sure,I'm a 300 month baby, ${baby.value}`
})
</script>
- watch
```js
<script lang="ts" setup>
import { ref, watch, watchEffect } from 'vue'
watch(() => baby.value, () => {
return `I'm sure,I'm a 300 month baby, ${baby.value}`
})
</script>
9.父子组件传值
9.1 父传子第一种
- 父组件
<ItemDialog ref="itemDialog"></ItemDialog>
const itemDialog = ref();
const addData = () => {
data.type = "add";
itemDialog.value.init(null,data.type);
}
- 子组件
const init = (id, type) => {
// 界面初始化接收参数
data.type = type;
}
//暴露方法
defineExpose({
init,
});
9.2 父传子第二种
- 父组件
<ShopCardList :shop-list="data.shopList"></ShopCardList>
import ShopCardList from "./Shop/components/shopCardList.vue";
- 子组件
import { reactive, onMounted,defineProps } from 'vue'
// Props 只读不能修改
const props = defineProps({
shopList: [],
})
9.3 子传父
- 父组件
<ScreeningList @get-screening-index="getScreeningIndex"></ScreeningList>
import ScreeningList from "./Shop/components/screeningList.vue";
const getScreeningIndex = (index) => {
data.params.screening = index;
getShopList();
}
- 子组件
const emit = defineEmits(['getScreeningIndex'])
const getShopListByScreening = (index) => {
emit('getScreeningIndex',index)
}
## 10.项目结构
- index.html:界面入口
删除页面白边
```html
<body style="margin: 0">
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
11.本地存储
11.1 cookie
- 不适合存储大量的数据。4k左右
- 一般由服务器生成,可设置失效时间。如果在浏览器端生成Cookie,默认是关闭浏览器后失效
- 通信时,每次都会携带在HTTP头中,如果使用cookie保存过多数据会带来性能问题
2.localStorage
- 是永久存储,浏览器关闭后数据不会丢失,除非主动删除数据。当关闭页面后重新打开,会读取上一次打开的页面数据。
- 除非被清除,否则永久保存,可变相设置失效时间。5MB
- 仅在客户端(即浏览器)中保存,不参与和服务器的通信
3.sessionStorage
- 在当前浏览器窗口关闭后自动删除
4.操作
sessionStorage.setItem("key", "value");
localStorage.setItem("aaa", "111");
var value = sessionStorage.getItem("key");
var site = localStorage.getItem("asd");
// 删除
sessionStorage.removeItem("key");
localStorage.removeItem("asd");
// 清除
sessionStorage.clear();
localStorage.clear();
12.动态路由和导航栏
12.1 框架界面
- frame/index
<template>
<el-container>
<el-aside width="200px">
<Sidebar class="sidebar-container"></Sidebar>
</el-aside>
<el-container >
<el-header>
<Navbar></Navbar>
</el-header>
<el-main>
<AppMain></AppMain>
</el-main>
</el-container>
</el-container>
</template>
<script lang="ts" setup>
import Navbar from './Navbar.vue';
import Sidebar from './Sidebar/index.vue';
import AppMain from "./AppMain.vue";
</script>
<style>
.sidebar-container {
transition: width 0.28s;
width: 200px;
background-color: #545c64;
height: 100%;
position: fixed;
font-size: 0px;
top: 0;
bottom: 0;
left: 0;
z-index: 1001;
overflow: hidden;
}
</style>
- frame/AppMain
A<template>
<router-view v-slot="{ Component }" :key="key">
<keep-alive>
<component :is="Component" />
</keep-alive>
</router-view>
</template>
<script lang="ts" setup>
import { computed } from 'vue'
import { useStore } from "vuex";
import { useRouter } from 'vue-router'
const store = useStore();
const router = useRouter();
const key = computed(() => {
return router
})
</script>
<style lang="scss" scoped>
.app-main {
min-height: calc(100vh - 50px);
width: 100%;
position: relative;
overflow: hidden;
}
.fixed-header+.app-main {
padding-top: 50px;
}
.hasTagsView {
.app-main {
min-height: calc(100vh - 84px);
}
.fixed-header+.app-main {
padding-top: 84px;
}
}
</style>
<style lang="scss">
.el-popup-parent--hidden {
.fixed-header {
padding-right: 15px;
}
}
</style>
- frame/Navbar
<template>
<div class="navbar">
<el-row>
<el-col :span="14" >
<div class="l-content">
<el-breadcrumb separator="/">
<el-breadcrumb-item
v-for="(item,index) in data.breadList"
:key="index"
:to="{ path: item.path }"
>{{item.name}}</el-breadcrumb-item>
</el-breadcrumb>
</div>
</el-col>
<el-col :span="10">
<div class="right-menu">
<el-dropdown class="avatar-container right-menu-item hover-effect"
trigger="click">
<div class="avatar-wrapper">
<img class="user-avatar" src="../assets/images/login.jpg" alt="">
<span class="user-name">{{ data.name }}</span>
<i class="el-icon-caret-bottom"/>
</div>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item>
<span style="display:block;"
@click="handleModifyPass()">
修改密码
</span>
</el-dropdown-item>
<el-dropdown-item divided>
<span style="display:block;"
@click="logout()">
退出系统
</span>
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</el-col>
</el-row>
<el-dialog title="修改密码"
:modal="true"
:append-to-body="false"
:close-on-click-modal="false"
v-model="data.dialogVisible"
width="400px">
<template #header>
<i class="el-icon-key"/> 修改密码
</template>
<el-form ref="form" :model="data.form" label-width="100px" :rules="data.rules">
<el-form-item label="账户名">
<el-input v-model="data.form.name" style="width:250px" :disabled="true"/>
</el-form-item>
<el-form-item label="旧密码" prop="oldPass">
<el-input v-model="data.form.oldPass" style="width:250px" show-password/>
</el-form-item>
<el-form-item label="新密码" prop="newPass">
<el-input v-model="data.form.newPass" style="width:250px" show-password/>
</el-form-item>
<el-form-item label="确认密码" prop="confirmPass">
<el-input v-model="data.form.confirmPass" style="width:250px" show-password/>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="data.dialogVisible = false">取 消</el-button>
<el-button type="danger" @click="handleSavePass()">确 定</el-button>
</span>
</el-dialog>
</div>
</template>
<script lang="js" setup>
import { useStore } from 'vuex'
import { onMounted, reactive, ref } from "vue";
import { useRouter, useRoute } from "vue-router";
import Api from '@/api/auth'
import {ElMessage} from "element-plus";
import { removeToken } from '@/utils/auth/auth.js'
import {getEncryptPassword} from "@/utils/passwordEncrypt";
import { onBeforeRouteUpdate } from "vue-router";
const store = useStore();
const router = useRouter()
const route = useRoute();
const data = reactive({
// 路由集合
breadList: [],
sidebarOpened: false,
dialogVisible: false,
form: {
oldPass: '',
newPass: '',
confirmPass: '',
accountId: localStorage.getItem("userId"),
},
rules: {
oldPass: [
{required: true, message: '请输入旧密码', trigger: 'blur'}
],
newPass: [
{
required: true,
// pattern: /(?=.*[0-9])(?=.*[a-zA-Z])(?=.*[^a-zA-Z0-9]).{8,30}/,
message: '请输入新密码',
trigger: 'blur'
}
],
confirmPass: [
// {validator: validatePass, trigger: 'blur'}
]
}
})
// Mounted
onMounted(() => {
data.name = localStorage.getItem("userName")
data.form.name = data.name;
getBreadcrumb();
})
/**
* 路由变化
*/
onBeforeRouteUpdate((val, oldVal) => {
getBreadcrumb(val.matched);
});
// Methods
const isHome = (route) => {
return route.name === "首页";
}
const getBreadcrumb = (matched) => {
if (matched === undefined) {
matched = route.matched;
}
//如果不是首页
if (!isHome(matched[0])) {
matched = [{ path: "/home", meta: { title: "首页" } }].concat(matched);
}
data.breadList = matched;
}
const validatePass = (rule, value, callback) => {
if (value === '') {
callback(new Error('请输入确认密码'));
} else if (value !== this.form.newPass) {
callback(new Error('两次输入密码不一致!'));
} else {
callback();
}
};
/**
* 修改密码
*/
const handleModifyPass = () => {
data.dialogVisible = true;
}
/**
* 确认修改密码
*/
const form = ref();
const handleSavePass = () => {
form.value.validate(valid => {
if (valid) {
data.form.newPass = getEncryptPassword(data.form.newPass, 'aes');
data.form.oldPass = getEncryptPassword(data.form.oldPass, 'aes');
Api.modifyPass(data.form).then(res => {
console.log(res)
if (res.code === '20000'){
ElMessage.success('修改成功');
logout();
} else {
ElMessage.error(res.message)
}
})
} else {
return false;
}
});
}
/**
* 退出系统
*/
const logout = () => {
Api.logout().then(res => {
removeToken();
router.push({
path: '/login',
})
})
}
</script>
<style lang="scss" scoped>
.l-content{
margin-top: 20px;
}
.navbar {
margin-left: 10px;
width: 99%;
height: 60px;
overflow: hidden;
position: relative;
background: #fafafa;
box-shadow: 0 1px 4px rgba(0, 21, 41, .08);
.right-menu {
float: right;
height: 100%;
line-height: 50px;
&:focus {
outline: none;
}
.right-menu-item {
display: inline-block;
padding: 0 8px;
height: 100%;
font-size: 18px;
color: #5a5e66;
vertical-align: text-bottom;
&.hover-effect {
cursor: pointer;
transition: background .3s;
&:hover {
background: rgba(0, 0, 0, .025)
}
}
}
.avatar-container {
margin-right: 30px;
.avatar-wrapper {
margin-top: 5px;
position: relative;
.user-avatar {
cursor: pointer;
width: 40px;
height: 40px;
border-radius: 20px;
vertical-align: middle;
margin-bottom: 8px;
}
.user-name {
font-size: 16px;
color: #000;
}
.el-icon-caret-bottom {
cursor: pointer;
position: absolute;
right: -20px;
top: 25px;
font-size: 12px;
}
}
}
}
}
</style>
- frame/Siderbar/index
通过后端获取配置
<template>
<div>
<el-scrollbar >
<el-menu style="margin-right: -1px;"
class="myMenu"
default-active="1"
:collapse="data.isCollapse"
:unique-opened="true"
:default-openeds="data.openeds"
:collapse-transition="false"
mode="vertical"
active-text-color="#ffd04b"
background-color="#545c64"
text-color="#fff"
>
<sidebar-item v-for="route in data.indexDate" :key="route.funId.toString()" :item="route" :base-path="route.url"/>
<sidebar-item v-for="route in data.menuData"
:key="route.funId.toString()"
:item="route"
:base-path="route.url"/>
</el-menu>
</el-scrollbar>
</div>
</template>
<script lang="js" setup>
import { useStore } from "vuex";
import { useRouter } from 'vue-router'
import SidebarItem from './SidebarItem.vue'
import { reactive } from "vue";
const store = useStore();
const router = useRouter()
const data = reactive({
showLogo: true,
isCollapse: false,
openeds: [],
menuData: [],
indexDate: [
{
'funId': '1',
'funName': '首页',
'url': '/homePage',
},
{
'funId': '2',
'funName': '菜谱管理',
'icon':'KnifeFork',
'children':[{
'funId': '21',
'funName': '菜谱评论管理',
'url': '/menucomment/index',
'icon': 'KnifeFork'
},{
'funId': '22',
'funName': '菜谱列表',
'url': '/menu/index',
'icon': 'KnifeFork'
}]
},
]
})
</script>
- frame/Siderbar/SiderbarItem
<template>
<div>
<!-- 有二级分类的一级分类 -->
<el-sub-menu
class="myMenu"
v-if="item.children && item.children.length > 0"
:key="item.funId"
:index="item.funId.toString()"
teleported>
<template #title>
<component :is="item.icon" style="width: 16px;height: 16px;"></component>
<span>{{item.funName}}</span>
</template>
<!--二级分类-->
<router-link v-for="(citem,cindex) in item.children"
:key="cindex" :to="resolvePath(citem.url)">
<el-menu-item :index="citem.url">
<template #title>
<component :is="citem.icon" style="width: 16px;height: 16px;"></component>
<span>{{citem.funName}}</span>
</template>
</el-menu-item>
</router-link>
</el-sub-menu>
<!--只有一级分类-->
<router-link class="myMenu"
v-else-if="item.children == null || item.children.length == 0"
:key="item.funId.toString()"
:to="resolvePath(item.url)"
:index="item.funId.toString()">
<el-menu-item :index="item.funId.toString()">
<template #title>
<component :is="item.icon" style="width: 16px;height: 16px;"></component>
<span>{{item.funName}}</span>
</template>
</el-menu-item>
</router-link>
</div>
</template>
<script lang="js" setup>
import {onMounted, reactive} from "vue";
import {
HelpFilled,
Flag,
} from '@element-plus/icons-vue'
const data = reactive({
})
// Props
const props = defineProps({
item: {
type: Object,
required: true
},
isNest: {
type: Boolean,
default: false
},
basePath: {
type: String,
default: ''
}
})
// Mounted
onMounted(() => {
})
// Methods
const resolvePath = (routePath) => {
return routePath;
}
</script>
<style scoped lang="scss">
a {
text-decoration: none;
}
</style>
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律