Django + Vue(试水篇)
Prerequisite
我参考的资料:
可能有用的资料:
一、跟着视频教程走
视频教程:Django(Ninja)+Vue前后端分离实现增删改查
博客地址:Django+Vue前后端分离项目
项目地址:音乐列表增删改查的前后端实现
技术栈:
后端:Python3、Django(Ninja)
前端:Vue3、Ts、Vite
数据库:SQLite3
创建环境命令:
- 创建前端:
mkdir front
- 创建后端:
mkdir back
- 创建后端环境:
django-admin startproject imitate
【back】 - 创建后端应用程序:
python manage.py startapp music
【back/imitate】 - 创建后端迁移文件:
python manage.py makemigrations
【back/imitate】 - 更新后端数据库:
python manage.py migrate
【back/imitate】 - 启动后端:
python manage.py runserver
【back/imitate】 - 后端接口测试:http://127.0.0.1:8000/api/docs
- 创建前端环境:
npm create vite@latest
【front】 - 下载前端所需库:
npm install
【front/music】 - 运行前端项目:
npm run dev
【front/music】 - 打包前端项目:
npm run build
【front/music】
项目后端
- 在 imitate/setting.py 中添加 music
- 在 music/model.py 中添加数据表类型
class Music(models.Model):
title = models.CharField(max_length=250)
artist = models.CharField(max_length=250)
duration = models.FloatField()
last_play = models.DateTimeField()
- 在 data 中创建 json 数据文件和脚本文件(tracks.json 和 load.py),运行
python load.py
import sqlite3
import json
con = sqlite3.connect("..\\db.sqlite3")
cur = con.cursor()
f = open(".\\tracks.json", "r")
data = json.loads(f.read())
f.close()
cur.executemany("INSERT INTO music_music VALUES(:id, :title, :artist, :duration, :last_play)", data)
con.commit()
cur.close()
con.close()
- 在 music/scheme.py 中添加数据库模型
from datetime import datetime
from ninja import Schema
class MusicSchema(Schema):
title: str
artist: str
duration: float
last_play: datetime
class NotFoundSchema(Schema):
message: str
- 使用 Ninja 进行增删改查,在 music/api.py 中添加如下内容:
from typing import List
from ninja import NinjaAPI
from music.models import Music
from music.scheme import MusicSchema, NotFoundSchema
api = NinjaAPI()
# 查
# 查询所有
@api.get("/musics", response=List[MusicSchema])
def musics(request):
return Music.objects.all()
# 查询单个
@api.get("/musics/{music_id}",
response={
200: List[MusicSchema],
404: NotFoundSchema
})
def musics(request, music_id: int):
try:
music = Music.objects.get(pk=music_id)
return [music]
except Music.DoesNotExist as e:
return 404, {"message": "Music does not exist"}
# 增
@api.post("/musics", response={201: MusicSchema})
def create_music(request, music: MusicSchema):
Music.objects.create(**music.dict())
return music
# 改
@api.put("musics/update/{music_id}",
response={
200: MusicSchema,
404: NotFoundSchema
})
def change_music(request, music_id: int, data: MusicSchema):
try:
music = Music.objects.get(pk=music_id)
for attribute, value in data.dict().items():
setattr(music, attribute, value)
music.save()
return 200, music
except Music.DoesNotExist as e:
return 404, {"message": "Music does not exist"}
# 删
@api.delete("/musics/{music_id}", response={200: None, 404: NotFoundSchema})
def delete_music(request, music_id: int):
try:
music = Music.objects.get(pk=music_id)
music.delete()
return 200
except Music.DoesNotExist as e:
return 404, {"message": "Could not find Music"}
- 在 imitate/urls.py 中添加路由
from django.contrib import admin
from django.urls import path
from music.api import api
urlpatterns = [
path('admin/', admin.site.urls),
path('api/', api.urls)
]
- 解决跨域问题
# 在 setting.py 中额外添加如下内容即可
INSTALLED_APPS = [
"corsheaders",
]
MIDDLEWARE = [
"corsheaders.middleware.CorsMiddleware",
"corsheaders.middleware.CorsMiddleware",
]
# 允许全部来源
CORS_ORIGIN_ALLOW_ALL = True # 如果为 True,将不使用白名单,并且将接受所有来源,默认为 False
- 在 http://127.0.0.1:8000/api/docs 中进行接口测试
项目前端
- 新建一个 views 文件夹,用于存储主视图(即 App.vue 直接引用主路由),内含主页界面和音乐列表界面(Home.vue 和 Music.vue)
<!-- src/views/Home.vue -->
<template>
<div>
<h1>hello</h1>
<router-link to="/music">
<input type="button" value="跳转到音乐列表" />
</router-link>
</div>
</template>
<script setup lang="ts"></script>
<style scoped></style>
<!-- src/views/Music.vue -->
<template>
<div>
<h1>hello!!!</h1>
</div>
</template>
<script setup lang="ts"></script>
<style scoped></style>
<!-- src/App.vue -->
<script setup lang="ts"></script>
<template>
<RouterView />
</template>
<style scoped></style>
PS:<script setup lang="ts"></script>
划重点,凡是用了 lang="ts"
必须要有 setup
(这是我总结出来的谬论),否则会报错
- 安装路由命令:
npm install vue-router@4
- 新建一个 router 文件夹,用于存储路由文件
// src/router/index.ts
import { createWebHistory, createRouter } from "vue-router";
import Home from "../views/Home.vue";
import Music from "../views/Music.vue";
const routes = [
{ path: "/", redirect: "/home" }, // 首页重定向至 home
{ path: "/home", component: Home },
{ path: "/music", component: Music },
];
const router = createRouter({
history: createWebHistory(),
routes,
});
export default router;
// src/main.ts
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
import router from './router'
createApp(App)
.use(router)
.mount('#app')
- 安装 axios 命令:
npm install --save axios vue-axios
- 新建一个 api 文件夹,用于接收后端数据,内含具体接口处理文件和逆天文件(music.ts 和 request.ts)
// src/api/request.ts
import axios from "axios";
axios.defaults.baseURL = "http://localhost:8000/api" // 后端端口
const instance = axios.create();
instance.defaults.timeout = 5000;
// 此处可以放置请求拦截器和响应式拦截器,常用于大型项目
// 这个小项目就不考虑
export default instance;
// src/api/music.ts
import request from "./request";
// 查询全部
export function reqMusicList() {
return request({ url: `/musics`, method: `get` });
}
// 查询单个
export function reqMusicListOne(id: number) {
return request({ url: `/musics/${id}`, method: `get` });
}
// 删除
export function reqDeleteMusic(id: number) {
return request({ url: `/musics/${id}`, method: `delete` });
}
// 增加或者修改
export function reqAddOrUpdateMusic(music: any, index: number) {
if (index != 0) {
return request({ url: `/musics/update/${index}`, method: "put", data: music });
} else {
//新增品牌
return request({ url: "/musics", method: "post", data: music });
}
}
// src/main.ts
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
import router from './router'
import axios from "axios"
import VueAxios from "vue-axios"
createApp(App)
.use(router)
.use(VueAxios, axios)
.mount('#app')
- 安装 Element Plus UI 组件命令:
npm install element-plus --save
- 安装 Element Plus UI 插件命令:
npm install -D unplugin-vue-components unplugin-auto-import
- 并进行如下配置
// vite.config.js
import { defineConfig } from "vite"
import vue from "@vitejs/plugin-vue"
import AutoImport from "unplugin-auto-import/vite"
import Components from "unplugin-vue-components/vite"
import { ElementPlusResolver } from "unplugin-vue-components/resolvers"
export default defineConfig({
plugins: [
vue(),
AutoImport({
resolvers: [ElementPlusResolver()],
}),
Components({
resolvers: [ElementPlusResolver()],
}),
],
})
- 重写 Music.vue 文件
<template>
<h1 align="center">音乐列表</h1>
<!-- 换行符 -->
<br />
<div align="center">
<!-- 代表空格 -->
<input type="text" v-model="music_id" /> <button @click="getOne">查询</button>
<button @click="showDialog">增加</button>
</div>
<!-- 表格 -->
<el-table :data="music_list" style="width: 100%">
<el-table-column prop="title" label="标题" />
<el-table-column prop="artist" label="作者" />
<el-table-column prop="duration" label="时长" />
<el-table-column prop="last_play" label="最后播放时间" />
<el-table-column align="right">
<template #default="scope">
<el-button size="small" @click="handleEdit(scope.$index, scope.row)">Edit</el-button>
<el-button size="small" type="danger" @click="handleDelete(scope.$index, scope.row)">Delete</el-button>
</template>
</el-table-column>
</el-table>
<!-- 表单 -->
<el-dialog v-model="dialogFormVisible" title="添加/修改品牌">
<el-form :model="form">
<el-form-item label="标题" :label-width="formLabelWidth">
<el-input v-model="form.title" autocomplete="off" />
</el-form-item>
<el-form-item label="作者" :label-width="formLabelWidth">
<el-input v-model="form.artist" autocomplete="off" />
</el-form-item>
<el-form-item label="时长" :label-width="formLabelWidth">
<el-input v-model="form.duration" autocomplete="off" />
</el-form-item>
<el-form-item label="最后播放时间" :label-width="formLabelWidth">
<el-input v-model="form.last_play" autocomplete="off" />
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogFormVisible = false">Cancel</el-button>
<el-button type="primary" @click="addOrUpdateMusic"> Confirm</el-button>
</span>
</template>
</el-dialog>
</template>
<script lang="ts">
import { reqMusicList, reqMusicListOne, reqDeleteMusic, reqAddOrUpdateMusic } from "../api/music";
import { reactive, defineComponent, onMounted, toRefs, ref } from "vue";
import { ElMessage } from "element-plus";
export default defineComponent({
setup() {
// 状态数据
const state = reactive({
music_list: [],
index: 0,
music_id: "",
form: {
title: "",
artist: "",
duration: "",
last_play: "",
},
});
// 函数相关的一些变量
const dialogFormVisible = ref(false);
const formLabelWidth = "140px";
// 增加或者修改
const addOrUpdateMusic = () => {
dialogFormVisible.value = false;
reqAddOrUpdateMusic(state.form, state.index).then((res) => {
if (res.status == 201) {
ElMessage({ type: "success", message: "添加成功" });
getList();
} else if (res.status == 200) {
ElMessage({ type: "success", message: "修改成功" });
getList();
// 初始化为0
state.index = 0;
} else {
ElMessage({ type: "error", message: "操作失败" });
}
});
};
// 删除
const handleDelete = (index: number, row: any) => {
reqDeleteMusic(index + 1).then((res) => {
if (res.status == 200) {
ElMessage({ type: "success", message: "删除成功" });
// 重新加载列表
getList();
}
});
};
// 查询所有
const getList = () => {
reqMusicList().then((res) => {
console.log(res);
state.music_list = res.data;
});
};
// 查询单个
const getOne = () => {
if (state.music_id != "0") {
reqMusicListOne(parseInt(state.music_id)).then((res) => {
state.music_list = res.data;
});
} else {
ElMessage({ type: "error", message: "id异常" });
}
};
//自定义的一些其它函数
const handleEdit = (index: number, row: any) => {
state.index = index + 1;
dialogFormVisible.value = true;
state.form = {
title: "",
artist: "",
duration: "",
last_play: "",
};
};
const showDialog = () => {
dialogFormVisible.value = true;
state.index = 0;
state.form = {
title: "",
artist: "",
duration: "",
last_play: "",
};
};
// 初始挂载,自带
onMounted(() => {
getList();
});
// 返回数据
return {
...toRefs(state),
formLabelWidth,
dialogFormVisible,
handleEdit,
handleDelete,
showDialog,
addOrUpdateMusic,
getOne,
};
},
});
</script>
<style scoped></style>
PS:前端只负责规范好 API 接口,传递数据给后端【music.ts 的 request() 发送请求】与接收后端给的响应【Music.vue 的 then() 中的 res 接收响应】
项目完结
喜欢划水摸鱼的废人