p24
1.let pro=defineProps(["width","fill","src"])
在template直接使用width,在setup使用是pro.wid;
2.let pro = defineProps({ prefix: { type: String, default: "#icon-" }, name: { type: String }, color: { type: String, default: "#000" }, width: { type: String, default: "30px" }, height: { type: String, default: "30px" } })
两种方式都可以.
- 在开发项目的时候经常会用到svg矢量图,而且我们使用SVG以后,页面上加载的不再是图片资源,
这对页面性能来说是个很大的提升,而且我们SVG文件比img要小的很多,放在项目中几乎不占用资源。
安装SVG依赖插件
java pnpm install vite-plugin-svg-icons -D
测试SVG图标使用
svg:图标外层容器节点, 内部需要与use标签结合使用
<svg>
<!-- 测试SVG图标使用
svg:图标外层容器节点,内部需要与use标签结合使用 -->
<!-- xlink:href执行用哪一个图标,属性值务必#icon-图标名字 -->
<!-- use标签fill属性可以设置图标的颜色 -->
<use xlink:href="#icon-icon" fill="red"></use>
</svg>
- xlink:href=“#icon-icon” 的`#icon-xxx xxx是svg文件名字 ,文件夹在src/assets/icons.
在vite.config.ts中配置插件
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
import path from 'path'
export default () => {
return {
plugins: [
createSvgIconsPlugin({
// Specify the icon folder to be cached
iconDirs: [path.resolve(process.cwd(), 'src/assets/icons')],
// Specify symbolId format
symbolId: 'icon-[dir]-[name]',
}),
],
}
}
p25
// 1.引入组件
import SvgIcon from '@/components/SvgIcon/index.vue'
import Pagination from '@/components/Pagination/index.vue'
// 每个组件都有名字和组件内容.需要是用app.component()注册.
console.log(SvgIcon,Pagination);
const GlobalComponent ={SvgIcon,Pagination};
console.log(Object.keys(GlobalComponent));
export default {
install(app){
Object.keys(GlobalComponent).forEach((item)=>{
app.component(item,GlobalComponent[item]);
})
}
}
- 这个引入组件 有组件名字和组件内容,需要使用app.component()引入.
import globalComponent from "@/components/index"
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
import '@/styles/index.scss'
const app=createApp(App)
app.use(globalComponent)
- main.ts可以在这里装插件.进入项目局自己运行.
p26
在使用scss语法!!!需要加上lang=“scss”
<style scoped lang="scss"></style>
接下来我们为项目添加一些全局的样式
在src/styles目录下创建一个index.scss文件,当然项目中需要用到清除默认样式,因此在index.scss引入reset.scss
@import reset.scss
在入口文件引入
import '@/styles'
但是你会发现在src/styles/index.scss全局样式文件中没有办法使用 变量 . 因此需要给项目中引入全局变量 变量.因此需要给项目中引入全局变量 变量.因此需要给项目中引入全局变量.
在style/variable.scss创建一个variable.scss文件!
在vite.config.ts文件配置如下:
export default defineConfig((config) => {
css: {
preprocessorOptions: {
scss: {
javascriptEnabled: true,
additionalData: '@import "./src/styles/variable.scss";',
},
},
},
}
- 在sass中可以在一个sass文件中,使用@import导入另一个sass文件.
- 使用全局变量可以规定项目的颜色切换.
- 在main.ts的东西会提前加载好.
27 mock接口
安装依赖: https://www.npmjs.com/package/vite-plugin-mock
pnpm install -D vite-plugin-mock mockjs
mock中配置文件会报错,是因为vite-plugin-mock版本匹配导致的,可以 pnpm i vite-plugin-mock@2.0.0
还有pinia最新版也不匹配 可以安装时候 pnpm i pinia@2.0.34
createUserList:次函数执行会返回一个数组,数组里面包含两个用户信息。
对外暴露一个数组:数组里面包含两个接口
登录假的接口
获取用户信息的假的接口
在 vite.config.js 配置文件启用插件。
mport { UserConfigExport, ConfigEnv } from 'vite'
import { viteMockServe } from 'vite-plugin-mock'
import vue from '@vitejs/plugin-vue'
export default ({ command })=> {
return {
plugins: [
vue(),
viteMockServe({
localEnabled: command === 'serve',
}),
],
}
}
在根目录创建mock文件夹:去创建我们需要mock数据与接口!!!
在mock文件夹内部创建一个user.ts文件
axios二次封装
-
import.meta.env 可以获得三种环境对应的文件信息.
例如: import.meta.env.VITE_APP_BASE_API, //获得前缀
api -
axios一次封装是使用 axios.create(baseurl,timeout)
-
对象的属性增加可以是
config.headers.token="123";
config.headers["token"]="123";
import axios from "axios"
import { ElMessage } from 'element-plus'
const request = axios.create({
// 基础路径 api
baseURL: import.meta.env.VITE_APP_BASE_API,
timeout: 5000 //超时时间
})
request.interceptors.request.use((config) => {
console.log(config);
config.headers["token"]="123";
// 请求拦截下来了,返回回去.
return config;
})
request.interceptors.response.use((response) => {
// 成功的回调。
return response.data;
}, (error) => {
// 状态码400 404 500错误
let msg = ""
const code = error.response.status;
switch (code) {
case 401:
msg = "token过期"
break;
case 403:
msg = "无权限访问";
break;
case 404:
msg = "页面不存在";
break;
case 500:
msg = "服务器出现问题";
break;
default:
msg = "网络错误";
}
ElMessage({
showClose: true,
message: msg,
type: 'error',
})
return Promise.reject(error);
// 失败的回调
})
export default request;
- 请求拦截器加上token
- 响应拦截器 有成功回调和失败回调. 在失败回调中可以是获得code,之后进行判断,赋予msg.使用提示框返回.
- api的第一次封装是在src/util/request
二次封装开发接口
开发接口中
// 与用户相关的接口
import request from "@/utils/request";
import { loginForm,loginResponseData,userInfoResponseData } from "./type"
enum UserApi {
LOGIN_URL = "/user/login",
USERINFO_URL = "/user/info"
}
// 形参使用接口约束.
export const LoginApi = (data: loginForm) => request.post<any,loginResponseData>(UserApi.LOGIN_URL, data)
export const UserInfoApi = () => request.get<any,userInfoResponseData>(UserApi.USERINFO_URL)
export interface loginForm{
username: string,
password: string
}
interface dataType{
token: string
}
export interface loginResponseData{
code:number,
data:dataType
}
interface checkUser {
userId: number,
avatar:string,
username: string,
password: string,
desc: string,
roles: string[],
buttons: string[],
routes: string[],
token: string,
}
interface InfoType{
checkUser:checkUser
}
export interface userInfoResponseData{
code:number,
data:InfoType
}
- 二次封装可以使用request.get() request.post() requets({})
- 使用接口类型来约束对象.
路由
- 组件分两种,一种是普通组件,写在components里的,还有一种是路由组件,这种就是路由组件
- 哈希模式:简单易用,兼容性好,但 URL 中包含 # 符号。适用于不需要复杂服务器配置的情况。
历史模式:支持干净的 URL,对 SEO 更友好,但需要服务器端支持。适用于需要更复杂路由管理的应用。(hash模式有#号.)
const routerData = [
{
// 登录成功后的页面
path: "/",
name: "home",
component: () => import("@/views/home/index.vue"),
},
{
// 登录
path: "/login",
name: "login",
component: () => import("@/views/login/index.vue"),
},
{
path: "/404",
name: "404",
component: () => import("@/views/404/index.vue")
},
{
path: "/:pathMatch(.*)*",
redirect: "/404",
name:"Any"
}
]
export default routerData;
- 最后一个就是没有匹配的到这里,之后到404.
<template>
<div>
<router-view></router-view>
</div>
</template>
<script setup lang="ts">
import { onMounted } from 'vue';
import { LoginApi } from "@/api/user/index";
onMounted(() => {
LoginApi({ username: "admin", password: "111111" })
})
</script>
<style scoped lang="scss"></style>
- 使用router-view标签.
p31 登录页面
.login_container{
width:100%;
height:100vh;
background-color: #2b4b6b;
}
- height:100%;是要依靠div里面的内容进行扩大. height:100vh;是整个屏幕占据
- element框架有根据屏幕大小设置大小.
p32
-
大仓库暴露后注册到main.ts
-
小仓库直接暴露据可以使用了。
-
信息需要存储到vuex的,就在action中请求接口,之后存储。
-
在vuex中操作state需要通过mutations,在pinia中是直接操作,使用this获得数据.
-
使用await async 里面根据结果返回return Promise的状态.
state: () => {
return {
token: localStorage.getItem("token")
}
},
------------------
async userLogin(data: loginForm) {
const result = await LoginApi(data);
if (result.code === 200) {
this.token = result.data.token;
localStorage.setItem("token", this.token);
return Promise.resolve()
} else {
return Promise.reject(new Error(result.data.message))
}
}
let submitForm = () => {
loadingState.value = true
let LoginResult = useStore.userLogin(form)
console.log(LoginResult)
LoginResult.then(() => {
$router.push('/')
ElNotification({
message: '登陆成功',
type: 'success'
})
loadingState.value = false
}).catch(error => {
ElNotification({
message: error.message,
type: 'error'
})
loadingState.value = false
})
}
- 存储到pinia,之后本地存储.
promise
.then(result => {
// 处理成功状态
console.log('成功:', result);
})
.catch(error => {
// 处理失败状态
console.error('失败:', error.data.message);
});
-
获得promise失败的要有message,成功的不用.
-
在 Vue 2 中,this.$router 是访问和操作路由的主要方式,而在 Vue 3 中,则使用 Composition API 和 useRouter
import { useRouter } from 'vue-router'
let $router = useRouter()
在组件的 setup 函数中调用 useRouter,它会返回 Vue Router 实例。
-
result中有成功的话没有messgae,失败的话有message.所以在失败返回promise.reject时候要返回信息.
-
let loadingState=ref(false)
在template中是直接使用loadingState这个;在代码中是loadingState.value=true. 才能进行赋值
ElNotification({
message: error.message,
type: 'error'
})
- 这是通知的通知栏.
<el-button type="primary" @click="submitForm(form)" class="btn" :loading="loadingState">
登录
</el-button>
-
这是可以加载动态的按钮
-
import type 用于仅导入类型,不会在最终的 JavaScript 代码中出现,而普通的 import 用于导入实际的值(如函数、对象等)
Ts的类型主要针对方法的形参和返回的数据。还有pinia的state。
封装localstorege
本来是没有封装,我们针对本地存储进行封装一下。
token.ts
export const SET_TOKEN = (token: string) => {
localStorage.setItem("token", token as string);
}
export const GET_TOKEN = () => {
localStorage.getItem("token")
}
p34 登录时间判断
根据时间判断,返回消息,
- 本来vue2是写在method中的,不可以引出来
- vue3可以把所有的方法写出来变成工具文件.之后引入,在进行执行函数.
await和async
async userLogin(data: loginForm) {
const result: loginResponseData = await LoginApi(data);
if (result.code === 200) {
this.token = result.data.token;
SET_TOKEN(this.token)
return Promise.resolve()
} else {
return Promise.reject(new Error(result.data.message))
}
}
- await 加载异步操作前面,可以直接获得数据.
- async修饰的方法一定会返回promise对象.这个对象有成功的状态和失败的状态.关于状态我们可以自己控制.
- 在使用async后,获得promise状态就用then.
规则校验
- 这是一个
- props对应着rules的属性名. trigger有change和blur的.
- 不符合条件的时候,会触发message
- model是绑定表单数据
- rules是规则校验
- ref是一个关于校验的结果
p37 layout布局
.layout_tabbar {
width: calc(100% - $base_menu_width);
height: $base_tabbar_height;
background-color: beige;
position: fixed;
top: 0;
left: $base_menu_width;
}
- 使用css的calc属性可以动态计算值,
::-webkit-scrollbar{
width: 10px;
}
::-webkit-scrollbar-track{
background: $base_menu_color;
}
::-webkit-scrollbar-thumb{
width: 10px;
border: 10px;
background-color: yellow;
}
- 这是滚动条的css -webkit-
- 超过父盒子的子盒子的使用overflow:auto
- 使用饿了么ui的滚动跳也可以解决问题,
p38
- 感觉关于h5很多部分的标题或者图片都可以另外封装处理.
<template>
<div class="logo" :v-if="setting.hidden">
<img :src="setting.logo_puture" alt="">
<p>{{setting.logo_title}}</p>
</div>
</template>
-----
**setting.ts**
export default {
logo_title: 'xxx管理系统',
logo_puture: '/public/logo.png',
hidden:true
}
- 路径可以从/开始代表从当前项目开始.
- 先设计好宽高,
- 弹性布局 横向是align-items;
p39
- 教学了使用饿了么ui制作菜单
<el-sub-menu index="1">
<template #title>
<el-icon>
<location />
</el-icon>
<span>首页</span>
</template>
<el-menu-item-group>
<el-menu-item index="1-1">item one</el-menu-item>
<el-menu-item index="1-2">item two</el-menu-item>
</el-menu-item-group>
</el-sub-menu>
<!-- 第二个 -->
<el-menu-item index="2">
<el-icon><icon-menu /></el-icon>
<span>Navigator Two</span>
</el-menu-item>
- 单个菜单是el-menu-item ;多个菜单是el-sub-menu
- 但是项目菜单一多,我们要怎么办
p40
<template>
<div>
<template v-for="(item,index) in menuList" key="item.path">
<!-- 没有下拉菜单 -->
<template v-if="!item.children">
<el-menu-item :index="index" v-if="!item.meta.hidden">
<el-icon>
<component :is="item.meta.icon"></component>
</el-icon>
<span>{{item.meta.title}}</span>
</el-menu-item>
</template>
<!-- 有下拉 start -->
<template v-if="item.children">
<el-sub-menu :index="index" :key="item.path">
<template #title>
<el-icon>
<component :is="item.meta.icon"></component>
</el-icon>
<span>{{item.meta.title}}</span>
</template>
<div v-for="i in item.children" :key="i.path">
<!-- 没有子 -->
<template v-if="!i.children && !i.meta.hidden">
<el-menu-item :index="i.index"><el-icon>
<component :is="item.meta.icon"></component>
</el-icon>
<span>{{i.meta.title}}</span></el-menu-item>
</template>
<!-- 有子 -->
<template v-if="i.children">
<el-sub-menu :index="i.index" :key="i.index">
<template #title>
<el-icon>
<component :is="item.meta.icon"></component>
</el-icon>
<span>{{i.meta.title}}</span>
</template>
<Menu :menuList="i.children" v-if="i.children">{{i.children}}</Menu>
</el-sub-menu>
</template>
</div>
</el-sub-menu>
</template>
</template>
</div>
</template>
<script setup lang="ts">
defineProps(['menuList'])
import { Document, Menu as IconMenu, Location, Setting } from '@element-plus/icons-vue'
</script>
<script lang="ts">
export default {
name: 'Menu'
}
</script>
<style scoped lang="scss">
</style>
- 前端递归 主要是不断减少的量的数据格式还是要一样.
{
// 登录
path: "/login",
name: "login",
component: () => import("@/views/login/index.vue"),
meta: {
title: "登录",
icon: "IconMenu",
hidden: true
}
},
-----------
<template v-if="!i.children && !i.meta.hidden">
<el-menu-item :index="i.index">{{i.name}}</el-menu-item>
</template>
-
hidden:true 表示隐藏该路由,在路由标签上使用v-if实现
<template #title>
<el-icon>
<component :is="item.meta.icon"></component>
</el-icon>
<span>{{i.meta.title}}</span>
</template>
- item.meta.icon 里面这是一个组件名字,component组件.
p40
- index在一个单选 多选下拉中.并且加上@click就可以获得每一个的路由.
- index是一个唯一标识符号,就是路径
router-view 展示路由绑定组件的地方.
<template>
<router-view v-slot="{ Component }">
<transition name="fade">
<component :is="Component" />
</transition>
</router-view>
</template>
<script setup lang="ts">
</script>
<style scoped lang="scss">
.fade-enter-from {
opacity: 0;
}
.fade-enter-active {
transition: all 1s;
background: red; // 添加背景颜色用于测试
}
.fade-enter-to {
opacity: 1;
}
</style>
展示地方可以设置动效.
p43 刷新菜单关闭
- url在刷新页面后路径不会改变,但是菜单会关闭我们要利用default-active来设置.
- 在vue3使用路由对象需要先引入,在实例化.在vue2可以直接this.$router获得,感觉vue3节约来很多东西
import { useRoute } from 'vue-router'
let $router = useRoute()
console.log($router.fullPath)
<div class="layout_slider">
<Logo></Logo>
<el-scrollbar class="scrollbar">
<el-menu :default-active="$router.fullPath" active-text-color="#ffd04b" background-color="##545c64" router class="el-menu-vertical-demo" text-color="#fff" @open="handleOpen" @close="handleClose">
<Menu :menuList=userData.menuRoutes> </Menu>
</el-menu>
</el-scrollbar>
</div>
<!-- 顶部导航 -->
<div class="layout_tabbar">456</div>
<!-- 内容展示 -->
<div class="layout_main">
<Main></Main>
</div>
</div>
</template>
<script setup lang="ts">
import { Document, Menu as IconMenu, Location, Setting } from '@element-plus/icons-vue'
import Logo from './logo/index.vue'
import Menu from '@/layout/Menu/index.vue'
import useUserStore from '@/store/modules/user/index.ts'
import Main from '@/views/main/index.vue'
let userData = useUserStore()
import { useRoute } from 'vue-router'
let $router = useRoute()
console.log($router.fullPath)
- 在layout的tabbar封装成为组件放到layout文件夹下.路由有关的放到views中.
- 给组件命名可以可以在开发者查看该组件和使用递归标签.
.layout_slider {
&.fold {
width: 60px;
}
}
- &.fold: & 符号在 SCSS 中代表父选择器,即 .layout_slider。所以 &.fold 代表 .layout_slider.fold。
- class的动态绑定 有 :class=“{fold? statas?true:false}” ;class"{类名:三元表达式}"
- 菜单栏之前有设置宽度,但是如果需要缩小的话,可以利用css的选择器优先级来设置.我们可以用两个类名进行设置宽度.
&.fold {
width: $base_menu_minWidth;
transition:all 0.3s;
}
- transition 就是用0.3s时间变化到类名的属性.
- all就是所有可以变化的属性。
p44 菜单折叠
- 这个是菜单的折叠面板。根据类名决定是否使用。
- 菜单栏需要设置最大的宽度和最小宽度。
- 根据fold类名的true和false,根据类名,决定css属性。、
面包屑
- 面包屑的数据我们通过
获得路由实例$router.matched 这个api获得多级路由的信息。
<el-breadcrumb-item v-for="(item,index) in $router.matched" :key="index" :to="{ path: item.path }">
{{item.meta.title}}
</el-breadcrumb-item>
- to可以获得到达的多级路由的信息。之后在router.ts中可以设置redirect进行重定向。
nexttick用法
- Vue.js 的响应式系统会在数据发生变化后对 DOM 进行更新。先更新数据刷新Dom,使用nextTick才可以操作新的dom.
全屏操作
let fullScreen = () => {
console.log(document.fullscreenElement)
let fullScreen = document.fullscreenElement
if (!fullScreen) {
document.documentElement.requestFullscreen()
} else {
document.exitFullscreen()
}
}
-
document.fullscreenElement 这是html元素。
-
beforecreate --. create – beforemount—mount – beforeupdate —update – beforeUnmount —unmount生命周期,更新的可以执行多次.
-
浏览器刷新全部周期执行一次.
-
登录成功后,会根据生命周期拿到头像和用户名.保存在pinia.
//进行导航的
import { useRouter } from 'vue-router'
const $router = useRouter()
//查看当前路由信息
import { useRoute } from 'vue-router'
const $router = useRoute()
- useRoute() 是用于访问当前的路由信息(如路由参数、查询参数等),而 useRouter() 是用于执行导航操作的。
- 在 Vue 3 和 Pinia 中,使用 useRouter 直接在模块外部是无效的,因为 useRouter 必须在 Vue 组件的 setup 函数或其他类似的上下文中调用。你不能在定义 Pinia store 的模块外部直接调用 useRouter()。
为了修复这个问题,你可以将 useRouter 放到 actions 中,在需要使用 router 进行导航的地方动态调用它。
- 这个route,router需要再setup函数里面使用或者在pinia函数里面使用.
如果退出登录后,记住上一次操作的url
这个场景是:你未登录的时候后在浏览一些页面, 但是接下来的操作需要登录, 当你登录后,在跳转到之前浏览的页面。如果第一次登录的话就需要用到判断是否有query.
- 把当前操作的url,放在url上的query参数.
- 路由导航的格式
$route.push({path:"",query:{key:value}})
query在url中是key=value
我们把获得用户信息的接口放在home的组件,会有用户信息,可是我们去到其他view,刷新一下就没有用户信息了.(主要是刷新).
onMounted(() => {
userUseStore.requestUserInfo()
})
-
我们之前是把获得用户信息的方法放在home中的(其中一个view页面),
-
只有访问home页面才会调用这个方法,但是我们可以放在更大范围的layout中,这样就可以一种加载.因为所有组件都在layout组件里面.所以每次刷新都会加载这个获得用户方法.
-
所以登录和获得用户信息是两回事是两个接口.
-
第二种解决方法是把请求的接口放在全局前置路由.
路由鉴权
全局路由守卫 和前置后值守卫 在更换路由信息会触发这个.
还有路由鉴权,是需要什么样的条件才能访问这个路由.
全局路由守卫
- 前置路由守卫和后置路由守卫
import router from "@/router"
import nprogress from "nprogress"
import "nprogress/nprogress.css"
router.beforeEach((to, from, next) => {
console.log(from);
console.log(to);
nprogress.start();
next();
// 放行函数
})
// 全局后置守卫
router.afterEach((to, from) => {
nprogress.done();
});
路由鉴权
if (token) {
console.log("有token");
// 已经登录
if (to.path != '/login') {
next()
} else {
next({ path: "/" })
}
} else {
console.log("无token");
// 没有登录
if (to.path == '/login') {
next()
} else {
next({ path: "/login" })
}
}
- 分为登录成功和未登录的情况.
- 登录成功有不可以访问登录页面和可以访问除了登录页外的.
- 未登录有可以访问login.不可以访问login外的页面.
绝对定位和静态定位
绝对定位脱离了文档流.静态定位还在文档流中,所以绝对定位会挡住静态定位的.
- 左边的使用静态,右边是绝对来控制left和top.这样就可以挡住左边的菜单栏.
token过期处理
async requestUserInfo() {
// 获得用户api。
const userInfo = await UserInfoApi()
console.log(userInfo);
if (userInfo.code != 200) return Promise.reject(new Error(userInfo.message))
const { username, avatar } = userInfo.data.checkUser
this.username = username
this.avatar = avatar
return Promise.resolve()
},
- 请求接口返回promise
if (!username) {
try {
await userStore.requestUserInfo()
next()
} catch (error) {
console.log(error);
userStore.logout()
next({ path: "/login" })
}
}
- token过期或者被修改token,处理错误.
import router from "@/router"
import nprogress from "nprogress"
import "nprogress/nprogress.css"
import store from "@/store"
import userUserStore from "@/store/modules/user"
import path from "path"
import setting from "./setting"
const userStore = userUserStore(store)
console.log(userStore.token);
const token = userStore.token;
router.beforeEach(async (to, from, next) => {
document.title = setting.logo_title + to.meta.title
nprogress.start();
console.log(to);
const username = userStore.username
if (token && localStorage.getItem("token")) {
console.log("有token");
if (to.path != '/login') {
// 来到主页
if (!username) {
try {
await userStore.requestUserInfo()
next()
} catch (error) {
console.log(error);
userStore.logout()
next({ path: "/login" })
}
} else {
// 有用户信息
next()
}
} else {
next({ path: "/" })
}
} else {
console.log("无token");
// 没有登录
if (to.path == '/login') {
next()
} else {
next({ path: "/login" })
}
}
// 放行函数
})
// 全局后置守卫
router.afterEach((to, from) => {
nprogress.done();
});
- 没有用户名的话,请求用户名字
- 有用户名放行
- 检查token和用户名.可以的话放行.不符合的话进行处理.
引入变量
- 设置一个文件setting.ts之后暴露出去,别人需要的时候就可以引入使用里面变量.
p51代理服务器
- 我们的脚本命令控制使用那三个文件的, VITE_SERVE=“http://sph-api.atguigu.cn”
- VITE_APP_BASE_API=“/api/” 写在
const request = axios.create({
// 基础路径 api
baseURL: import.meta.env.VITE_APP_BASE_API,
timeout: 5000 //超时时间
})
/api/ 用来给代理服务器识别的.
- 之后代理服务器把这一段重写 ,在前端的/api/user/add,中间api给服务器识别和重写了.在后端是/user/add
服务器代理
import { defineConfig, loadEnv } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'
import { UserConfigExport, ConfigEnv } from 'vite'
import { viteMockServe } from 'vite-plugin-mock'
// 引入svg需要的插件.
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
export default defineConfig(({ command, mode }) => {
//获取各种环境下的对应的变量
const env = loadEnv(mode, process.cwd(), "");
console.log("环境");
console.log(env);
return {
plugins: [
vue(),
viteMockServe({
// default
mockPath: 'mock',
localEnabled: command === 'serve',//保证开发阶段可以使用mock接口.
}),
createSvgIconsPlugin({
// Specify the icon folder to be cached
iconDirs: [path.resolve(process.cwd(), 'src/assets/icons')],
// Specify symbolId format
symbolId: 'icon-[dir]-[name]',
}),
],
resolve: {
dedupe: [
'vue'
],
alias: {
"@": path.resolve("./src") // 相对路径别名配置,使用 @ 代替 src
}
},
css: {
preprocessorOptions: {
scss: {
javascriptEnabled: true,
additionalData: '@import "./src/styles/variable.scss";',
},
},
},
server: {
proxy: {
[env.VITE_APP_BASE_API]: {
target: env.VITE_SERVE, // 正确使用环境变量
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''),
},
}
},
}
})
TS设计
export interface userBaseData {
code: number,
message: string,
ok: boolean
}
// 用户登录返回信息.
export interface userResponseData extends userBaseData{
data: string,
}
// 登录所需表单.
export interface loginForm {
username: string,
password: string
}
// 用户详情的返回接口
export interface LoginInfoResponseData extends userBaseData{
data: {
routes: string[],
buttons: string[],
roles: string[],
name: string,
avatar: string
}
}
export interface ResponseData {
code: number
message: string
ok: boolean
}
//已有的品牌的ts数据类型
export interface TradeMark {
id?: number
tmName: string
logoUrl: string
}
//包含全部品牌数据的ts类型
export type Records = TradeMark[]
//获取的已有全部品牌的数据ts类型
export interface TradeMarkResponseData extends ResponseData {
data: {
records: Records
total: number
size: number
current: number
searchCount: boolean
pages: number
}
}
插槽
插槽(Slot)是 Vue.js 中一种用于在子组件中插入内容的机制。它允许父组件向子组件传递自定义内容,这使得子组件更加灵活和可复用。插槽通常用于在组件内部自定义显示部分的内容,比如按钮、列表、卡片等内容。
插槽的基本概念
在 Vue.js 中,插槽可以看作是一个占位符,父组件可以在这个占位符中插入特定内容。插槽有两种常见类型:普通插槽和具名插槽。Vue 还支持作用域插槽,它允许父组件接收来自子组件的动态数据。
基本插槽示例
假设你有一个 MyComponent
子组件,希望允许父组件定义其内部内容:
<!-- MyComponent.vue -->
<template>
<div class="box">
<slot></slot> <!-- 插槽占位符 -->
</div>
</template>
在 MyComponent
中,我们使用 <slot></slot>
表示插槽。父组件可以向该插槽传入内容:
<!-- ParentComponent.vue -->
<template>
<MyComponent>
<p>这是一段由父组件提供的内容。</p>
</MyComponent>
</template>
结果是:
<div class="box">
<p>这是一段由父组件提供的内容。</p>
</div>
这里,父组件定义的 <p>
标签内容被插入到了 MyComponent
组件中的插槽位置。
具名插槽
有时,你可能希望在子组件的不同部分插入不同的内容,这时可以使用具名插槽。
<!-- MyComponent.vue -->
<template>
<header>
<slot name="header"></slot> <!-- 具名插槽 -->
</header>
<main>
<slot></slot> <!-- 默认插槽 -->
</main>
<footer>
<slot name="footer"></slot> <!-- 具名插槽 -->
</footer>
</template>
在父组件中,你可以使用具名插槽来插入不同部分的内容:
<!-- ParentComponent.vue -->
<template>
<MyComponent>
<template #header>
<h1>这是头部内容</h1>
</template>
<p>这是主体内容</p>
<template #footer>
<p>这是底部内容</p>
</template>
</MyComponent>
</template>
结果是:
<header>
<h1>这是头部内容</h1>
</header>
<main>
<p>这是主体内容</p>
</main>
<footer>
<p>这是底部内容</p>
</footer>
作用域插槽
作用域插槽是一种特殊的插槽,它允许子组件向父组件传递数据,并让父组件根据这些数据渲染内容。
<!-- MyComponent.vue -->
<template>
<ul>
<slot :items="items"></slot> <!-- 通过插槽传递数据 -->
</ul>
</template>
<script>
export default {
data() {
return {
items: ['苹果', '香蕉', '橘子']
};
}
};
</script>
在父组件中,你可以通过作用域插槽接收这些数据:
<!-- ParentComponent.vue -->
<template>
<MyComponent>
<template v-slot:default="{ items }">
<li v-for="item in items" :key="item">{{ item }}</li>
</template>
</MyComponent>
</template>
这里,v-slot:default="{ items }"
意味着父组件接收子组件传递的 items
数据,并基于这些数据动态生成列表。
总结
- 普通插槽:允许父组件向子组件插入自定义内容。
- 具名插槽:允许在子组件的不同部分插入不同内容。
- 作用域插槽:允许父组件接收子组件传递的数据,并根据这些数据动态渲染内容。
插槽使得组件更加灵活和可复用,通过允许父组件控制子组件中的部分内容,你可以更方便地组合、定制和扩展组件的行为。
分页组件
export const reqHasTrademark = (page: number, limit: number) =>
request.get<any, any>(API.TRADEMARK_URL + `${page}/${limit}`)
-
先写好接口,参数有第几页和一页多少个.
-
生命周期函数根据ref值,请求reqHasTrademark 接口.
-
const getHasTrademark = async (pager = 1) => {}
这是前端的设定默认值.
function greet(name = 'Guest') {
console.log(`Hello, ${name}!`);
}
// 调用时传递了参数
greet('Alice'); // 输出: Hello, Alice!
// 调用时没有传递参数,使用默认值
greet(); // 输出: Hello, Guest!
这是获得上传图片后的链接。
//图片上传成功钩子
const handleAvatarSuccess: UploadProps['onSuccess'] = (
response,
uploadFile,
) => {
//response:即为当前这次上传图片post请求服务器返回的数据
//收集上传图片的地址,添加一个新的品牌的时候带给服务器
trademarkParams.logoUrl = response.data
//图片上传成功,清除掉对应图片校验结果
formRef.value.clearValidate('logoUrl')
}
新增和修改品牌的区别就是有id,只有存储过的品牌才会有id属性。
-
v-if 可以让一个组件走完生命周期,并且复活。
-
Object.assign()是浅拷贝
//添加品牌按钮的回调
const addTrademark = () => {
//对话框显示
dialogFormVisible.value = true
//清空收集数据
trademarkParams.id = 0
trademarkParams.tmName = ''
trademarkParams.logoUrl = ''
//第一种写法:ts的问号语法
formRef.value?.clearValidate('tmName')
formRef.value?.clearValidate('logoUrl')
/* nextTick(() => {
formRef.value.clearValidate('tmName')
formRef.value.clearValidate('logoUrl')
}) */
}
- ref会存储表单的校验结果,里面有清除验证信息,
数据库设计
- 我们可以不断添加分类的属性.
-
上面一个组件 下面一个组件。
-
三个属性值给store。下面的监听第三个有的话就发送请求。
-
模板中的
-
一二三级分类都是在同一个数据表中。
-
属性key和属性value也是在同一个表中。
-
添加一次性是一个属性key多个属性value.
-
浅拷贝还是保持响应式对象.
在你的代码中,:ref="(vc: any) => inputArr[$index] = vc"
是一个动态赋值的方式,用来将 <el-input>
组件的引用存储到 inputArr
数组中。这里是具体的含义:
-
ref
的作用:在 Vue.js 中,ref
用于获取对 DOM 元素或组件实例的引用。当你给一个元素或组件添加ref
属性时,可以在 Vue 实例中通过this.$refs
来访问这个元素或组件。 -
动态赋值:在你的代码中,
ref
的值是一个函数(vc: any) => inputArr[$index] = vc
。这个函数会在组件被渲染时调用,将vc
(即<el-input>
组件的引用)赋值给inputArr
数组中对应的索引$index
。这使得你可以在inputArr
数组中存储多个<el-input>
组件的引用。
举个例子:
假设你有一个表格,每一行都有一个 <el-input>
。当表格渲染时,你希望能在后续的操作中访问这些输入框,例如在某些情况下聚焦到特定的输入框。使用动态 ref
可以让你在 inputArr
数组中存储每个输入框的引用,以便后续操作。
代码示例:
data() {
return {
inputArr: [] // 存储每个 <el-input> 的引用
};
},
methods: {
toLook(row, index) {
// 处理输入框失去焦点后的逻辑
console.log(this.inputArr[index]); // 访问当前行的输入框引用
},
toEdit(row, index) {
// 切换到编辑模式
row.flag = true;
this.$nextTick(() => {
// 使用 this.inputArr[index] 来访问和操作输入框
this.inputArr[index]?.focus(); // 示例:使输入框获取焦点
});
}
}
在上面的示例中,inputArr
数组中存储了每个 <el-input>
的引用,你可以在方法中通过 this.inputArr[index]
访问到特定的输入框实例并进行操作。
- 修改业务更多使用到了浅拷贝的东西,把id带过来了
Object.assign(attrParams, JSON.parse(JSON.stringify(row)))
路由销毁
在 Vue.js 中,路由跳转是否销毁组件取决于具体的情况和配置。一般来说,Vue Router 会根据路由的变化来决定是否销毁组件。以下是一些情况和相关的说明:
1. 路由跳转:
-
普通路由跳转:当你通过 Vue Router 进行路由跳转时,如果目标路由组件不同于当前路由组件,Vue 会销毁当前组件并创建目标组件。这是因为 Vue Router 默认会销毁和创建组件来反映路由的变化。
-
组件缓存:如果你使用了
<keep-alive>
组件来包裹路由组件,那么在路由跳转时,组件不会被销毁,而是会被缓存起来。这样,组件的状态和数据会被保留,用户返回到之前的路由时,可以恢复之前的状态。<keep-alive> <router-view></router-view> </keep-alive>
2. beforeDestroy
和 destroyed
生命周期钩子:
-
当组件被销毁时,会触发
beforeDestroy
和destroyed
生命周期钩子。你可以在这些钩子中执行一些清理操作,例如移除事件监听器、清理定时器等。beforeDestroy() { // 在组件销毁之前执行的代码 }, destroyed() { // 组件已销毁,执行清理操作 }
3. 动态组件:
-
如果你使用了动态组件(例如
component
元素)并通过:is
指令进行切换,那么组件会被销毁和重新创建,类似于路由跳转的行为。<component :is="currentComponent"></component>
4. router-view
的使用:
<router-view>
组件会根据路由的变化来渲染相应的组件。路由跳转时,<router-view>
会卸载当前的组件并加载新组件。
如果你希望在路由跳转时保留组件的状态,可以考虑使用 keep-alive
,或者在路由切换时将数据存储到 Vuex 或其他状态管理方案中。
如果你有具体的情况或代码片段,欢迎分享,我可以提供更详细的帮助。
在 Vue 3 中,onUnmounted
是一个生命周期钩子,用于在组件卸载时执行一些清理操作。你的代码片段 onUnmounted(() => { categoryStore.$reset() })
是在组件卸载时重置 categoryStore
的状态。
categoryStore.$reset()
-
onUnmounted
:这是一个 Vue 3 的 Composition API 生命周期钩子,它在组件从 DOM 中卸载时被调用。用于在组件销毁时执行清理任务,如取消订阅、清理定时器、重置状态等。 -
categoryStore.$reset()
:这是你在onUnmounted
钩子中调用的一个方法。假设categoryStore
是你在 Pinia 中定义的一个 store,$reset()
方法用于重置 store 的状态到初始值。这是确保当组件卸载时,store 的状态被恢复到默认状态的好方法。
使用场景
使用 onUnmounted
钩子来重置 store 状态是一个好的做法,特别是在以下情况下:
-
组件状态清理:当组件卸载时,你希望将相关的 store 状态恢复到初始状态,以避免旧数据影响新数据。
-
内存泄漏:如果组件使用了定时器、事件监听器等资源,确保在组件卸载时进行清理,以避免内存泄漏。
示例
下面是一个使用 onUnmounted
钩子的完整示例,其中 categoryStore
是一个 Pinia store,$reset
方法用于重置其状态:
import { defineComponent, onUnmounted } from 'vue';
import { useCategoryStore } from '@/stores/categoryStore'; // 假设这是你的 Pinia store
export default defineComponent({
setup() {
const categoryStore = useCategoryStore();
// 在组件卸载时重置 store 状态
onUnmounted(() => {
categoryStore.$reset();
});
return {
categoryStore
};
}
});
注意事项
-
store 的设计:确保你的 Pinia store 有一个
$reset
方法或其他方法可以用来重置状态。否则,你需要自定义重置逻辑。 -
依赖清理:除了重置 store 状态,你还需要确保其他依赖(如事件监听器、网络请求等)也在组件卸载时被适当清理。
caregory组件
- 把一二三级分类存储到category的仓库中,三个下拉框都是一样的数据,只是最后请求的结果要根据不同的组件解决,例如在spu组件内watch监听
//监听三级分类ID变化
watch(
() => categoryStore.c3Id,
() => {
//当三级分类发生变化的时候清空对应的数据
records.value = []
//务必保证有三级分类ID
if (!categoryStore.c3Id) return
getHasSpu()
}
)
在attr中
watch(
() => categoryStore.c3Id,
() => {
attrArr.value = []
//获取分类的ID
console.log('获得c3')
if (categoryStore.c3Id !== '') {
getAttr()
}
}
)
这样就可以填完三个下拉框,使用不同的接口.
Spu模块
SPU(Standard Product Unit):标准化产品单元。是商品信息聚合的最小单位,是一组可复用、易检索的标准化信息的集合,该集合描述了一个产品的特性。通俗点讲,属性值、特性相同的商品就可以称为一个SPU。
spu就是有产品名字和属性,但是属性还没有值.
- spu的属性值已经设定好了,但是还没有选择.
sku则是有属性值
sku就是选择属性的值.
修改和添加的页面是差不多的。页面1的四个地方都需要发请求拿数据,我们在这一部分分别编写4个部分的API以及ts类型
- 因为没有这个四个的数据,进去详细页面需要再次请求;
权限
用户管理里面设置角色, 角色里面可以设置菜单权限.
用户管理
- 添加用户的时候没有id.可以区分
//收集用户信息的响应式数据
let userParams = reactive<User>({
username: '',
name: '',
password: ''
})
- 修改用户的时候,有id.
<el-button type="primary" size="small" icon="Edit" @click="updateUser(row)">编辑</el-button>
--------------------
const updateUser = data => {
drawer.value = true
Object.assign(userParams, data)
}
- 拿到row之后给data.
更换了本账号就会退出
因为后端已经没有这个用户,token找不到这个用户,所以在前端做好处理token出现问题的时候.就是注销登录状态.
多选框
<el-form-item label="职位列表">
<!-- v-model="checkAll" 控制是否确认全选 -->
<!-- handleCheckAllChange 全选了把数据给userRole -->
<!-- isIndeterminate 不确定的样式(确定样式) -->
<el-checkbox @change="handleCheckAllChange"
v-model="checkAll" :indeterminate="isIndeterminate">全选</el-checkbox>
<!-- 显示职位的的复选框 -->
<el-checkbox-group v-model="userRole" @change="handleCheckedCitiesChange">
<!-- 负责渲染的列表 -->
<!-- label是选择后的放到userRole中, @change有改变选择框的话里面有监控改变全选款的样式. -->
<el-checkbox v-for="(role, index) in allRole" :key="index" :label="role">{{ role.roleName
}}</el-checkbox>
</el-checkbox-group>
</el-form-item>
//确定按钮的回调(分配职位)
const confirmClick = async () => {
//收集参数
let data: SetRoleData = {
userId: userParams.id as number,
roleIdList: userRole.value.map(item => {
return item.id as number
})
}
//分配用户的职位
let result: any = await reqSetUserRole(data)
if (result.code == 200) {
//提示信息
ElMessage({ type: 'success', message: '分配职务成功' })
//关闭抽屉
drawer1.value = false
//获取更新完毕用户的信息,更新完毕留在当前页
getHasUser(pageNo.value)
}
}
- 在数据库是一个用户表 一个角色(职位)表 一个多对多表记录两个的数据.
<template>
<el-table
ref="multipleTableRef"
:data="tableData"
style="width: 100%"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55" />
<el-table-column label="Date" width="120">
<template #default="scope">{{ scope.row.date }}</template>
</el-table-column>
<el-table-column property="name" label="Name" width="120" />
<el-table-column property="address" label="Address" />
</el-table>
<div style="margin-top: 20px">
<el-button @click="toggleSelection([tableData[1], tableData[2]])">
Toggle selection status of second and third rows
</el-button>
<el-button @click="toggleSelection()">Clear selection</el-button>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import { ElTable } from 'element-plus'
interface User {
date: string
name: string
address: string
}
const multipleTableRef = ref<InstanceType<typeof ElTable>>()
const multipleSelection = ref<User[]>([])
const toggleSelection = (rows?: User[]) => {
if (rows) {
rows.forEach((row) => {
// TODO: improvement typing when refactor table
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
multipleTableRef.value!.toggleRowSelection(row, undefined)
})
} else {
multipleTableRef.value!.clearSelection()
}
}
const handleSelectionChange = (val: User[]) => {
multipleSelection.value = val
}
const tableData: User[] = [
{
date: '2016-05-03',
name: 'Tom',
address: 'No. 189, Grove St, Los Angeles',
},
{
date: '2016-05-02',
name: 'Tom',
address: 'No. 189, Grove St, Los Angeles',
},
{
date: '2016-05-04',
name: 'Tom',
address: 'No. 189, Grove St, Los Angeles',
},
{
date: '2016-05-01',
name: 'Tom',
address: 'No. 189, Grove St, Los Angeles',
},
{
date: '2016-05-08',
name: 'Tom',
address: 'No. 189, Grove St, Los Angeles',
},
{
date: '2016-05-06',
name: 'Tom',
address: 'No. 189, Grove St, Los Angeles',
},
{
date: '2016-05-07',
name: 'Tom',
address: 'No. 189, Grove St, Los Angeles',
},
]
</script>
- 在模板中,@selection-change=“handleSelectionChange” 事件监听器会捕获表格中选中行的变化,然后 handleSelectionChange 会将最新的选中行更新到 multipleSelection 变量中。
因此,multipleSelection 是存储你勾选的表格行数据的变量。
//批量删除按钮的回调
const deleteSelectUser = async () => {
//整理批量删除的参数
const idsList: any = multipleSelection.value.map(item => {
return item.id
})
//批量删除的请求
const result: any = await reqSelectUser(idsList)
if (result.code == 200) {
ElMessage({ type: 'success', message: '删除成功' })
getHasUser(userArr.value.length > 1 ? pageNo.value : pageNo.value - 1)
}
}
角色管理(职位管理)
例如:后端 前端就是一种职业,我们需要控制他们的添加和修改.
<el-dialog v-model="dialogVisite" :title="RoleParams.id?'修改职位' : '添加职位'">
<el-form :model="RoleParams" :rules="rules" ref="form" label-width="100px">
<el-form-item label="职位名称" prop="roleName">
<el-input placeholder="请你输入职位名称" v-model="RoleParams.roleName"></el-input>
</el-form-item>
</el-form>
<template #footer>
<el-button type="primary" size="default" @click="dialogVisite = false">
取消
</el-button>
<el-button type="primary" size="default" @click="save">确定</el-button>
</template>
</el-dialog>
树形结构
// 只有叶子结点可以被选中
const filterSelectArr = (allData: any, initArr: any) => {
console.log('allData')
console.log(allData)
allData.forEach((item: any) => {
if (item.select && item.level == 4) {
initArr.push(item.id)
}
if (item.children && item.children.length > 0) {
filterSelectArr(item.children, initArr)
}
})
return initArr
}
- 通过for循环得到更小的单位.
- 树形结构需要每个节点都需要有id,用来标识每个节点
- 依靠id来给节点勾选.
依靠最底层的的select字段来判断是否选中,从而加入选中的数组中
- 角色与权限,还有多对多的表.
- 角色勾选权限,有多少个勾选就有多少个id.
<el-popconfirm :title="`你确定要删除${row.roleName}?`" width="260px" @confirm="removeRole(row.id)">
<template #reference>
<el-button type="primary" size="small" icon="Delete">
删除
</el-button>
</template>
</el-popconfirm>
- 在消息提示框中是@confim是执行方法.
树形结构
- 根据角色id获得权限.
//已有的职位的数据
const setPermisstion = async (row: RoleData) => {
console.log(row)
//抽屉显示出来
drawer.value = true
//收集当前要分类权限的职位的数据
Object.assign(RoleParams, row)
//根据职位获取权限的数据
// 获得角色的权限数据
let result: MenuResponseData = await reqAllMenuList(RoleParams.id as number)
console.log(result.data)
if (result.code == 200) {
menuArr.value = result.data
selectArr.value = filterSelectArr(menuArr.value, [])
console.log('selectArr')
console.log(selectArr)
}
}
- menuArr获得所有菜单
- selectArr获得勾选的数据
传递过来的是勾选的.
<template #default>
<!-- 树形控件 -->
<el-tree ref="tree" :data="menuArr" show-checkbox node-key="id" :default-checked-keys="selectArr" :props="defaultProps" />
</template>
- :default-checked-keys=“selectArr” 这是获得选择的.
保存的时候
const handler = async () => {
//职位的ID
const roleId = RoleParams.id as number
//选中节点的ID
let arr = tree.value.getCheckedKeys()
let permissionId = arr
//下发权限
let result: any = await reqSetPermisstion(roleId, permissionId)
if (result.code == 200) {
//抽屉关闭
drawer.value = false
//提示信息
ElMessage({ type: 'success', message: '分配权限成功' })
//页面刷新
// window.location.reload()
}
}
- 保存的数据是多个id.
- 获得选择的数据方法是 let arr = tree.value.getCheckedKeys(),之后进行保存.
菜单管理
- name和code展示出来了
//添加菜单按钮的回调
const addPermisstion = (row: Permisstion) => {
//清空数据
Object.assign(menuData, {
id: 0,
code: '',
level: 0,
name: '',
pid: 0,
})
//对话框显示出来
dialogVisible.value = true
//收集新增的菜单的level数值
menuData.level = row.level + 1
//给谁新增子菜单
menuData.pid = row.id as number
}
//编辑已有的菜单
const updatePermisstion = (row: Permisstion) => {
dialogVisible.value = true
//点击修改按钮:收集已有的菜单的数据进行更新
Object.assign(menuData, row)
}
- 传入父亲id和自身level.还有code和name
重点! 实现权限管理
- name在vue插件可以用,还有权限管理
- 一级路由和二级路由需要进行梳理和整理 分为静态路由和异步路由和任意路由
- 静态路由
import { createRouter, createWebHashHistory } from "vue-router";
import routerData from "./router";
const router = createRouter({
// 路由模式hash
history: createWebHashHistory(),
routes: routerData
});
export default router;
const routerData = [
{
// 登录成功后的页面
path: "/",
name: "layout",
component: () => import("@/layout/index.vue"),
redirect: "/home",
meta: {
title: "layout",
icon: "Aim",
},
children: [{
path: "/home",
name: "Home",
component: () => import("@/views/home/index.vue"),
meta: {
title: "首页1",
icon: "Promotion",
}
}]
},
{
// 登录
path: "/login",
name: "Login",
component: () => import("@/views/login/index.vue"),
meta: {
title: "登录",
icon: "InfoFilled",
hidden: true
}
},
{
path: "/404",
name: "404",
component: () => import("@/views/404/index.vue"),
meta: {
title: "404",
icon: "Share",
hidden: true //true隐藏 false显示
}
},
{
path: "/:pathMatch(.*)*",
redirect: "/404",
name: "Any",
meta: {
title: "任意路由",
icon: "el-icon-s-home",
hidden: true
}
},
....
}
]
export default routerData;
- 这些都是静态路由.
如果把有些路由放到异步路由中所有用户都访问不到.
- 一个用户登录返回职位和他的职位对应的权限. 还有异步路由信息.
- 用户表 (users): 记录用户基本信息。
- 角色表 (roles): 定义不同的角色,比如“管理员”、“后端工程师”等。
- 权限表 (permissions): 定义具体的权限点,比如可以访问哪些路由,执行哪些操作。
- 角色-权限关联表 (role_permissions): 用于存储每个角色有哪些权限。
- 用户-角色关联表 (user_roles): 记录每个用户拥有哪些角色。
权限表
- 每个占据一个id,还有用户管理
- 有code和name.
按钮权限与路由权限的关系
按钮权限(如 btn.User.add, btn.Role.assign)决定了用户能在某个页面上执行哪些操作。因此,如果用户拥有该页面上的按钮权限,就推断用户应该能够访问该页面。因此,按钮权限的存在可以推断页面路由的访问权限。