Skip to content

开发示例

本文展示一个完整的开发示例:实现图书的增删改查功能

image-20251030145934465

新建应用

开发的第一步,是新建自己的Django应用。应用命名规范

  • 固定前缀:应用名称以 myapp_ 开头。
  • 功能后缀:后缀需体现应用核心用途。
    • 示例 1:演示用应用命名为 myapp_demo
    • 示例 2:学校管理类应用命名为 myapp_school

操作示例(以myapp_demo为例)

shell
python manage.py startapp myapp_demo

修改项目配置文件:mysite\mysite\settings.py,注册应用

python
MY_APPS = ["myapp_demo"]  # 请将新的自定义应用添加到这里

创建Django应用路由文件:mysite\myapp_demo\urls.py

python
from django.urls import path
from rest_framework.routers import SimpleRouter

# 创建(不带后缀/)路由器实例
router = SimpleRouter(trailing_slash=False)

urlpatterns = []
urlpatterns += router.urls

修改项目urls.py文件:mysite\mysite\urls.py

python
urlpatterns = [
    # ...
    # myapp_demo 应用
    path("admin-api/demo/", include("myapp_demo.urls")),
]

编写后端代码

TIP

建议与《AI辅助编码(后端)》一起阅读

创建模型

模型命名规范

  • 模型类名称:应用标识+资源对象
  • 数据库表名称:对应为应用标识小写_资源对象小写(蛇形命名法)

示例

  • myapp_demo的图书管理:模型类DemoBook,对应表demo_book
  • myapp_school的学生管理:模型类SchoolStudent,对应表school_student

操作示例(以DemoBook为例)

  • 打开后端项目,新建文件mysite\myapp_demo\book\models.py,创建模型
  • 模型继承BaseModel
python
from django.db import models
from mars_framework.db.base import BaseModel
from enum import Enum


class BookCategoryEnum(Enum):
    """图书分类枚举"""

    HISTORY = 1  # 历史
    ECONOMY = 2  # 经济
    CHILDREN = 3  # 儿童


class DemoBook(BaseModel):
    id = models.BigAutoField(primary_key=True, db_comment="图书ID", help_text="图书ID")
    title = models.CharField(
        max_length=255,
        db_comment="书名",
        help_text="书名",
    )
    author = models.CharField(
        max_length=100,
        db_comment="作者",
        help_text="作者",
    )
    publisher = models.CharField(
        max_length=100,
        db_comment="出版社",
        help_text="出版社",
    )
    publish_date = models.DateTimeField(
        db_comment="出版日期",
        help_text="出版日期",
    )
    category = models.SmallIntegerField(
        choices=[(item.value, item.name) for item in BookCategoryEnum],
        db_comment="图书分类",
        help_text="图书分类",
    )
    total_copies = models.IntegerField(
        db_comment="总册数",
        help_text="总册数",
    )
    price = models.DecimalField(
        max_digits=10,
        decimal_places=2,
        db_comment="单价",
        help_text="单价(2位小数)",
    )

    class Meta:
        managed = True
        db_table = "demo_book"
        db_table_comment = "图书信息表"
        ordering = ["-id"]

修改文件mysite\myapp_demo\models.py,导入新增的模型

python
"""根据Django的要求,导入本APP定义的所有模型"""

from .book.models import DemoBook

生成数据库迁移文件,并迁移

sh
# myapp_demo应用生成数据库表
python manage.py makemigrations myapp_demo
python manage.py migrate myapp_demo

编写序列化器

新建文件mysite\myapp_demo\book\serializers.py,分别编写增删改查和导出时使用的序列化器

python
from rest_framework import serializers
from .models import DemoBook


class DemoBookSerializer(serializers.ModelSerializer):
    """图书序列化器"""

    class Meta:
        model = DemoBook
        fields = [
            "id",
            "title",
            "author",
            "publisher",
            "publish_date",
            "category",
            "total_copies",
            "price",
            "create_time",
        ]
        read_only_fields = ["id", "create_time"]


class DemoBookExportSerializer(serializers.ModelSerializer):
    """图书导出序列化器"""

    class Meta:
        model = DemoBook
        fields = ["id", "title", "author", "publish_date", "category", "total_copies"]

编写过滤器

新建文件mysite\myapp_demo\book\filters.py,编写过滤器

  • 过滤器用于生成查询条件
