VueJS 3 + Strapi CRUD示例代码
这两天刚上手VueJS3,这篇内容是我用Vue3做前端,Strapi做服务端写的前后端分离的基础CRUD的功能实现。
实现了部门和人员管理的功能,主要特性:
- 支持数据的增删改查
- 头像图片的上传
- BootStrap5框架+Modal组件的应用
- 使用Axios做http请求
1. Strapi服务端
数据服务端 Backend
首先安装strapi,建两个Content-Type,分别是Department和Employee,字段设置如下:
Department
Employee
注意Employee中的department字段
需要和Department建立关联
至此,服务端就到这里,不做过多阐述。
2. VueJS3前端
前端 Frontend
前端主要由Vuejs3完成,文件结构如下:
下面重点是核心文件
2.1 variables.js
variables.js
- 存放服务端api接口的地址
const variables = { API_URL: 'http://localhost:4000/', PHOTO_URL: 'http://localhost:4000', }
这里有待优化,因为开始我把所有的API_URL
调用后面直接加上对应的content-type
,没带/
,比如axios
请求departments
只需要 API_URL + “departments”
,然而Strapi图片地址的数据前面都带了/
,如果继续使用API_URL
会多一个斜杠,实际地址变成http://server//
upload/xxx.jpg出现404错误,懒得改了,直接用了新的PHOTO_URL来解决这个问题。
2.2 app.js
app.js
- Vue3路由配置文件
const routes = [ {path: '/home', component: home}, {path: '/employee', component: employee}, {path: '/department', component: department} ] const router = new VueRouter.createRouter({ history: VueRouter.createWebHashHistory(), routes, }) const app = Vue.createApp({}) // Make sure to _use_ the router instance to make the // whole app router-aware. app.use(router) app.mount('#app')
2.3 index.html
index.html
- APP启动入口文件
<!DOCTYPE html> <head> <meta charset="utf-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" /> <meta name="description" content="" /> <meta name="author" content="" /> <title>Page title</title> <link href="./public/css/styles.css" rel="stylesheet" /> <link rel="icon" type="image/x-icon" href="./public/assets/img/favicon.png" /> <script data-search-pseudo-elements defer src="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.1/js/all.min.js" crossorigin="anonymous"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/feather-icons/4.24.1/feather.min.js" crossorigin="anonymous"></script> </head> <body> <div id="app" class="container"> <h3 class="d-flex justify-content-center"> Vue JS front End </h3> <h5 class="d-flex justify-content-center"> Employee Management Portal </h5> <nav class="navbar navbar-expand-sm bg-light navbar-dark"> <ul class="navbar-nav"> <li class="nav-item m-1"> <router-link class="btn btn-light btn-outline-primary" to="/home">Home</router-link> </li> <li class="nav-item m-1"> <router-link class="btn btn-light btn-outline-primary" to="/department">Department</router-link> </li> <li class="nav-item m-1"> <router-link class="btn btn-light btn-outline-primary" to="/employee">Employee</router-link> </li> </ul> </nav> <router-view></router-view> </div> <script src="variables.js"></script> <script src="https://unpkg.com/axios/dist/axios.min.js"></script> <script src="https://unpkg.com/vue@next"></script> <script src="https://unpkg.com/vue-router@4"></script> <script src="components/home.js"></script> <script src="components/department.js"></script> <script src="components/employee.js"></script> <script src="app.js"></script> <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js" crossorigin="anonymous"></script> <script src="./public/js/scripts.js"></script> </body> </html>
2.4 components/departments.js
组件部分
departments.js
- 部门管理的实现
const department={ template: /*html*/ ` <div> <button type="button" class="btn btn-primary m-2 float-end" data-bs-toggle="modal" data-bs-target="#exampleModal" @click="addClick()">Add Department</button> <table class="table table-striped"> <thead> <tr> <th> DepartmentId </th> <th> DepartmentName </th> <th> Options </th> </tr> </thead> <tbody> <tr v-for="dep in departments"> <td>{{ dep.id }}</td> <td>{{ dep.name }}</td> <td> <button type="button" class="btn bt-light mr-1" data-bs-toggle="modal" data-bs-target="#exampleModal" @click="editClick(dep)"> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-pencil-square" viewBox="0 0 16 16"> <path d="M15.502 1.94a.5.5 0 0 1 0 .706L14.459 3.69l-2-2L13.502.646a.5.5 0 0 1 .707 0l1.293 1.293zm-1.75 2.456-2-2L4.939 9.21a.5.5 0 0 0-.121.196l-.805 2.414a.25.25 0 0 0 .316.316l2.414-.805a.5.5 0 0 0 .196-.12l6.813-6.814z"/> <path fill-rule="evenodd" d="M1 13.5A1.5 1.5 0 0 0 2.5 15h11a1.5 1.5 0 0 0 1.5-1.5v-6a.5.5 0 0 0-1 0v6a.5.5 0 0 1-.5.5h-11a.5.5 0 0 1-.5-.5v-11a.5.5 0 0 1 .5-.5H9a.5.5 0 0 0 0-1H2.5A1.5 1.5 0 0 0 1 2.5v11z"/> </svg> </button> <button type="button" class="btn bt-light mr-1" @click="deleteClick(dep.id)"> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-trash" viewBox="0 0 16 16"> <path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z"/> <path fill-rule="evenodd" d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1zM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z"/> </svg> </button> </td> </tr> </tbody> </table> <div class="modal fade" id="exampleModal" tabindex="-1" aria-labelledby="exampleModalLabel" aria-hidden="true"> <div class="modal-dialog modal-lg modal-dialog-centered"> <div class="modal-content"> <div class="modal-header"> <h5 class="modal-title" id="exampleModalLabel">{{ modalTitle }}</h5> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> </div> <div class="modal-body"> <div class="input-group mb-3"> <span class="input-group-text">Department Name</span> <input type="text" class="form-control" v-model="name"> </div> <button type="button" @click="createClick()" v-if="id==0" class="btn btn-primary"> Create </button> <button type="button" @click="updateClick()" v-if="id!=0" class="btn btn-primary"> Update </button> </div> </div> </div> </div> </div> `, data(){ return { departments: [], modalTitle: "", name: "", id: 0 } }, methods: { refreshData(){ axios.get(variables.API_URL + 'departments') .then((response) => { this.departments = response.data; }); }, addClick(){ this.modalTitle = "Add Department"; this.id = 0; this.name = ""; }, editClick(dep){ this.modalTitle = "Edit Department"; this.id = dep.id; this.name = dep.name; }, createClick(){ axios.post(variables.API_URL + 'departments',{ name: this.name }) .then((response) => { this.refreshData(); alert(response.statusText); //console.log(response); }); }, updateClick(){ axios.put(variables.API_URL + 'departments/' + this.id,{ name: this.name }) .then((response) => { this.refreshData(); alert(response.statusText); //console.log(response); }); }, deleteClick(id){ if(!confirm("Are you sure?")){ return; } axios.delete(variables.API_URL + 'departments/' + id,{ name: this.name }) .then((response) => { this.refreshData(); alert(response.statusText); //console.log(response); }); }, }, mounted() { this.refreshData(); }, }
2.5 components/employee.js
employee.js
- 人员管理的实现
const employee={ template: /*html*/ ` <div> <button type="button" class="btn btn-primary m-2 float-end" data-bs-toggle="modal" data-bs-target="#exampleModal" @click="addClick()">Add Employee</button> <table class="table table-striped"> <thead> <tr> <th> Employee Id </th> <th> Employee Name </th> <th> Department </th> <th> DOJ </th> <th> Options </th> </tr> </thead> <tbody> <tr v-for="emp in employees"> <td>{{ emp.id }}</td> <td>{{ emp.name }}</td> <td>{{ emp.department.name }}</td> <td>{{ emp.DOJ }}</td> <td> <button type="button" class="btn bt-light mr-1" data-bs-toggle="modal" data-bs-target="#exampleModal" @click="editClick(emp)"> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-pencil-square" viewBox="0 0 16 16"> <path d="M15.502 1.94a.5.5 0 0 1 0 .706L14.459 3.69l-2-2L13.502.646a.5.5 0 0 1 .707 0l1.293 1.293zm-1.75 2.456-2-2L4.939 9.21a.5.5 0 0 0-.121.196l-.805 2.414a.25.25 0 0 0 .316.316l2.414-.805a.5.5 0 0 0 .196-.12l6.813-6.814z"/> <path fill-rule="evenodd" d="M1 13.5A1.5 1.5 0 0 0 2.5 15h11a1.5 1.5 0 0 0 1.5-1.5v-6a.5.5 0 0 0-1 0v6a.5.5 0 0 1-.5.5h-11a.5.5 0 0 1-.5-.5v-11a.5.5 0 0 1 .5-.5H9a.5.5 0 0 0 0-1H2.5A1.5 1.5 0 0 0 1 2.5v11z"/> </svg> </button> <button type="button" class="btn bt-light mr-1" @click="deleteClick(emp.id)"> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-trash" viewBox="0 0 16 16"> <path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z"/> <path fill-rule="evenodd" d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1zM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z"/> </svg> </button> </td> </tr> </tbody> </table> <div class="modal fade" id="exampleModal" tabindex="-1" aria-labelledby="exampleModalLabel" aria-hidden="true"> <div class="modal-dialog modal-lg modal-dialog-centered"> <div class="modal-content"> <div class="modal-header"> <h5 class="modal-title" id="exampleModalLabel">{{ modalTitle }}</h5> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" id="btnCloseModal"></button> </div> <div class="modal-body"> <div class="d-flex flex-row bd-highlight mb-3"> <div class="p-2 w-50 bd-highlight"> <div class="input-group mb-3"> <span class="input-group-text">Name</span> <input type="text" class="form-control" v-model="name"> </div> <div class="input-group mb-3"> <span class="input-group-text">Department</span> <select class="form-select" v-model="department"> <option v-for="dep in departments" :value="dep.id" :key="dep.id" :selected="dep.id == this.id"> {{ dep.name }} </option> </select> </div> <div class="input-group mb-3"> <span class="input-group-text">DOJ</span> <input type="date" class="form-control" v-model="DateOfJoining"> </div> </div> <div class="p-2 w-50 bd-highlight"> <img width="250" height="250" :src="profile" /> <input class="m-2" type="file" @change="imageUpload" id="ProfileUpload"> </div> </div> <button type="button" @click="createClick()" v-if="id==0" class="btn btn-primary"> Create </button> <button type="button" @click="updateClick()" v-if="id!=0" class="btn btn-primary"> Update </button> </div> </div> </div> </div> </div> `, data(){ return { departments: [], employees: [], modalTitle: "", name: "", id: 0, department: "", DateOfJoining: "", profile: variables.PHOTO_URL + "/uploads/profile_1bfe3b2ecc.jpg", PHOTO_SERVER: variables.PHOTO_URL, imgId: 0, } }, methods: { refreshData(){ axios.get(variables.API_URL + 'employees') .then((response) => { this.employees = response.data; //console.log(response.data); }); axios.get(variables.API_URL + 'departments') .then((response) => { this.departments = response.data; }); }, addClick(){ this.modalTitle = "Add Employee"; this.id = 0; this.name = ""; this.department = ""; this.DateOfJoining = ""; this.profile = variables.PHOTO_URL + "/uploads/profile_1bfe3b2ecc.jpg"; }, editClick(emp){ this.modalTitle = "Edit Employee"; this.id = emp.id; this.name = emp.name; this.department = emp.department.id; this.DateOfJoining = emp.DOJ; this.profile = (emp.profile) ? this.PHOTO_SERVER + emp.profile.url : this.PHOTO_SERVER + "/uploads/profile_1bfe3b2ecc.jpg"; }, createClick(){ axios.post(variables.API_URL + 'employees',{ name: this.name, department: this.department, DOJ: this.DateOfJoining, }) .then((response) => { this.refreshData(); alert(response.statusText); //console.log(this.imgId); axios.put(variables.API_URL + "employees/" + response.data.id, {profile: this.imgId}).then((res)=>{ //handle success console.log(res.data); }).catch((err)=>{ //handle error }) //console.log(response.data); //CLOSE MODAL let btnClose = document.querySelector('#btnCloseModal'); btnClose.click(); }); }, updateClick(){ axios.put(variables.API_URL + 'employees/' + this.id,{ name: this.name, department: this.department, DOJ: this.DateOfJoining, //profile: this.PhotoFileName }) .then((response) => { this.refreshData(); alert(response.statusText); //console.log(response); }); //CLOSE MODAL let btnClose = document.querySelector('#btnCloseModal'); btnClose.click(); }, deleteClick(id){ if(!confirm("Are you sure?")){ return; } axios.delete(variables.API_URL + 'employees/' + id,{ name: this.name }) .then((response) => { this.refreshData(); alert(response.statusText); //console.log(response); }); }, imageUpload(){ let formData = new FormData(); let imageFile = document.querySelector('#ProfileUpload'); formData.append('files', imageFile.files[0]); axios.post( variables.API_URL + "upload/", formData) .then((response)=>{ this.profile = this.PHOTO_SERVER + response.data[0].url; console.log(response); //Link image to a content type const imageId = response.data[0].id; this.imgId = imageId; if(!this.id) return; axios.put(variables.API_URL + "employees/" + this.id, {profile: imageId}).then((res)=>{ //handle success //console.log(res.data); }).catch((err)=>{ //handle error }) }).catch((error)=>{ //handle error console.log(error); }) } }, mounted() { this.refreshData(); }, }
2.6 components/home.js
home.js
- 首页文件,未加工
const home={ template: /*html*/ ` <h1>This is Home</h1> ` }
3 图片上传的思路
关于Strapi图片上传的处理方法及注意事项
1.图片统一post请求发送到http://server/upload/
地址中,而不是对应content-type
的media
字段中
2.上传完成后需要拿到image的ID
3.在创建或更新的content-type的数据中建立media字段的对应连接,使用的是put
方法,提交的数据格式为{mediaFieldName: [id]}
,没错只需要提交ID