文章目录
- 前端:[vue3+vite5前端(一)](https://blog.csdn.net/weixin_44894663/article/details/140476800)
- 后端(python):[fastapi开发项目](https://blog.csdn.net/weixin_44894663/article/details/140621187)
- 部署: [win10搭建minikube(driver=docker)部署项目](https://blog.csdn.net/weixin_44894663/article/details/143974909)
- 10. 按需引入elemet plus(失败...不想再尝试)和 @方式导包路径配置
- 11. vite设置环境配置文件
- 12 表单页和列表页el-swith按钮展示
- 13 列表页枚举类型展示
- 14 选择和树形选择简单使用
- 15 父子组件通信总结(单方向传递)
- 16 状态管理pinia
- 17 动态路由(后端管理路由)
- 18 报错QA
- 19. 前端生产环境打包
- 20. 按钮权限管理: 用于管理前端页面标签和按钮的展示,用于前端层面鉴权
- 21. vue3页面刷新
- 22. 根据页面查询参数`?redirect_to`,`vite`配置请求代理到参数指定的后端服务
前端: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
时只提交子节点的数据
check
、check-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
方法时要一致
@onClose
:onClose
是父组件告知子组件可以使用的方法名称,名称任意,但要与子组件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” | 不能这样 vue-router中的addRoute方法无法识别到@路径,导致component为undefined,页面空白 |
const componentPath = item.component | 不能这样 vue-router中的addRoute方法获取到component的值是/src/view/${componentPath}.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-server
:npm 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_to
,vite
配置请求代理到参数指定的后端服务
场景:适用于联调测试时,一个前端环境对应多个后端,如用户管理页面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
}
}