python
from django_filters import rest_framework as filters
from .models import DemoBook


class DemoBookFilter(filters.FilterSet):
    title = filters.CharFilter(
        field_name="title", lookup_expr="icontains", label="书名(模糊查询)"
    )
    author = filters.CharFilter(
        field_name="author", lookup_expr="icontains", label="作者(模糊查询)"
    )
    publish_date = filters.DateTimeFilter(
        field_name="publish_date", lookup_expr="exact", label="出版日期"
    )
    category = filters.NumberFilter(
        field_name="category", lookup_expr="exact", label="图书分类"
    )

    class Meta:
        model = DemoBook
        fields = ["title", "author", "publish_date", "category"]

编写视图

新建文件mysite\myapp_demo\book\views.py,编写视图

python
from rest_framework.permissions import AllowAny
from drf_spectacular.utils import extend_schema

from mars_framework.viewsets.base import CustomModelViewSetNoSimple
from mars_framework.permissions.base import HasPermission
from .models import DemoBook, BookCategoryEnum
from .serializers import (
    DemoBookSerializer,
    DemoBookExportSerializer,
)
from .filters import DemoBookFilter


@extend_schema(tags=["管理后台-demo-图书"])
class DemoBookViewSet(CustomModelViewSetNoSimple):
    queryset = DemoBook.objects.all()
    serializer_class = DemoBookSerializer
    filterset_class = DemoBookFilter
    action_serializers = {
        "export": DemoBookExportSerializer,
    }
    action_permissions = {
        "create": [HasPermission("demo:book:create")],
        "destroy": [HasPermission("demo:book:delete")],
        "update": [HasPermission("demo:book:update")],
        "retrieve": [HasPermission("demo:book:query")],
        "list": [HasPermission("demo:book:query")],
        "export": [HasPermission("demo:book:export")],
    }
    export_name = "图书列表"
    export_fields_labels = {
        "id": "图书ID",
        "title": "书名",
        "author": "作者",
        "publish_date": "出版日期",
        "category": "图书分类",
        "total_copies": "总册数",
    }
    export_data_map = {
        "category": {
            BookCategoryEnum.HISTORY.value: "历史",
            BookCategoryEnum.ECONOMY.value: "经济",
            BookCategoryEnum.CHILDREN.value: "儿童",
        },
    }

(可选)新建文件mysite\myapp_demo\book\services.py,可将复杂的业务逻辑放这里面。

注册路由

编写视图后,修改应用的路由文件:mysite\myapp_demo\urls.py,注册相应路由

python
from django.urls import path
from rest_framework.routers import SimpleRouter


# 创建(不带后缀/)路由器实例
router = SimpleRouter(trailing_slash=False)

# 管理后台-demo-图书
from .book.views import DemoBookViewSet
router.register(r"book", DemoBookViewSet, basename="book")

urlpatterns = []
urlpatterns += router.urls

后端测试

启动后端项目,若有报错,按提示修复

shell
python manage.py runserver

浏览器访问接口文档,查看是否已新增了相关接口

image-20251024172626236

编写前端代码

TIP

建议与《AI辅助编码(前端)》一起阅读

编写枚举

打开前端项目,修改文件:src\utils\constants.ts ,编写枚举,用于前端数据字典

ts
// ========== DEMO 模块 ==========
//  图书分类枚举
export const BookCategoryEnum = {
  HISTORY: 1, // 历史
  ECONOMY: 2, // 经济
  CHILDREN: 3 // 儿童
}

编写Axios API接口

打开前端项目,新建文件src\api\demo\book\index.ts ,编写Axios API接口,用于调用后端API接口。

ts
import request from '@/config/axios'

export interface BookVO {
  id?: number
  title: string
  author: string
  publisher: string
  publishDate: Date
  category: number // 1-历史, 2-经济, 3-儿童
  totalCopies: number
  price: number
  createTime?: Date
}

// 查询图书列表
export const getBookPage = async (params: PageParam) => {
  return await request.get({ url: '/demo/book', params })
}

// 查询图书详情
export const getBook = async (id: number) => {
  return await request.get({ url: `/demo/book/${id}` })
}

// 新增图书
export const createBook = async (data: BookVO) => {
  return await request.post({ url: '/demo/book', data })
}

// 修改图书
export const updateBook = async (data: BookVO) => {
  return await request.put({ url: `/demo/book/${data.id}`, data })
}

