中后台开发笔记
安装脚手架
- ...
拉取manager模板
- ...
- 配置代理(解决是否允许跨域访问的问题)
//在vite.config.js文件下配置
// 配置前端服务地址和端口
server: {
host: "0.0.0.0",
port: 5173,
// 是否开启 https
https: false,
//让开发服务器帮我们做代理
proxy:{
// http://localhost:5173/api
"/api":{
target:"http://localhost:8173",//API服务地址
changeOrigin:true,//开启跨域
rewrite:(path)=>path.replace(/^\/api/,"")
}
}
},
拉取server数据库模板
- 下载源代码
git clone https://gitee.com/steveouyang/mongo-expresser.git
- 安装依赖
cd mongo-expresser
npm install
- 修改配置文件
{
"dbName": "mydb",//自动生成的数据库名称
"port": 8002,//项目运行端口
"jwtSecret": "jinwandalaohu",//jwtToken秘钥
"tokenAge": "3600s",//登录token有效期
// 模块接口
"routes": [
{
"name": "film", //模块名称电影 = 数据库中的collection名称 = RESTful风格的接口前缀
// 接口中间件配置
"middlewares": {
"create": ["adminCheck"], // 为单个添加接口【POST:/film/0】配置管理员校验中间件
"createMany": ["adminCheck"], // 为批量添加接口【POST:/film/-1】配置管理员校验中间件
"retrieve": [], // 详情查询接口【GET:/film/:id】无需登录鉴权
"retrieveMany": [], // 批量查询接口【GET:/film/0】无需登录鉴权
"update": ["adminCheck"], // 为数据更新接口【PUT:/film/:id】配置管理员校验中间件
"delete": ["adminCheck"] // 为数据删除接口【DELETE:/film/:id】配置管理员校验中间件
}
},
{
"name": "city", //模块名称城市 = 数据库中的collection名称 = RESTful风格的接口前缀
// 接口中间件配置
"middlewares": {
"middlewares": {
"create": ["adminCheck"], // 为单个添加接口【POST:/city/0】配置管理员校验中间件
"createMany": ["adminCheck"], // 为批量添加接口【POST:/city/-1】配置管理员校验中间件
"retrieve": ["loginCheck"], // 详情查询接口【GET:/city/:id】配置登录校验中间件
"retrieveMany": ["loginCheck"], // 批量查询接口【GET:/city/0】配置登录校验中间件
"update": ["adminCheck"], // 为数据更新接口【PUT:/city/:id】配置管理员校验中间件
"delete": ["adminCheck"] // 为数据删除接口【DELETE:/city/:id】配置管理员校验中间件
}
}
}
]
}
- 运行项目
npm run start
现在所有的工程模块已经具备增删改查功能!!!
调试阶段(可借助mongodb+postman进行调试)
静态资源模块:
GET:baseurl/<public下的文件目录>
文件上传服务
POST:baseurl/file/upload
-- 上传文件页面
baseurl/page/file_upload.html
必不可少的用户管理模块(带权限校验功能)
--用户注册
POST:baseurl/user/register + form或json格式请求体
--用户登录
POST:baseurl/user/login + form/json格式的请求体
业务模块
此处以音乐模块为例:
-- 添加单个音乐
POST:baseurl/music/0 + form或json格式的请求体
-- 批量添加
POST:baseurl/music/-1 + json数组格式的请求体
-- 删除音乐
DELETE:baseurl/music/:id
-- 修改音乐
PUT:baseurl/music/:id + form或json格式的请求体
-- 查询音乐详情
GET:baseurl/music/:id
-- 查询所有音乐
GET:baseurl/music/0
使用身份鉴权
- 用户模块默认检测三个字段:username,password,admin
- 后续通过admin字段的true/false判断是否是管理员身份登录
- 用户登陆成功后的返回信息里会携带jwtToken,其密钥即为配置文件中配置的密钥
- 对后续接口的返回需要在请求头当中携带token
headers:{
"authorization":登录时发放的token值
}
使用ElementPlus(vue3)进行样式布局 ps:(vue2:Element ui)
- 基本使用步骤:
//在 main.js 里面引入 elementplus
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
- Icon图标比较特殊,需要在用到组件内,再次声明
import {
ArrowLeftBold,
SwitchButton,
} from "@element-plus/icons-vue";
部署路由
在router/index.js
routes: [
//配置首页路由,一般使用同步路由
{
path: '/',
name: 'home',
component: HomeView
},
//其余组件路由,使用异步路由
/* 用户管理 */
{
path: '/user',
name: 'user',
component: () => import('../views/UserList.vue')
},
/* 404 */
{
path:"/:pm(.*)*",
name:"notfound",
component: () => import('@views/NotFound.vue'),
}
]
<el-menu-item index="4">
<el-icon><User /></el-icon>
<RouterLink class="tab" to="/user">用户管理</RouterLink>
</el-menu-item>
//必须要有routerView,这是作为路由的出口,没有无法显示路由
//在APP.vue当中部署的routerView,一级组件(一级路由)都会显示在routerView,部署二级路由,routerView应当部署在自己一级路由下
//这里使用 keepAlive + 动态路由,因为路由出口里面就是装载当前显示路由(组件)
//配合着tabs,使用时,也可以这样;无需像使用button切换路由那样,引入且手动切换组件名
<router-view v-slot="{ Component }">
<transition name="slide-fade" mode="out-in">
<keep-alive>
<component :is="Component" />
</keep-alive>
</transition>
</router-view>
组件封装
- MyPageHeader:@在中后台管理项目当中,页头都大差不差,为了以后我们开发方便,所有封装了这样一个组件;
- 实现思路:先把基础样式写好=>进行抽取=>根据业务需求,看哪些数据是需要动态传入=>采用prop+emit+v-model+expose(组件暴露出API供父组件自行使用)=>还可有预留插槽给父组件,自行配置内容;最终这个组件复用程度非常高
- 模板配置(支持父组件prop传入数据,子组件发送自定义事件,还可以对子组件添加相应的style,以及子组件自己的插槽的默认内容和默认方法)
<template>
<div class="page-header">
<div class="left" :style="leftStyle">
<el-icon class="left-icon" size="23" @click="$router.back">
<ArrowLeftBold />
</el-icon>
</div>
<div class="middle" :style="titleStyle">{{ title }}</div>
<div class="right">
<p class="page-header-right-img-box" :style="imgStyle"><img :src="imgUrl" alt="" class="page-header-right-img">
</p>
<span class="page-header-right-welcome">欢迎回来:</span>
<span class="page-header-right-host" v-OvertToCovert:mouseover="'host-mouseover'">{{host}}</span>
</div>
<div class="quit" :style="quitStyle">
<slot name="right-icon">
<el-icon size="25" class="quit-icon" @click="emit('onRightClick')">
<SwitchButton />
</el-icon>
</slot>
</div>
</div>
</template>
- 完整代码
<template>
<div class="page-header">
<div class="left" :style="leftStyle">
<el-icon class="left-icon" size="23" @click="$router.back">
<ArrowLeftBold />
</el-icon>
</div>
<div class="middle" :style="titleStyle">{{ title }}</div>
<div class="right">
<p class="page-header-right-img-box" :style="imgStyle"><img :src="imgUrl" alt="" class="page-header-right-img">
</p>
<span class="page-header-right-welcome">欢迎回来:</span>
<span class="page-header-right-host" v-OvertToCovert:mouseover="'host-mouseover'">{{host}}</span>
</div>
<div class="quit" :style="quitStyle">
<slot name="right-icon">
<el-icon size="25" class="quit-icon" @click="emit('onRightClick')">
<SwitchButton />
</el-icon>
</slot>
</div>
</div>
</template>
<script setup>
import {
ArrowLeftBold,
SwitchButton
} from '@element-plus/icons-vue'
import { defineEmits, defineProps } from "vue"
const {title,host,imgUrl,leftStyle,titleStyle,quitStyle,imgStyle } = defineProps({
title: {
type: String,
default: "欢迎光临卖座电影管理后台"
},
host: {
type: String,
default: "铁锤"
},
imgUrl:{
type:String,
default:"../../public/imgs/own.jpg"
},
leftStyle:{
type:Object,
default:{}
},
titleStyle:{
type:Object,
default:{}
},
imgStyle:{
type:Object,
default:{}
},
quitStyle:{
type:Object,
default:{}
}
})
const emit = defineEmits({
onRightClick:null,
onLeftClick:null
})
</script>
<style lang="scss" scoped>
@import "@assets/variable.scss";
@import "@assets/mixin.scss";
.page-header {
padding: 0 20px;
line-height: 50px;
background-color: blueviolet;
display: flex;
height: 50px;
.left {
margin-top: 10px;
position: relative;
width: 28px;
height: 28px;
border-radius: 50%;
background-color: $grayc;
.left-icon {
color: white;
top: 4px;
left: 3px;
position: absolute;
text-align: center;
line-height: 35px;
}
}
.middle {
width: 250px;
// font-size: 18px;
color: white;
margin-left: 30px;
}
.right {
position: relative;
display: flex;
justify-content: right;
flex: 1;
.page-header-right-img-box {
position: relative;
top: 11px;
right: 140px;
width: 30px;
height: 30px;
background-color: $grayc;
border-radius: 50%;
.page-header-right-img {
position: absolute;
// top: -1px;
width: 30px;
border-radius: 50%;
}
}
.page-header-right-welcome {
position: absolute;
right: 68px;
font-size: 14px;
color: white;
}
.page-header-right-host {
position: absolute;
left: 560px;
font-size: 14px;
color: rgb(240, 240, 98);
}
.host-mouseover{
color: aqua;
cursor: pointer;
}
}
.quit {
.quit-icon{
margin-top: 12px;
color: white;
}
}
}
</style>
- 实现效果图
知识点拾遗
- 回忆: input的v-model
<input
:value="searchText"
@input="searchText = $event.target.value"
/>
//语法糖
<input v-model="searchText" />
- 组件通信:prop-down , event-up , provide + inject , expose , v-model , store(中央数据仓库)
- 组件之间的v-model:本质上就是,父组件给子组件(prop)传递一个数据,子组件接收并改变这个数据时,及时的传递给父组件(emit),且携带着变化的数据
- 默认情况下,prop名字为 modelValue , emit名字为 update:modelValue
- 单个v-model绑定:prop => modelValue | emit => update:modelValue
- 多个v-model绑定:prop => v-model:参数名 | emit => update:参数名
//父组件
<father>
<CustomInput
:modelValue="searchText"
@update:modelValue="newValue => searchText = newValue"/>
//语法糖
<CustomInput v-model="searchText" />
</father>
//子组件
//子组件在内部需要声明 prop + emit(且emit发送自定义事件时,携带最新的数据)
<!-- CustomInput.vue -->
<script setup>
defineProps(['modelValue'])
defineEmits(['update:modelValue'])
</script>
<template>
<input
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
/>
</template>
//父组件
<UserName
v-model:first-name="first"
v-model:last-name="last"
/>
//子组件
<script setup>
defineProps({
firstName: String,
lastName: String
})
defineEmits(['update:firstName', 'update:lastName'])
</script>
<template>
<input
type="text"
:value="firstName"
@input="$emit('update:firstName', $event.target.value)"
/>
<input
type="text"
:value="lastName"
@input="$emit('update:lastName', $event.target.value)"
/>
</template>