vue3+vite5前端(二)

文章目录

前端:vue3+vite5前端(一)
后端(python):fastapi开发项目
部署: win10搭建minikube(driver=docker)部署项目

在这里插入图片描述
在这里插入图片描述

10. 按需引入elemet plus(失败…不想再尝试)和 @方式导包路径配置
10.1 安装插件unplugin-vue-components unplugin-auto-impor(按需导入失败不在尝试)

安装命令

 cnpm install -D unplugin-vue-components unplugin-auto-import
 cnpm install --save-dev @types/node

一直报错: Failed to resolve import "@/router/index" from "src/main.ts". Does the file exist?

[vite] Internal server error: Failed to resolve import "@/style.css" from "src/main.ts". Does the file exist? Plugin: vite:import-analysis(一直报错启动失败,最后按需导入失败。element plus2.7.6,网上又说版本问题,我猜是main.ts中使用的导包没改导致的,先记录问题)

10.2 配置@导包

首先在vite.config.ts添加配置:

import { fileURLToPath, URL } from 'node:url'


export default defineConfig({
  plugins: 
    resolve: {
        alias: {
          "@": fileURLToPath(new URL('./src', import.meta.url)),
        }
     },

tsconfig.json或者tsconfig.app.json文件添加配置:

{
  "compilerOptions": {
    "baseUrl": "./",
    "paths": {
      "@/*": ["./src/*"]
    }
  },
}
11. vite设置环境配置文件
11.1 项目目录创建.env.development文件,示例:

.env 用于公共配置项

.env.development 用于开发环境配置项

.env.production 用于生产环境配置项

配置项以VITE_开头才会加载到vite

VITE_ENV = 'development'
VITE_API_HOST = 
VITE_PORT = 3000
VITE_API_BASE_URL = '/api'
11.2 vite.config.ts文件添加配置,加载不同环境配置文件
import { defineConfig, loadEnv } from 'vite'


export default defineConfig((configEnv) => {
	// configenv也可改为{env, mode}
	const env = loadEnv(configEnv.mode, process.cwd())
	
	return {
		plugins: [vue()],
        
        // 不想用VITE开头的
        envPrefix:“自定义环境变量名开头,APP_// 示例
        server:{
            host: env.VITE_API_HOST || '0.0.0.0',
            // disableHostCheck: true,
            open: false,
            port: env.VITE_PORT || 8000,
            strictPort: true,
            proxy: {
                'api': {
                    target: env.VITE_API_TARGET_URL,
                    changeOrigin: true,
                    rewrite: (path) => path.replace(/^\/api/, '')
                }
            }
        }
	}
}
11.3 在package.json中修改默认命令应对开发和打包
  "scripts": {
    "dev": "vite --mode development",
    "build:dev": "vite build --mode development",
    "build:pro": "vite build --mode production",
    "build": "vue-tsc -b && vite build",
    "preview": "vite preview"
  },
11.4 项目代码中使用使用环境变量

xxx.ts文件中:

const baseURL = import.meta.env.VITE_API_BASE_URL

想添加提示,在项目目录中找到vite-env.d.ts文件添加:

declare global {
  declare interface ImportMetaEnv {
    // 在这里为所有的环境变量定义属性名和类型...
    readonly VITE_API_BASE_URL: string;
  }
  
  declare interface ImportMeta {
    readonly env: ImportMetaEnv
  }
}
12 表单页和列表页el-swith按钮展示

效果:
在这里插入图片描述

el-switch按钮属性:

active-text 展示 switch 打开时的文字描述

inactive-text switch 的状态为 off 时的文字描述(与active-text 都展示,支持字体颜色切换)

active-value / inactive-value switch 状态为 on/off 时 时的值

@change事件绑定切换动作后的处理逻辑
列表页:

<el-table :data="tableData">
	<el-table-column prop="status" label="状态" width="120">
            <template #default="scope">
              <el-switch
                v-model="scope.row.status"
                :active-value=0
                :inactive-value=1
                @change="onRefreshStatus(scope.row.status)"
              />
              <span v-if="scope.row.status === 1">禁用</span>
              <span v-else>启用</span>
            </template>
    </el-table-column>
</el-table>

表单:

<el-form>
	<el-form-item label="状态" prop="status">
        <el-switch
          v-model="form.status"
          :active-value=0
          :inactive-value=1
        />
        <span v-if="form.status === 1">禁用</span>
        <span v-else>启用</span>
      </el-form-item>
</el-form>
13 列表页枚举类型展示

效果:
在这里插入图片描述

以列表页性别字段展示男女示例

formatter 属性用来格式化内容 ,支持函数

<template>
    <el-table :data="tableData">
        <el-table-column prop="gender" label="性别" :formatter="formatter" width="120" />
    </el-table>
</template>

<script setup lang="ts">
import type { TableColumnCtx } from 'element-plus'
// User定义数据类型
import type { User } from '@/view/user/userType'

const formatter = (row: User, column: TableColumnCtx<User>) => {
  switch (row.gender){
    case 0:
      return '男'
    default:
      return '女'
  }
}
</script>
14 选择和树形选择简单使用
14.1 el-select选择器:

效果:
在这里插入图片描述

multiple: 支持多选,此时 v-model 的值为当前选中值所组成的数组

collapse-tags:多选时展示选中的选项个数

collapse-tags-tooltip:鼠标悬停时可展示具体选中的哪些选项

<template>
<el-table :data="tableData">
	<el-form-item label="分配角色" prop="role_ids">
        <el-select
          v-model="form.role_ids"
          multiple
          collapse-tags
          collapse-tags-tooltip
          placeholder="请选择该用户要分配的角色"
          style="width: 240px"
        >          
          <el-option
            v-for="item in allRoleList"
            :key="item.role_id"
            :label="item.role_name"
            :value="item.role_id"
          />
        </el-select>
      </el-form-item>
</el-table>
</template>

<script setup lang="ts">
    
const allRoleList = [
  {
    role_id: '1',
    role_name: '角色id1',
  },
  {
    role_id: '2',
    role_name: '角色id2',
  }
]
</script>
14.2 el-tree-select选择器:

在这里插入图片描述

visible-change 事件和visible监听下拉菜单的显示状态
check-strictly="true"表示父子 不互相关联 ,即勾选父节点不勾选下面的子节点,同时可以提交所勾选的节点,值为false时只提交子节点的数据

checkcheck-change绑定的函数解决选中父节点不勾选子节点,选取子节点可勾选父节点。使用该属性必须设置node-key的值(值确保能区分,如以下示例使用的是allMenuList中的value)

<template>
<el-table :data="tableData">
    <el-form-item label="分配菜单" prop="menu_ids">
      <el-tree-select
        placeholder="请选择该角色要分配的菜单"
        v-model="form.menu_ids"
        :data="allMenuList"
        multiple
        :render-after-expand="false"
        show-checkbox
        collapse-tags
        collapse-tags-tooltip
        :visible="isSelectVisible"
        :check-strictly="true"
        @visible-change="handleSelectClose"
        :default-checked-keys="checkedKeys"
        style="width: 240px"
        node-key="value"
        @check="hanleCheck"
        @check-change="checkChange"
        :props="{label: 'permission_name', value:'value'}"
      >
        <template #default="{ node, data }">
          <--   node是el-tree-select选项数据,data是后端返回的查询数据       -->
          <span class="custom-tree-node">
              <span>{{ node.label }}<el-tag type="primary" effect="light" round size="small">{{ formatter(data.menu_type) }}</el-tag></span>
          </span>
        </template>
      </el-tree-select>
    </el-form-item>
</el-table>
</template>

<script setup lang="ts">
import { ref } from 'vue'    

const isSelectVisible = ref(true)

const handleSelectClose = () => {
  isSelectVisible.value = false
}
    
const allMenuList = [
  {
    value: '3',
    label: 'Level one 3',
    children: [
      {
        value: '3-1',
        label: 'Level two 3-1',
        children: [
          {
            value: '3-1-1',
            label: 'Level three 3-1-1',
          },
        ],
      },
      {
        value: '3-2',
        label: 'Level two 3-2',
        children: [
          {
            value: '3-2-1',
            label: 'Level three 3-2-1',
          },
        ],
      },
    ],
  },
]

const hanleCheck = (data: any, node: any) => {
  // 获取当前节点是否被选中
  const curNode = treeRef.value!.getNode(data)
  const isChecked = curNode.checked;
  // 如果当前节点被选中,则遍历下级子节点并选中,如果当前节点取消选中,则遍历下级节点并取消
  console.log("isChecked", isChecked)
  if (isChecked) {
    // 判断该节点是否有下级节点,如果有那么遍历设置下级节点为选中
    data.children && data.children.length > 0 && setChildreChecked(data.children, true)
  } else {
    // 如果节点取消选中,则取消该节点下的子节点选中
    data.children && data.children.length > 0 && setChildreChecked(data.children, false)
  }
  function setChildreChecked(node: any, isChecked: boolean) {
    node.forEach((item: any) => {
      item.children && item.children.length > 0 && setChildreChecked(item.children, isChecked);
      // 修改勾选状态
      treeRef.value!.setChecked(item.value, isChecked, false)
    })
  }
  // 获取所有选中的节点treeRef.value.getCheckedKeys()
}

const checkChange = (data: any, checked: boolean, indeterminate: any) => {
  console.log(data, checked, indeterminate)
  // 选中全部子节点,父节点也默认选中,但是子节点再次取消勾选或者全部子节点取消勾选也不会影响父节点勾选状态
  let checkNode = treeRef.value!.getNode(data) //获取当前节点
  
  // 勾选部分子节点,父节点变为全选状态
  if (
    checkNode.parent &&
    checkNode.parent.childNodes.some((ele: any) => ele.checked)
  ) {
    checkNode.parent.checked = true
    checkNode.parent.indeterminate = false
  } else {
    checkNode.parent.checked = false
    checkNode.parent.indeterminate = false
  }
}

const formatter = (menuType: any) => {
  switch (menuType){
    case MenuType.Dir:
      return '目录'
    case MenuType.Menu:
      return '菜单'
    case MenuType.Button:
      return '按钮'
    case MenuType.DropdownMenu:
      return '下拉菜单'
    case MenuType.DynamicRoute:
      return '动态路由'
    default:
      return '其它'
  }
}
</script>
<style scoped>
.custom-tree-node {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: space-between;
  font-size: 14px;
  padding-right: 8px;
}
</style>

自定义树形选择器选项展示内容

方法一:el-tree-select使用:props="{label: 'permission_name', value:'id'}"可指定选项中展示内容label字段名为'permission_name'对应的数据,并配合node-key="id"保证提交数据使用字段value对应的值。

方法二:使用插槽<template #default="{ node, data }">或者官方文档上另外一种:render-content="renderContent"

15 父子组件通信总结(单方向传递)

场景: 父组件Parent.vue需要使用子组件Son.vue,要求一打开父组件就弹出子组件这个提示弹窗。

父组件Parent.vue

centerDialogVisible:用于控制弹窗是否展示(不要的话,下次就这个页面弹窗不展示)

onClose:用于关闭子组件弹窗(不要的话,下次就这个页面弹窗不展示)

:centerDialogVisible:父组件传递给子组件的属性,值是父组件中定义的常量或变量,centerDialogVisible是属性名,名称任意,但要与子组件使用defineProps方法时要一致

@onCloseonClose是父组件告知子组件可以使用的方法名称,名称任意,但要与子组件defineEmits方法时要一致,值是父组件中定义的方法,用于子组件更新父组件传递的数据或者其他操作,因为子组件不能直接修改父组件传递的属性对应数据。

<template>
    <Son :centerDialogVisible=centerDialogVisible @onClose="onClose"/>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import Son from '@/view/conponent/Son.vue'

const centerDialogVisible = ref(true)

const onClose = () => {
    centerDialogVisible.value = false
}

</script>

<style scoped>

</style>

子组件Son.vue:

<template>
    
    <el-dialog  :modelValue="props.centerDialogVisible" title="提示" width="500" center>
        <el-text class="mx-1" type="primary" style="font-size: 25px">正在开发中<el-icon><Loading /></el-icon>...{{ delay }}】秒后将自动跳转到首页</el-text>
        
        <template #footer>
        <div class="dialog-footer">
            <el-button type="primary" @click="onClose">
            确定
            </el-button>
        </div>
        </template>
    </el-dialog>
</template>

<script setup lang="ts">
import {
   Loading,
} from '@element-plus/icons-vue'
import { ref, onMounted, onUnmounted } from 'vue'
import { defineProps, defineEmits } from 'vue'
import router from '@/router/index'

const props = defineProps({
    centerDialogVisible: Boolean
})
const emit = defineEmits(["onClose"])
const onClose = () => {
    emit('onClose')
    stopCountDown()
}
const delay = ref(30)
let timerId: any = null
const startCountDown = () => {
    timerId = setInterval(() => {
        if (delay.value > 0) {
            delay.value--
        } else {
            stopCountDown()
    }}, 1000)
}

const stopCountDown = () => {
    clearInterval(timerId)
    timerId = null
    router.push("Home")
}

onMounted(() => {
    startCountDown()
})

onUnmounted(() => {
    stopCountDown()
})

</script>

<style scoped>
</style>
16 状态管理pinia
16.1 安装pinia
cnpm install pinia
cnpm install pinia pinia-plugin-persistedstate

应用场景:一个 Store 应该包含可以在整个应用中访问的数据。这包括在许多地方使用的数据,例如显示在导航栏中的用户信息,以及需要通过页面保存的数据,例如一个非常复杂的多步骤表单。

另一方面,你应该避免在 Store 中引入那些原本可以在组件中保存的本地数据,例如,一个元素在页面中的可见性。

16.2 main.ts 加载pinia插件
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
import App from './App.vue'

const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
const app = createApp(App)

app.use(pinia)
app.mount('#app')

16.3 定义store方式1:setup store
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export const useLoginStore = defineStore(
    'useLoginStore',
    () => {
        // ref() 就是 state 属性
        const token = ref('')

        // computed() 就是 getters
        const clearToken = computed(()=> {
            token.value = ""
        })

        // function() 就是 actions
        function setToken(newToken: string) {
            token.value = newToken
        }

        return { token, clearToken, setToken }
    },
    {
        // 开启持久化
        persist: true
    }
)
16.4 定义store方式2:option store

该种方式可以直接使用$reset方法将state重置为原始值,不需要自己定义这个方法。

import { defineStore } from 'pinia'

export const useUserRoleStore = defineStore(
    'useUserRoleStore',
    {
            state: () => ({ token: "" }),

            getters: {
                clearToken: (state) => state.token =  ""
            },

            actions: {
                setToken(newToken: string) {
                  this.token = newToken
                },
            },
    		// 开启持久化
    		persist: true
    }
)
16.5 存储jwt示例:

登录Login.vue

<script setup lang="ts">
import { useLoginStore } from "@/store/useLoginStore"

 const loginStore = useLoginStore()
 const res = await login(ruleForm)
 // 方式1: 使用pinia存储access_token
 loginStore.setToken(res.data.access_token)
 // 方式2: 使用session存储
 // sessionStorage.setItem('token', res.data.access_token)
 
 </script>

请求http.ts

import axios from 'axios'
import { useLoginStore } from "@/store/useLoginStore"
// 创建axios实例
const instance = axios.create({
    // 接口
    baseURL: import.meta.env.VITE_API_BASE_URL || '/mock',
    // 超时时间
    timeout: 50000,
});
// 请求拦截
instance.interceptors.request.use(
    config => {
        // 方式2: 从session获取登录保存的token
        // let token = sessionStorage.getItem('token')
        const loginStore = useLoginStore()
        if (loginStore.token) {
            // 请求头添加jwt, 方式1: 从pinia获取登录保存的token
            config.headers['Authorization'] = 'Bearer ' + loginStore.token
        }
        return config
    },
    (error: any) => {
        console.log("interceptors.request")
        return Promise.reject(error)
    }
);

默认pinia持久化数据保存在浏览器Local Storage(浏览器鼠标右键 ——【检查】—— 【Application】可查看)
在这里插入图片描述

17 动态路由(后端管理路由)

路由嵌套结构示例:

    {
        path: '/home',
        name: 'home',
        meta: {
            title: '首页'
        },
        component: () => import('@/view/Home.vue'),
        redirect: '/index',
        children: [
            {
                path: '/index',
                name: 'Index',
                meta: {
                    title: '首页'
                },
                component: () => import('@/view/Index.vue')
            }
        ]
    }
17.1 静态路由处理(router/index.ts):
import type { RouteRecordRaw } from 'vue-router'
import { createRouter, createWebHashHistory } from 'vue-router'
import { useLoginStore } from "@/store/useLoginStore"
import { useUserRoleStore } from "@/store/useUserRoleStore"
import { nextTick, ref } from 'vue'


const baseRoutes: RouteRecordRaw[] = [
    {
        path: '/',
        redirect: '/login'
    },{
        path: '/login',
        name: 'login',
        meta: {
            title: '登录'
        },
        component: () => import('@/view/Login.vue'),
    },
    {
        path: '/home',
        name: 'Home',
        meta: {
            title: '首页'
        },
        component: () => import('@/view/Home.vue'),
    }
]


const router = createRouter({
    history:createWebHashHistory(),
    routes: baseRoutes 
})

// 动态添加路由逻辑


// 导出路由
export default router

404路由(等待动态路由处理完成后添加在router的最后面):

const notFoundRoutes = [{

    path: '/404',
    name: 'NotFound',
    meta: {
        title: '404'
    },
    component: () => import('@/view/NotFound.vue')
},
// 未知路由重定向
{
    path: '/:pathMatch(.*)',
    redirect: '/404',
},]
17.2 路由守卫动态添加路由逻辑:
let isLoad = ref(true)

// 挂载路由导航守卫
router.beforeEach(async (to, from, next) => {
    
    // 修改页面title
    if (to.meta.title) {
        document.title = 'management系统' + to.meta.title
    }
    // 放行登录页面
    if (to.path === '/login') {
        return next()
    }

    await nextTick()
    // 获取token
    // const token = sessionStorage.getItem('token')
    const loginStore = useLoginStore()
    if (!loginStore.token) {
        return next({ path: '/login', replace: true })
    } 
    const userRoleStore = useUserRoleStore()
    console.log(router.getRoutes(), "打印当前所有可加载的路由")
    // 获取动态路由
    if (userRoleStore.menuList.length === 0) {
        // 从未加载过动态路由
        await userRoleStore.initDymamicRoutesMenu()
        notFoundRoutes.forEach((item) => {
            router.addRoute(item)
        })
        isLoad.value = false
        next(to.fullPath)
    } else if (isLoad.value && userRoleStore.menuList.length > 0) {
        // F5刷新,重新加载动态路由
        userRoleStore.addDymamicRoutes(userRoleStore.menuList)
        notFoundRoutes.forEach((item) => {
            router.addRoute(item)
        })
        isLoad.value = false
        next(to.fullPath)
    } else {
        next()
    }
    
})

17.3 定义动态路由处理的store(store/useUserRoleStore.ts)
import { defineStore } from 'pinia'
import { getIndexInfo } from '@/api/home/homeApi'
import { ElMessage } from "element-plus"
import { ref } from 'vue'
import router from '@/router/index'
import getAllDynamicRoutes from '@/untils/addRouter'


export const useUserRoleStore = defineStore(
    'useUserRoleStore', 
    () => { 
        // 菜单列表
        const menuList = ref([])
        
        // 未查看消息数量
        const curMessageNum = ref(0)
        
        // 下拉菜单列表
        const dropdownList = ref([])
        
        // 根据菜单列表添加路由
        function addDymamicRoutes(allMenuList) {
            let allDynamicRoutes = getAllDynamicRoutes(allMenuList)
            allDynamicRoutes.forEach((item) => {
                // 判断是否重复添加路由
                let isExistRouteByPath = router.getRoutes().find(r => r.path === item.path)
                if (!isExistRouteByPath){
                    // 将动态路由添加为Home静路由的子路由
                    router.addRoute('Home', item)
                }
            })
        }

        async function initDymamicRoutesMenu() {
            // 请求后端接口,返回菜单列表等信息
            const res = await getIndexInfo()
            if (res.data) {
              if (res.data.code === 200 && res.data.data) {
                curMessageNum.value = res.data.data.notice_total
                dropdownList.value = res.data.data.dropdown_list
                menuList.value = res.data.data.menu_list
                addDymamicRoutes(menuList.value)
                console.log(router.getRoutes(), "获取当前能匹配的所有路由")
              } else {
                ElMessage.error("服务器报错")
              }
            } else {
              ElMessage.error("服务器内部错误")
            }
        }
        return { menuList, curMessageNum, dropdownList, addDymamicRoutes, initDymamicRoutesMenu }
     },{
        persist: true
     }

)
17.4 后端返回的菜单路由处理逻辑(untils/addRouter.ts):
// 获取所有项目view目录下vue
const modules = import.meta.glob('@/view/**/**.vue')


function getAllChildMenus(menu) {
    let childMenus = []
    if (menu.menu_type === 1) {
        const componentPath = menu.component
        let obj = {
            path: menu.index,
            name: menu.name,
            meta: {
                title: menu.label,
            },
            // component: componentPath
            // component: () => import('/src/view/' + componentPath + '.vue')
            // component: () => import(`@/view/Index.vue`)
            // component: modules[`@/view/${menu.component}.vue`]
            component: modules[`/src/view/${componentPath}.vue`]
        }
        childMenus.push(obj)
    } else if (menu.children) {
        menu.children.forEach(child => {
            childMenus = childMenus.concat(getAllChildMenus(child))
        });
    }
    return childMenus
}


function getAllDynamicRoutes(menuList) {
    let initdynamicRoutes = []
    const dynamicChildRoutes = menuList.reduce((acc, menu) => {
        return acc.concat(getAllChildMenus(menu))
    }, [])
    if (dynamicChildRoutes.length > 0) {
        initdynamicRoutes.push(...dynamicChildRoutes)
    }
    return initdynamicRoutes
}


export default getAllDynamicRoutes

后端返回component字段实例:

                {
                    "path": "/index",
                    "name": "首页",
                    "meta": {
                        "title": "首页"
                    },
                    "component": "Index"
                }
17.5 大坑1:后端返回菜单信息中component字段是字符串,import处理不当问题记录
导入方式问题
component: () => import(`@/view/Index.vue`)静态路由,添加无问题,但不符合预期
// 后端返回的完整导包路径为component:“@/view/Index.vue”
component: () => import(`${item.component}`)
不能这样
vue-router中的addRoute方法无法识别到@路径,导致component为undefined,页面空白
const componentPath = item.component
component: () => import(`@/view/${componentPath}.vue`)
不能这样
vue-router中的addRoute方法获取到component的值是/src/view/${componentPath}.vue,离谱
component = () => import(`@/view/${item.component}.vue`)不能这样
报错:dynamic-import-helper.js:10 Uncaught (in promise) Error: Unknown variable dynamic import: …/view/user/UserList.vue.Note that variables only represent file names one level deep.
vue-router中的addRoute方法获取到component的值是/src/view/${item.component}.vue,离谱
component: () => import(/* @vite-ignore */‘/src/view/’ + componentPath + ‘.vue’)可以
但是vite会报错The above dynamic import cannot be analyzed by Vite…you can use the /* @vite-ignore / comment inside the import() call to suppress this warning。需要添加/ @vite-ignore */
const modules = import.meta.glob(‘@/view//.vue’)
component: modules[/src/view/${componentPath}.vue]
可以,推荐
17.6 大大大坑2:解决动态路由跳转空白页问题和报错vue-router.mjs:35 [Vue Router warn]: No match found for location with path “/index”

场景1:加载动态路由后,页面跳转报错

原因:vue-router中addRoute异步加载路由,还未等待动态路由加载完成就使用next()放行

解决办法:使用next(to.fullPath),再次进入路由守卫

    if (userRoleStore.menuList.length === 0) {
        // 从未加载过动态路由
        await userRoleStore.initDymamicRoutesMenu()
        notFoundRoutes.forEach((item) => {
            router.addRoute(item)
        })
        // 更新isLoad的值,表示以加载过动态路由,不需要再次从后端接口添加动态路由
        isLoad.value = false
        next(to.fullPath)
    }

场景2: 浏览器F5刷新,页面跳转报错

原因:F5刷新会导致动态路由丢失,需要重新加载一次

解决办法:使用变量isLoad判断是否加载过动态路由,F5刷新isLoad变为初始值true,已加载过动态路由直接从store的菜单列表再次加载添加动态路由

let isLoad = ref(true)

if (isLoad.value && userRoleStore.menuList.length > 0) {
        // F5刷新,重新加载动态路由
        userRoleStore.addDymamicRoutes(userRoleStore.menuList)
        notFoundRoutes.forEach((item) => {
            router.addRoute(item)
        })
        isLoad.value = false
        next(to.fullPath)
    }

18 报错QA
18.1 TypeError: Assignment to constant variable.

场景: 在<script setup lang="ts"></script>中使用reactive定义一个目录列表menuList,想在请求里面对齐值进行更新报错

<script setup lang="ts">
const menuList = reactive([]])

const initIndexInfo = async (params?:any) => {
    const res = await getMenu(params)
	if (res.data)  {
		menuList = res.data.data
	}
}
</script>`

原因:reactive定义的常量不能直接整个重新赋值,支持单个属性更新。换成ref定义

<script setup lang="ts">
const menuList = ref([])

const initIndexInfo = async (params?:any) => {
    const res = await getMenu(params)
	if (res.data)  {
		menuList.value = res.data.data
	}
}
</script>
18.2 [Vue warn]:Failed to resolve component: el-scrollabr

If this is a native custom element, make sure to exclude it from component resolution via compilerOptions.isCustomElement.

原因:搜索el-scrollabr发现是需要使用滚动条,但是组件名称应该是el-scrollbar

18.3 [Vue warn]:Invalid prop: type check failed for prop “router”. Expected Boolean, got String with value “true”

原因:router=true报错,需要数据绑定才是boolean,不然写成字符串形式

                    <el-menu
                        default-active="1"
                        class="el-menu-vertical-home"
                        :router=true
                        active-color="#bfcbd9"
                        :unique-opened=true
                        :collapse="isCollapse"
                    >
18.4 [Vue warn]: Invalid prop: custom validator check failed for prop “size”.

[Vue warn]: Invalid prop: validation failed for prop "size". Expected one of ["", "default", "small", "large"], got value "samll".

原因:size="samll"应该是size="small"

<el-avatar size="samll" :src="avaterImgSrc" style="margin: 10px; float: right" />
19. 前端生产环境打包
19.1 项目目录执行命令:npm run build:pro或者vite build:pro
19.2 坑1:浏览器访问index.html后空白,且js or css文件报错cors error,状态码404或者405

解决办法:vite.config.ts中增加一下代码:

export default defineConfig((configEnv) => {
  const env = loadEnv(configEnv.mode, process.cwd())
  return {
    base: './',
  }
})
19.3 坑2:利用 http-server调试打包文件报错405

安装http-servernpm install http-server -g

执行命令:http-server -p 3000 -P http://127.0.0.1:8080

-p: 指定前端服务启动端口

-P--proxy:指定后端服务代理地址(不设置的话,跨域请求后端报错405)

20. 按钮权限管理: 用于管理前端页面标签和按钮的展示,用于前端层面鉴权
20.1 定义按钮通用英文名称
# 新增按钮
addBtn

# 编辑按钮
modifyBtn

# 详情按钮
detailBtn

# 删除按钮
deleteBtn

# 搜索按钮
searchBtn

# 启用按钮
refreshStatusBtn

按钮标识符 :每个页面唯一,{页面名称}/{按钮通用英文名称}。如:用户管理列表新增按钮标识符为user/addBtn

20.2 后端服务(python fastapi)根据当前用户的角色查询所有按钮

数据表结构可参考17.数据库

@router.get("/buttons")
async def get_menu_role_list(cur_user: Annotated[User, Depends(get_current_user_v1)], db: AsyncSession = Depends(get_db)):
    """
    首页获取当前用户and菜单and角色等信息
    :param cur_user:
    :param db:
    :return:
    """
    # 角色
    role_ids = await get_roles_by_user_id(db, cur_user.id)
    # 按钮(权限)
    button_list = await get_buttons_by_role_ids(db, role_ids)
    res = {
            "button_list": button_list,
        }
    return         


async def get_roles_by_user_id(db: AsyncSession, user_id: int):
    """
    根据用户id查询当前用户的所有角色
    :param db:
    :param user_id:
    :return:
    """
    res = await db.execute(select(Role.id).join(UserRole, UserRole.role_id==Role.id).where(UserRole.user_id==user_id, Role.status==Status.USED.value, Role.is_delete==IsDelete.NOT_DELETED.value))
    return res.scalars().all()


async def get_buttons_by_role_ids(db: AsyncSession, role_ids: list):
    """
    获取当前用户所有的按钮
    RoleMenus表存储了所有角色被分配的菜单,通过Menu表按钮所在菜单id(即parent_menu_id)和RoleMenus表菜单menu_id联合查询按钮类型的数据。
    :param db:
    :param role_ids:
    :return:
    """
    res = await db.execute(select(Menu.permission_identifier).join(RoleMenus, RoleMenus.menu_id==Menu.parent_menu_id).where(RoleMenus.role_id.in_(role_ids),Menu.id!=1, Menu.menu_type==MenuType.BUTTON.value, Menu.status==Status.USED.value, Menu.is_delete==IsDelete.NOT_DELETED.value))
    return res.scalars().all()
20.3 vue3使用pinia状态管理按钮

定义useButtonStore.ts

import { defineStore } from 'pinia'
import { getIndexInfo } from '@/api/home/homeApi'


export const useButtonStore = defineStore(
    'useButtonStore', 
    () => {
        const buttonList = ref([])
        async function getButtonFromBackend {
            // 请求后端接口
            const res = await getIndexInfo()
            if (res.data) {
              if (res.data.code === 200 && res.data.data) {
                buttonList.value = res.data.data.button_list
              } else {
                ElMessage.error("服务器报错")
              }
            } else {
              ElMessage.error("服务器内部错误")
            }
        }
        return {  buttonList, getButtonFromBackend }
     },{
        persist: true
     }
)
20.4 vue3自定义指令

main.ts中添加:

import { useButtonStore } from "@/store/useButtonStore"

// 使用自定义指令:v-permissions
app.directive('permissions', {
    mounted(el, binding) {
        // el: 按钮当前标签,binding.value:标签传入的按钮标识符
        if (!useUserRoleStore().buttonList?.includes(binding.value)) {
            el.parentNode && el.parentNode.removeChild(el)
        }
    }
})

20.5 vue3使用自定义指令检查按钮权限

以用户管理列表中编辑按钮为例:

v-permissions:判断当期用户角色是否有按钮权限

v-show: 额外的条件判断按钮是否展示

<template>

    <el-dialog v-model="dialogVisible" title="编辑用户" v-permissions="`user/modifyBtn`">
        <!-- 编辑弹窗页面 <EditUser :form="EditData" @onCloseEditDialog="onCloseEditDialog" /> -->
    </el-dialog>

	<el-button link type="primary" size="small" @click="onEdit(user)" v-permissions="`user/modifyBtn`" v-show="user.status === Status.Used">编辑</el-button>

</template>

<script setup lang="ts">
import { ref } from 'vue'

// 枚举
enum Status {
    Used,
    NotUsed
}

const user = ref({
    "id": 1,
    "name": "zhangsan",
    "status": Status.Used
})
    
// 编辑
const EditData = ref()
const onEdit = async (row: User) => {
    dialogVisible.value = true
    EditData.value = row
}
</script>

<style scoped>
</style>
21. vue3页面刷新
21.1 方法一:使用 window.location.reload ,刷新页面会空白
window.location.reload()
21.2 方法二:使用vue router编程式导航,重新导航当前页面不会刷新
import { useRouter } from 'vue-router'
const router = useRouter()

router.replace("/home")
// router.push("/home")
21.3 方法3:使用父子组件通信手动触发更新 provide / inject

vue router路由:

const baseRoutes = [
    {
        path: '/',
        redirect: '/parent'
    },
    {
        path: '/parent',
        name: 'Parent',
        meta: {
            title: 'father'
        },
        component: () => import('@/Parent.vue'),
        children: [{
            path: 'son',
            component: () => import('@/Son.vue'),
      },]
    }
]

父组件Parent.vue

<template>
    父组件
	<button @click="reload">刷新</button>
	<router-view v-if="isload"/>
</template>
<script setup>
import { ref, nextTick, provide } from 'vue'
    
const isload = ref(true)

const reload = () => {
    isload,value = false
    nextTick(() => {
        isload,value = true
    }
}
             
provide("fatherRelaod", reload)
</script>

子组件Son.vue

<template>
	子组件
	<button @click="sonReload">刷新</button>
</template>

<script setup>
import { ref, nextTick, inject } from 'vue'

const reload = inject("fatherRelaod")

const sonReload = () => {
    reload()
}
</script>
22. 根据页面查询参数?redirect_tovite配置请求代理到参数指定的后端服务

场景:适用于联调测试时,一个前端环境对应多个后端,如用户管理页面http://localhost:3000/?redirect_to=http://127.0.0.1:8080/#/user/list那么列表页请求将访问http://127.0.0.1:8080
vite.config.ts请求代理中添加proxy.on('proxyReq', (proxyReq, req, res)

    // 跨域
    server:{
      host: env.VITE_API_HOST || '0.0.0.0',
      // disableHostCheck: true,
      open: false,
      port: env.VITE_PORT || 8000,
      strictPort: true,
      proxy: {
        '/api': {
          target: env.VITE_API_TARGET_URL,
          changeOrigin: true,
          // 将请求中的/api删除,前端/api/user,那么后端是/user
          rewrite: (path) => path.replace(/^\/api/, '')
          configure: (proxy, options) => {
            proxy.on('proxyReq', (proxyReq, req, res) => {
              // 从请求URL中获取redirect_to参数
              const redirectUrl = req.url.split('?redirect_to=')[1];
              if (redirectUrl) {
                // 解码URL并设置新的代理目标
                const targetUrl = decodeURIComponent(redirectUrl);
                options.target = targetUrl;
              }
            })
          }
        }
      }
    }

axios请求拦截器添加:

instance.interceptors.request.use(async (config) => {
    const params = new URLSearchParams(window.location.search)
    const redirectTo = params.get('redirect_to')
    if (redirectTo) {
        config.params = config.params || {}
        config.params.redirect_to = redirectTo
    }
}
回答: `<el-select collapse-tags>`是element-ui的选择器组件(el-select)中的一个属性,用于控制选择标签的折叠行为。当标签过多时,使用该属性可以将多余的标签折叠显示,并在最后一个标签处显示一个折叠提示。引用中的代码示例展示了如何在element-ui的选择器组件中使用`<el-select collapse-tags>`属性。在示例中,使用`collapse-tags`属性来实现禁用状态下折叠标签的功能。同时,为了修改标签的样式,可以在样式表中使用引用中的代码进行设置。12<em>3</em> #### 引用[.reference_title] - *1* [elementui el-select collapse-tags文字过长换行问题](https://blog.csdn.net/dadawdawdadadw/article/details/127811121)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT3_1"}} ] [.reference_item] - *2* [使用el-tooltip展示el-select多选项collapse-tags collapse-tags-tooltip](https://blog.csdn.net/zhaowenxue/article/details/125006432)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT3_1"}} ] [.reference_item] - *3* [el-select 多选 添加collapse-tags之后换行问题](https://blog.csdn.net/weixin_53558474/article/details/126577918)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT3_1"}} ] [.reference_item] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值