# 快速开发

# 需求

  • 创建一个项目管理app;
  • 字段包含:项目名称、项目编码、项目状态;
  • 功能:包含项目管理的增删改查、导出。

# 后端

# 1. 创建App

  • 通过命令创建App python manage.py startapp demo

# 2. 在fuadmin/setting.py里添加我们的app

DEBUG = locals().get('DEBUG', True)
ALLOWED_HOSTS = locals().get('ALLOWED_HOSTS', ['*'])
DEMO = locals().get('DEMO', False)

# Application definition

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'django_celery_beat',
    'django_celery_results',
    'system',
    'demo',
]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 3. 创建 models 模型

  • 编写项目管理模型内容 demo/models.py,如下:
from django.db import models
from utils.models import CoreModel


class Demo(CoreModel):
    name = models.CharField(null=False, max_length=64, verbose_name="项目名称", help_text="项目名称")
    code = models.CharField(max_length=32, verbose_name="项目编码", help_text="项目编码")
    status = models.CharField(max_length=64, verbose_name="项目状态", help_text="项目状态")

    class Meta:
        db_table = "Demo"
        verbose_name = '项目演示'
        verbose_name_plural = verbose_name
        ordering = ('-create_datetime',)
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 4. 迁移数据库文件

  • 执行迁移命令:
    • python3 manage.py makemigrations demo
    • python3 manage.py migrate demo
  • 迁移成功后,通过数据库可查看到

# 5. 创建api、路由接口

  • 创建api.py

from typing import List
from ninja import Router, ModelSchema, Query, Field
from ninja.pagination import paginate

from demo.models import Demo
from utils.fu_crud import create, delete, update, retrieve, ImportSchema, export_data, import_data
from utils.fu_ninga import MyPagination, FuFilters

router = Router()


# 设置过滤字段
class Filters(FuFilters):
    name: str = Field(None, alias="name")
    code: str = Field(None, alias="code")
    status: int = Field(None, alias="status")
    id: str = Field(None, alias="demo_id")


# 设置请求接收字段
class DemoSchemaIn(ModelSchema):
    class Config:
        model = Demo
        model_fields = ['name', 'code', 'sort', 'status']


# 设置响应字段
class DemoSchemaOut(ModelSchema):
    class Config:
        model = Demo
        model_fields = "__all__"


# 创建Demo
@router.post("/demo", response=DemoSchemaOut)
def create_demo(request, data: DemoSchemaIn):
    demo = create(request, data, Demo)
    return demo


# 删除Demo
@router.delete("/demo/{demo_id}")
def delete_demo(request, demo_id: int):
    delete(demo_id, Demo)
    return {"success": True}


# 更新Demo
@router.put("/demo/{demo_id}", response=DemoSchemaOut)
def update_demo(request, demo_id: int, data: DemoSchemaIn):
    demo = update(request, demo_id, data, Demo)
    return demo


# 获取Demo
@router.get("/demo", response=List[DemoSchemaOut])
@paginate(MyPagination)
def list_demo(request, filters: Filters = Query(...)):
    qs = retrieve(request, Demo, filters)
    return qs


# 导入
@router.get("/demo/all/export")
def export_demo(request):
    title_dict = {
        'name': '名称',
        'code': '编码',
        'status': '状态',
        'sort': '排序',
    }
    return export_data(request, Demo, DemoSchemaOut, title_dict)


# 导出
@router.post("/demo/all/import")
def import_demo(request, data: ImportSchema):
    title_dict = {
        '名称': 'name',
        '编码': 'code',
        '状态': 'status',
        '排序': 'sort',
    }
    return import_data(request, Demo, DemoSchemaIn, data, title_dict)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
  • 创建router.py
from ninja import Router
from demo.api import router

demo_router = Router()
demo_router.add_router('/', router, tags=["Demo"])
1
2
3
4
5

# 6. 在 fuadmin/api.py中添加刚才创建的router

api.add_router('/system/', system_router)
api.add_router('/demo/', demo_router)
1
2

# 7. 功能接口已完成,文档查看: http://127.0.0.1:8000/api/docs (opens new window)

# 前端

# 1. 创建 api 文件

  • 在目录web/src/views/demo/下创建api.js: 代码如下:

import { defHttp } from '/@/utils/http/axios';

enum DeptApi {
  prefix = '/api/demo/demo',
}

export const getList = (params) => {
  return defHttp.get({ url: DeptApi.prefix, params });
};