// 删除图书
export const deleteBook = async (id: number) => {
  return await request.delete({ url: `/demo/book/${id}` })
}

// 导出图书
export const exportBook = async (params) => {
  return await request.download({ url: '/demo/book/export', params })
}

编写表单界面

新建文件src\views\demo\book\BookForm.vue,编写表单界面,用于弹框新增和修改界面

vue
<template>
  <Dialog v-model="dialogVisible" :title="dialogTitle" width="800px">
    <el-form
      ref="formRef"
      v-loading="formLoading"
      :model="formData"
      :rules="formRules"
      label-width="100px"
    >
      <el-form-item label="图书标题" prop="title">
        <el-input v-model="formData.title" placeholder="请输入图书标题" />
      </el-form-item>
      <el-form-item label="作者" prop="author">
        <el-input v-model="formData.author" placeholder="请输入作者" />
      </el-form-item>
      <el-form-item label="出版社" prop="publisher">
        <el-input v-model="formData.publisher" placeholder="请输入出版社" />
      </el-form-item>
      <el-form-item label="出版日期" prop="publishDate">
        <el-date-picker
          v-model="formData.publishDate"
          type="date"
          placeholder="请选择出版日期"
        />
      </el-form-item>
      <el-form-item label="图书分类" prop="category">
        <el-select v-model="formData.category" placeholder="请选择图书分类">
          <el-option
            v-for="(value, key) in BookCategoryEnum"
            :key="value"
            :label="categoryLabels[key]"
            :value="value"
          />
        </el-select>
      </el-form-item>
      <el-form-item label="总藏书量" prop="totalCopies">
        <el-input
          v-model.number="formData.totalCopies"
          type="number"
          placeholder="请输入总藏书量"
          min="0"
        />
      </el-form-item>
      <el-form-item label="价格" prop="price">
        <el-input
          v-model.number="formData.price"
          type="number"
          placeholder="请输入价格"
          min="0"
          step="0.01"
        />
      </el-form-item>
    </el-form>
    <template #footer>
      <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
      <el-button @click="dialogVisible = false">取 消</el-button>
    </template>
  </Dialog>
</template>

<script lang="ts" setup>
import { BookCategoryEnum } from '@/utils/constants'
import * as BookApi from '@/api/demo/book'

defineOptions({ name: 'DemoBookForm' })

const { t } = useI18n()
const message = useMessage()

// 图书分类标签映射
const categoryLabels = {
  HISTORY: '历史',
  ECONOMY: '经济',
  CHILDREN: '儿童'
}

const dialogVisible = ref(false)
const dialogTitle = ref('')
const formLoading = ref(false)
const formType = ref<'create' | 'update'>('create')
const formData = ref<BookApi.BookVO>({
  id: undefined,
  title: '',
  author: '',
  publisher: '',
  publishDate: new Date(),
  category: BookCategoryEnum.HISTORY,
  totalCopies: 0,
  price: 0,
  createTime: undefined
})

const formRules = reactive({
  title: [{ required: true, message: '图书标题不能为空', trigger: 'blur' }],
  author: [{ required: true, message: '作者不能为空', trigger: 'blur' }],
  publisher: [{ required: true, message: '出版社不能为空', trigger: 'blur' }],
  publishDate: [{ required: true, message: '出版日期不能为空', trigger: 'change' }],
  category: [{ required: true, message: '图书分类不能为空', trigger: 'change' }],
  totalCopies: [
    { required: true, message: '总藏书量不能为空', trigger: 'blur' },
    { type: 'number' as const, min: 0, message: '总藏书量不能为负数', trigger: 'blur' }
  ],
  price: [
    { required: true, message: '价格不能为空', trigger: 'blur' }
  ]
})

const formRef = ref()

/** 打开弹窗 */
const open = async (type: 'create' | 'update', id?: number) => {
  dialogVisible.value = true
  dialogTitle.value = t('action.' + type)
  formType.value = type
  resetForm()

  if (type === 'update' && id) {
    formLoading.value = true
    try {
      const detail = await BookApi.getBook(id)
      formData.value = { ...detail }
      // 转换日期格式为Date对象
      if (detail.publishDate) {
        formData.value.publishDate = new Date(detail.publishDate)
      }
    } finally {
      formLoading.value = false
    }
  }
}

defineExpose({ open })

