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