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

新建应用
开发的第一步,是新建自己的Django应用。应用命名规范
- 固定前缀:应用名称以
myapp_开头。 - 功能后缀:后缀需体现应用核心用途。
- 示例 1:演示用应用命名为
myapp_demo - 示例 2:学校管理类应用命名为
myapp_school
- 示例 1:演示用应用命名为
操作示例(以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_bookmyapp_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浏览器访问接口文档,查看是否已新增了相关接口

编写前端代码
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
菜单管理
系统管理——菜单管理

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

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

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

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

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

| 菜单名称 | 权限标识 | 显示排序 |
|---|---|---|
| 图书新增 | demo:book:create | 1 |
| 图书删除 | demo:book:delete | 2 |
| 图书修改 | demo:book:update | 3 |
| 图书查询 | demo:book:query | 4 |
| 图书导出 | demo:book:export | 5 |

TIP
菜单管理详细使用说明:《后端手册-菜单管理》
角色管理
完成新增菜单后,点击系统管理——角色管理——菜单权限,对用户进行授权。
例如:对超级管理员角色(也可以选择其它角色)授权图书管理


授权后的用户,重新登录,才能看到新增的菜单。
小结
至此,已从0到1实现图书的增删改查功能。配合《AI辅助编码》使用,能大幅提高开发效率。如果遇到问题,可参考《开发常见错误处理》