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

项目前端

  • 新建一个 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">
    <!-- &nbsp; 代表空格 -->
    <input type="text" v-model="music_id" /> &nbsp; <button @click="getOne">查询</button> &nbsp;
    <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 接收响应】

项目完结

posted @ 2023-03-21 06:33  筱团  阅读(601)  评论(0编辑  收藏  举报