admin管理员组文章数量:1532346
本项目采用最新的Vue3+组合式API开发方式
使用主流技术栈:
vue3
+typescript
+vue-router
+pinia
+element-plus
+axios
+echarts
GitHub仓库地址
初始化项目
环境准备:
node v18.17.0
pnpm v8.6.12
pnpm
安装:参考
- 使用
vite
构建项目:
pnpm create vite
- 进入项目目录后安装依赖:
pnpm install
- 启动项目
pnpm run dev --host
- 删除默认的
/src/style.css
文件,同时在main.ts
中也删除 - 安装
Vue VSCode Snippets
扩展 - 删除默认的
App.Vue
文件内容,输入v3ts
选组合式API生成模板后修改:
<template>
<div>
<h1>App根组件</h1>
</div>
</template>
- 删除自带的
/src/components/HelloWorld.vue
组件和/src/assets/vue.svg
图标 - 修改页面标题
index.html
<title>Vue3-template</title>
- 在
VSCode
中搜索并安装扩展:Volar
- 设置Volar Takeover 模式
- 将
tsconfig.json
和tsconfig.node.json
中的moduleResolution
选项设置为node
,CTRL+SHIFT+P
:RELOAD WINDOW
项目集成
集成element-plus
- 安装依赖
pnpm i element-plus
- 在入口文件
main.ts
中注册插件
import { createApp } from 'vue'
import App from './App.vue'
//引入element-plus 插件与样式
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
//获取应用实例对象
const app = createApp(App)
//注册element-plus插件
app.use(ElementPlus)
//将应用挂载到挂载点上
app.mount('#app')
- 安装图标组件库
pnpm i @element-plus/icons-vue
- 安装
Element UI Snippets
扩展 - 在
main.ts
配置element组件使用中文
//配置element-plus中文
//@ts-ignore忽略当前文件ts类型的检测否则有红色提示(打包会失败)
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
注册element-plus插件
app.use(ElementPlus, {
locale: zhCn, //配置element-plus中文
})
- 在
App.vue
中测试效果:
<template>
<div>
<el-button type="primary" size="default" :icon="Plus">主要按钮</el-button>
<el-button type="success" size="small" :icon="Edit">编辑按钮</el-button>
<el-button type="danger" size="default" :icon="Delete">删除按钮</el-button>
<el-pagination
:page-sizes="[100, 200, 300, 400]"
layout="total, sizes, prev, pager, next, jumper"
:total="400"
/>
</div>
</template>
<script setup lang="ts">
//引入图表组件
import { Plus,Edit,Delete } from '@element-plus/icons-vue'
</script>
<style scoped></style>
src别名配置
- 安装依赖
pnpm install @types/node
- 在
vite.config.ts
中引入path
import path from 'path'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
"@": path.resolve("./src") // 相对路径别名配置,使用 @ 代替 src
}
}
})
- TypeScript编译配置
// tsconfig.json
{
"compilerOptions": {
"baseUrl": "./", // 解析非相对模块的基地址,默认是当前目录
"paths": { //路径映射,相对于baseUrl
"@/*": ["src/*"]
}
}
}
- 在
main.ts
中使用@
引入App.vue
import App from '@/App.vue'
- 新建
@/components/Test.vue
测试组件@
<template>
<div>
<h1>测试组件@</h1>
</div>
</template>
<script setup lang="ts">
</script>
<style scoped></style>
- 在
App.vue
直接引用测试组件
<template>
<div>
<el-button type="primary" size="default" :icon="Plus">主要按钮</el-button>
<el-button type="success" size="small" :icon="Edit">编辑按钮</el-button>
<el-button type="danger" size="default" :icon="Delete">删除按钮</el-button>
<el-pagination
:page-sizes="[100, 200, 300, 400]"
layout="total, sizes, prev, pager, next, jumper"
:total="400"
/>
<!-- 引入测试组件 -->
<Test></Test>
</div>
</template>
<script setup lang="ts">
//引入图表组件
import { Plus,Edit,Delete } from '@element-plus/icons-vue'
import Test from '@/components/Test.vue'//引入测试组件
</script>
<style scoped></style>
环境变量的配置
- 项目根目录分别添加 开发、生产和测试环境的文件
.env.development
.env.production
.env.test
- 文件内容
# 变量必须以 VITE_ 为前缀才能暴露给外部读取
NODE_ENV = 'development'
VITE_APP_TITLE = 'xxxx'
VITE_APP_BASE_API = '/api'
VITE_SERVE="http://xxx"
# 变量必须以 VITE_ 为前缀才能暴露给外部读取
NODE_ENV = 'production'
VITE_APP_TITLE = 'xxx系统'
VITE_APP_BASE_API = '/prod-api'
VITE_SERVE="http://xxx"
# 变量必须以 VITE_ 为前缀才能暴露给外部读取
NODE_ENV = 'test'
VITE_APP_TITLE = 'xxx系统'
VITE_APP_BASE_API = '/test-api'
VITE_SERVE="http://xxx"
- 配置运行命令:
package.json
"scripts": {
"dev": "vite --open",
"build:test": "vue-tsc && vite build --mode test",
"build:pro": "vue-tsc && vite build --mode production",
"preview": "vite preview"
},
- 在项目中可以通过
import.meta.env
获取环境变量
SVG图标配置
在开发项目的时候经常会用到svg矢量图,而且我们使用SVG以后,页面上加载的不再是图片资源,这对页面性能来说是个很大的提升,而且我们SVG文件比img要小的很多,放在项目中几乎不占用资源。
- 安装SVG依赖插件
pnpm install vite-plugin-svg-icons -D
pnpm install fast-glob
- 在
vite.config.ts
中配置插件
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
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]',
}),
],
}
}
- 在
main.ts
入口文件中导入
import 'virtual:svg-icons-register'
-
新建
src/assets/icons
,导入用到的svg
图标 -
将
svg
封装为全局组件:因为项目很多模块需要使用图标,因此把它封装为全局组件,在src/components
目录下创建一个SvgIcon
文件夹,在SvgIcon
文件夹下再新建index.vue
<template>
<div>
<svg :style="{ width: width, height: height }">
<use :xlink:href="prefix + name" :fill="color"></use>
</svg>
</div>
</template>
<script setup lang="ts">
defineProps({
//xlink:href属性值的前缀
prefix: {
type: String,
default: '#icon-'
},
//svg矢量图的名字
name: String,
//svg图标的颜色
color: {
type: String,
default: ""
},
//svg宽度
width: {
type: String,
default: '16px'
},
//svg高度
height: {
type: String,
default: '16px'
}
})
</script>
<style scoped></style>
- 修改
App.vue
测试SVG,删除之前测试@
创建的Test.vue
<template>
<div>
<h1>测试SVG</h1>
<!-- 测试SVG图标使用 -->
<svg-icon name="welcome"></svg-icon>
</div>
</template>
<script setup lang="ts">
import SvgIcon from '@/components/SvgIcon/index.vue'
</script>
<style scoped>
</style>
使用自定义插件方式注册SVG为全局组件
注册为全局组件后就不用在每次使用的时候都用
import
引入
- 在
src/components
文件夹下创建index.ts
文件:用于注册components
文件夹内部全部全局组件(./Pagination/index.vue
直接用v3ts模板显示一句话就行)
//引入项目中全部的全局组件
import SvgIcon from './SvgIcon/index.vue';
import Pagination from './Pagination/index.vue';
import type { App, Component } from 'vue';
//全局对象
const allGlobalComponents: { [name: string]: Component } = { SvgIcon, Pagination };
export default {
//务必叫做install方法
install(app: App) {
//注册项目全部的全局组件
Object.keys(allGlobalComponents).forEach((key: string) => {
//注册为全局组件
app.component(key, allGlobalComponents[key]);
})
}
}
- 在
main.ts
入口文件中引入src/index.ts
文件,通过app.use
方法安装自定义插件
import gloablComponent from '@/components';
app.use(gloablComponent);
- 在
App.vue
中直接使用全局组件,不用import
导入
<template>
<div>
<h1>测试SVG</h1>
<!-- 测试SVG图标使用 -->
<svg-icon name="welcome"></svg-icon>
<!-- 测试自定义组件方式注册的全局组件 -->
<Pagination />
</div>
</template>
<script setup lang="ts">
// import SvgIcon from '@/components/SvgIcon/index.vue'
</script>
<style scoped></style>
集成sass
- 安装依赖
pnpm install sass sas-loader
- 在
App.vue
中测试项目能否使用scss
语法
<template>
<div>
<h1>测试SCSS</h1>
</div>
</template>
<script setup lang="ts">
</script>
<style scoped lang="scss">
div{
h1{
color: red;
}
}
</style>
- 为项目添加全局样式:在
src/styles
下新建index.scss
文件和reset.scss
,在npm官网搜索reset.scss
后复制内容到项目文件中,项目中可能需要用到清除默认样式,因此在index.scss
中引入reset.scss
//引入q默认样式
@import './reset.scss';
- 在
main.ts
入口文件引入全局样式
import '@/styles/index.scss'
- 给项目中引入全局变量:在
styles
下创建一个variable.scss
文件,在vite.config.ts
文件配置如下
export default defineConfig((config) => {
css: {
preprocessorOptions: {
scss: {
javascriptEnabled: true,
additionalData: '@import "./src/styles/variable.scss";',
},
},
},
}
}
- 在
variable.scss
下设置全局变量
//为项目提供scss全局变量
//定义项目主题颜色
$base-color: purple;
- 在
App.vue
中使用全局变量
<style scoped lang="scss">
div {
h1 {
color: $base-color;
}
}
</style>
mock数据
- 安装依赖
pnpm install -D vite-plugin-mock@2.9.6 mockjs
- 在
vite.config.js
配置文件启用插件
import { viteMockServe } from 'vite-plugin-mock'
export default defineConfig(({ command }) => {
return {
plugins: [
...,
viteMockServe({
//保证开发阶段可以使用mock数据
localEnabled: command === 'serve',
})
],
...
})
- 在根目录创建
mock
文件夹:在mock
文件夹内部创建一个user.ts
文件,写入需要的mock
数据与接口
//用户信息数据
//createUserList此函数执行会返回一个用户信息数组,包含两个用户信息
function createUserList() {
return [
{
userId: 1,
avatar:
'https://wpimg.wallstcn/f778738c-e4f8-4870-b634-56703b4acafe.gif',
username: 'admin',
password: '111111',
desc: '平台管理员',
roles: ['平台管理员'],
buttons: ['cuser.detail'],
routes: ['home'],
token: 'Admin Token',
},
{
userId: 2,
avatar:
'https://wpimg.wallstcn/f778738c-e4f8-4870-b634-56703b4acafe.gif',
username: 'system',
password: '111111',
desc: '系统管理员',
roles: ['系统管理员'],
buttons: ['cuser.detail', 'cuser.user'],
routes: ['home'],
token: 'System Token',
},
]
}
//对外暴露一个数组:数组里面包含两个接口:登录接口、获取用户信息接口
export default [
// 用户登录接口
{
url: '/api/user/login',//请求地址
method: 'post',//请求方式
response: ({ body }) => {
//获取请求体携带过来的用户名与密码
const { username, password } = body;
//调用获取用户信息函数,用于判断是否有此用户
const checkUser = createUserList().find(
(item) => item.username === username && item.password === password,
)
//没有用户返回失败信息
if (!checkUser) {
return { code: 201, data: { message: '账号或者密码不正确' } }
}
//如果有返回成功信息
const { token } = checkUser
return { code: 200, data: { token } }
},
},
// 获取用户信息
{
url: '/api/user/info',
method: 'get',
response: (request) => {
//获取请求头携带token
const token = request.headers.token;
//查看用户信息是否包含有次token用户
const checkUser = createUserList().find((item) => item.token === token)
//没有返回失败的信息
if (!checkUser) {
return { code: 201, data: { message: '获取用户信息失败' } }
}
//如果有返回成功信息
return { code: 200, data: { checkUser } }
},
},
]
- 安装
axios
pnpm i axios
- 在
main.ts
测试mock
能否使用(测试完后删除)
//测试代码:测试mock数据和接口能否使用
import axios from 'axios'
//登录接口
axios({
url: '/api/user/login',//请求地址
method: 'post',//请求方式
data: {
username: 'admin',
password: '111111'
}
})
axios二次封装
在开发项目的时候避免不了与后端进行交互,因此需要使用axios
插件实现发送网络请求。在开发项目的时候经常会把axios进行二次封装。
- 使用请求拦截器,可以在请求拦截器中处理一些业务(开始进度条、请求头携带公共参数)
- 使用响应拦截器,可以在响应拦截器中处理一些业务(进度条结束、简化服务器返回的数据、处理http网络错误)
- 在
src
目录下创建utils/request.ts
//进行axios二次封装:使用请求与响应拦截器
import axios from 'axios'
import { ElMessage } from 'element-plus'
// 1.利用axios对象的create方法创建一个新的axios实例
let request = axios.create({
// 配置基础路径
baseURL: import.meta.env.VITE_APP_BASE_API,//基础路径上会携带/api
timeout: 5000//超时设置
})
//2.请求拦截器
request.interceptors.request.use((config) => {
//config是请求配置对象,headers是请求头对象,经常给服务器端传递token
//返回配置对象
return config
})
//3.响应拦截器
request.interceptors.response.use((response) => {
//成功回调
//简化数据:在/mock/user.ts中可以看到响应数据是code和data,响应拦截器只返回data即可
return response.data
}, (error) => {
//失败回调:处理http网络错误
//定义一个变量:存储网络错误信息
let message = '';
//http状态码判断
let statusCode = error.response.status;
switch (statusCode) {
case 401:
message = 'token过期';
break;
case 403:
message = '无权访问';
break;
case 404:
message = '请求资源不存在';
break;
case 500:
message = '服务器内部错误';
break;
default:
message = '网络错误';
break;
}
//提示错误信息
ElMessage({
type: 'error',
message
})
//返回一个失败的promise对象
return Promise.reject(error)
})
//4.导出axios实例 对外暴露
export default request
- 在
App.vue
测试(测试完后删除)
<template>
<div>
<h1>测试axios二次封装</h1>
</div>
</template>
<script setup lang="ts">
import request from './utils/request';
import { onMounted } from 'vue';
//当组件挂载完成后执行
onMounted(() => {
request({
url: '/user/login',
method: 'post',
data: {
username: 'admin',
password: '111111'
}
}).then(res => {
console.log(res);
})
})
</script>
<style scoped></style>
有真实接口的可以在这直接跳到:完善部分功能——直接使用真实接口
API接口统一管理
在开发项目的时候接口可能很多,因此需要统一管理
- 在
src
目录下创建api/user/index.ts
和api/user/type.ts
文件统一管理用户登录、用户信息获取相关的接口和数据类型
//统一管理项目用户相关的接口
import request from '@/utils/request'
import type { LoginParams, LoginResultModel, UserInfoModel } from './type'
//统一管理接口
enum API {
LOGIN_URL = '/user/login',//登录接口
USERINFO_URL = '/user/info',//获取用户信息接口
}
//对外暴露请求函数
//登录接口方法
export const reqLogin = (data: LoginParams) => request.post<any, LoginResultModel>(API.LOGIN_URL, data)
//获取用户信息接口方法
export const reqUserInfo = () => request.get<any, UserInfoModel>(API.USERINFO_URL)
types.ts
嫌麻烦可以不用,直接在上面用any
就行,但是后期维护不方便
//登录接口需要携带参数ts类型
export interface LoginParams {
username: string,
password: string
}
interface dataType {
token?: string
message?: string
}
//登录接口返回数据类型
export interface LoginResultModel {
code: number,
data: dataType
}
interface userInfo {
userId: number,
avatar: string,
username: string,
password: string,
desc: string,
roles: string[],
buttons: string[],
routes: string[],
token: string
}
interface user{
checkUser: userInfo
}
//定义服务器返回用户信息的数据类型
export interface UserInfoModel {
code: number,
data: user
}
- 在
App.vue
中测试(测试结束后删除)
<template>
<div>
<h1>App根组件</h1>
</div>
</template>
<script setup lang="ts">
import { onMounted } from 'vue';
import { reqLogin } from './api/user';
onMounted(() => {
reqLogin({ username: 'admin', password: '111111' })
}
)
</script>
<style scoped></style>
路由配置
- 安装依赖
pnpm install vue-router@4.1.6
- 新建
src/views
文件夹专门放置路由的页面,/views
下新建login
、home
和404
目录 login
下新建index.vue
<template>
<div>
<h1>我是一级路由登录</h1>
</div>
</template>
<script setup lang="ts">
</script>
<style scoped></style>
home
下新建index.vue
<template>
<div>
<h1>我是一级路由展示登录成功以后的数据</h1>
</div>
</template>
<script setup lang="ts">
</script>
<style scoped></style>
404
下新建index.vue
<template>
<div>
<h1>我是一级路由404</h1>
</div>
</template>
<script setup lang="ts">
</script>
<style scoped></style>
- 新建
src/router/routers.ts
文件夹专门放置路由表
//对外暴露配置路由(常量路由)
export const constantRoutes = [
{
//登录
path: '/login',
component: () => import('@/views/login/index.vue'),
name: 'login'//命名路由,后面做权限管理时用到
}
,
{
//登录成功以后展示数据的路由
path: '/',
component: () => import('@/views/home/index.vue'),
name: 'home'//命名路由,后面做权限管理时用到
}
,
{
//404
path: '/404',
component: () => import('@/views/404/index.vue'),
name: '404'//命名路由,后面做权限管理时用到
}
,
{
//任意路由
path: '/:pathMatch(.*)*',
redirect: '/404',
name: 'Any'
}
]
- 新建
src/router/index.ts
实现模板路由配置
//通过vue-router插件实现模板路由配置
import { createRouter, createWebHashHistory } from 'vue-router'
import { constantRoutes } from './routers'
//创建路由器
let router = createRouter({
//路由模式hash
history: createWebHashHistory(),
routes: constantRoutes,
//滚动行为
scrollBehavior() {
return {
left: 0,
top: 0
}
}
})
export default router
- 在
main.ts
中引入
//引入路由
import router from './router';
//注册模板路由
app.use(router);
- 在
App.vue
中展示
<template>
<div>
<router-view></router-view>
</div>
</template>
<script setup lang="ts">
</script>
<style scoped></style>
登录模块
- 安装仓库依赖
pinia
pnpm i pinia
- 新建
src\store\index.ts
//仓库大仓库
import {createPinia} from 'pinia'
//创建大仓库
let pinia = createPinia()
//导出大仓库:入口文件需要安装仓库
export default pinia
- 在入口文件
main.ts
引入仓库
//引入仓库
import pinia from './store';
//安装仓库pinia
app.use(pinia);
- 新建
/src/utils/storage.ts
封装本地存储的读取与存储方法
//封装本地存储的读取与存储方法
//本地存储存储TOKEN
export const SET_TOKEN = (token: string) => {
localStorage.setItem('TOKEN', token)
}
//本地存储读取TOKEN
export const GET_TOKEN = () => {
return localStorage.getItem("TOKEN")
}
- 创建用户仓库
/store/moudules/types/type.ts
声明用到的数据类型
//定义小仓库数据state类型
export interface UserState {
token: string | null
}
- 新建
/src/utils/time.ts
获取一个结果:当前早上|上午|下午|晚上
//封装一个函数:获取一个结果:当前早上|上午|下午|晚上
export const getTime = () => {
let message = ''
//使用内置构造函数Date()获取当前时间
let hours = new Date().getHours()
//判断当前时间
if (hours >= 6 && hours < 9) {
message = '早上'
} else if (hours >= 9 && hours < 12) {
message = '上午'
} else if (hours >= 12 && hours < 18) {
message = '下午'
} else {
message = '晚上'
}
return message
}
- 创建用户仓库
/store/moudules/user.ts
//创建用户相关的小仓库
import { defineStore } from 'pinia'
//引入接口
import { reqLogin } from '@/api/user'
//引入数据类型
import type { LoginParams, LoginResultModel } from '@/api/user/type'
import type { UserState } from './types/type'
//引入操作本地存储的工具方法
import { SET_TOKEN, GET_TOKEN } from '@/utils/storage'
//创建用户小仓库
let useUserStore = defineStore(
//小仓库的名字
'User', {
//小仓库存储数据的地方
state: (): UserState => {
return {
token: GET_TOKEN(),//用户唯一标识
}
},
//处理异步和逻辑的地方
actions: {
//用户登录的方法
async login(data: LoginParams) {
//登录请求
let result: LoginResultModel = await reqLogin(data)
//登录请求:成功200->token
//登录请求:失败201->message
if (result.code === 200) {
//登录成功
//pinia仓库存储一下token
//由于pinia存储数据利用的是js对象,所以未持久化
this.token = (result.data.token as string)
//本地存储持久化token
SET_TOKEN((result.data.token as string))
//保证当前async函数返回值是一个成功的promise
return 'ok'
} else {
//登录失败
return Promise.reject(new Error(result.data.message))
}
}
},
getters: {
}
})
//对外暴露获取小仓库的方法
export default useUserStore
- 登录路由静态组件
src\views\login\index.vue
<template>
<div class="login_container">
<el-row>
<el-col :span="12" :xs="0"></el-col>
<el-col :span="12">
<el-form class="login_form">
<h1>你好</h1>
<h2>欢迎来到xxx系统</h2>
<el-form-item>
<el-input :prefix-icon="User" v-model="loginFrom.username"></el-input>
</el-form-item>
<el-form-item>
<el-input type="password" :prefix-icon="Lock" v-model="loginFrom.password" show-password></el-input>
</el-form-item>
<el-form-item>
<el-button :loading="loading" class="login_btn" type="primary" size="default"
@click="login">登录</el-button>
</el-form-item>
</el-form>
</el-col>
</el-row>
</div>
</template>
<script setup lang="ts">
import { User, Lock } from '@element-plus/icons-vue'
import { reactive, ref } from 'vue'
import { useRouter } from 'vue-router'
import { ElNotification } from 'element-plus';
//引入用户相关的小仓库
import useUserStore from '@/store/modules/user';
//引入获取当前时间的函数
import { getTime } from '@/utils/time';
let userStore = useUserStore()
//获取路由器
let $router = useRouter()
//定义变量控制按钮加载效果
let loading = ref(false)
//收集账号与密码数据
let loginFrom = reactive({
//默认值
username: 'admin',
password: '111111'
})
//点击了登录按钮,登录按钮回调
const login = async () => {
//按钮加载效果:开始加载
loading.value = true
// 通知仓库发送登录请求
try {
//请求成功->进入首页展示数据
await userStore.login(loginFrom)
//编程式导航跳转到展示数据首页
$router.push('/')
//登录成功的提示信息
ElNotification({
type: 'success',
title: `嗨,${getTime()}好`,
message: '欢迎回来'
})
//登录成功后,按钮加载效果:结束加载
loading.value = false
} catch (error) {
//请求失败->弹出登陆失败信息
//按钮加载效果:结束加载
loading.value = false
//登录失败的提示信息
ElNotification({
type: 'error',
title: '登录失败',
message: (error as Error).message
})
}
}
</script>
<style scoped lang="scss">
.login_container {
width: 100%;
height: 100vh;
background: url('@/assets/images/background.jpg') no-repeat;
background-size: cover;
}
.login_form {
position: relative;
width: 80%;
top: 30vh;
background: url('@/assets/images/login_form.png') no-repeat;
background-size: cover;
padding: 40px;
h1 {
color: white;
font-size: 40px;
}
h2 {
color: white;
font-size: 20px;
margin: 20px 0;
}
.login_btn {
width: 100%;
}
}
</style>
表单数据校验
Form 表单 | Element Plus (element-plus):Form 组件允许你验证用户的输入是否符合规范,来帮助你找到和纠正错误。
Form
组件提供了表单验证的功能,只需为rules
属性传入约定的验证规则,并将form-Item
的prop
属性设置为需要验证的特殊键值即可
src\views\login\index.vue
<template>
<el-form class="login_form" :model="loginFrom" :rules="rules" ref = "loginForms">
<el-form-item prop="username"></el-form-item>
<el-form-item prop="password"></el-form-item>
</el-form-item>
</template>
<script setup lang="ts">
//获取el-form组件
let loginForms = ref()
//点击了登录按钮,登录按钮回调
const login = async () => {
await loginForms.value.validate()
...
}
//定义表单校验需要的配置对象
const rules = {
//规则对象属性
username: [
{
required: true, // required,代表这个字段务必要校验的
min: 6, //min:文本长度至少多少位
max: 10, // max:文本长度最多多少位
message: '账号长度至少六位', // message:错误的提示信息
trigger: 'change' //trigger:触发校验表单的时机 change->文本发生变化触发校验, blur:失去焦点的时候触发校验规则
}
],
password: [
{
required: true,
min: 6,
max: 15,
message: '密码长度至少六位',
trigger: 'change'
}
]
}
</srcipt>
- 自定义校验规则:
src\views\login\index.vue
<script setup lang="ts">
//自定义校验规则需要的函数
const validatorUserName = (rule: any, value: any, callback: any) => {
//rule:当前校验规则对象
//value:当前表单项的值
//如果符合条件callBack放行即为通过
//如果不符合条件callBack传入错误信息即为不通过
if (/^\d{5,10}$/.test(value)) {
callback()
} else {
callback(new Error('账号必须是5-10位的数字'))
}
}
const rules = {
username: [
{
trigger: 'change',
validator: validatorUserName
}
],
...
}
</script>
Layout模块
- 新建
src/layout/index.vue
<template>
<div class="layout_container">
<!-- 左侧菜单 -->
<div class="layout_slider" :class="{ fold: LayoutSettingStore.fold ? true : false }">
<Logo />
<!-- 展示菜单 -->
<!-- 滚动组件 -->
<el-scrollbar class="scollbar">
<!-- 菜单组件 -->
<el-menu :collapse="LayoutSettingStore.fold ? true : false" :default-active="$route.path"
background-color="#001529" text-color="white" active-text-color="yellowgreen">
<!-- 根据路由动态生成菜单 -->
<Menu :menuList="userStore.menuRoutes"></Menu>
</el-menu>
</el-scrollbar>
</div>
<!-- 顶部导航 -->
<div class="layout_tabbar" :class="{ fold: LayoutSettingStore.fold ? true : false }">
<!-- layout组件的顶部导航tabbar -->
<Tabbar />
</div>
<!-- 内容展示区 -->
<div class="layout_main" :class="{ fold: LayoutSettingStore.fold ? true : false }">
<!-- <p style="height:10000px;background-color:red;">我是一个段落</p> -->
<Main />
</div>
</div>
</template>
<script setup lang="ts">
//引入左侧菜单logo子组件
import Logo from './logo/index.vue'
//引入菜单组件
import Menu from './menu/index.vue'
//右侧内容展示区
import Main from './main/index.vue'
//获取路由对象
import { useRoute } from 'vue-router'
//获取用户相关的小仓库
import useUserStore from '@/store/modules/user';
//引入顶部导航组件tabbar
import Tabbar from './tabbar/index.vue'
import useLayOutSettingStore from '@/store/modules/setting';
//获取layout配置仓库
let LayoutSettingStore = useLayOutSettingStore()
let userStore = useUserStore();
//获取路由对象
let $router = useRoute()
console.log($router.path)
</script>
<script lang="ts">
export default {
name: 'Layout'
}
</script>
<style scoped lang="scss">
.layout_container {
width: 100%;
height: 100vh;
.layout_slider {
color: white;
width: $base-menu-width;
height: 100vh;
background: $base-menu-bg;
transition: all 0.3s;
.scollbar {
color: white;
width: 100%;
height: calc(100vh - #{$base-menu-logo-height});
.el-menu {
border-right: none;
}
}
&.fold {
width: $base-menu-min-width;
}
}
.layout_tabbar {
position: fixed;
width: calc(100% - #{$base-menu-width});
height: $base-tabbar-height;
top: 0px;
left: $base-menu-width;
transition: all 0.3s;
&.fold {
width: calc(100vw - $base-menu-min-width);
left: $base-menu-min-width;
}
}
.layout_main {
position: absolute;
width: calc(100% - #{$base-menu-width});
height: calc(100vh - #{$base-tabbar-height});
background: yellowgreen;
top: $base-tabbar-height;
left: $base-menu-width;
padding: 20px;
overflow: auto;
transition: all 0.3s;
&.fold {
width: calc(100vw - $base-menu-min-width);
left: $base-menu-min-width;
}
}
}</style>
- 在
/src/styles/variable.scss
添加全局变量变量
//为项目提供scss全局变量
//定义项目主题颜色
$base-color: purple;
//左侧的菜单的宽度
$base-menu-width: 260px;
$base-menu-min-width: 50px;
//左侧菜单的背景色
$base-menu-bg: #001529;
//顶部导航的高度
$base-tabbar-height: 50px;
//左侧菜单logo高度
$base-menu-logo-height: 50px;
//左侧菜单logo右侧文字大小
$base-logo-title-fontSize: 20px
- 在
src/styles/index.scss
添加全局的样式
//引入清楚默认样式
@import './reset.scss';
//滚动条外观设置
::-webkit-scrollbar {
width: 10px;
}
::-webkit-scrollbar-track {
background: $base-menu-bg;
}
::-webkit-scrollbar-thumb {
width: 10px;
background: yellowgreen;
border-radius: 10px;
}
- 新建
src/layout/logo/index.vue
将logo
拆分为一个子组件
<template>
<div class="logo" v-if="setting.logoHidden">
<img :src="setting.logo" alt="">
<p>{{ setting.title }}</p>
</div>
</template>
<script setup lang="ts">
//引入设置标题和Logo的配置文件
import setting from '@/setting';
</script>
<script lang="ts">
export default {
name: 'Logo'
}
</script>
<style scoped lang="scss">
.logo {
width: 100%;
height: $base-menu-logo-height;
color: white;
display: flex;
align-items: center;
padding: 10px;
img {
width: 40px;
height: 40px;
}
p {
font-size: $base-logo-title-fontSize;
margin-left: 50px;
}
}
</style>
- 新建
src/setting.ts
:用于项目logo和标题配置
//用于项目logo和标题的配置
export default {
title: 'XXXXXX系统',//项目的标题
logo: '/public/logo.png',//项目的logo
logoHidden: true,//是否隐藏logo
}
- 新建
src/layout/menu/index.vue
:封装动态菜单项,根据项目路由生成
<template>
<template v-for="(item, index) in menuList" :key="item.path">
<!-- 没有子路由 -->
<template v-if="!item.children">
<el-menu-item :index="item.path" v-if="!item.meta.hidden" @click="goRoute">
<el-icon>
<component :is="item.meta.icon"></component>
</el-icon>
<template #title>
<span>{{ item.meta.title }}</span>
</template>
</el-menu-item>
</template>
<!-- 有子路由但是只有一个子路由 -->
<template v-if="item.children && item.children.length == 1">
<el-menu-item :index="item.children[0].path" v-if="!item.children[0].meta.hidden" @click="goRoute">
<el-icon>
<component :is="item.children[0].meta.icon"></component>
</el-icon>
<template #title>
<span>{{ item.children[0].meta.title }}</span>
</template>
</el-menu-item>
</template>
<!-- 有子路由且个数大于一个 -->
<el-sub-menu :index="item.path" v-if="item.children && item.children.length > 1">
<template #title>
<el-icon>
<component :is="item.meta.icon"></component>
</el-icon>
<span>{{ item.meta.title }}</span>
</template>
<Menu :menuList="item.children"></Menu>
</el-sub-menu>
</template>
</template>
<script setup lang="ts">
//获取父组件传递过来的全部路由数组
defineProps(['menuList'])
import { useRouter } from 'vue-router'
//获取路由器对象
let $router = useRouter()
//点击菜单的回调
const goRoute = (vc: any) => {
//跳转路由
$router.push(vc.index)
}
</script>
<script lang="ts">
export default {
name: 'Menu'
}
</script>
<style scoped></style>
- 将路由放到仓库
/src/store/modules/user.ts
中
//引入路由(常量路由)
import { constantRoutes } from '@/router/routers'
//创建用户小仓库
let useUserStore = defineStore(
...,
state: (): UserState => {
return {
token: GET_TOKEN(),//用户唯一标识
menuRoutes: constantRoutes,//用来生成用户菜单路由(数组)
}
},
...
})
- 定义数据类型
src/store/modules/types/type.ts
import { RouteRecordRaw } from "vue-router";
//定义小仓库数据state类型
export interface UserState {
token: string | null;
menuRoutes: RouteRecordRaw[]
}
- 在
src/components/index.ts
引入element-plus
的icon
图标组件
//引入element-plus提供的全部图标组件
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
export default {
install(app: App) {
...;
//注册element-plus提供的全部图标组件
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
}
}
- 修改路由
/src/router/routers.ts
:登录后跳转到刚才新建的一级路由页面;并根据路由动态生成菜单项;展示路由元信息
//对外暴露配置路由(常量路由)
export const constantRoutes = [
{
//登录
path: '/login',
component: () => import('@/views/login/index.vue'),
name: 'login',//命名路由,后面做权限管理时用到
meta: {
title: '登录',//菜单标题
hidden: true,//是否隐藏菜单 true隐藏 false显示
icon: "promotion"//菜单图标 支持element-plus提供的图标组件
}
}
,
{
//登录成功以后展示数据的路由
path: '/',
component: () => import('@/layout/index.vue'),
name: 'layout',//命名路由,后面做权限管理时用到
meta: {
title: '',//菜单标题
hidden: false,
icon: ''
},
redirect: '/home',
children: [
{
path: '/home',
component: () => import('@/views/home/index.vue'),
meta: {
title: '首页',//菜单标题
hidden: false,
icon: 'HomeFilled'
},
}
]
}
,
{
//404
path: '/404',
component: () => import('@/views/404/index.vue'),
name: '404',//命名路由,后面做权限管理时用到
meta: {
title: '404',//菜单标题
hidden: true
},
}
,
{
path: '/screen',
component: () => import('@/views/screen/index.vue'),
name: 'Screen',
meta: {
title: '数据大屏',//菜单标题
hidden: false,
icon: 'Monitor'
}
},
{
path: '/acl',
component: () => import('@/layout/index.vue'),
name: 'Acl',
meta: {
title: '权限管理',//菜单标题
hidden: false,
icon: 'Lock'
},
redirect: '/acl/user',
children: [
{
path: '/acl/user',
component: () => import('@/views/acl/user/index.vue'),
name: 'User',
meta: {
title: '用户管理',//菜单标题
hidden: false,
icon: 'User'
}
},
{
path: '/acl/role',
component: () => import('@/views/acl/role/index.vue'),
name: 'Role',
meta: {
title: '角色管理',//菜单标题
hidden: false,
icon: 'UserFilled'
}
},
{
path: '/acl/permission',
component: () => import('@/views/acl/permission/index.vue'),
name: 'permission',
meta: {
title: '菜单管理',//菜单标题
hidden: false,
icon: 'Tools'
}
}
]
},
{
path: '/product',
component: () => import('@/layout/index.vue'),
name: 'Product',
meta: {
title: '商品管理',//菜单标题
hidden: false,
icon: 'Shop'
},
redirect: '/product/trademark',
children: [
{
path: '/product/trademark',
component: () => import('@/views/product/trademark/index.vue'),
name: 'Trademark',
meta: {
title: '品牌管理',//菜单标题
hidden: false,
icon: 'ShoppingCartFull'
}
},
{
path: '/product/attr',
component: () => import('@/views/product/attr/index.vue'),
name: 'Attr',
meta: {
title: '属性管理',//菜单标题
hidden: false,
icon: 'ChromeFilled'
}
},
{
path: '/product/spu',
component: () => import('@/views/product/spu/index.vue'),
name: 'Spu',
meta: {
title: 'SPU管理',//菜单标题
hidden: false,
icon: 'Calendar'
}
},
{
path: '/product/sku',
component: () => import('@/views/product/sku/index.vue'),
name: 'Sku',
meta: {
title: 'SKU管理',//菜单标题
hidden: false,
icon: 'Orange'
}
}
]
},
{
//任意路由
path: '/:pathMatch(.*)*',
redirect: '/404',
name: 'Any',
meta: {
title: '任意路由',//菜单标题
hidden: true
},
}
]
- 新建
src/views/screen/index.vue
数据大屏组件
<template>
<div>
<h1>我是 数据大屏一级路由组件</h1>
</div>
</template>
<script setup lang="ts">
</script>
<style scoped></style>
- 新建
src/views/acl
权限管理组件,其下有三个二级路由,新建三个文件user/index.vue
、role/index.vue
、permission/index.vue
,模板如下
<template>
<div>
<h1>菜单管理</h1>
</div>
</template>
<script setup lang="ts">
</script>
<style scoped></style>
<template>
<div>
<h1>角色管理</h1>
</div>
</template>
<script setup lang="ts">
</script>
<style scoped></style>
<template>
<div class="box">
<h1>用户管理</h1>
</div>
</template>
<script setup lang="ts">
</script>
<style scoped lang="scss">
.box {
width: 100%;
height: 400px;
background-color: rgb(216, 101, 216);
}
</style>
- 新建
src/views/product
产品管理组件,其下有attr/index.vue
、sku/index.vue
、spu/index.vue
、trademark/index.vue
<template>
<div>
<h1>属性管理</h1>
</div>
</template>
<script setup lang="ts">
</script>
<style scoped></style>
- 新建
src/layout/main/index.vue
,封装右侧内容展示区。可以添加过渡动画
<template>
<!-- 路由组件出口 -->
<router-view v-slot="{ Component }">
<transition name="fade">
<!-- 渲染layout一级路由组件的子路由 -->
<component :is="Component" v-if="flag" />
</transition>
</router-view>
</template>
<script setup lang="ts">
import { watch, ref, nextTick } from "vue";
import useLayOutSettingStore from '@/store/modules/setting';
let layoutSettingStore = useLayOutSettingStore()
//控制当前组件是否销毁重建
let flag = ref(true)
//监听仓库内部数据是否发生变化,如果发生变化,说明用户点击过刷新按钮
watch(() => layoutSettingStore.refresh, () => {
//点击刷新按钮,路由组件销毁
flag.value = false
nextTick(() => {
//路由组件销毁后,再次渲染路由组件
flag.value = true
})
})
</script>
<script lang="ts">
export default {
name: 'Main'
}
</script>
<style scoped lang="scss">
.fade-enter-from {
opacity: 0;
transform: scale(0)
}
.fade-enter-active {
transition: all .3s;
}
.fade-enter-to {
opacity: 1;
transform: scale(1)
}
</style>
- 新建顶部组件
src/layout/tabbar/index.vue
<template>
<div class="tabbar">
<div class="tabbar_left">
<Breadcrumb />
</div>
<div class="tabbar_right">
<Setting />
</div>
</div>
</template>
<script setup lang="ts">
import Breadcrumb from './breadcrumb/index.vue'
import Setting from './setting/index.vue'
</script>
<script lang="ts">
export default {
name: 'Tabbar'
}
</script>
<style scoped lang="scss">
.tabbar {
width: 100%;
height: 100%;
display: flex;
justify-content: space-between;
background-image: linear-gradient(to right, rgb(232, 223, 223), rgb(201, 178, 178), rgb(197, 165, 165));
.tabbar_left {
display: flex;
align-items: center;
margin-left: 20px;
}
.tabbar_right {
display: flex;
align-items: center;
}
}
</style>
- 新建
src/layout/tabbar/breadcrumb/index.vue
和src/layout/tabbar/setting/index.vue
:分割顶部组件的左右区域组件
<template>
<!-- 顶部左侧静态 -->
<el-icon style="margin-right:10px" @click="changeIcon">
<component :is="LayoutSettingStore.fold?'Fold':'Expand'"></component>
</el-icon>
<!-- 左侧面包屑 -->
<el-breadcrumb separator-icon="ArrowRight">
<!-- 面包动态展示路由名字与标题 -->
<el-breadcrumb-item v-for="(item, index) in $router.matched" :key="index" v-show="item.meta.title" :to="item.path">
<!-- 图标 -->
<el-icon>
<component :is="item.meta.icon"></component>
</el-icon>
<!-- 面包屑展示匹配路由的标题 -->
<span>{{ item.meta.title }}</span>
</el-breadcrumb-item>
</el-breadcrumb>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { useRoute } from 'vue-router'
import useLayOutSettingStore from '@/store/modules/setting'
//获取layout配置相关的仓库
let LayoutSettingStore = useLayOutSettingStore()
//获取路由对象
let $router = useRoute()
//点击图标的方法
const changeIcon = () => {
//图标进行切换
LayoutSettingStore.fold = !LayoutSettingStore.fold
}
const handler = () => {
console.log($router.matched)
}
</script>
<script lang="ts">
export default {
name: 'Breadcrumb'
}
</script>
<style scoped></style>
<template>
<el-button size="small" icon="Refresh" circle=true @click="updateRefresh"></el-button>
<el-button size="small" icon="FullScreen" circle=true @click="fullScreen"></el-button>
<el-button size="small" icon="Setting" circle=true></el-button>
<img src="../../../../public/logo.png" style="width:24px;height:24px;margin:0px 10px">
<!-- 下拉菜单 -->
<el-dropdown>
<span class="el-dropdown-link">
admin
<el-icon class="el-icon--right">
<arrow-down />
</el-icon>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item>退出登录</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
<script setup lang="ts">
//获取小仓库
import useLayOutSettingStore from '@/store/modules/setting';
let LayoutSettingStore = useLayOutSettingStore()
//刷新按钮点击回调
const updateRefresh = () => {
//刷新当前路由
LayoutSettingStore.refresh = !LayoutSettingStore.refresh
}
const fullScreen = () => {
//DOM对象的一个属性:可以用来当前是否为全屏【全屏:true】
let full = document.fullscreenElement
if (!full) {
//切换为全屏模式
document.documentElement.requestFullscreen()
} else {
//退出全屏模式
document.exitFullscreen()
}
}
</script>
<script lang="ts">
export default {
name: 'Setting'
}
</script>
<style scoped></style>
- 新建仓库
src/store/modules/setting.ts
:保存折叠变量;刷新变量
//小仓库:layout组件相关配置仓库
import { defineStore } from 'pinia';
let useLayOutSettingStore = defineStore('SettingStore', {
state: () => {
return {
fold: false,//是否折叠菜单
refresh: false,//是否刷新页面
}
}
})
export default useLayOutSettingStore;
完善部分功能
登陆获取用户信息
- 修改
src/store/modules/user.ts
:获取用户信息后存储到pinia
仓库
//引入接口
import { reqLogin, reqUserInfo } from '@/api/user'
//小仓库存储数据的地方
state: (): UserState => {
return {
token: GET_TOKEN(),//用户唯一标识
menuRoutes: constantRoutes,//用来生成用户菜单路由(数组)
username: '',
avatar: '',
}
},
//处理异步和逻辑的地方
actions: {
//用户登录的方法
async login(data: LoginParams) {
//登录请求
let result: LoginResultModel = await reqLogin(data)
//登录请求:成功200->token
//登录请求:失败201->message
if (result.code === 200) {
//登录成功
//pinia仓库存储一下token
//由于pinia存储数据利用的是js对象,所以未持久化
this.token = (result.data.token as string)
//本地存储持久化token
SET_TOKEN((result.data.token as string))
//保证当前async函数返回值是一个成功的promise
return 'ok'
} else {
//登录失败
return Promise.reject(new Error(result.data.message))
}
},
//获取用户信息方法
async getUserInfo() {
//获取用户信息存储到仓库中
let result = await reqUserInfo()
//如果获取用户信息成功,则存储
if (result.code == 200) {
this.username = result.data.checkUser.username
this.avatar = result.data.checkUser.avatar
} else {
return Promise.reject(new Error(result.message))
}
}
},
- 修改
src/utils/request.ts
:请求拦截器在发送请求时从仓库获取token
放入请求头
//引入用户相关的仓库
import useUserStore from '@/store/modules/user'
request.interceptors.request.use((config) => {
//获取用户相关的小仓库:获取token
let userStore = useUserStore();
if (userStore.token) {
config.headers.token = userStore.token
}
//config是请求配置对象,headers是请求头对象,经常给服务器端传递token
// config.headers.token='123456'
//返回配置对象
return config
})
- 新增数据类型
src/store/modules/types/type.ts
:用户名和头像地址
export interface UserState {
token: string | null;
menuRoutes: RouteRecordRaw[],
username: string,
avatar: string
}
- 修改
src/layout/tabbar/setting/index.vue
:显示用户名和头像
<img :src="userStore.avatar" style="width:24px;height:24px;margin:0px 10px; border-radius:50%;">
<span class="el-dropdown-link">
{{ userStore.username }}
<el-icon class="el-icon--right">
<arrow-down />
</el-icon>
</span>
import useUserStore from '@/store/modules/user';
let userStore = useUserStore()
- 在
src/views/home/index.vue
中测试使用仓库中存储的用户名
<template>
<div>
<h1>我是一级路由展示登录成功以后的数据{{ UserStore.username }}</h1>
</div>
</template>
<script setup lang="ts">
//引入组合式API函数之生命周期函数
import { onMounted } from 'vue'
//获取仓库
import useUserStore from '@/store/modules/user'
let UserStore = useUserStore()
//目前首页挂载完毕发请求获取用户信息
onMounted(() => {
UserStore.getUserInfo()
})
</script>
<style scoped></style>
退出登录
- 修改
src/layout/tabbar/setting/index.vue
:给退出绑定事件
<el-dropdown-item @click="logout">退出登录</el-dropdown-item>
<script setup lang="ts">
import { useRouter } from 'vue-router';
//获取路由对象
let $router = useRouter()
//退出登录点击回调
const logout = () => {
//1.向服务器发送请求【退出登录接口,服务器将TOKEN设置为无效】
//2.清空仓库的TOKEN
//3.跳转到登陆页面
userStore.userLogout()
$router.push({ path: '/login' })
}
</script>
- 在
src/store/modules/user.ts
添加退出登录的方法
//引入操作本地存储的工具方法
import { SET_TOKEN, GET_TOKEN, REMOVE_TOKEN } from '@/utils/storage'
//退出登录
async userLogout() {
//清空PINIA仓库
this.token = ''
this.username = ''
this.avatar = ''
REMOVE_TOKEN()
},
- 在
src/utils/storage.ts
封装删除TOKEN的方法
//本地存储删除数据方法
export const REMOVE_TOKEN = () => {
localStorage.removeItem('TOKEN')
}
路由鉴权
- 安装进度条插件
pnpm i nprogress
- 在
src
下新建permission.ts
:利用全局前置守卫和全局后置守卫完成路由鉴权
//路由鉴权
//鉴权:某一个路由什么条件下才能访问
import router from '@/router'
import setting from './setting'
import nprogress from 'nprogress' //引入进度条
//关闭进度条加载的右侧小圈
nprogress.configure({ showSpinner: false })
//获取仓库中的token
import useUserStore from './store/modules/user'
import pinia from './store'
let userStore = useUserStore(pinia) //获取仓库中的token
//引入进度条样式
import 'nprogress/nprogress.css'
//全局前置守卫:项目中任何一个路由发生改变之前,都会先经过这里
router.beforeEach(async (to: any, from: any, next: any) => {
document.title = `${setting.title} - ${to.meta.title}`
//to:即将要进入的目标路由对象
//from:当前导航正要离开的路由
//next:调用该方法后,才能进入下一个钩子
nprogress.start(); //开启进度条
//获取仓库中的token
let token = userStore.token
//获取用户名
let username = userStore.username
//用户登录判断
if (token) { //如果有token
//登陆成功,访问登录页,直接跳转到首页
if (to.path == '/login') {
next({ path: '/' })
} else {
//登陆成功访问其余的路由,放行
//有用户信息
if (username) {
next()
} else {
//没有用户信息,发请求获取用户信息再放行
try {
//发请求获取用户信息
await userStore.getUserInfo()
next()
} catch (error) {//请求失败
//token失效,重新登录
//用户手动修改了本地存储的token
userStore.userLogout()
next({ path: '/login' })
}
}
}
} else {
//用户未登录判断
if (to.path === '/login') { //如果访问的是登录页
next() //放行
} else {//访问的不是登录页
next({ path: '/login' }) //跳转到登录页
}
}
})
//全局后置首位
router.afterEach((to: any, from: any) => {
nprogress.done() //关闭进度条
}) //不需要next(),因为已经跳转了
//根据是否有TOKEN判断是否登录
//登陆后可以访问除login外所有路由
//未登录只能访问login,访问其他路由就跳转到login
- 在
src/main.ts
引入permission.ts
//引入路由鉴权文件
import './permission';
- 修改
src/store/modules/user.ts
if (result.code == 200) {
this.username = result.data.checkUser.username
this.avatar = result.data.checkUser.avatar
return 'ok'
} else {
return Promise.reject('获取用户信息失败')
}
- 删除
views/home/index.vue
中发送请求用户信息的代码,因为已经在路由前置守卫中实现了获取用户信息
<template>
<div>
<h1>我是一级路由展示登录成功以后的数据</h1>
</div>
</template>
<script setup lang="ts">
</script>
<style scoped></style>
- 修改
src/layout/index.vue
的过渡动画,删除左侧菜单的过渡动画
<!-- 滚动组件 -->
<el-scrollbar class="scollbar">
.layout_container {
width: 100%;
height: 100vh;
.layout_slider {
color: white;
width: $base-menu-width;
height: 100vh;
background: $base-menu-bg;
transition: all 0.3s;
.scollbar {
color: white;
width: 100%;
height: calc(100vh - #{$base-menu-logo-height});
.el-menu {
border-right: none;
}
}
}
真实接口替代mock接口
- 修改三个
.env
文件:配置服务器地址
VITE_SERVE="http://sph-api.atguigu"
- 在
vite.config.ts
配置代理跨域
import { defineConfig,loadEnv } from 'vite'
export default defineConfig(({ command,mode }) => {
//获取各种运行环境下的变量(dev/prod/test)
let env = loadEnv(mode,process.cwd())
return {
...,
//代理跨域
server: {
proxy: {
[env.VITE_APP_BASE_API]: {
//服务器域名
target: env.VITE_SERVE,
//是否需要代理跨域
changeOrigin: true,
//路径重写:由于服务器接口不含/api,所以将/api替换为空
rewrite: (path) => path.replace(/^\/api/, '')
}
}
}
}
})
- 重写
src/api/user/type.ts
//登录接口需要携带参数ts类型
export interface LoginFormDate {
username: string,
password: string
}
//定义全部接口返回数据都拥有的ts类型
export interface ResponseData {
code: number,
message: string,
ok: boolean
}
//定义登录接口返回数据的ts类型
export interface LoginResponseData extends ResponseData {
data: string
}
//定义获取用户信息返回的数据类型
export interface UserInfoResponseData extends ResponseData {
data: {
routers: string[],
buttons: string[],
roles: string[],
name: string,
avatar: string,
}
}
- 重写
src/api/user/index.ts
//统一管理项目用户相关的接口
import request from '@/utils/request'
import type { LoginFormDate, LoginResponseData, UserInfoResponseData } from './type'
//统一管理接口
enum API {
LOGIN_URL = '/admin/acl/index/login',//登录接口
USERINFO_URL = '/admin/acl/index/info',//获取用户信息接口
LOGOUT_URL = '/admin/acl/index/logout',//退出登录接口
}
//对外暴露请求函数
//登录接口方法
export const reqLogin = (data: LoginFormDate) => request.post<any, LoginResponseData>(API.LOGIN_URL, data)
//获取用户信息接口方法
export const reqUserInfo = () => request.get<any, UserInfoResponseData>(API.USERINFO_URL)
//退出登录接口方法
export const reqLogout = () => request.post<any, any>(API.LOGOUT_URL)
- 修改
src/store/modules/user.ts
的用户登录、获取用户信息和退出登录的方法
//引入接口
import { reqLogin, reqUserInfo,reqLogout } from '@/api/user'
//引入数据类型
import type { LoginFormDate, LoginResponseData, UserInfoResponseData } from '@/api/type'
//创建用户小仓库
let useUserStore = defineStore(
//小仓库的名字
'User', {
//小仓库存储数据的地方
state: (): UserState => {
return {
token: GET_TOKEN(),//用户唯一标识
menuRoutes: constantRoutes,//用来生成用户菜单路由(数组)
username: '',
avatar: '',
}
},
//处理异步和逻辑的地方
actions: {
//用户登录的方法
async login(data: LoginFormDate) {
//登录请求
let result: LoginResponseData = await reqLogin(data)
//登录请求:成功200->token
//登录请求:失败201->message
if (result.code === 200) {
//登录成功
//pinia仓库存储一下token
//由于pinia存储数据利用的是js对象,所以未持久化
this.token = (result.data as string)
//本地存储持久化token
SET_TOKEN((result.data as string))
//保证当前async函数返回值是一个成功的promise
return 'ok'
} else {
//登录失败
return Promise.reject(new Error(result.data))
}
},
//获取用户信息方法
async getUserInfo() {
//获取用户信息存储到仓库中
let result: UserInfoResponseData = await reqUserInfo()
console.log(result)
//如果获取用户信息成功,则存储
if (result.code == 200) {
this.username = result.data.name
this.avatar = result.data.avatar
return 'ok'
} else {
return Promise.reject(new Error(result.message))
}
},
//退出登录
async userLogout() {
//退出登录请求
let result: any = await reqLogout()
if (result.code == 200) {
//清空PINIA仓库
this.token = ''
this.username = ''
this.avatar = ''
REMOVE_TOKEN()
return 'ok'
} else {
//退出登录的接口请求失败
return Promise.reject(new Error(result.message))
}
},
},
getters: {
}
})
- 在
src/views/login/index.vue
修改默认密码
//收集账号与密码数据
let loginFrom = reactive({
//默认值
username: 'admin',
password: 'atguigu123'
})
- 在
src/layout/tabbar/setting/index.vue
添加等待退出登录成功的代码
const logout = async () => {
//1.向服务器发送请求【退出登录接口,服务器将TOKEN设置为无效】
//2.清空仓库的TOKEN
//3.跳转到登陆页面
await userStore.userLogout()
$router.push({ path: '/login' })
}
- 在
src/permission.ts
中也添加等待退出登录成功代码
try {
//发请求获取用户信息
await userStore.getUserInfo()
next()
} catch (error) {//请求失败
//token失效,重新登录
//用户手动修改了本地存储的token
await userStore.userLogout()
next({ path: '/login' })
}
首页模块
- 修改
src/views/home/index.vue
<template>
<el-card>
<div class="box">
<img :src="userStore.avatar" alt="" class="avatar">
<div class="bottom">
<h3 class="title">{{ getTime() }}好~{{ userStore.username }}</h3>
<p class="subtitle">{{ setting.title }}</p>
</div>
</div>
</el-card>
<div class="bottoms">
<svg-icon name="welcome" width="500px" height="500px"></svg-icon>
</div>
</template>
<script setup lang="ts">
//引入用户相关仓库
import useUserStore from '@/store/modules/user'
import { getTime } from '@/utils/time'
import setting from '@/setting';
//获取用户仓库实例
let userStore = useUserStore()
</script>
<style scoped>
.box {
display: flex;
.avatar {
width: 100px;
height: 100px;
border-radius: 50%;
}
.bottom {
margin-left: 20px;
.title {
font-size: 30px;
font-weight: 900;
margin-bottom: 30px;
}
.subtitle {
font-style: italic;
color: rgb(121, 129, 135);
}
}
}
.bottoms {
display: flex;
justify-content: center;
margin-top: 10px;
}</style>
品牌管理模块
- 在
src/layout/index.vue
删除主题模块的背景颜色,使用默认白色
.layout_main {
...
//background: yellowgreen;
}
- 完成
src/views/trademark/index.vue
<template>
<el-card class="box-card">
<!-- 卡片顶部添加品牌按钮 -->
<el-button type="primary" size="default" icon="Plus" @click="addTradeMark">添加品牌</el-button>
<!-- 表格组件:用于展示已有数据 -->
<el-table style="margin: 10px 0px;" border :data="trademarkList">
<el-table-column label="序号" width="80px" align="center" type="index"></el-table-column>
<!-- <el-table-column label="品牌名称" prop="tmName"></el-table-column> -->
<!-- el-table-colum默认用div展示数据,也可以用插槽展示数据,样式可以自定义 -->
<el-table-column label="品牌名称">
<template #="{ row, $index }">
<pre style="color:hotpink">{{ row.tmName }}</pre>
</template>
</el-table-column>
<el-table-column label="品牌LOGO">
<template #="{ row, $index }">
<img :src="row.logoUrl" alt="无图片" style="width:100px;height: 100px;">
</template>
</el-table-column>
<el-table-column label="品牌操作">
<template #="{ row, $index }">
<el-button type="primary" size="mini" icon="Edit" @click="$event => updateTradeMark(row)">编辑</el-button>
<el-popconfirm :title="`您确定要删除${row.tmName}?`" width="230px" icon="Delete"
@confirm='removeTradeMark(row.id)'>
<template #reference>
<el-button type="danger" size="mini" icon="Delete">删除</el-button>
</template>
</el-popconfirm>
</template>
</el-table-column>
</el-table>
<!-- 分页器组件
paginaation组件的属性:
1. current-page:当前页码
2. page-size:每页显示条数
3. page-sizes:每页显示条数的下拉框选项
4. background:是否为分页按钮添加背景色
5. layout:组件布局,子组件名用逗号分隔
6. total:总条数
-->
<el-pagination @size-change="sizeChange" @current-change="getTrademarkList" v-model:current-page="pageNum"
v-model:page-size="pageSize" :page-sizes="[5, 10, 15, 20]" :background="true"
layout="prev, pager, next, jumper,->, sizes,total " :total="total" />
</el-card>
<!-- 对话框组件:添加品牌、修改品牌业务时使用 -->
<!-- v-model:控制对话框显示【true】与隐藏【false】
title:设置对话框左上角的标题 -->
<el-dialog v-model="dialogTableVisible" :title="trademarkForm.id ? '修改品牌' : '添加品牌'">
<el-form style="width:80%" :model="trademarkForm" :rules="rules" ref="formRef">
<el-form-item label="品牌名称" label-width="100px" prop="tmName">
<el-input placeholder="请输入品牌名称" v-model="trademarkForm.tmName"></el-input>
</el-form-item>
<el-form-item label="品牌LOGO" label-width="100px" prop="logoUrl">
<!-- upload相关属性: action:上传的API地址,不带/api代理服务器不工作-->
<el-upload class="avatar-uploader" action="/api/admin/product/baseTrademark/save" :show-file-list="false"
:on-success="handleAvatarSuccess" :before-upload="beforeAvatarUpload">
<img v-if="trademarkForm.logoUrl" :src="trademarkForm.logoUrl" class="avatar" />
<el-icon v-else class="avatar-uploader-icon">
<Plus />
</el-icon>
</el-upload>
</el-form-item>
</el-form>
<!-- 具名插槽footer -->
<template #footer>
<el-button @click="cancel">取 消</el-button>
<el-button type="primary" @click="confirm">确 定</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
//引入组合式API函数ref
import { ref, onMounted, reactive } from 'vue'
import { reqGetTrademarkList, reqAddOrUpdateTrademark, reqDeleteTrademark } from '@/api/product/trademark'
import type { Records, TrademarkRespnseData, Trademark } from '@/api/product/trademark/types'
import { ElMessage, type UploadProps } from 'element-plus'
import { async } from 'fast-glob';
//当前页码
let pageNum = ref<number>(1)
//每页展示多少条数据
let pageSize = ref<number>(5)
//存储已有品牌数据总数
let total = ref<number>(0)
//存储已有品牌的数据
let trademarkList = ref<Records>([])
//控制对话框显示与隐藏
let dialogTableVisible = ref<boolean>(false)
//收集新增品牌数据
let trademarkForm = reactive<Trademark>({
tmName: '',
logoUrl: ''
})
//获取el-form组件实例
let formRef = ref()
//将获取已有品牌数据的接口封装为一个函数:在任何情况下都可以调用
const getTrademarkList = async (pager = 1) => {
//当每次调用获取品牌数据的函数时,将pageNum变量的值重置为1
//当切换页码时,传入参数会替换掉默认值1
pageNum.value = pager
//调用接口
let res: TrademarkRespnseData = await reqGetTrademarkList(pageNum.value, pageSize.value)
if (res.code == 200) {
//将获取到的数据赋值给total变量
total.value = res.data.total
//将获取到的数据赋值给trademarkList变量
trademarkList.value = res.data.records
}
}
//组件挂载完成后调用获取品牌数据的函数
onMounted(() => {
getTrademarkList()
})
//当下拉菜单改变每一页数据量时调用的函数
const sizeChange = () => {
//调用获取品牌数据的函数(向后端发请求获取数据)
getTrademarkList()
}
//点击【添加品牌】按钮的回调
const addTradeMark = () => {
//重置表单数据
trademarkForm.id = 0
trademarkForm.tmName = ''
trademarkForm.logoUrl = ''
//重置表单校验结果
formRef.value?.clearValidate('tmName')
formRef.value?.clearValidate('logoUrl')
//将对话框显示
dialogTableVisible.value = true
}
//点击【编辑】按钮的回调
const updateTradeMark = (row: Trademark) => {
//将获取到的品牌数据赋值给trademarkForm变量
trademarkForm.id = row.id
trademarkForm.logoUrl = row.logoUrl
trademarkForm.tmName = row.tmName
//重置表单校验结果
formRef.value?.clearValidate('tmName')
formRef.value?.clearValidate('logoUrl')
//将对话框显示
dialogTableVisible.value = true
}
//点击添加|修改品牌的【取消】按钮的回调
const cancel = () => {
//将对话框隐藏
dialogTableVisible.value = false
}
//点击添加|修改品牌的【确定】按钮的回调
const confirm = async () => {
//在发送请求之前,先进行整个表单的校验
//如果通过校验才执行下面的代码
await formRef.value.validate()
//将对话框隐藏
dialogTableVisible.value = false
//调用添加|修改品牌的接口
let result: any = reqAddOrUpdateTrademark(trademarkForm)
//判断接口调用是否成功
if (result.code == 200) {
//添加|修改成功
//重新获取品牌数据
getTrademarkList()
//提示用户添加成功
ElMessage.success(trademarkForm.id ? '修改品牌成功' : '添加品牌成功')
} else {
//添加|修改失败
//提示用户添加失败
ElMessage.error(trademarkForm.id ? '修改品牌失败' : '添加品牌失败')
}
//再次发请求获取品牌数据
getTrademarkList(trademarkForm.id ? pageNum.value : 1)
// getTrademarkList(pageNum.value)
}
//上传图片之前的钩子函数——约束文件的类型和大小
const beforeAvatarUpload: UploadProps['beforeUpload'] = (rawFile) => {
//rawFile:上传的文件
//文件类型
const isJPG = rawFile.type === 'image/jpeg'
const isPNG = rawFile.type === 'image/png'
//文件大小
const isLt2M = rawFile.size / 1024 / 1024 < 2
if (!isJPG && !isPNG) {
//提示用户文件类型不符合要求
ElMessage.error('上传头像图片只能是 JPG/PNG 格式!')
return false
}
if (!isLt2M) {
//提示用户文件大小不符合要求
ElMessage.error('上传头像图片大小不能超过 2MB!')
return false
}
return true
}
//上传图片成功的钩子函数
const handleAvatarSuccess: UploadProps['onSuccess'] = (response, uploadFile) => {
//response:后端返回的数据
//uploadFile:上传的文件
if (response.code == 200) {
//将上传成功的图片地址赋值给logoUrl变量,用于点击【确定】时提交给后端
trademarkForm.logoUrl = response.data
//清除图片校验结果
formRef.value.clearValidate('logoUrl')
ElMessage.success('上传图片成功')
} else {
ElMessage.error('上传图片失败')
}
}
//品牌自定义校验规则
// const validtorTmName = (rule: any, value: any, callBack: any) => {
// //rule:当前校验规则
// //value:用户输入的品牌名称
// //callBack:回调函数
// if (value.trim().length >= 2) {
// callBack()
// } else {
// //和下面的message不能并存
// callBack(new Error('品牌名称不能少于2个字符'))
// }
// }
//品牌LOGO自定义校验规则
const validtorLogoUrl = (rule: any, value: any, callBack: any) => {
//value:图片的地址
if (value) {
callBack()
} else {
callBack(new Error('品牌LOGO务必上传'))
}
}
//表单验证规则
const rules = {
tmName: [
{ required: true, message: '品牌名称不能为空', trigger: 'change' }
],
logoUrl: [//没有触发校验的时机
{ required: true, trigger: 'change', validator: validtorLogoUrl }
]
}
//删除品牌的气泡确认框的【确认】按钮的回调
const removeTradeMark = async (id: number) => {
//调用删除品牌的接口
let result: any = await reqDeleteTrademark(id)
console.log(result)
console.log(result.code)
//判断接口调用是否成功
if (result.code == 200) {
//删除成功
//重新获取品牌数据
getTrademarkList()
//提示用户删除成功
ElMessage.success('删除品牌成功')
//再次发请求获取品牌数据
getTrademarkList(trademarkList.value.length > 1 ? pageNum.value : pageNum.value - 1)
} else {
//删除失败
//提示用户删除失败
ElMessage.error('删除品牌失败')
}
}
</script>
<style scoped>
.avatar-uploader .avatar {
width: 178px;
height: 178px;
display: block;
}
.avatar-uploader .el-upload {
border: 1px dashed var(--el-border-color);
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
transition: var(--el-transition-duration-fast);
}
.avatar-uploader .el-upload:hover {
border-color: var(--el-color-primary);
}
.el-icon.avatar-uploader-icon {
font-size: 28px;
color: #8c939d;
width: 178px;
height: 178px;
text-align: center;
}
</style>
- 新建一个文件夹
src/api/product/trademark/index.ts
封装请求
//统一管理品牌管理模块接口
import request from "@/utils/request";
import type { TrademarkRespnseData, Trademark } from "./types";
//品牌管理模块接口地址
enum API {
//获取品牌分页列表的接口地址
GET_TRADEMARK_LIST = "/admin/product/baseTrademark/",
//添加品牌
ADD_TRADEMARK = "/admin/product/baseTrademark/save",
//修改品牌
UPDATE_TRADEMARK = "/admin/product/baseTrademark/update",
//删除品牌
Delete_TRADEMARK = "/admin/product/baseTrademark/remove/"
}
//获取品牌分页列表的接口方法
//page:获取第几页的数据——默认值为1
//limit:每页显示多少条数据——默认值为10
export const reqGetTrademarkList = (page: number, limit: number) => request.get<any, TrademarkRespnseData>(API.GET_TRADEMARK_LIST + `${page}/${limit}`);
//添加品牌的接口方法
//export const reqAddTrademark = (data: Trademark) => request.post<any,any>(API.ADD_TRADEMARK, data);
//修改品牌的接口方法
//export const reqUpdateTrademark = (data: Trademark) => request.put<any,any>(API.UPDATE_TRADEMARK, data);
//添加与修改已有品牌接口方法
export const reqAddOrUpdateTrademark = (data: Trademark) => {
//修改已有品牌的数据
if (data.id) {
return request.put<any, any>(API.UPDATE_TRADEMARK, data)
} else {
//新增品牌
return request.post<any, any>(API.ADD_TRADEMARK, data)
}
}
//删除品牌的接口方法
export const reqDeleteTrademark = (id: number) => request.delete<any, any>(API.Delete_TRADEMARK + id);
- 配一个
types.ts
定义数据类型
export interface ResponseData {
code: number;
message: string;
ok: boolean;
}
//已有的品牌数据类型
export interface Trademark {
//?表示可选,可有可无,新增时没有id,修改时有id
id?: number;
tmName: string;
logoUrl: string;
}
//包含全部品牌数据类型
export type Records = Trademark[];
//获取的已有品牌数据类型
export interface TrademarkRespnseData extends ResponseData {
data: {
records: Records;
total: number;
size: number;
pages: number;
current: number;
searchCount: boolean;
}
}
版权声明:本文标题:Vue项目实战 内容由热心网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:https://m.elefans.com/dianzi/1724876007a995460.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论