/** 提交表单 */
const emit = defineEmits(['success'])
const submitForm = async () => {
  if (!formRef.value) return

  try {
    await formRef.value.validate()
    formLoading.value = true

    const data = { ...formData.value }
    // 处理日期格式
    if (data.publishDate instanceof Date) {
      data.publishDate = new Date(data.publishDate)
    }

    if (formType.value === 'create') {
      await BookApi.createBook(data)
      message.success(t('common.createSuccess'))
    } else {
      await BookApi.updateBook(data)
      message.success(t('common.updateSuccess'))
    }

    dialogVisible.value = false
    emit('success')
  } finally {
    formLoading.value = false
  }
}

/** 重置表单 */
const resetForm = () => {
  formData.value = {
    id: undefined,
    title: '',
    author: '',
    publisher: '',
    publishDate: new Date(),
    category: BookCategoryEnum.HISTORY,
    totalCopies: 0,
    price: 0,
    createTime: undefined
  }
  formRef.value?.resetFields()
}
</script>

编写主界面

新建文件src\views\demo\book\index.vue,编写主界面,包含搜索栏和列表展示

vue
<template>
  <ContentWrap>
    <!-- 搜索栏 -->
    <el-form
      class="-mb-15px"
      :model="queryParams"
      ref="queryFormRef"
      :inline="true"
      label-width="68px"
    >
      <el-form-item label="书名" prop="title">
        <el-input
          v-model="queryParams.title"
          placeholder="请输入书名"
          clearable
          class="!w-240px"
          @keyup.enter="handleQuery"
        />
      </el-form-item>
      <el-form-item label="作者" prop="author">
        <el-input
          v-model="queryParams.author"
          placeholder="请输入作者"
          clearable
          class="!w-240px"
          @keyup.enter="handleQuery"
        />
      </el-form-item>
      <el-form-item label="出版日期" prop="publishDate">
        <el-date-picker
          v-model="queryParams.publishDate"
          type="date"
          placeholder="请选择出版日期"
          clearable
          class="!w-240px"
        />
      </el-form-item>
      <el-form-item label="图书分类" prop="category">
        <el-select
          v-model="queryParams.category"
          placeholder="请选择图书分类"
          clearable
          class="!w-240px"
        >
          <el-option
            v-for="(label, value) in bookCategoryMap"
            :key="value"
            :label="label"
            :value="value"
          />
        </el-select>
      </el-form-item>
      <el-form-item>
        <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
        <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
        <el-button
          type="primary"
          plain
          @click="openForm('create')"
          v-hasPermi="['demo:book:create']"
        >
          <Icon icon="ep:plus" class="mr-5px" /> 新增
        </el-button>
        <el-button
          type="success"
          plain
          @click="handleExport"
          :loading="exportLoading"
          v-hasPermi="['demo:book:export']"
        >
          <Icon icon="ep:download" class="mr-5px" /> 导出
        </el-button>
      </el-form-item>
    </el-form>
  </ContentWrap>

  <!-- 列表展示 -->
  <ContentWrap>
    <el-table v-loading="loading" :data="list">
      <el-table-column label="ID" align="center" prop="id" />
      <el-table-column label="书名" align="center" prop="title" />
      <el-table-column label="作者" align="center" prop="author" />
      <el-table-column label="出版社" align="center" prop="publisher" />
      <el-table-column
        label="出版日期"
        align="center"
        prop="publishDate"
        :formatter="dateFormatter"
      />
      <el-table-column label="图书分类" align="center" prop="category">
        <template #default="scope">
          <span>{{ bookCategoryMap[scope.row.category] || '-' }}</span>
        </template>
      </el-table-column>
      <el-table-column label="总册数" align="center" prop="totalCopies" />
      <el-table-column label="单价" align="center" prop="price">
        <template #default="scope">
          <span>¥{{ scope.row.price }}</span>
        </template>
      </el-table-column>
      <el-table-column label="操作" align="center">
        <template #default="scope">
          <el-button
            link
            type="primary"
            @click="openForm('update', scope.row.id)"
            v-hasPermi="['demo:book:update']"
          >
            编辑
          </el-button>
          <el-button
            link
            type="danger"
            @click="handleDelete(scope.row.id)"
            v-hasPermi="['demo:book:delete']"
          >
            删除
          </el-button>
        </template>
      </el-table-column>
    </el-table>
    <!-- 分页 -->
    <Pagination
      :total="total"
      v-model:page="queryParams.pageNo"
      v-model:limit="queryParams.pageSize"
      @pagination="getList"
    />
  </ContentWrap>

  <!-- 表单弹窗(添加/修改) -->
  <BookForm ref="formRef" @success="getList" />
