最终效果
vim fu-admin/backend/fuadmin/settings.py
"""
Django settings for fuadmin project.
Generated by 'django-admin startproject' using Django 4.0.4.
For more information on this file, see
https://docs.djangoproject.com/en/4.0/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/4.0/ref/settings/
"""
import os
from datetime import timedelta
from pathlib import Path
from conf.env import *
BASE_DIR = Path(__file__).resolve().parent.parent
SECRET_KEY = 'django-insecure-x4k4^#6wovi1aep8%ow!5fr%(9o#1u=+0+nzi($_j=^d*ui6g3'
DEBUG = locals().get('DEBUG', True)
ALLOWED_HOSTS = locals().get('ALLOWED_HOSTS', ['*'])
DEMO = locals().get('DEMO', False)
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',
'generator',
'demo2',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'utils.middleware.ApiLoggingMiddleware',
]
ROOT_URLCONF = 'fuadmin.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [BASE_DIR / 'templates']
,
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'fuadmin.wsgi.application'
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
LANGUAGE_CODE = 'zh-hans'
TIME_ZONE = 'Asia/Shanghai'
USE_I18N = True
USE_L10N = True
USE_TZ = False
STATIC_URL = 'static/'
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
AUTH_USER_MODEL = 'system.Users'
USERNAME_FIELD = 'username'
ALL_MODELS_OBJECTS = []
if DATABASE_TYPE == "MYSQL":
DATABASES = {
"default": {
"ENGINE": "django.db.backends.mysql",
"HOST": DATABASE_HOST,
"PORT": DATABASE_PORT,
"USER": DATABASE_USER,
"PASSWORD": DATABASE_PASSWORD,
"NAME": DATABASE_NAME,
}
}
elif DATABASE_TYPE == "SQLSERVER":
DATABASES = {
"default": {
"ENGINE": "mssql",
"HOST": DATABASE_HOST,
"PORT": DATABASE_PORT,
"USER": DATABASE_USER,
"PASSWORD": DATABASE_PASSWORD,
"NAME": DATABASE_NAME,
'ATOMIC_REQUESTS': True,
'OPTIONS': {
'driver': 'ODBC Driver 17 for SQL Server',
},
}
}
else:
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
'OPTIONS': {
'timeout': 20,
},
}
}
CACHES = {
"default": {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": f'{
REDIS_URL}/1',
"TIMEOUT": None,
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
}
},
}
SERVER_LOGS_FILE = os.path.join(BASE_DIR, "logs", "server.log")
ERROR_LOGS_FILE = os.path.join(BASE_DIR, "logs", "error.log")
LOGS_FILE = os.path.join(BASE_DIR, "logs")
if not os.path.exists(os.path.join(BASE_DIR, "logs")):
os.makedirs(os.path.join(BASE_DIR, "logs"))
STANDARD_LOG_FORMAT = (
"[%(asctime)s][%(name)s.%(funcName)s():%(lineno)d] [%(levelname)s] %(message)s"
)
CONSOLE_LOG_FORMAT = (
"[%(asctime)s][%(name)s.%(funcName)s():%(lineno)d] [%(levelname)s] %(message)s"
)
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"standard": {
"format": STANDARD_LOG_FORMAT},
"console": {
"format": CONSOLE_LOG_FORMAT,
"datefmt": "%Y-%m-%d %H:%M:%S",
},
"file": {
"format": CONSOLE_LOG_FORMAT,
"datefmt": "%Y-%m-%d %H:%M:%S",
},
},
"handlers": {
"file": {
"level": "INFO",
"class": "logging.handlers.RotatingFileHandler",
"filename": SERVER_LOGS_FILE,
"maxBytes": 1024 * 1024 * 100,
"backupCount": 5,
"formatter": "standard",
"encoding": "utf-8",
},
"error": {
"level": "ERROR",
"class": "logging.handlers.RotatingFileHandler",
"filename": ERROR_LOGS_FILE,
"maxBytes": 1024 * 1024 * 100,
"backupCount": 3,
"formatter": "standard",
"encoding": "utf-8",
},
"console": {
"level": "INFO",
"class": "logging.StreamHandler",
"formatter": "console",
},
},
"loggers": {
"": {
"handlers": ["console", "error", "file"],
"level": "INFO",
},
"django": {
"handlers": ["console", "error", "file"],
"level": "INFO",
"propagate": False,
},
'django.db.backends': {
'handlers': ["console", "error", "file"],
'propagate': False,
'level': "INFO"
},
"uvicorn.error": {
"level": "INFO",
"handlers": ["console", "error", "file"],
},
"uvicorn.access": {
"handlers": ["console", "error", "file"],
"level": "INFO"
},
},
}
CELERY_BROKER_URL = f'{
REDIS_URL}/2'
DJANGO_CELERY_BEAT_TZ_AWARE = False
CELERY_ENABLE_UTC = False
CELERY_WORKER_CONCURRENCY = 2
CELERY_MAX_TASKS_PER_CHILD = 5
CELERY_TIMEZONE = TIME_ZONE
CELERY_RESULT_BACKEND = 'django-db'
CELERY_BEAT_SCHEDULER = 'django_celery_beat.schedulers:DatabaseScheduler'
TOKEN_LIFETIME = 12 * 60 * 60
WHITE_LIST = ['/api/system/userinfo', '/api/system/permCode', '/api/system/menu/route/tree', '/api/system/user/*',
'/api/system/user/set/repassword']
API_LOG_ENABLE = True
API_LOG_METHODS = ['POST', 'GET', 'DELETE', 'PUT']
API_MODEL_MAP = {
}
INITIALIZE_RESET_LIST = []
vim fu-admin/backend/demo2/models.py
from django.db import models
from utils.models import CoreModel
class Demo2(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 = "Demo2"
verbose_name = '项目演示2'
verbose_name_plural = verbose_name
ordering = ('-create_datetime',)
vim fu-admin/backend/demo2/api.py
from typing import List
from ninja import Router, ModelSchema, Query, Field
from ninja.pagination import paginate
from demo2.models import Demo2
from utils.fu_crud import create, delete, update, retrieve, ImportSchema, export_data, import_data
from utils.fu_ninja 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 = Demo2
model_fields = ['name', 'code', 'sort', 'status']
class DemoSchemaOut(ModelSchema):
class Config:
model = Demo2
model_fields = "__all__"
@router.post("/demo2", response=DemoSchemaOut)
def create_demo(request, data: DemoSchemaIn):
demo = create(request, data, Demo2)
return demo
@router.delete("/demo2/{demo_id}")
def delete_demo(request, demo_id: int):
delete(demo_id, Demo2)
return {
"success": True}
@router.put("/demo2/{demo_id}", response=DemoSchemaOut)
def update_demo(request, demo_id: int, data: DemoSchemaIn):
demo = update(request, demo_id, data, Demo2)
return demo
@router.get("/demo2", response=List[DemoSchemaOut])
@paginate(MyPagination)
def list_demo(request, filters: Filters = Query(...)):
qs = retrieve(request, Demo2, filters)
return qs
@router.get("/demo2/all/export")
def export_demo(request):
title_dict = {
'name': '名称',
'code': '编码',
'status': '状态',
'sort': '排序',
}
return export_data(request, Demo2, DemoSchemaOut, title_dict)
@router.post("/demo2/all/import")
def import_demo(request, data: ImportSchema):
title_dict = {
'名称': 'name',
'编码': 'code',
'状态': 'status',
'排序': 'sort',
}
return import_data(request, Demo2, DemoSchemaIn, data, title_dict)
vim fu-admin/backend/demo2/router.py
from ninja import Router
from demo2.api import router
demo_router = Router()
demo_router.add_router('/', router, tags=["Demo2"])
vim fu-admin/backend/fuadmin/api.py
from demo.router import demo_router
from demo2.router import demo_router as demo2_router
from system.router import system_router
from utils.fu_auth import GlobalAuth
from utils.fu_ninja import FuNinjaAPI
from generator.router import generator_router
api = FuNinjaAPI(auth=GlobalAuth())
@api.exception_handler(Exception)
def a(request, exc):
if hasattr(exc, 'errno'):
return api.create_response(request, data=[], msg=str(exc), code=exc.errno)
else:
return api.create_response(request, data=[], msg=str(exc), code=500)
api.add_router('/system/', system_router)
api.add_router('/demo/', demo_router)
api.add_router('/generator/', generator_router)
api.add_router('/demo2/', demo2_router)
--------前端--------
vim fu-admin/web/src/views/demo2/api.ts
import {
defHttp } from '/@/utils/http/axios';
enum DeptApi {
prefix = '/api/demo2/demo2',
}
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 });
};
vim fu-admin/web/src/views/demo2/data.ts
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,
},
];
vim fu-admin/web/src/views/demo2/index.vue
<template>
<div>
<BasicTable @register="registerTable">
<template #tableTitle>
<Space style="height: 40px">
<a-button
type="primary"
v-auth="['demo2:add']"
preIcon="ant-design:plus-outlined"
@click="handleCreate"
>
新增
</a-button>
<a-button
type="error"
v-auth="['demo2: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="['demo2:update']"
/>
<a-button
type="success"
v-auth="['demo2: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: ['demo2:update'],
onClick: handleEdit.bind(null, record),
},
{
icon: 'ant-design:delete-outlined',
type: 'button',
color: 'error',
placement: 'left',
auth: ['demo2: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: 'Demo2',
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>
vim fu-admin/web/src/views/demo2/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>