说明
后端采用一对多的数据结构,前端采用主从表单的布局 以订单作为案例
- 主表:订单表
- 从表:明细表
实现的功能
- 一对多数据结构的增删改查
- 时间字段的筛选
- 封装明细表组件
- 数据代理
- 实时保存
- 实时计算
- 表尾合计
- 分页显示
- 自定义配置项
后端
新建了一个apps
数据模型:models.py
from django.db import models
# Create your models here.
from dvadmin.utils.models import CoreModel
table_prefix = 'apps_'
class OrderModel(CoreModel):
order_number = models.CharField(max_length=10, unique=True, verbose_name="订单编号")
customer = models.CharField(max_length=10, verbose_name="购买客户")
order_date = models.DateField(verbose_name="下单日期")
payment_type = models.CharField(max_length=10, verbose_name="支付方式")
is_invoice = models.BooleanField(default=False, verbose_name="是否需要发票")
express = models.CharField(max_length=10, verbose_name="物流快递")
address = models.CharField(max_length=255, verbose_name="收货地址")
def __str__(self):
return self.order_number
class Meta:
db_table = table_prefix + "order"
verbose_name = "订单表"
verbose_name_plural = verbose_name
ordering = ("id",)
class OrderRecordModel(CoreModel):
order = models.ForeignKey(
to="OrderModel",
verbose_name="订单",
on_delete=models.PROTECT,
db_constraint=False,
null=True,
blank=True,
help_text="订单编号", )
product = models.CharField(max_length=10, verbose_name="购买产品")
specification = models.CharField(max_length=10, null=True, blank=True, verbose_name="型号/规格")
price = models.FloatField(default=0, verbose_name="单价")
quantity = models.FloatField(default=0, verbose_name="数量")
amount = models.FloatField(default=0, verbose_name="金额")
def __str__(self):
return self.product
class Meta:
db_table = table_prefix + "order_record"
verbose_name = "订单明细表"
verbose_name_plural = verbose_name
ordering = ("-id",)
序列化器:serializers.py
'''
@author: MuziLi
@contact: 1537080775@qq.com
@file: serializers.py
@time: 2023-05-13 22:37
@SoftWare: PyCharm
'''
from apps.order.models import OrderModel, OrderRecordModel
from dvadmin.utils.serializers import CustomModelSerializer
class OrderSerializer(CustomModelSerializer):
"""
订单查询序列化
"""
class Meta:
model = OrderModel
fields = "__all__"
class OrderCreateUpdateSerializer(CustomModelSerializer):
"""
订单新增,修改序列化
"""
class Meta:
model = OrderModel
fields = "__all__"
class OrderImportSerializer(CustomModelSerializer):
"""
订单导入,导出序列化
"""
class Meta:
model = OrderModel
fields = "__all__"
class OrderRecordSerializer(CustomModelSerializer):
"""
订单明细查询序列化
"""
order = OrderSerializer() # 订单
class Meta:
model = OrderRecordModel
fields = "__all__"
class OrderRecordCreateUpdateSerializer(CustomModelSerializer):
"""
订单明细新增,修改序列化
"""
class Meta:
model = OrderRecordModel
fields = "__all__"
class OrderRecordImportSerializer(CustomModelSerializer):
"""
订单明细导入,导出序列化
"""
class Meta:
model = OrderRecordModel
fields = "__all__"
筛选:filter.py
import django_filters
from apps.order.models import OrderModel
class OrderFilter(django_filters.rest_framework.FilterSet):
customer = django_filters.CharFilter(field_name='customer', lookup_expr='contains', help_text="购买客户")
order_date = django_filters.BaseRangeFilter(field_name="order_date")
class Meta:
model = OrderModel
fields = "__all__"
视图:views.py
# Create your views here.
from apps.order.filter import OrderFilter
from apps.order.models import OrderModel, OrderRecordModel
from apps.order.serializers import OrderSerializer, OrderCreateUpdateSerializer, OrderRecordSerializer, \
OrderRecordCreateUpdateSerializer
from dvadmin.utils.viewset import CustomModelViewSet
class OrderViewSet(CustomModelViewSet):
"""
订单表视图类
"""
queryset = OrderModel.objects.all()
serializer_class = OrderSerializer
create_serializer_class = OrderCreateUpdateSerializer
update_serializer_class = OrderCreateUpdateSerializer
filter_class = OrderFilter # 筛选
class OrderRecordViewSet(CustomModelViewSet):
"""
订单明细表视图类
"""
queryset = OrderRecordModel.objects.all()
serializer_class = OrderRecordSerializer
create_serializer_class = OrderRecordCreateUpdateSerializer
update_serializer_class = OrderRecordCreateUpdateSerializer
路由:urls.py
from rest_framework.routers import SimpleRouter
from apps.order.views import OrderRecordViewSet, OrderViewSet
router = SimpleRouter()
router.register("order", OrderViewSet) # 订单
router.register("order_record", OrderRecordViewSet) # 订单明细
urlpatterns = [
]
urlpatterns += router.urls
注册
APP注册
在
application/settings.py文件中注册创建的app应用
INSTALLED_APPS=[
...
'apps.orders', # 订单管理
]
路由注册
在
application/urls.py注册路由
urlpatterns = (
[
...
path("api/apps/", include("apps.order.urls")), # 订单管理
...
]
)
后端运行
- 进入后端项目目录
cd backend - 在项目根目录中,复制
./conf/env.example.py文件为一份新的到./conf/env.py下,并重命名为env.py - 在
env.py中配置数据库信息(默认数据库为sqlite3,测试演示可忽略此步骤) - 安装依赖环境
pip3 install -r requirements.txt - 执行迁移命令
python3 manage.py makemigrationspython3 manage.py migrate - 初始化数据
python3 manage.py init - 初始化省市县数据
python3 manage.py init_area - 启动项目
python3 manage.py runserver 0.0.0.0:8000
前端
封装了一个子表的组件,可以适用于其他项目
子表组件
路径
src\components\sub-table\index.vue
<!--
* @文件介绍: 子表组件-主页面
-->
<template>
<el-card>
<vxe-grid ref="xGrid" v-bind="gridOptions" @edit-closed="editClosedEvent">
<!--自定义插槽 toolbar buttons 插槽-->
<template #toolbar_buttons>
<vxe-button
:disabled="!gridOptions.editConfig.enabled"
size="mini"
@click="addStartRow"
icon="fa fa-plus"
>
新增
</vxe-button>
</template>
<!--自定义插槽 操作列 插槽-->
<template #operate="{ row }">
<el-button
:disabled="!gridOptions.editConfig.enabled"
size="mini"
type="success"
icon="el-icon-plus"
@click="addEndRow(row)"
circle
></el-button>
<el-button
:disabled="!gridOptions.editConfig.enabled"
size="mini"
type="danger"
icon="el-icon-delete"
@click="delRow(row)"
circle
></el-button>
</template>
</vxe-grid>
</el-card>
</template>
<script>
import XEUtils from 'xe-utils';
import { request } from '@/api/service';
export default {
name: 'sub-table',
props: {
gridConfig: Object, // 配置文件
parent: Object, // 父级配置
},
data() {
return {
gridOptions: {
border: true,
keepSource: true,
align: 'center', // 所有的表头列的对齐方式
size: 'mini',
// 工具栏配置
toolbarConfig: { refresh: true, slots: { buttons: 'toolbar_buttons' } },
// 可编辑配置项
editConfig: {
enabled: true,
trigger: 'click',
mode: 'row',
showStatus: true,
},
// 分页配置项
pagerConfig: {
pageSize: 10,
pageSizes: [10, 100, 200, 500, 1000, 2000],
},
// 数据代理配置项
proxyConfig: {
props: {
result: 'data.data', // 配置响应结果列表字段,需要配置分页属性
total: 'data.total', // 配置响应结果总页数字段
},
// 只接收Promise,具体实现自由发挥
ajax: {
// 当点击工具栏查询按钮或者手动提交指令 query或reload 时会被触发
query: ({ page, sorts, filters, form }) => {
const queryParams = {
limit: page.pageSize,
page: page.currentPage,
};
queryParams[this.parent.name] = this.parent.id;
if (this.parent.id) {
return request({
url: this.gridConfig.urlPrefix,
method: 'get',
params: { ...queryParams },
});
} else {
return [];
}
},
},
},
},
};
},
created() {
// 合并配置文件
XEUtils.merge(this.gridOptions, this.gridConfig);
// 判断是否可编辑
this.gridOptions.editConfig.enabled = this.gridOptions.mode !== 'view';
},
methods: {
// 即时保存
async editClosedEvent({ row, column }) {
row[this.parent.name] = this.parent.id;
const $xGrid = this.$refs.xGrid;
const errMap = await $xGrid.validate(row).catch((errMap) => errMap); // 校验规则
if (errMap) {
this.$message.error('数据校验未通过!');
} else {
if (!row.id) {
// 新增数据
this.CreateObj(row).then((res) => {
$xGrid.commitProxy('query');
this.$message.success(res.msg);
});
} else {
// 修改数据
this.UpdateObj(row).then((res) => {
$xGrid.reloadRow(row);
this.$message.success(res.msg);
});
}
}
},
// 从当前行后面新增,实现复制行
addEndRow(row) {
const records = XEUtils.clone(row, true); // 深拷贝
delete records.id; // 删除ID
this.$refs.xGrid.insertAt(records, row);
},
// 从开头新增行
addStartRow() {
this.$refs.xGrid.insert();
},
// 删除行
delRow(row) {
if (row.id) {
this.$confirm('确定要删除这条数据吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}).then(() => {
this.DelObj(row.id).then((res) => {
this.$refs.xGrid.commitProxy('query');
this.$message.success(res.msg);
});
});
} else {
this.$refs.xGrid.remove(row);
}
},
// 新增API
CreateObj(row) {
return request({
url: this.gridConfig.urlPrefix,
method: 'post',
data: row,
});
},
// 修改API
UpdateObj(row) {
return request({
url: this.gridConfig.urlPrefix + row.id + '/',
method: 'put',
data: row,
});
},
// 删除API
DelObj(id) {
return request({
url: this.gridConfig.urlPrefix + id + '/',
method: 'delete',
});
},
},
};
</script>
<style>
.vxe-select--panel {
z-index: 10080 !important;
}
.vxe-table--context-menu-wrapper {
z-index: 10081 !important;
}
</style>
接口文件:api.js
/*
* @文件介绍: 老师管理接口
*/
import { request, downloadFile } from '@/api/service';
export const urlPrefix = '/api/apps/order/';
/**
* 列表查询
* @param {*} query
* @returns
*/
export function GetList(query) {
return request({
url: urlPrefix,
method: 'get',
params: { ...query },
});
}
/**
* 单例查询
* @param {*} id
* @returns
*/
export function GetObj(id) {
return request({
url: urlPrefix + id + '/',
method: 'get',
});
}
/**
* 新增
* @param {*} obj
* @returns
*/
export function AddObj(obj) {
return request({
url: urlPrefix,
method: 'post',
data: obj,
});
}
/**
* 修改
* @param {*} obj
* @returns
*/
export function UpdateObj(obj) {
return request({
url: urlPrefix + obj.id + '/',
method: 'put',
data: obj,
});
}
/**
* 删除
* @param {*} id
* @returns
*/
export function DelObj(id) {
return request({
url: urlPrefix + id + '/',
method: 'delete',
data: { id },
});
}
/**
* 批量删除
* @param {*} keys
* @returns
*/
export function BatchDel(keys) {
return request({
url: urlPrefix + 'multiple_delete/',
method: 'delete',
data: { keys },
});
}
/**
* 导出
* @param params
*/
export function ExportData(params) {
return downloadFile({
url: urlPrefix + 'export_data/',
params: params,
method: 'get',
});
}
配置文件:crud.js
/*
* @文件介绍: 老师管理配置文件
*/
import XEUtils from 'xe-utils';
export const crudOptions = (vm) => {
return {
pageOptions: {
compact: true,
export: { local: false }, // 服务器导出
},
options: {
height: '100%',
rowKey: 'id',
},
formOptions: {
width: '80%',
defaultSpan: 12, // 默认的表单 span
size: 'mini',
},
viewOptions: {
componentType: 'form', //form=使用表单组件,row=使用行展示组件
},
selectionRow: {
align: 'center',
width: 40,
},
columns: [
{
title: '订单编号',
key: 'order_number',
form: {
rules: [
// 表单校验规则
{ required: true, message: '订单编号必填项' },
],
component: {
clearable: true,
},
},
},
{
title: '购买客户',
key: 'customer',
search: {
disabled: false,
},
form: {
rules: [
// 表单校验规则
{ required: true, message: '购买客户必填项' },
],
component: {
clearable: true,
},
},
},
{
title: '下单日期',
key: 'order_date',
type: 'date',
width: 90,
search: {
disabled: false,
width: 250,
component: {
props: {
type: 'daterange',
},
},
},
form: {
rules: [
// 表单校验规则
{ required: true, message: '下单日期必填项' },
],
component: {
clearable: true,
props: {
valueFormat: 'yyyy-MM-dd',
clearable: true,
pickerOptions: {
disabledDate: (time) => {
// 不允许选择从今往后的日期
return time.getTime() >= Date.now();
},
},
},
},
},
// 提交时,处理数据
valueResolve(row, col) {
if (row[col.key] instanceof Array) {
row[col.key] = row[col.key].join(',');
}
},
},
{
title: '支付方式',
key: 'payment_type',
search: {
disabled: false,
},
type: 'select',
dict: {
data: [
{ value: '微信', label: '微信' },
{ value: '支付宝', label: '支付宝' },
{ value: '其他', label: '其他' },
],
},
form: {
rules: [
// 表单校验规则
{ required: true, message: '支付方式必填项' },
],
component: {
clearable: true,
},
},
},
{
title: '是否需要发票',
key: 'is_invoice',
type: 'radio',
dict: {
data: [
{ value: false, label: '不需要' },
{ value: true, label: '需要' },
],
},
form: {
value: false,
},
},
{
title: '物流快递',
key: 'express',
search: {
disabled: false,
},
type: 'select',
dict: {
data: [
{ value: '中国邮政', label: '中国邮政' },
{ value: '顺丰', label: '顺丰' },
{ value: '申通', label: '申通' },
{ value: '中通', label: '中通' },
{ value: '韵达', label: '韵达' },
],
},
form: {
rules: [
// 表单校验规则
{ required: true, message: '物流快递必填项' },
],
component: {
clearable: true,
},
},
},
{
title: '收货地址',
key: 'address',
form: {
rules: [
// 表单校验规则
{ required: true, message: '收货地址必填项' },
],
component: {
span: 24,
clearable: true,
},
},
},
{
title: '备注',
key: 'description',
search: {
disabled: true,
},
type: 'textarea',
form: {
component: {
span: 24,
placeholder: '请输入内容',
showWordLimit: true,
maxlength: '200',
props: {
type: 'textarea',
},
},
},
},
// {
// title: '子表临时数据',
// key: 'inspection',
// type: 'button',
// form: {
// valueChange(key, value, form, others) {
// const orderRecord = vm.$refs.OrderRecordRef.$refs.xGrid;
// const insertRecords = orderRecord.getInsertRecords();
// console.log(insertRecords);
// },
// },
// },
{
title: '订单明细',
key: 'order_record',
show: false,
addForm: {
component: {
show: false,
},
},
form: {
title: '',
slot: true,
component: {
span: 24, // 该字段占据多宽,24为占满一行
},
itemProps: {
// el-form-item的配置
// https://element.eleme.cn/#/zh-CN/component/form#form-item-attributes
labelWidth: '0px', // 可以隐藏表单项的label
},
},
},
],
};
};
// 订单明细表配置
export const gridConfig = (vm) => {
return {
urlPrefix: '/api/apps/order_record/', // 订单明细api地址
id: 'order_record',
size: 'small',
editConfig: { trigger: 'dblclick', mode: 'row', showStatus: true },
showFooter: true,
footerMethod({ columns, data }) {
const footerData = [
columns.map((column, _columnIndex) => {
if (_columnIndex === 0) {
return '平均';
}
if (['price'].includes(column.property)) {
return XEUtils.round(XEUtils.mean(data, 'price'), 2);
}
if (['amount'].includes(column.property)) {
return XEUtils.round(XEUtils.mean(data, 'amount'), 2);
}
return null;
}),
columns.map((column, _columnIndex) => {
if (_columnIndex === 0) {
return '和值';
}
if (['quantity'].includes(column.property)) {
return XEUtils.sum(data, 'quantity');
}
if (['amount'].includes(column.property)) {
return XEUtils.sum(data, 'amount');
}
return null;
}),
];
return footerData;
},
// 列配置
columns: [
{
title: '操作',
fixed: 'left',
width: 100,
slots: {
default: 'operate',
},
editRender: {
enabled: false,
},
},
// 审核显示字段
{
field: 'product',
title: '购买产品',
editRender: {
name: '$select',
options: [
{ value: '苹果', label: '苹果' },
{ value: '菠萝', label: '菠萝' },
{ value: '荔枝', label: '荔枝' },
],
},
},
{
field: 'specification',
title: '型号/规格',
editRender: {
name: '$input',
},
},
{
field: 'price',
title: '单价',
editRender: {
name: '$input',
defaultValue: 0,
immediate: true, // 实时更新
props: {
type: 'float',
precision: 2,
min: 0,
step: 1,
},
events: {
change({ row }) {
// 计算金额
row.amount = row.price * row.quantity;
},
},
},
},
{
field: 'quantity',
title: '数量',
editRender: {
name: '$input',
defaultValue: 0,
immediate: true,
props: {
type: 'float',
precision: 2,
min: 0,
step: 1,
},
events: {
change({ row }) {
// 计算金额
row.amount = row.price * row.quantity;
},
},
},
},
{
field: 'amount',
title: '金额',
editRender: {
enabled: false,
defaultValue: 0,
},
},
{
field: 'description',
title: '备注',
editRender: {
name: 'input',
},
},
],
// 校验规则配置项
editRules: {
product: [{ required: true, message: '购买产品' }],
},
};
};
页面文件:index.vue
<!--
* @文件介绍:订单管理
-->
<template>
<d2-container :class="{ 'page-compact': crud.pageOptions.compact }">
<template slot="header">订单管理</template>
<d2-crud-x ref="d2Crud" v-bind="_crudProps" v-on="_crudListeners">
<div slot="header">
<crud-search
ref="search"
:options="crud.searchOptions"
@submit="handleSearch"
/>
<el-button-group>
<el-button
size="small"
v-permission="'Create'"
type="primary"
@click="addRow"
>
<i class="el-icon-plus" /> 新增
</el-button>
<importExcel :api="api" v-permission="'Import'">导入 </importExcel>
</el-button-group>
<crud-toolbar v-bind="_crudToolbarProps" v-on="_crudToolbarListeners" />
</div>
<span slot="PaginationPrefixSlot" class="prefix">
<el-button
class="square"
size="mini"
title="批量删除"
@click="batchDelete"
icon="el-icon-delete"
:disabled="!multipleSelection || multipleSelection.length == 0"
/>
</span>
<!-- 明细插槽 -->
<template slot="order_recordFormSlot">
<OrderRecord
ref="OrderRecordRef"
:gridConfig="orderCecord.gridConfig"
:parent="orderCecord.parent"
/>
</template>
</d2-crud-x>
</d2-container>
</template>
<script>
import * as api from './api';
import OrderRecord from '@/components/sub-table';
import { crudOptions, gridConfig } from './crud';
import { d2CrudPlus } from 'd2-crud-plus';
export default {
name: 'teacher',
mixins: [d2CrudPlus.crud],
components: { OrderRecord },
data() {
return {
api: api.urlPrefix,
orderCecord: {
gridConfig: gridConfig(this),
parent: {
name: 'order',
id: undefined,
},
},
};
},
methods: {
// 对话框打开前被调用
doDialogOpened({ row, mode }) {
this.orderCecord.gridConfig.mode = mode;
this.orderCecord.parent.id = row.id;
},
getCrudOptions() {
return crudOptions(this);
},
pageRequest(query) {
return api.GetList(query);
},
addRequest(row) {
return api.AddObj(row);
},
updateRequest(row) {
return api.UpdateObj(row);
},
delRequest(row) {
return api.DelObj(row.id);
},
batchDelRequest(ids) {
return api.BatchDel(ids);
},
doServerExport(context) {
return api.ExportData({ ...context.search });
},
},
};
</script>
前端运行
- 进入前端项目目录
cd web - 安装依赖
npm install --registry=https://registry.npm.taobao.org - 启动服务
npm run dev