</template>

<script lang="ts" setup>
import { dateFormatter } from '@/utils/formatTime'
import download from '@/utils/download'
import { BookCategoryEnum } from '@/utils/constants'
import * as BookApi from '@/api/demo/book/index'
import BookForm from './BookForm.vue'

defineOptions({ name: 'DemoBook' })

const message = useMessage() // 消息弹窗
const { t } = useI18n() // 国际化

// 图书分类映射
const bookCategoryMap = {
  [BookCategoryEnum.HISTORY]: '历史',
  [BookCategoryEnum.ECONOMY]: '经济',
  [BookCategoryEnum.CHILDREN]: '儿童'
}

const loading = ref(true) // 列表的加载中
const total = ref(0) // 列表的总页数
const list = ref<BookApi.BookVO[]>([]) // 列表的数据
const queryParams = reactive({
  pageNo: 1,
  pageSize: 10,
  title: '',
  author: '',
  publishDate: undefined,
  category: undefined
})
const queryFormRef = ref() // 搜索的表单
const exportLoading = ref(false) // 导出的加载中

/** 查询图书列表 */
const getList = async () => {
  loading.value = true
  try {
    const data = await BookApi.getBookPage(queryParams)
    list.value = data.list
    total.value = data.total
  } finally {
    loading.value = false
  }
}

/** 搜索按钮操作 */
const handleQuery = () => {
  queryParams.pageNo = 1
  getList()
}

/** 重置按钮操作 */
const resetQuery = () => {
  queryFormRef.value.resetFields()
  handleQuery()
}

/** 添加/修改操作 */
const formRef = ref()
const openForm = (type: string, id?: number) => {
  formRef.value.open(type, id)
}

/** 删除按钮操作 */
const handleDelete = async (id: number) => {
  try {
    // 删除的二次确认
    await message.delConfirm()
    // 发起删除
    await BookApi.deleteBook(id)
    message.success(t('common.delSuccess'))
    // 刷新列表
    await getList()
  } catch {}
}

/** 导出按钮操作 */
const handleExport = async () => {
  try {
    // 导出的二次确认
    await message.exportConfirm()
    // 发起导出
    exportLoading.value = true
    const data = await BookApi.exportBook(queryParams)
    download.excel(data, '图书列表.xlsx')
  } catch {
  } finally {
    exportLoading.value = false
  }
}

/** 初始化 **/
onMounted(() => {
  getList()
})
</script>

前端测试

先启动后端项目,然后启动前端项目。若有报错,按提示修复

shell
pnpm run dev

集成前后端代码

分别启动后端和前端项目,打开浏览器访问:http://localhost:8080

默认账号:admin 密码:admin123

菜单管理

系统管理——菜单管理

image-20251027170709433

新增目录

  • 上级菜单:主类目
  • 菜单类型:目录
  • 路由地址:以/开头,加上应用名称。例如myapp_demo应用则填写/demo

image-20251029141853153

新增菜单

在上一步新增的目录上,点击新增

image-20251028143807382

  • 菜单类型:菜单
  • 路由地址:book
  • 组件地址:demo/book/index
  • 组件名字:DemoBook
  • 权限标识:demo:book:query

image-20251028155845307

新增按钮

在上一步新增的菜单上,点击新增

image-20251028174451977

菜单类型:按钮。按下面表格,依次新增。

image-20251029142133939

菜单名称权限标识显示排序
图书新增demo:book:create1
图书删除demo:book:delete2
图书修改demo:book:update3
图书查询demo:book:query4
图书导出demo:book:export5

image-20251028174410955

TIP

菜单管理详细使用说明:《后端手册-菜单管理》

角色管理

完成新增菜单后,点击系统管理——角色管理——菜单权限,对用户进行授权。

例如:对超级管理员角色(也可以选择其它角色)授权图书管理

image-20251028153207585

image-20251028153313076

授权后的用户,重新登录,才能看到新增的菜单。

小结

至此,已从0到1实现图书的增删改查功能。配合《AI辅助编码》使用,能大幅提高开发效率。如果遇到问题,可参考《开发常见错误处理》