/**
 * 保存或更新
 */

export const createOrUpdate = (params, isUpdate) => {
  if (isUpdate) {
    return defHttp.put({ url: DeptApi.prefix + '/' + params.id, params });
  } else {
    return defHttp.post({ url: DeptApi.prefix, params });
  }
};

export const importData = (params) => {
  return defHttp.post({ url: DeptApi.prefix + '/all/import', params });
};

export const exportData = () => {
  return defHttp.get(
          { url: DeptApi.prefix + '/all/export', responseType: 'blob' },
          { isReturnNativeResponse: true },
  );
};

/**
 * 删除
 */

export const deleteItem = (id) => {
  return defHttp.delete({ url: DeptApi.prefix + '/' + id });
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

# 2. 创建 data 文件

  • 在目录web/src/views/demo/下创建data.js: 代码如下:

import { BasicColumn } from '/@/components/Table';
import { FormSchema } from '/@/components/Table';

export const columns: BasicColumn[] = [
  {
    title: '项目名称',
    dataIndex: 'name',
    width: 200,
  },
  {
    title: '项目编码',
    dataIndex: 'code',
    width: 180,
  },
  {
    title: '项目排序',
    dataIndex: 'sort',
    width: 100,
  },
  {
    title: '项目状态',
    dataIndex: 'status',
    width: 100,
  },
  {
    title: '创建时间',
    dataIndex: 'create_datetime',
    width: 180,
  },
];

export const searchFormSchema: FormSchema[] = [
  {
    field: 'name',
    label: '项目名称',
    component: 'Input',
    colProps: { span: 6 },
  },
];

export const formSchema: FormSchema[] = [
  {
    field: 'id',
    label: 'id',
    component: 'Input',
    show: false,
  },
  {
    field: 'name',
    label: '项目名称',
    required: true,
    component: 'Input',
  },
  {
    field: 'code',
    label: '项目编码',
    required: true,
    component: 'Input',
  },
  {
    field: 'status',
    component: 'DictSelect',
    label: '项目状态',
    componentProps: {
      dictCode: 'project_status',
    },
  },
  {
    field: 'sort',
    label: '岗位排序',
    component: 'InputNumber',
    required: true,
  },
];
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75

# 3. 创建 index 文件

  • 在目录web/src/views/demo/下创建index.vue: 代码如下:
<template>
  <div>
    <BasicTable @register="registerTable">
      <template #tableTitle>
        <Space style="height: 40px">
          <a-button
                  type="primary"
                  v-auth="['demo:add']"
                  preIcon="ant-design:plus-outlined"
                  @click="handleCreate"
          >
            新增
          </a-button>
          <a-button
                  type="error"
                  v-auth="['demo:delete']"
                  preIcon="ant-design:delete-outlined"
                  @click="handleBulkDelete"
          >
            删除
          </a-button>
          <BasicUpload
                  :maxSize="20"
                  :maxNumber="1"
                  @change="handleChange"
                  class="my-5"
                  type="warning"
                  text="导入"
                  v-auth="['demo:update']"
          />
          <a-button
                  type="success"
                  v-auth="['demo:update']"
                  preIcon="carbon:cloud-download"
                  @click="handleExportData"
          >
            导出
          </a-button>
        </Space>
      </template>
      <template #action="{ record }">
        <TableAction
                :actions="[
            {
              type: 'button',
              icon: 'clarity:note-edit-line',
              color: 'primary',
              auth: ['demo:update'],
              onClick: handleEdit.bind(null, record),
            },
            {
              icon: 'ant-design:delete-outlined',
              type: 'button',
              color: 'error',
              placement: 'left',
              auth: ['demo:delete'],
              popConfirm: {
                title: '是否确认删除',
                confirm: handleDelete.bind(null, record.id),
              },
            },
          ]"
        />
      </template>
    </BasicTable>
    <DemoDrawer @register="registerDrawer" @success="handleSuccess" />
  </div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';

