vue-ts-demo
npm i -g @vue/cli
PS D:\kwai\vue3\project> vue create vue3-te-demo
element-plus
一个 Vue 3 UI 框架 | Element Plus
https://element-plus.org/zh-CN/guide/installation.html
安装:
npm install element-plus --save
完整引入使用:
使用:
按需引入使用
首先你需要安装unplugin-vue-components
和 unplugin-auto-import
这两款插件
shell
npm install -D unplugin-vue-components unplugin-auto-import
修改vue.config.js中的内容为:
const { defineConfig } = require('@vue/cli-service')
const AutoImport = require('unplugin-auto-import/webpack')
const Components = require('unplugin-vue-components/webpack')
const { ElementPlusResolver } = require('unplugin-vue-components/resolvers')
module.exports = defineConfig({
transpileDependencies: true,
configureWebpack:{
plugins: [
AutoImport({
resolvers: [ElementPlusResolver()],
}),
Components({
resolvers: [ElementPlusResolver()],
}),
],
}
})
如果报错:RROR TypeError: AutoImport is not a function TypeError,则需要降版本:
ERROR TypeError: AutoImport is not a function TypeError: AutoImport is not a-CSDN博客
使用:
重启项目
快速生成模版快捷键
登录页面
表单:
解决外边距塌陷:
css解决外边距塌陷的7种方法_css外边距塌陷-CSDN博客
内容:
<template>
<div class="login-box">
<el-form
ref="ruleFormRef"
:model="ruleForm"
status-icon
:rules="rules"
label-width="80px"
class="demo-ruleForm"
>
<h2>后台管理系统</h2>
<el-form-item label="姓名" prop="name">
<el-input v-model="ruleForm.name" type="name" autocomplete="off" />
</el-form-item>
<el-form-item label="密码" prop="pass">
<el-input v-model="ruleForm.pass" type="password" autocomplete="off" />
</el-form-item>
<el-form-item>
<el-button class="login-btn" type="primary" @click="submitForm(ruleFormRef)"
>登录</el-button
>
<el-button class="login-btn" @click="resetForm(ruleFormRef)">重置</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script setup lang="ts">
import { reactive, ref } from 'vue'
import type { FormInstance, FormRules } from 'element-plus'
const ruleFormRef = ref<FormInstance>()
const rules = reactive<FormRules>({
name: [
{
required: true,
trigger: 'blur',
},
{
min: 3,
max: 5,
message: 'Length should be 3 to 5',
trigger: 'blur',
},
],
pass: [
{
required: true,
trigger: 'blur',
},
{
min: 3,
max: 5,
message: 'Length should be 6 to 8',
trigger: 'blur',
},
]
});
const ruleForm = reactive({
pass: '',
name: '',
})
const submitForm = (formEl: FormInstance | undefined) => {
if (!formEl) return
formEl.validate((valid) => {
if (valid) {
console.log('submit!')
} else {
console.log('error submit!')
return false
}
})
}
const resetForm = (formEl: FormInstance | undefined) => {
if (!formEl) return
formEl.resetFields()
}
</script>
<style scoped lang="scss">
.login-box{
width: 100%;
height: 100%;
background:url("../assets/bg.png");
padding: 1px;
text-align: center;
.demo-ruleForm{
width: 500px;
background-color: white;
margin:100px auto;
padding: 20px;
border-radius: 20px;
}
.login-btn{
width: 40%;
}
h2{
margin-bottom: 16px;
}
}
</style>
使用ts对数据类型进行限制
新建一个文件夹type用于数据存放
export interface LoginForm{
pass:string,
name:string,
}
export class LoginData{
ruleForm:LoginForm={pass:"",name:""}
}
在之前对pass name的数据定义中,并没有规范类型
在定义类型之后:双向绑定
登录请求
axios中文网|axios API 中文文档 | axios
npm install axios --save-dev
新建 request/index.ts 使用index.ts的好处
https://juejin.cn/post/7221004205271646245
index.ts
import axios from "axios";
const mockType = 'cbzMock' // fastMock
const baseURL = mockType === 'cbzMock' ? 'https://mock.presstime.cn/mock/63569fbbbee0a00099ca48a1/api/vue-ts-mall-demo' : 'https://www.fastmock.site/mock/bf1fcb3c2e2945669c2c8d0ecb8009b8/api'
//创建axios实例
const service = axios.create({
baseURL: baseURL,
timeout: 5000,
headers: {
"Content-type" : "application/json;charset=utf-8"
}
})
//请求拦截
service.interceptors.request.use((config) => {
config.headers = config.headers || {}
if(localStorage.getItem("token")){
config.headers.token = localStorage.getItem("token") || ""
}
return config
})
//响应拦截
service.interceptors.response.use(({ data }) => {
const code : number = data.data.code
if(code != 200){
return Promise.reject(data)
}
return data
},(err) => {
console.log(err)
})
export default service
api.ts
import service from "@/request/index";
import {LoginData} from "@/type/login";
// 登录接口
export function login(data: LoginData) {
return service({
url: "/login",
method: "POST",
data
})
}
// 商品列表接口
export function getGoodsList(){
return service({
url: "/getGoodsList",
method: "GET"
})
}
// 用户列表接口
export function getUserList(){
return service({
url: "/getUserList",
method: "GET"
})
}
// 角色列表接口
export function getRoleList(){
return service({
url: "/getRoleList",
method: "GET"
})
}
// 权限列表接口
export function getAuthorityList(){
return service({
url: "/getAuthorityList",
method: "GET"
})
}
登录操作
const submitForm = (formEl: FormInstance | undefined) => {
if (!formEl) return
formEl.validate((valid) => {
if (valid) {
console.log('submit!')
//假装成功!
login(ruleForm).then( (res)=>console.log(res) ).catch( (err)=>{
console.log(err)
//登录成功,切换到主页面
router.replace({ name:'mainview'});
}
);
} else {
console.log('error submit!')
return false
}
})
}
主页面
布局容器
https://element-plus.org/zh-CN/component/container.html
header
<template>
<div>
main view
</div>
<div class="common-layout">
<el-container>
<el-header>
<el-row :gutter="20">
<el-col :span="4"> <img src="../assets/logo.png" alt="" class="icon-logo"> <div class="grid-content ep-bg-purple" /></el-col>
<el-col :span="16"> <h2 class="title">后台管理系统</h2> <div class="grid-content ep-bg-purple" /></el-col>
<el-col :span="4"> <span class="quit">退出登录</span> <div class="grid-content ep-bg-purple" /></el-col>
</el-row>
</el-header>
<el-container>
<el-aside width="200px">Aside</el-aside>
<el-main>Main</el-main>
</el-container>
</el-container>
</div>
</template>
<script setup lang="ts">
</script>
<style scoped lang="scss">
.el-header{
height: 80px;
background: #666;
.icon-logo{
height: 80px;
}
.title ,.quit{
text-align: center;
height: 80px;
line-height: 80px;
}
}
</style>
height 和line-height 的设置https://www.cnblogs.com/pwindy/p/13026176.html
实现上下居中的对齐,如果不设置line-height ,。则会居上
Aside
如何实现侧边栏一直到底部:
<template>
<div>
main view
</div>
<div class="common-layout">
<el-container>
<el-header>
<el-row :gutter="20">
<el-col :span="4"> <img src="../assets/logo.png" alt="" class="icon-logo"> <div class="grid-content ep-bg-purple" /></el-col>
<el-col :span="16"> <h2 class="title">后台管理系统</h2> <div class="grid-content ep-bg-purple" /></el-col>
<el-col :span="4"> <span class="quit">退出登录</span> <div class="grid-content ep-bg-purple" /></el-col>
</el-row>
</el-header>
<el-container>
<el-aside width="200px">
<el-col :span="120">
<el-menu
active-text-color="#ffd04b"
background-color="#545c64"
class="el-menu-vertical-demo"
default-active="2"
text-color="#fff"
>
<el-menu-item index="2">
<el-icon><icon-menu /></el-icon>
<span>商品列表</span>
</el-menu-item>
</el-menu>
</el-col>
</el-aside>
<el-main>Main</el-main>
</el-container>
</el-container>
</div>
</template>
<script setup lang="ts">
import {
Menu as IconMenu,
Setting,
} from '@element-plus/icons-vue'
const handleOpen = (key: string, keyPath: string[]) => {
console.log(key, keyPath)
}
const handleClose = (key: string, keyPath: string[]) => {
console.log(key, keyPath)
}
</script>
<style scoped lang="scss">
.el-header{
height: 80px;
background: #666;
.icon-logo{
height: 80px;
}
.title ,.quit{
text-align: center;
height: 80px;
line-height: 80px;
}
}
.el-aside{
.el-menu{
height: calc(100vh - 80px);
}
}
</style>
可以通过计算:100vh即 100%的垂直视口 - header高度80px
.el-aside{
.el-menu{
height: calc(100vh - 80px);
}
}
动态路由
<template>
<div class="common-layout">
<el-container>
<el-header>
<el-row :gutter="20">
<el-col :span="4"> <img src="../assets/logo.png" alt="" class="icon-logo"> <div class="grid-content ep-bg-purple" /></el-col>
<el-col :span="16"> <h2 class="title">后台管理系统</h2> <div class="grid-content ep-bg-purple" /></el-col>
<el-col :span="4"> <span class="quit">退出登录</span> <div class="grid-content ep-bg-purple" /></el-col>
</el-row>
</el-header>
<el-container>
<el-aside width="120px">
<el-menu
active-text-color="#ffd04b"
background-color="#545c64"
class="el-menu-vertical-demo"
default-active="2"
text-color="#fff"
router
>
<!--router开启路由模式 可以通过标签的index 进行跳转-->
<el-menu-item :index="item.path" v-for="item in subRouterList" :key="item.path">
<span>{{item.meta.title}}</span>
</el-menu-item>
</el-menu>
</el-aside>
<!--设置路由出口-->
<el-main>
<router-view></router-view>
</el-main>
</el-container>
</el-container>
</div>
</template>
<script setup lang="ts">
import {
} from '@element-plus/icons-vue'
import { useRouter } from 'vue-router';
const router = useRouter();
const subRouterList = router.getRoutes().filter( v=>
v.meta.isShow)
console.log(subRouterList)
</script>
<style scoped lang="scss">
.el-header{
height: 80px;
background: #666;
.icon-logo{
height: 80px;
}
.title ,.quit{
text-align: center;
height: 80px;
line-height: 80px;
}
}
.el-aside{
.el-menu{
height: calc(100vh - 80px);
}
}
</style>
子路由设置:children
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
import GoodsView from '@/views/GoodsView.vue'
import LoginView from '../views/LoginView.vue'
import MainView from '../views/MainView.vue'
import UserView from '@/views/UserView.vue'
const routes: Array<RouteRecordRaw> = [
{
path: '/mainview',
name: 'mainview',
component: MainView,
children:[
{
path:'goods',
name:'goods',
meta:{
isShow:true,
title:"商品列表"
},
component:GoodsView
},
{
path:'user',
name:'user',
meta:{
isShow:true,
title:"用户列表"
},
component:UserView
}
]
},
{
path: '/login',
name: 'login',
component:LoginView
}
]
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes
})
export default router
商品页面
搜索
列表
分页展示
列表数据的获取:
数据定义:
export interface Goods{
id:number,
userId:number,
title:string,
introduce:string,
}
interface SelectData{
id:number,
userId:number,
title:string,
introduce:string,
curPage:number,
count:number,
pageSize:number,
}
export class GoodsPages{
// 被选择的数据, 查询时用
selected_data:SelectData ={
userId: 0,
id: 0,
title: "",
introduce: "",
curPage: 1,
count: 0,
pageSize: 10};
// 展示的商品数据
goods_list: Goods[] = []
}
数据获取:
<script setup lang="ts">
import { computed, onMounted } from 'vue';
import { getGoodsList } from '@/request/api';
import {GoodsPages } from '@/type/goods';
import { reactive } from 'vue'
//定义数据
const goods_data = reactive(new GoodsPages())
//当前页数据
const curPagaData = reactive( {
comList:computed( ()=>{
return goods_data.goods_list.slice((goods_data.selected_data.curPage-1)*10,goods_data.selected_data.curPage*10);
})
} )
onMounted( ()=>{
console.log('onMounted');
//请求数据
getGoodsList()
.then( (res)=>{
goods_data.goods_list = res.data.data;
console.log('goodsList',goods_data.goods_list)
})
.catch( (err)=>{
console.log(err)
})
}
)
</script>
分页展示逻辑:
<div>
curPagaData.comList 是列表当前页展示的数据
<el-table :data=curPagaData.comList border style="width: 100%">
<el-table-column prop="id" label="id" width="180" /> 使用列表中的字段与 每一列标题
<el-table-column prop="title" label="title" width="180" />
<el-table-column prop="introduce" label="introduce" />
</el-table>
分页展示
<el-pagination layout="prev, pager, next"
v-model:page-size=goods_data.selected_data.pageSize 页面的大小:设置为10
v-model:current-page=goods_data.selected_data.curPage 当前是第几页
:total=goods_data.goods_list.length /> 总页数:是请求到的列表的总数
</div>
查询逻辑
<div class="select-box">
<el-form :inline="true" :model="goods_data.selected_data" class="demo-form-inline">
<el-form-item label="用户Title">
<el-input v-model="goods_data.selected_data.title" placeholder="用户Title" clearable />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="onSubmit">查询</el-button>
</el-form-item>
</el-form>
</div>
//查询数据
const onSubmit = () => {
console.log('submit! suc',goods_data.selected_data.title)
//如果是空的,则使用原来的数据
if (goods_data.selected_data.title === ''){
goods_data.goods_list = last_data;
goods_data.selected_data.curPage=1;
return;
}
//过滤数据
let filterArry:Goods[]=[];
//过滤数据
filterArry = goods_data.goods_list.filter( (value)=>{
return value.title.indexOf(goods_data.selected_data.title) !== -1
} );
goods_data.goods_list = filterArry;
goods_data.selected_data.curPage=1;
}
onMounted( ()=>{
getGoodsList()
.then( (res)=>{
goods_data.goods_list = res.data.data;
//记录当前的数据,用于恢复
last_data = goods_data.goods_list;
console.log('goodsList',goods_data.goods_list)
})
.catch( (err)=>{
console.log(err)
})
}
)
用户列表
展示
自定义列模版:展示用户角色
<template #default="scope">
<el-text class="mx-1" v-for="item in scope.row.role" :key="item.role"> {{item.roleName +" "}}</el-text>
选择器
数据定义:
/*
获取用户列表接口 GET /getUserList
请求报文:无
响应报文:
{
"data": {
"code": 200,
"data": [
{
"id": 1,
"nickName": "小明",
"userName": "小明",
"role": [
{
"role": 1,
"roleName": "管理员"
},
{
"role": 2,
"roleName": "普通用户"
}
]
},
{
"id": 2,
"nickName": "红红",
"userName": "红红",
"role": [
{
"role": 1,
"roleName": "管理员"
}
]
},
{
"id": 3,
"nickName": "绿绿",
"userName": "绿绿",
"role": [
{
"role": 2,
"roleName": "普通用户"
}
]
}
]
}
}
获取角色列表接口 GET /getRoleList
请求报文:无
响应报文:
{
"data": {
"code": 200,
"data": [
{
"roleName": "管理员",
"roleId": 1,
"authority": [
1,
2,
4,
5,
6,
7,
8,
9,
11,
13,
14,
15,
16
]
},
{
"roleName": "普通用户",
"roleId": 2,
"authority": [
1,
3,
4,
6,
7,
8,
9,
11,
12,
13
]
}
]
}
}
*/
interface RoloInfo{
role:number,
roleName:string,
}
interface UserInfo{
id:number,
nickName:string,
userName:string
role:RoloInfo[],
}
interface RoleType{
roleName:string,
roleId:number,
authority:Array<number>[],
}
interface QueryUser{
nickName: string, // 用户别名
role: number // 角色编号
}
export class UserPage{
roleList:RoleType[]=[];
userList:UserInfo[]=[];
selectedData:QueryUser={nickName:'',role:0}
}
vue:
<template>
<div class="select-box">
<el-form :inline="true" :model="userPage.selectedData" class="demo-form-inline">
<el-form-item label="用户Title">
<el-input v-model="userPage.selectedData.nickName" placeholder="用户nickName" clearable />
</el-form-item>
<el-form-item label="用户roleid">
<el-select v-model="userPage.selectedData.role" placeholder="全部" style="width: 240px">
<el-option
:key="0"
label="全部"
:value="0"
/>
<el-option
v-for="item in userPage.roleList"
:key="item.roleId"
:label="item.roleName"
:value="item.roleId"
/>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="onSubmit">查询</el-button>
</el-form-item>
</el-form>
</div>
<div>
<el-table :data=userPage.userList border style="width: 100%">
<el-table-column prop="id" label="编号" width="180" />
<el-table-column prop="nickName" label="用户昵称" width="180" />
<el-table-column prop="role" label="用户角色" >
<template #default="scope">
<el-text class="mx-1" v-for="item in scope.row.role" :key="item.role"> {{item.roleName +" "}}</el-text>
</template>
</el-table-column>
</el-table>
</div>
</template>
<script setup lang="ts">
import { getRoleList, getUserList } from '@/request/api';
import { UserInfo, UserPage } from '@/type/userinfo';
import { onMounted, reactive } from 'vue';
const userPage = reactive(new UserPage())
let lastUserList:UserInfo[]=[];
onMounted(()=>{
getUserList().then(
(res)=>{
userPage.userList = res.data.data;
lastUserList =userPage.userList;
console.log("userList",userPage.userList);
}
)
getRoleList().then(
(res)=>{
userPage.roleList =res.data.data;
console.log("roleList",userPage.roleList);
}
)
})
const onSubmit=()=>{
console.log('onsubmit',userPage.selectedData.role,userPage.selectedData.nickName);
if (userPage.selectedData.role===0&& userPage.selectedData.nickName===''){
userPage.userList = lastUserList;
return;
}
let list:UserInfo[]= userPage.userList.filter(
(value)=>{
let found = false;
if (userPage.selectedData.role===0){
return value.nickName.indexOf(userPage.selectedData.nickName)!=-1;
}
if (userPage.selectedData.nickName===''){
for(let i=0;i<value.role.length;++i){
if (value.role[i].role === userPage.selectedData.role){
return true;
}
}
}
if (value.nickName.indexOf(userPage.selectedData.nickName)!=-1){
for(let i=0;i<value.role.length;++i){
if (value.role[i].role === userPage.selectedData.role){
return true;
}
}
}
return found;
}
);
userPage.userList = list;
}
</script>
<style scoped>
</style>
编辑
编辑按钮
<el-table-column prop="role" label="操作" >
<template #default="scope">
<el-button type="primary" @click="handleEditUser(scope.row)">编辑</el-button>
</template>
</el-table-column>
弹出编辑框
<!-- 编辑用户的弹出窗-->
<el-dialog v-model="userPage.editShow" title="编辑用户信息">
<el-form :model="userPage.editUser">
<el-form-item label="用户昵称" label-width="120px">
<el-input v-model="userPage.editUser.nickName" autocomplete="off" />
</el-form-item>
<el-form-item label="用户角色" label-width="120px">
<el-select multiple v-model="userPage.editUser.role" class="m-2" size="large" placeholder="请选择角色">
<el-option
v-for="item in userPage.roleList"
:key="item.roleId"
:label="item.roleName"
:value="item.roleId"
/>
</el-select>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="userPage.editShow = false">取消</el-button>
<el-button type="primary" @click="ensureEditUser">修改</el-button>
</span>
</template>
</el-dialog>
数据结构
// 用户编辑接口
export interface UserEdit {
id: number, // 用户id
nickName: string, // 用户昵称
role: number[], // 用户角色
userName: string // 用户名
}
export class UserPage{
roleList:RoleType[]=[];
userList:UserInfo[]=[];
selectedData:QueryUser={nickName:'',role:0}
editShow = false // 是否显示编辑用户弹出窗
// 编辑用户时用到的对象
editUser: UserEdit = {
id: 0,
nickName: "",
role: [],
userName: ""
}
}
// 编辑用户弹窗
const handleEditUser = (row: UserInfo) => {
userPage.editShow = true
userPage.editUser = {
id: row.id,
nickName: row.nickName,
role: row.role.map((value) => value.role),
userName: ""
}
}
const ensureEditUser = () => {
console.log(userPage.editUser)
userPage.editShow = false;
let obj= userPage.userList.find( (value)=>{ value.id === userPage.editUser.id})
for(let i=0;i<userPage.userList.length;++i){
if (userPage.editUser.id === userPage.userList[i].id){
userPage.userList[i].nickName= userPage.editUser.nickName;
userPage.userList[i].role =[];
for(let item of userPage.roleList){
if (userPage.editUser.role.find( (value)=>( value === item.roleId) )){
userPage.userList[i].role.push({
role:item.roleId,
roleName:item.roleName
})
}
}
}
}
console.log(userPage.userList)
}
查询
let lastUserList:UserInfo[]=[];
const onSubmit=()=>{
console.log('onsubmit',userPage.selectedData.role,userPage.selectedData.nickName);
if (userPage.selectedData.role===0&& userPage.selectedData.nickName===''){
userPage.userList = lastUserList;
return;
}
let list:UserInfo[]= userPage.userList.filter(
(value)=>{
let found = false;
if (userPage.selectedData.role===0){
return value.nickName.indexOf(userPage.selectedData.nickName)!=-1;
}
if (userPage.selectedData.nickName===''){
for(let i=0;i<value.role.length;++i){
if (value.role[i].role === userPage.selectedData.role){
return true;
}
}
}
if (value.nickName.indexOf(userPage.selectedData.nickName)!=-1){
for(let i=0;i<value.role.length;++i){
if (value.role[i].role === userPage.selectedData.role){
return true;
}
}
}
return found;
}
);
userPage.userList = list;
}
代码
通过百度网盘分享的文件:src.zip
链接:https://pan.baidu.com/s/1p9YR2W0-DuIZs1_UJwDh6g?pwd=kdhs
提取码:kdhs