import { BasicTable, useTable, TableAction } from '/@/components/Table';
import { usePermission } from '/@/hooks/web/usePermission';
import { useDrawer } from '/@/components/Drawer';
import DemoDrawer from './Drawer.vue';
import { Space } from 'ant-design-vue';
import { BasicUpload } from '/@/components/Upload';
import { deleteItem, getList, exportData, importData } from './api';
import { columns, searchFormSchema } from './data';
import { message } from 'ant-design-vue';
import { useMessage } from '/@/hooks/web/useMessage';
import { downloadByData } from '/@/utils/file/download';
export default defineComponent({
  name: 'Demo',
  components: { BasicTable, DemoDrawer, TableAction, BasicUpload, Space },
  setup() {
    const [registerDrawer, { openDrawer }] = useDrawer();
    const { createConfirm } = useMessage();
    const { hasPermission } = usePermission();
    const [registerTable, { reload, getSelectRows }] = useTable({
      api: getList,
      columns,
      formConfig: {
        labelWidth: 80,
        schemas: searchFormSchema,
      },
      useSearchForm: true,
      showTableSetting: true,
      tableSetting: { fullScreen: true },
      bordered: true,
      showIndexColumn: false,
      rowSelection: {
        type: 'checkbox',
      },
      actionColumn: {
        width: 150,
        title: '操作',
        dataIndex: 'action',
        slots: { customRender: 'action' },
        fixed: undefined,
      },
    });

    function handleCreate() {
      openDrawer(true, {
        isUpdate: false,
      });
    }

    function handleEdit(record: Recordable) {
      openDrawer(true, {
        record,
        isUpdate: true,
      });
    }

    async function handleDelete(id: number) {
      await deleteItem(id);
      message.success('删除成功');
      await reload();
    }

    function handleBulkDelete() {
      if (getSelectRows().length == 0) {
        message.warning('请选择一个选项');
      } else {
        createConfirm({
          iconType: 'warning',
          title: '提示',
          content: '是否确认删除?',
          async onOk() {
            for (const item of getSelectRows()) {
              await deleteItem(item.id);
            }
            message.success('删除成功');
            await reload();
          },
        });
      }
    }

    async function handleChange(list: string[]) {
      console.log(list[0]);
      await importData({ path: list[0] });
      message.success(`导入成功`);
      await reload();
    }

    async function handleExportData() {
      const response = await exportData();
      await downloadByData(response.data, '项目数据.xlsx');
    }

    function handleSuccess() {
      message.success('请求成功');
      reload();
    }

    return {
      registerTable,
      registerDrawer,
      handleCreate,
      handleEdit,
      handleDelete,
      handleSuccess,
      hasPermission,
      handleBulkDelete,
      getSelectRows,
      handleExportData,
      handleChange,
    };
  },
});
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184

# 4. 创建 demo 文件

  • 在目录web/src/views/demo/下创建Drawer.vue: 代码如下:
<template>
  <BasicDrawer
          v-bind="$attrs"
          @register="registerDrawer"
          showFooter
          :title="getTitle"
          width="50%"
          @ok="handleSubmit"
  >
    <BasicForm @register="registerForm" />
  </BasicDrawer>
</template>
<script lang="ts">
import { defineComponent, ref, computed, unref } from 'vue';
import { BasicForm, useForm } from '/@/components/Form/index';
import { BasicDrawer, useDrawerInner } from '/@/components/Drawer';
import { createOrUpdate } from './api';
import { formSchema } from './data';

export default defineComponent({
  name: 'ButtonDrawer',
  components: { BasicDrawer, BasicForm },
  emits: ['success', 'register'],
  setup(_, { emit }) {
    const isUpdate = ref(true);

    const [registerForm, { resetFields, setFieldsValue, validate }] = useForm({
      labelWidth: 100,
      schemas: formSchema,
      showActionButtonGroup: false,
      baseColProps: { lg: 12, md: 24 },
    });

    const [registerDrawer, { setDrawerProps, closeDrawer }] = useDrawerInner(async (data) => {
      await resetFields();
      setDrawerProps({ confirmLoading: false });
      isUpdate.value = !!data?.isUpdate;

      if (unref(isUpdate)) {
        await setFieldsValue({
          ...data.record,
        });
      }
    });

    const getTitle = computed(() => (!unref(isUpdate) ? '新增项目' : '编辑项目'));

    async function handleSubmit() {
      try {
        const values = await validate();
        setDrawerProps({ confirmLoading: true });
        await createOrUpdate(values, unref(isUpdate));
        closeDrawer();
        emit('success');
      } finally {
        setDrawerProps({ confirmLoading: false });
      }
    }

    return {
      registerDrawer,
      registerForm,
      getTitle,
      handleSubmit,
    };
  },
});
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68

# 5. 在菜单中添加demo

# 6. 在菜单中添加按钮权限

# 完成

  • 刷新页面打开 项目演示,则是一个简单完整的 CRUD 完成。
  • 如有问题可参考:demo 代码。