mirror of
https://github.com/fastapiadmin/FastapiAdmin.git
synced 2026-09-23 05:10:57 +00:00
refactor: 重构代码结构并优化命名一致性
feat(backend): 新增模块示例和路径配置 fix(backend): 修复菜单模型名称唯一性约束 refactor(backend): 重构日志模块和路径配置 style(backend): 统一API方法命名规范 refactor(frontend): 重构API方法命名规范 fix(frontend): 修复权限校验问题 refactor: 移动定时任务测试文件到正确模块 docs: 更新模板文件路径 refactor: 优化日志记录和错误处理 fix: 修复排序默认值问题
This commit is contained in:
@@ -10,7 +10,7 @@ from app.core.router_class import OperationLogRoute
|
||||
from app.core.base_params import PaginationQueryParam
|
||||
from app.common.request import PaginationService
|
||||
from app.utils.common_util import bytes2file_response
|
||||
from app.core.logger import logger
|
||||
from app.core.logger import log
|
||||
|
||||
from app.api.v1.module_system.auth.schema import AuthSchema
|
||||
from .param import GenTableQueryParam
|
||||
@@ -40,7 +40,7 @@ async def gen_table_list_controller(
|
||||
"""
|
||||
result_dict_list = await GenTableService.get_gen_table_list_service(auth=auth, search=search)
|
||||
result_dict = await PaginationService.paginate(data_list=result_dict_list, page_no=page.page_no, page_size=page.page_size)
|
||||
logger.info('获取代码生成业务表列表成功')
|
||||
log.info('获取代码生成业务表列表成功')
|
||||
return SuccessResponse(data=result_dict, msg="获取代码生成业务表列表成功")
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ async def get_gen_db_table_list_controller(
|
||||
"""
|
||||
result_dict_list = await GenTableService.get_gen_db_table_list_service(auth=auth, search=search)
|
||||
result_dict = await PaginationService.paginate(data_list=result_dict_list, page_no=page.page_no, page_size=page.page_size)
|
||||
logger.info('获取数据库表列表成功')
|
||||
log.info('获取数据库表列表成功')
|
||||
return SuccessResponse(data=result_dict, msg="获取数据库表列表成功")
|
||||
|
||||
|
||||
@@ -84,7 +84,7 @@ async def import_gen_table_controller(
|
||||
"""
|
||||
add_gen_table_list = await GenTableService.get_gen_db_table_list_by_name_service(auth, table_names)
|
||||
result = await GenTableService.import_gen_table_service(auth, add_gen_table_list)
|
||||
logger.info('导入表结构成功')
|
||||
log.info('导入表结构成功')
|
||||
return SuccessResponse(msg="导入表结构成功", data=result)
|
||||
|
||||
|
||||
@@ -104,7 +104,7 @@ async def gen_table_detail_controller(
|
||||
- JSONResponse: 包含业务表详细信息的JSON响应
|
||||
"""
|
||||
gen_table_detail_result = await GenTableService.get_gen_table_detail_service(auth, table_id)
|
||||
logger.info(f'获取table_id为{table_id}的信息成功')
|
||||
log.info(f'获取table_id为{table_id}的信息成功')
|
||||
return SuccessResponse(data=gen_table_detail_result, msg="获取业务表详细信息成功")
|
||||
|
||||
|
||||
@@ -124,7 +124,7 @@ async def create_table_controller(
|
||||
- JSONResponse: 包含创建结果的JSON响应
|
||||
"""
|
||||
result = await GenTableService.create_table_service(auth, sql)
|
||||
logger.info('创建表结构成功')
|
||||
log.info('创建表结构成功')
|
||||
return SuccessResponse(msg="创建表结构成功", data=result)
|
||||
|
||||
|
||||
@@ -146,7 +146,7 @@ async def update_gen_table_controller(
|
||||
- JSONResponse: 包含编辑结果的JSON响应
|
||||
"""
|
||||
result_dict = await GenTableService.update_gen_table_service(auth, data, table_id)
|
||||
logger.info('编辑业务表信息成功')
|
||||
log.info('编辑业务表信息成功')
|
||||
return SuccessResponse(data=result_dict, msg="编辑业务表信息成功")
|
||||
|
||||
|
||||
@@ -166,14 +166,14 @@ async def delete_gen_table_controller(
|
||||
- JSONResponse: 包含删除结果的JSON响应
|
||||
"""
|
||||
result = await GenTableService.delete_gen_table_service(auth, ids)
|
||||
logger.info('删除业务表信息成功')
|
||||
log.info('删除业务表信息成功')
|
||||
return SuccessResponse(msg="删除业务表信息成功", data=result)
|
||||
|
||||
|
||||
@GenRouter.patch("/batch/output", summary="批量生成代码", description="批量生成代码")
|
||||
async def batch_gen_code_controller(
|
||||
table_names: List[str] = Body(..., description="表名列表"),
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_generator:gencode:operate"]))
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_generator:gencode:patch"]))
|
||||
) -> StreamResponse:
|
||||
"""
|
||||
批量生成代码
|
||||
@@ -186,7 +186,7 @@ async def batch_gen_code_controller(
|
||||
- StreamResponse: 包含批量生成代码的ZIP文件流响应
|
||||
"""
|
||||
batch_gen_code_result = await GenTableService.batch_gen_code_service(auth, table_names)
|
||||
logger.info(f'批量生成代码成功,表名列表:{table_names}')
|
||||
log.info(f'批量生成代码成功,表名列表:{table_names}')
|
||||
return StreamResponse(
|
||||
data=bytes2file_response(batch_gen_code_result),
|
||||
media_type='application/zip',
|
||||
@@ -210,7 +210,7 @@ async def gen_code_local_controller(
|
||||
- JSONResponse: 包含生成结果的JSON响应
|
||||
"""
|
||||
result = await GenTableService.generate_code_service(auth, table_name)
|
||||
logger.info(f'生成代码,表名:{table_name},到指定路径成功')
|
||||
log.info(f'生成代码,表名:{table_name},到指定路径成功')
|
||||
return SuccessResponse(msg="生成代码到指定路径成功", data=result)
|
||||
|
||||
|
||||
@@ -230,7 +230,7 @@ async def preview_code_controller(
|
||||
- JSONResponse: 包含预览代码的JSON响应
|
||||
"""
|
||||
preview_code_result = await GenTableService.preview_code_service(auth, table_id)
|
||||
logger.info(f'预览代码,表id:{table_id},成功')
|
||||
log.info(f'预览代码,表id:{table_id},成功')
|
||||
return SuccessResponse(data=preview_code_result, msg="预览代码成功")
|
||||
|
||||
|
||||
@@ -250,5 +250,5 @@ async def sync_db_controller(
|
||||
- JSONResponse: 包含同步数据库结果的JSON响应
|
||||
"""
|
||||
result = await GenTableService.sync_db_service(auth, table_name)
|
||||
logger.info(f'同步数据库,表名:{table_name},成功')
|
||||
log.info(f'同步数据库,表名:{table_name},成功')
|
||||
return SuccessResponse(msg="同步数据库成功", data=result)
|
||||
@@ -5,7 +5,7 @@ from sqlalchemy import and_, select, text
|
||||
from typing import List, Optional, Sequence, Dict, Union, Any
|
||||
from sqlglot.expressions import Expression
|
||||
|
||||
from app.core.logger import logger
|
||||
from app.core.logger import log
|
||||
from app.config.setting import settings
|
||||
from app.core.base_crud import CRUDBase
|
||||
|
||||
@@ -262,7 +262,7 @@ class GenTableCRUD(CRUDBase[GenTableModel, GenTableSchema, GenTableSchema]):
|
||||
else:
|
||||
gen_db_table_list = (await self.db.execute(text(query_sql), {"table_names": tuple(unique_table_names)})).fetchall()
|
||||
except Exception as e:
|
||||
logger.error(f"查询表信息时发生错误: {e}")
|
||||
log.error(f"查询表信息时发生错误: {e}")
|
||||
# 查询错误时直接抛出,不需要事务处理
|
||||
raise
|
||||
|
||||
@@ -299,7 +299,7 @@ class GenTableCRUD(CRUDBase[GenTableModel, GenTableSchema, GenTableSchema]):
|
||||
result = await self.db.execute(query, {"table_name": table_name})
|
||||
return result.scalar() is not None
|
||||
except Exception as e:
|
||||
logger.error(f"检查表格存在性时发生错误: {e}")
|
||||
log.error(f"检查表格存在性时发生错误: {e}")
|
||||
# 出错时返回False,避免误报表已存在
|
||||
return False
|
||||
|
||||
@@ -324,7 +324,7 @@ class GenTableCRUD(CRUDBase[GenTableModel, GenTableSchema, GenTableSchema]):
|
||||
await self.db.execute(text(sql))
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"创建表时发生错误: {e}")
|
||||
log.error(f"创建表时发生错误: {e}")
|
||||
return False
|
||||
|
||||
|
||||
@@ -487,7 +487,7 @@ class GenTableColumnCRUD(CRUDBase[GenTableColumnModel, GenTableColumnSchema, Gen
|
||||
)
|
||||
return columns_list
|
||||
except Exception as e:
|
||||
logger.error(f"获取表{table_name}的字段列表时出错: {str(e)}")
|
||||
log.error(f"获取表{table_name}的字段列表时出错: {str(e)}")
|
||||
# 确保即使出错也返回空列表而不是None
|
||||
raise
|
||||
|
||||
|
||||
@@ -7,13 +7,14 @@ from typing import Any, List, Dict, Literal, Optional
|
||||
from sqlglot.expressions import Add, Alter, Create, Delete, Drop, Expression, Insert, Table, TruncateTable, Update
|
||||
from sqlglot import parse as sqlglot_parse
|
||||
|
||||
from app.config.path_conf import BASE_DIR
|
||||
from app.config.setting import settings
|
||||
from app.core.logger import logger
|
||||
from app.core.logger import log
|
||||
from app.core.exceptions import CustomException
|
||||
from app.utils.gen_util import GenUtils
|
||||
from app.utils.jinja2_template_util import Jinja2TemplateUtil
|
||||
|
||||
from app.api.v1.module_system.auth.schema import AuthSchema
|
||||
from .tools.jinja2_template_util import Jinja2TemplateUtil
|
||||
from .tools.gen_util import GenUtils
|
||||
from .schema import GenTableSchema, GenTableOutSchema, GenTableColumnSchema, GenTableColumnOutSchema
|
||||
from .param import GenTableQueryParam
|
||||
from .crud import GenTableColumnCRUD, GenTableCRUD
|
||||
@@ -294,7 +295,7 @@ class GenTableService:
|
||||
table_out = GenTableOutSchema.model_validate(gen_table)
|
||||
result.append(table_out)
|
||||
except Exception as e:
|
||||
logger.warning(f"转换业务表时出错: {str(e)}")
|
||||
log.error(f"转换业务表时出错: {str(e)}")
|
||||
continue
|
||||
return result
|
||||
|
||||
@@ -318,7 +319,7 @@ class GenTableService:
|
||||
render_content = await env.get_template(template).render_async(**context)
|
||||
preview_code_result[template] = render_content
|
||||
except Exception as e:
|
||||
logger.error(f"渲染模板 {template} 时出错: {str(e)}")
|
||||
log.error(f"渲染模板 {template} 时出错: {str(e)}")
|
||||
# 即使某个模板渲染失败,也继续处理其他模板
|
||||
preview_code_result[template] = f"渲染错误: {str(e)}"
|
||||
return preview_code_result
|
||||
@@ -376,7 +377,7 @@ class GenTableService:
|
||||
render_content = await env.get_template(template_file).render_async(**render_info[2])
|
||||
zip_file.writestr(output_file, render_content)
|
||||
except Exception as e:
|
||||
logger.error(f"批量生成代码时处理表 {table_name} 出错: {str(e)}")
|
||||
log.error(f"批量生成代码时处理表 {table_name} 出错: {str(e)}")
|
||||
# 继续处理其他表,不中断整个过程
|
||||
continue
|
||||
|
||||
@@ -507,18 +508,18 @@ class GenTableService:
|
||||
try:
|
||||
file_name = Jinja2TemplateUtil.get_file_name(template, gen_table)
|
||||
# 默认写入到项目根目录(backend的上一级)
|
||||
project_root = str(settings.BASE_DIR.parent)
|
||||
project_root = str(BASE_DIR.parent)
|
||||
full_path = os.path.join(project_root, file_name)
|
||||
|
||||
# 确保路径在项目根目录内,防止路径遍历攻击
|
||||
if not os.path.abspath(full_path).startswith(os.path.abspath(project_root)):
|
||||
logger.warning(f"路径越界,回退到项目根目录: {file_name}")
|
||||
log.error(f"路径越界,回退到项目根目录: {file_name}")
|
||||
# 回退到项目根目录下的generated文件夹
|
||||
full_path = os.path.join(project_root, "generated", os.path.basename(file_name))
|
||||
|
||||
return full_path
|
||||
except Exception as e:
|
||||
logger.error(f"生成路径时出错: {str(e)}")
|
||||
log.error(f"生成路径时出错: {str(e)}")
|
||||
return None
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
|
||||
from fastapi import APIRouter, Depends, UploadFile, Body, Path
|
||||
from fastapi.responses import StreamingResponse, JSONResponse
|
||||
from app.common.response import SuccessResponse, StreamResponse
|
||||
from app.core.dependencies import AuthPermission
|
||||
from app.core.router_class import OperationLogRoute
|
||||
from app.api.v1.module_system.auth.schema import AuthSchema
|
||||
from app.core.base_params import PaginationQueryParam
|
||||
from app.utils.common_util import bytes2file_response
|
||||
from app.core.logger import log
|
||||
from app.core.base_schema import BatchSetAvailable
|
||||
|
||||
from .service import {{ class_name }}Service
|
||||
from .schema import {{ class_name }}CreateSchema, {{ class_name }}UpdateSchema
|
||||
from .param import {{ class_name }}QueryParam
|
||||
|
||||
{{ class_name }}Router = APIRouter(route_class=OperationLogRoute, prefix='/{{ business_name }}', tags=["{{ function_name }}模块"])
|
||||
|
||||
@{{ class_name }}Router.get("/detail/{id}", summary="获取{{ function_name }}详情", description="获取{{ function_name }}详情")
|
||||
async def get_{{ business_name }}_detail_controller(
|
||||
id: int = Path(..., description="ID"),
|
||||
auth: AuthSchema = Depends(AuthPermission(["{{ permission_prefix }}:query"]))
|
||||
) -> JSONResponse:
|
||||
"""获取{{ function_name }}详情接口"""
|
||||
result_dict = await {{ class_name }}Service.detail_{{ business_name }}_service(auth=auth, id=id)
|
||||
log.info(f"获取{{ function_name }}详情成功 {id}")
|
||||
return SuccessResponse(data=result_dict, msg="获取{{ function_name }}详情成功")
|
||||
|
||||
@{{ class_name }}Router.get("/list", summary="查询{{ function_name }}列表", description="查询{{ function_name }}列表")
|
||||
async def get_{{ business_name }}_list_controller(
|
||||
page: PaginationQueryParam = Depends(),
|
||||
search: {{ class_name }}QueryParam = Depends(),
|
||||
auth: AuthSchema = Depends(AuthPermission(["{{ permission_prefix }}:query"]))
|
||||
) -> JSONResponse:
|
||||
"""查询{{ function_name }}列表接口(数据库分页)"""
|
||||
result_dict = await {{ class_name }}Service.page_service(
|
||||
auth=auth,
|
||||
page_no=page.page_no if page.page_no is not None else 1,
|
||||
page_size=page.page_size if page.page_size is not None else 10,
|
||||
search=search,
|
||||
order_by=page.order_by
|
||||
)
|
||||
log.info("查询{{ function_name }}列表成功")
|
||||
return SuccessResponse(data=result_dict, msg="查询{{ function_name }}列表成功")
|
||||
|
||||
@{{ class_name }}Router.post("/create", summary="创建{{ function_name }}", description="创建{{ function_name }}")
|
||||
async def create_{{ business_name }}_controller(
|
||||
data: {{ class_name }}CreateSchema,
|
||||
auth: AuthSchema = Depends(AuthPermission(["{{ permission_prefix }}:create"]))
|
||||
) -> JSONResponse:
|
||||
"""创建{{ function_name }}接口"""
|
||||
result_dict = await {{ class_name }}Service.create_{{ business_name }}_service(auth=auth, data=data)
|
||||
log.info("创建{{ function_name }}成功")
|
||||
return SuccessResponse(data=result_dict, msg="创建{{ function_name }}成功")
|
||||
|
||||
@{{ class_name }}Router.put("/update/{id}", summary="修改{{ function_name }}", description="修改{{ function_name }}")
|
||||
async def update_{{ business_name }}_controller(
|
||||
data: {{ class_name }}UpdateSchema,
|
||||
id: int = Path(..., description="ID"),
|
||||
auth: AuthSchema = Depends(AuthPermission(["{{ permission_prefix }}:update"]))
|
||||
) -> JSONResponse:
|
||||
"""修改{{ function_name }}接口"""
|
||||
result_dict = await {{ class_name }}Service.update_{{ business_name }}_service(auth=auth, id=id, data=data)
|
||||
log.info("修改{{ function_name }}成功")
|
||||
return SuccessResponse(data=result_dict, msg="修改{{ function_name }}成功")
|
||||
|
||||
@{{ class_name }}Router.delete("/delete", summary="删除{{ function_name }}", description="删除{{ function_name }}")
|
||||
async def delete_{{ business_name }}_controller(
|
||||
ids: list[int] = Body(..., description="ID列表"),
|
||||
auth: AuthSchema = Depends(AuthPermission(["{{ permission_prefix }}:delete"]))
|
||||
) -> JSONResponse:
|
||||
"""删除{{ function_name }}接口"""
|
||||
await {{ class_name }}Service.delete_{{ business_name }}_service(auth=auth, ids=ids)
|
||||
log.info(f"删除{{ function_name }}成功: {ids}")
|
||||
return SuccessResponse(msg="删除{{ function_name }}成功")
|
||||
|
||||
@{{ class_name }}Router.patch("/available/setting", summary="批量修改{{ function_name }}状态", description="批量修改{{ function_name }}状态")
|
||||
async def batch_set_available_{{ business_name }}_controller(
|
||||
data: BatchSetAvailable,
|
||||
auth: AuthSchema = Depends(AuthPermission(["{{ permission_prefix }}:patch"]))
|
||||
) -> JSONResponse:
|
||||
"""批量修改{{ function_name }}状态接口"""
|
||||
await {{ class_name }}Service.set_available_{{ business_name }}_service(auth=auth, data=data)
|
||||
log.info(f"批量修改{{ function_name }}状态成功: {data.ids}")
|
||||
return SuccessResponse(msg="批量修改{{ function_name }}状态成功")
|
||||
|
||||
@{{ class_name }}Router.post('/export', summary="导出{{ function_name }}", description="导出{{ function_name }}")
|
||||
async def export_{{ business_name }}_list_controller(
|
||||
search: {{ class_name }}QueryParam = Depends(),
|
||||
auth: AuthSchema = Depends(AuthPermission(["{{ permission_prefix }}:export"]))
|
||||
) -> StreamingResponse:
|
||||
"""导出{{ function_name }}接口"""
|
||||
result_dict_list = await {{ class_name }}Service.list_{{ business_name }}_service(search=search, auth=auth)
|
||||
export_result = await {{ class_name }}Service.batch_export_service(obj_list=result_dict_list)
|
||||
log.info('导出{{ function_name }}成功')
|
||||
|
||||
return StreamResponse(
|
||||
data=bytes2file_response(export_result),
|
||||
media_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
headers={
|
||||
'Content-Disposition': 'attachment; filename={{ table_name }}.xlsx'
|
||||
}
|
||||
)
|
||||
|
||||
@{{ class_name }}Router.post('/import', summary="导入{{ function_name }}", description="导入{{ function_name }}")
|
||||
async def import_{{ business_name }}_list_controller(
|
||||
file: UploadFile,
|
||||
auth: AuthSchema = Depends(AuthPermission(["{{ permission_prefix }}:import"]))
|
||||
) -> JSONResponse:
|
||||
"""导入{{ function_name }}接口"""
|
||||
batch_import_result = await {{ class_name }}Service.batch_import_{{ business_name }}_service(file=file, auth=auth, update_support=True)
|
||||
log.info("导入{{ function_name }}成功")
|
||||
|
||||
return SuccessResponse(data=batch_import_result, msg="导入{{ function_name }}成功")
|
||||
|
||||
@{{ class_name }}Router.post('/download/template', summary="获取{{ function_name }}导入模板", description="获取{{ function_name }}导入模板", dependencies=[Depends(AuthPermission(["{{ permission_prefix }}:download"]))])
|
||||
async def export_{{ business_name }}_template_controller() -> StreamingResponse:
|
||||
"""获取{{ function_name }}导入模板接口"""
|
||||
example_import_template_result = await {{ class_name }}Service.import_template_download_{{ business_name }}_service()
|
||||
log.info('获取{{ function_name }}导入模板成功')
|
||||
|
||||
return StreamResponse(
|
||||
data=bytes2file_response(example_import_template_result),
|
||||
media_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
headers={
|
||||
'Content-Disposition': 'attachment; filename={{ table_name }}_template.xlsx'
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,123 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
|
||||
from typing import Dict, List, Optional, Sequence, Union, Any
|
||||
|
||||
from app.core.base_crud import CRUDBase
|
||||
from app.api.v1.module_system.auth.schema import AuthSchema
|
||||
from .model import {{ class_name }}Model
|
||||
from .schema import {{ class_name }}CreateSchema, {{ class_name }}UpdateSchema, {{ class_name }}OutSchema
|
||||
|
||||
|
||||
class {{ class_name }}CRUD(CRUDBase[{{ class_name }}Model, {{ class_name }}CreateSchema, {{ class_name }}UpdateSchema]):
|
||||
"""{{ function_name }}数据层"""
|
||||
|
||||
def __init__(self, auth: AuthSchema) -> None:
|
||||
"""
|
||||
初始化CRUD数据层
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
"""
|
||||
super().__init__(model={{ class_name }}Model, auth=auth)
|
||||
|
||||
async def get_by_id_{{ business_name }}_crud(self, id: int, preload: Optional[List[Union[str, Any]]] = None) -> Optional[{{ class_name }}Model]:
|
||||
"""
|
||||
详情
|
||||
|
||||
参数:
|
||||
- id (int): 对象ID
|
||||
- preload (Optional[List[Union[str, Any]]]): 预加载关系,未提供时使用模型默认项
|
||||
|
||||
返回:
|
||||
- Optional[{{ class_name }}Model]: 模型实例或None
|
||||
"""
|
||||
return await self.get(id=id, preload=preload)
|
||||
|
||||
async def list_{{ business_name }}_crud(self, search: Optional[Dict] = None, order_by: Optional[List[Dict[str, str]]] = None, preload: Optional[List[Union[str, Any]]] = None) -> Sequence[{{ class_name }}Model]:
|
||||
"""
|
||||
列表查询
|
||||
|
||||
参数:
|
||||
- search (Optional[Dict]): 查询参数
|
||||
- order_by (Optional[List[Dict[str, str]]]): 排序参数
|
||||
- preload (Optional[List[Union[str, Any]]]): 预加载关系,未提供时使用模型默认项
|
||||
|
||||
返回:
|
||||
- Sequence[{{ class_name }}Model]: 模型实例序列
|
||||
"""
|
||||
return await self.list(search=search, order_by=order_by, preload=preload)
|
||||
|
||||
async def create_{{ business_name }}_crud(self, data: {{ class_name }}CreateSchema) -> Optional[{{ class_name }}Model]:
|
||||
"""
|
||||
创建
|
||||
|
||||
参数:
|
||||
- data ({{ class_name }}CreateSchema): 创建模型
|
||||
|
||||
返回:
|
||||
- Optional[{{ class_name }}Model]: 模型实例或None
|
||||
"""
|
||||
return await self.create(data=data)
|
||||
|
||||
async def update_{{ business_name }}_crud(self, id: int, data: {{ class_name }}UpdateSchema) -> Optional[{{ class_name }}Model]:
|
||||
"""
|
||||
更新
|
||||
|
||||
参数:
|
||||
- id (int): 对象ID
|
||||
- data ({{ class_name }}UpdateSchema): 更新模型
|
||||
|
||||
返回:
|
||||
- Optional[{{ class_name }}Model]: 模型实例或None
|
||||
"""
|
||||
return await self.update(id=id, data=data)
|
||||
|
||||
async def delete_{{ business_name }}_crud(self, ids: List[int]) -> None:
|
||||
"""
|
||||
批量删除
|
||||
|
||||
参数:
|
||||
- ids (List[int]): 对象ID列表
|
||||
|
||||
返回:
|
||||
- None
|
||||
"""
|
||||
return await self.delete(ids=ids)
|
||||
|
||||
async def set_available_{{ business_name }}_crud(self, ids: List[int], status: bool) -> None:
|
||||
"""
|
||||
批量设置可用状态
|
||||
|
||||
参数:
|
||||
- ids (List[int]): 对象ID列表
|
||||
- status (bool): 可用状态
|
||||
|
||||
返回:
|
||||
- None
|
||||
"""
|
||||
return await self.set(ids=ids, status=status)
|
||||
|
||||
async def page_{{ business_name }}_crud(self, offset: int, limit: int, order_by: Optional[List[Dict[str, str]]] = None, search: Optional[Dict] = None, preload: Optional[List[Union[str, Any]]] = None) -> Dict:
|
||||
"""
|
||||
分页查询
|
||||
|
||||
参数:
|
||||
- offset (int): 偏移量
|
||||
- limit (int): 每页数量
|
||||
- order_by (Optional[List[Dict[str, str]]]): 排序参数
|
||||
- search (Optional[Dict]): 查询参数
|
||||
- preload (Optional[List[Union[str, Any]]]): 预加载关系,未提供时使用模型默认项
|
||||
|
||||
返回:
|
||||
- Dict: 分页数据
|
||||
"""
|
||||
order_by_list = order_by or [{'id': 'asc'}]
|
||||
search_dict = search or {}
|
||||
return await self.page(
|
||||
offset=offset,
|
||||
limit=limit,
|
||||
order_by=order_by_list,
|
||||
search=search_dict,
|
||||
out_schema={{ class_name }}OutSchema,
|
||||
preload=preload
|
||||
)
|
||||
@@ -0,0 +1,32 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
{% for model_import in model_import_list %}
|
||||
{{ model_import }}
|
||||
{% endfor %}
|
||||
{% if table.sub %}
|
||||
from sqlalchemy.orm import relationship
|
||||
{% endif %}
|
||||
from typing import Optional
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.base_model import CreatorMixin
|
||||
|
||||
|
||||
class {{ class_name }}Model(CreatorMixin):
|
||||
"""
|
||||
{{ function_name }}表
|
||||
"""
|
||||
|
||||
__tablename__ = '{{ table_name }}'
|
||||
__table_args__ = {'comment': '{{ function_name }}'}
|
||||
__loader_options__ = ["creator"]
|
||||
|
||||
{% for column in columns %}
|
||||
{% if column.column_name not in ['id', 'creator_id', 'description', 'created_at', 'updated_at'] %}
|
||||
{{ column.column_name }}: Mapped[Optional[{{ column.python_type }}]] = mapped_column({{ column.column_type|get_sqlalchemy_type }}, {% if column.pk %}primary_key=True, {% endif %}{% if column.increment %}autoincrement=True, {% endif %}{% if column.required or column.pk %}nullable=False{% else %}nullable=True{% endif %}, comment='{{ column.column_comment }}')
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% if table.sub %}
|
||||
{{ sub_class_name }}_list = relationship('{{ sub_class_name }}', back_populates='{{ business_name }}')
|
||||
{% endif %}
|
||||
@@ -0,0 +1,47 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from typing import Optional
|
||||
from fastapi import Query
|
||||
|
||||
from app.core.validator import DateTimeStr
|
||||
|
||||
class {{ class_name }}QueryParam:
|
||||
"""{{ function_name }}查询参数"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
{% for column in columns %}
|
||||
{% if column.query_type == 'LIKE' %}
|
||||
{{ column.column_name }}: Optional[{{ column.python_type }}] = Query(None, description="{{ column.column_comment }}"),
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% for column in columns %}
|
||||
{% if column.column_name == 'EQ' %}
|
||||
{{ column.column_name }}: Optional[{{ column.python_type }}] = Query(None, description="{{ column.column_comment }}"),
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
creator: Optional[int] = Query(None, description="创建人"),
|
||||
start_time: Optional[DateTimeStr] = Query(None, description="开始时间", example="2025-01-01 00:00:00"),
|
||||
end_time: Optional[DateTimeStr] = Query(None, description="结束时间", example="2025-12-31 23:59:59"),
|
||||
) -> None:
|
||||
|
||||
# 模糊查询字段
|
||||
{% for column in columns %}
|
||||
{% if column.query_type == 'LIKE' %}
|
||||
self.{{ column.column_name }} = ("like", {{ column.column_name }})
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
# 精确查询字段
|
||||
{% for column in columns %}
|
||||
{% if column.query_type == 'EQ' %}
|
||||
self.{{ column.column_name }} = {{ column.column_name }}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
self.creator_id = creator
|
||||
|
||||
# 时间范围查询
|
||||
if start_time and end_time:
|
||||
self.created_at = ("between", (start_time, end_time))
|
||||
@@ -0,0 +1,35 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
|
||||
{% if table.sub %}
|
||||
from typing import List, Optional
|
||||
{% else %}
|
||||
from typing import Optional
|
||||
{% endif %}
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
from app.core.base_schema import BaseSchema
|
||||
|
||||
class {{ class_name }}CreateSchema(BaseModel):
|
||||
"""
|
||||
{{ function_name }}新增模型
|
||||
"""
|
||||
|
||||
{% for column in columns %}
|
||||
{% if column.column_name not in ['id', 'creator_id', 'created_at', 'updated_at'] %}
|
||||
{{ column.column_name }}: Optional[{{ column.python_type }}] = Field(default=None, description='{{ column.column_comment }}')
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
|
||||
class {{ class_name }}UpdateSchema({{ class_name }}CreateSchema):
|
||||
"""
|
||||
{{ function_name }}更新模型
|
||||
"""
|
||||
...
|
||||
|
||||
|
||||
class {{ class_name }}OutSchema({{ class_name }}CreateSchema, BaseSchema):
|
||||
"""
|
||||
{{ function_name }}响应模型
|
||||
"""
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
@@ -0,0 +1,225 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
|
||||
import io
|
||||
from typing import Any, List, Dict, Optional
|
||||
from fastapi import UploadFile
|
||||
import pandas as pd
|
||||
|
||||
from app.core.base_schema import BatchSetAvailable
|
||||
from app.core.exceptions import CustomException
|
||||
from app.utils.excel_util import ExcelUtil
|
||||
from app.core.logger import log
|
||||
from app.api.v1.module_system.auth.schema import AuthSchema
|
||||
from .schema import {{ class_name }}CreateSchema, {{ class_name }}UpdateSchema, {{ class_name }}OutSchema
|
||||
from .param import {{ class_name }}QueryParam
|
||||
from .crud import {{ class_name }}CRUD
|
||||
|
||||
|
||||
class {{ class_name }}Service:
|
||||
"""
|
||||
{{ function_name }}服务层
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
async def detail_{{ business_name }}_service(cls, auth: AuthSchema, id: int) -> Dict:
|
||||
"""详情"""
|
||||
obj = await {{ class_name }}CRUD(auth).get_by_id_{{ business_name }}_crud(id=id)
|
||||
if not obj:
|
||||
raise CustomException(msg="该数据不存在")
|
||||
return {{ class_name }}OutSchema.model_validate(obj).model_dump()
|
||||
|
||||
@classmethod
|
||||
async def list_{{ business_name }}_service(cls, auth: AuthSchema, search: Optional[{{ class_name }}QueryParam] = None, order_by: Optional[List[Dict[str, str]]] = None) -> List[Dict]:
|
||||
"""列表查询"""
|
||||
search_dict = search.__dict__ if search else None
|
||||
obj_list = await {{ class_name }}CRUD(auth).list_{{ business_name }}_crud(search=search_dict, order_by=order_by)
|
||||
return [{{ class_name }}OutSchema.model_validate(obj).model_dump() for obj in obj_list]
|
||||
|
||||
@classmethod
|
||||
async def page_{{ business_name }}_service(cls, auth: AuthSchema, page_no: int, page_size: int, search: Optional[{{ class_name }}QueryParam] = None, order_by: Optional[List[Dict[str, str]]] = None) -> Dict:
|
||||
"""分页查询(数据库分页)"""
|
||||
search_dict = search.__dict__ if search else {}
|
||||
order_by_list = order_by or [{'id': 'asc'}]
|
||||
offset = (page_no - 1) * page_size
|
||||
result = await {{ class_name }}CRUD(auth).page_{{ business_name }}_crud(
|
||||
offset=offset,
|
||||
limit=page_size,
|
||||
order_by=order_by_list,
|
||||
search=search_dict
|
||||
)
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
async def create_{{ business_name }}_service(cls, auth: AuthSchema, data: {{ class_name }}CreateSchema) -> Dict:
|
||||
"""创建"""
|
||||
# 检查唯一性约束
|
||||
{% for column in columns %}
|
||||
{% if column.is_unique == '1' %}
|
||||
obj = await {{ class_name }}CRUD(auth).get({{ column.column_name }}=data.{{ column.column_name }})
|
||||
if obj:
|
||||
raise CustomException(msg='创建失败,{{ column.column_comment }}已存在')
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
obj = await {{ class_name }}CRUD(auth).create_{{ business_name }}_crud(data=data)
|
||||
return {{ class_name }}OutSchema.model_validate(obj).model_dump()
|
||||
|
||||
@classmethod
|
||||
async def update_{{ business_name }}_service(cls, auth: AuthSchema, id: int, data: {{ class_name }}UpdateSchema) -> Dict:
|
||||
"""更新"""
|
||||
# 检查数据是否存在
|
||||
obj = await {{ class_name }}CRUD(auth).get_by_id_{{ business_name }}_crud(id=id)
|
||||
if not obj:
|
||||
raise CustomException(msg='更新失败,该数据不存在')
|
||||
|
||||
# 检查唯一性约束
|
||||
{% for column in columns %}
|
||||
{% if column.is_unique == '1' %}
|
||||
exist_obj = await {{ class_name }}CRUD(auth).get({{ column.column_name }}=data.{{ column.column_name }})
|
||||
if exist_obj and exist_obj.id != id:
|
||||
raise CustomException(msg='更新失败,{{ column.column_comment }}重复')
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
obj = await {{ class_name }}CRUD(auth).update_{{ business_name }}_crud(id=id, data=data)
|
||||
return {{ class_name }}OutSchema.model_validate(obj).model_dump()
|
||||
|
||||
@classmethod
|
||||
async def delete_{{ business_name }}_service(cls, auth: AuthSchema, ids: List[int]) -> None:
|
||||
"""删除"""
|
||||
if len(ids) < 1:
|
||||
raise CustomException(msg='删除失败,删除对象不能为空')
|
||||
for id in ids:
|
||||
obj = await {{ class_name }}CRUD(auth).get_by_id_{{ business_name }}_crud(id=id)
|
||||
if not obj:
|
||||
raise CustomException(msg=f'删除失败,ID为{id}的数据不存在')
|
||||
await {{ class_name }}CRUD(auth).delete_{{ business_name }}_crud(ids=ids)
|
||||
|
||||
@classmethod
|
||||
async def set_available_{{ business_name }}_service(cls, auth: AuthSchema, data: BatchSetAvailable) -> None:
|
||||
"""批量设置状态"""
|
||||
await {{ class_name }}CRUD(auth).set_available_{{ business_name }}_crud(ids=data.ids, status=data.status)
|
||||
|
||||
@classmethod
|
||||
async def batch_export_{{ business_name }}_service(cls, obj_list: List[Dict[str, Any]]) -> bytes:
|
||||
"""批量导出"""
|
||||
mapping_dict = {
|
||||
{% for column in columns %}
|
||||
'{{ column.column_name }}': '{{ column.column_comment }}',
|
||||
{% endfor %}
|
||||
'creator': '创建者',
|
||||
}
|
||||
|
||||
data = obj_list.copy()
|
||||
for item in data:
|
||||
# 状态转换
|
||||
if 'status' in item:
|
||||
item['status'] = '正常' if item.get('status') else '停用'
|
||||
# 创建者转换
|
||||
creator_info = item.get('creator')
|
||||
if isinstance(creator_info, dict):
|
||||
item['creator'] = creator_info.get('name', '未知')
|
||||
elif creator_info is None:
|
||||
item['creator'] = '未知'
|
||||
|
||||
return ExcelUtil.export_list2excel(list_data=data, mapping_dict=mapping_dict)
|
||||
|
||||
@classmethod
|
||||
async def batch_import_{{ business_name }}_service(cls, auth: AuthSchema, file: UploadFile, update_support: bool = False) -> str:
|
||||
"""批量导入"""
|
||||
header_dict = {
|
||||
{% for column in columns %}
|
||||
'{{ column.column_comment }}': '{{ column.column_name }}',
|
||||
{% endfor %}
|
||||
}
|
||||
|
||||
try:
|
||||
contents = await file.read()
|
||||
df = pd.read_excel(io.BytesIO(contents))
|
||||
await file.close()
|
||||
|
||||
if df.empty:
|
||||
raise CustomException(msg="导入文件为空")
|
||||
|
||||
missing_headers = [header for header in header_dict.keys() if header not in df.columns]
|
||||
if missing_headers:
|
||||
raise CustomException(msg=f"导入文件缺少必要的列: {', '.join(missing_headers)}")
|
||||
|
||||
df.rename(columns=header_dict, inplace=True)
|
||||
|
||||
# 验证必填字段
|
||||
{% for column in columns %}
|
||||
{% if column.required == '1' %}
|
||||
missing_rows = df[df['{{ column.column_name }}'].isnull()].index.tolist()
|
||||
if missing_rows:
|
||||
raise CustomException(msg="{{ column.column_comment }}不能为空,第{0}行".format([i+1 for i in missing_rows]))
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
error_msgs = []
|
||||
success_count = 0
|
||||
count = 0
|
||||
|
||||
for index, row in df.iterrows():
|
||||
count += 1
|
||||
try:
|
||||
data = {
|
||||
{% for column in columns %}
|
||||
"{{ column.column_name }}": row['{{ column.column_name }}'],
|
||||
{% endfor %}
|
||||
}
|
||||
# 使用CreateSchema做校验后入库
|
||||
create_schema = {{ class_name }}CreateSchema.model_validate(data)
|
||||
|
||||
# 检查唯一性约束
|
||||
{% for column in columns %}
|
||||
{% if column.is_unique == '1' %}
|
||||
exists_obj = await {{ class_name }}CRUD(auth).get({{ column.column_name }}=create_schema.{{ column.column_name }})
|
||||
if exists_obj:
|
||||
if update_support:
|
||||
await {{ class_name }}CRUD(auth).update(id=exists_obj.id, data=create_schema)
|
||||
success_count += 1
|
||||
else:
|
||||
error_msgs.append(f"第{count}行: {{ column.column_comment }} {create_schema.{{ column.column_name }}} 已存在")
|
||||
continue
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
await {{ class_name }}CRUD(auth).create_crud(data=create_schema)
|
||||
success_count += 1
|
||||
except Exception as e:
|
||||
error_msgs.append(f"第{count}行: {str(e)}")
|
||||
continue
|
||||
|
||||
result = f"成功导入 {success_count} 条数据"
|
||||
if error_msgs:
|
||||
result += "\n错误信息:\n" + "\n".join(error_msgs)
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
log.error(f"批量导入失败: {str(e)}")
|
||||
raise CustomException(msg=f"导入失败: {str(e)}")
|
||||
|
||||
@classmethod
|
||||
async def import_template_download_{{ business_name }}_service(cls) -> bytes:
|
||||
"""下载导入模板"""
|
||||
header_list = [
|
||||
{% for column in columns %}
|
||||
'{{ column.column_comment }}',
|
||||
{% endfor %}
|
||||
]
|
||||
selector_header_list = []
|
||||
option_list = []
|
||||
|
||||
# 添加下拉选项
|
||||
{% for column in columns %}
|
||||
{% if column.html_type == 'select' and column.dict_type %}
|
||||
selector_header_list.append('{{ column.column_comment }}')
|
||||
option_list.append({'{{ column.column_comment }}': []})
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
return ExcelUtil.get_excel_template(
|
||||
header_list=header_list,
|
||||
selector_header_list=selector_header_list,
|
||||
option_list=option_list
|
||||
)
|
||||
@@ -0,0 +1,133 @@
|
||||
-- 统一的菜单 SQL(兼容 MySQL / PostgreSQL),对齐到 system_menu 表结构
|
||||
|
||||
{# 布尔值与保留字列名处理 #}
|
||||
{% set b_true = 1 if db_type == 'mysql' else true %}
|
||||
{% set b_false = 0 if db_type == 'mysql' else false %}
|
||||
{% set order_col = '`order`' if db_type == 'mysql' else '"order"' %}
|
||||
|
||||
{# 公共字段列表(按实际库字段) #}
|
||||
{# name, type, order, status, permission, icon, route_name, route_path, component_path, redirect, hidden, keep_alive, always_show, title, params, affix, parent_id, description, created_at, updated_at #}
|
||||
|
||||
{% if db_type == 'mysql' %}
|
||||
-- 父菜单(类型=2:菜单)
|
||||
INSERT INTO `system_menu` (name, type, {{ order_col }}, status, permission, icon, route_name, route_path, component_path, redirect, hidden, keep_alive, always_show, title, params, affix, parent_id, description, created_at, updated_at)
|
||||
VALUES (
|
||||
'{{ function_name }}',
|
||||
2,
|
||||
1,
|
||||
{{ b_true }},
|
||||
'{{ permission_prefix }}:query',
|
||||
NULL,
|
||||
'{{ business_name|snake_to_camel }}',
|
||||
'/{{ module_name }}/{{ business_name }}',
|
||||
'{{ module_name }}/{{ business_name }}/index',
|
||||
NULL,
|
||||
{{ b_false }},
|
||||
{{ b_true }},
|
||||
{{ b_false }},
|
||||
'{{ function_name }}',
|
||||
NULL,
|
||||
{{ b_false }},
|
||||
{{ parent_menu_id }},
|
||||
'{{ function_name }}菜单',
|
||||
now(),
|
||||
now()
|
||||
);
|
||||
|
||||
-- 获取父菜单ID(MySQL)
|
||||
SELECT @parentId := LAST_INSERT_ID();
|
||||
|
||||
-- 按钮权限(类型=3:按钮/权限)
|
||||
INSERT INTO `system_menu` (name, type, {{ order_col }}, status, permission, icon, route_name, route_path, component_path, redirect, hidden, keep_alive, always_show, title, params, affix, parent_id, description, created_at, updated_at)
|
||||
VALUES ('{{ function_name }}查询', 3, 1, {{ b_true }}, '{{ permission_prefix }}:query', NULL, NULL, NULL, NULL, NULL, {{ b_false }}, {{ b_true }}, {{ b_false }}, '{{ function_name }}查询', NULL, {{ b_false }}, @parentId, NULL, NOW(), NOW());
|
||||
INSERT INTO `system_menu` (name, type, {{ order_col }}, status, permission, icon, route_name, route_path, component_path, redirect, hidden, keep_alive, always_show, title, params, affix, parent_id, description, created_at, updated_at)
|
||||
VALUES ('{{ function_name }}新增', 3, 2, {{ b_true }}, '{{ permission_prefix }}:create', NULL, NULL, NULL, NULL, NULL, {{ b_false }}, {{ b_true }}, {{ b_false }}, '{{ function_name }}新增', NULL, {{ b_false }}, @parentId, NULL, NOW(), NOW());
|
||||
INSERT INTO `system_menu` (name, type, {{ order_col }}, status, permission, icon, route_name, route_path, component_path, redirect, hidden, keep_alive, always_show, title, params, affix, parent_id, description, created_at, updated_at)
|
||||
VALUES ('{{ function_name }}修改', 3, 3, {{ b_true }}, '{{ permission_prefix }}:update', NULL, NULL, NULL, NULL, NULL, {{ b_false }}, {{ b_true }}, {{ b_false }}, '{{ function_name }}修改', NULL, {{ b_false }}, @parentId, NULL, NOW(), NOW());
|
||||
INSERT INTO `system_menu` (name, type, {{ order_col }}, status, permission, icon, route_name, route_path, component_path, redirect, hidden, keep_alive, always_show, title, params, affix, parent_id, description, created_at, updated_at)
|
||||
VALUES ('{{ function_name }}删除', 3, 4, {{ b_true }}, '{{ permission_prefix }}:delete', NULL, NULL, NULL, NULL, NULL, {{ b_false }}, {{ b_true }}, {{ b_false }}, '{{ function_name }}删除', NULL, {{ b_false }}, @parentId, NULL, NOW(), NOW());
|
||||
INSERT INTO `system_menu` (name, type, {{ order_col }}, status, permission, icon, route_name, route_path, component_path, redirect, hidden, keep_alive, always_show, title, params, affix, parent_id, description, created_at, updated_at)
|
||||
VALUES ('{{ function_name }}导出', 3, 5, {{ b_true }}, '{{ permission_prefix }}:export', NULL, NULL, NULL, NULL, NULL, {{ b_false }}, {{ b_true }}, {{ b_false }}, '{{ function_name }}导出', NULL, {{ b_false }}, @parentId, NULL, NOW(), NOW());
|
||||
INSERT INTO `system_menu` (name, type, {{ order_col }}, status, permission, icon, route_name, route_path, component_path, redirect, hidden, keep_alive, always_show, title, params, affix, parent_id, description, created_at, updated_at)
|
||||
VALUES ('{{ function_name }}导入', 3, 6, {{ b_true }}, '{{ permission_prefix }}:import', NULL, NULL, NULL, NULL, NULL, {{ b_false }}, {{ b_true }}, {{ b_false }}, '{{ function_name }}导入', NULL, {{ b_false }}, @parentId, NULL, NOW(), NOW());
|
||||
INSERT INTO `system_menu` (name, type, {{ order_col }}, status, permission, icon, route_name, route_path, component_path, redirect, hidden, keep_alive, always_show, title, params, affix, parent_id, description, created_at, updated_at)
|
||||
VALUES ('{{ function_name }}批量状态修改', 3, 7, {{ b_true }}, '{{ permission_prefix }}:patch', NULL, NULL, NULL, NULL, NULL, {{ b_false }}, {{ b_true }}, {{ b_false }}, '{{ function_name }}批量状态修改', NULL, {{ b_false }}, @parentId, NULL, NOW(), NOW());
|
||||
INSERT INTO `system_menu` (name, type, {{ order_col }}, status, permission, icon, route_name, route_path, component_path, redirect, hidden, keep_alive, always_show, title, params, affix, parent_id, description, created_at, updated_at)
|
||||
VALUES ('{{ function_name }}下载导入模板', 3, 8, {{ b_true }}, '{{ permission_prefix }}:download', NULL, NULL, NULL, NULL, NULL, {{ b_false }}, {{ b_true }}, {{ b_false }}, '{{ function_name }}下载导入模板', NULL, {{ b_false }}, @parentId, NULL, NOW(), NOW());
|
||||
|
||||
{% elif db_type == 'postgres' %}
|
||||
-- 菜单 SQL(PostgreSQL DO 块方案)
|
||||
DO $$
|
||||
DECLARE
|
||||
parent_id INTEGER;
|
||||
BEGIN
|
||||
-- 插入父菜单并获取ID
|
||||
INSERT INTO public.system_menu (
|
||||
name, type, {{ order_col }}, status, permission, icon, route_name, route_path,
|
||||
component_path, redirect, hidden, keep_alive, always_show, title,
|
||||
params, affix, parent_id, description, created_at, updated_at
|
||||
)
|
||||
VALUES (
|
||||
'{{ function_name }}',
|
||||
2,
|
||||
1,
|
||||
{{ b_true }},
|
||||
'{{ permission_prefix }}:query',
|
||||
NULL,
|
||||
'{{ business_name|snake_to_camel }}',
|
||||
'/{{ module_name }}/{{ business_name }}',
|
||||
'{{ module_name }}/{{ business_name }}/index',
|
||||
NULL,
|
||||
{{ b_false }},
|
||||
{{ b_true }},
|
||||
{{ b_false }},
|
||||
'{{ function_name }}',
|
||||
NULL,
|
||||
{{ b_false }},
|
||||
{{ parent_menu_id }},
|
||||
'{{ function_name }}菜单',
|
||||
NOW(),
|
||||
NOW()
|
||||
) RETURNING id INTO parent_id;
|
||||
|
||||
-- 插入所有子菜单按钮(单条 INSERT 语句,性能更好)
|
||||
INSERT INTO public.system_menu (
|
||||
name, type, {{ order_col }}, status, permission, icon, route_name, route_path,
|
||||
component_path, redirect, hidden, keep_alive, always_show, title,
|
||||
params, affix, parent_id, description, created_at, updated_at
|
||||
) VALUES
|
||||
('{{ function_name }}查询', 3, 1, {{ b_true }}, '{{ permission_prefix }}:query', NULL, NULL, NULL, NULL, NULL, {{ b_false }}, {{ b_true }}, {{ b_false }}, '{{ function_name }}查询', NULL, {{ b_false }}, parent_id, NULL, NOW(), NOW()),
|
||||
('{{ function_name }}新增', 3, 2, {{ b_true }}, '{{ permission_prefix }}:create', NULL, NULL, NULL, NULL, NULL, {{ b_false }}, {{ b_true }}, {{ b_false }}, '{{ function_name }}新增', NULL, {{ b_false }}, parent_id, NULL, NOW(), NOW()),
|
||||
('{{ function_name }}修改', 3, 3, {{ b_true }}, '{{ permission_prefix }}:update', NULL, NULL, NULL, NULL, NULL, {{ b_false }}, {{ b_true }}, {{ b_false }}, '{{ function_name }}修改', NULL, {{ b_false }}, parent_id, NULL, NOW(), NOW()),
|
||||
('{{ function_name }}删除', 3, 4, {{ b_true }}, '{{ permission_prefix }}:delete', NULL, NULL, NULL, NULL, NULL, {{ b_false }}, {{ b_true }}, {{ b_false }}, '{{ function_name }}删除', NULL, {{ b_false }}, parent_id, NULL, NOW(), NOW()),
|
||||
('{{ function_name }}导出', 3, 5, {{ b_true }}, '{{ permission_prefix }}:export', NULL, NULL, NULL, NULL, NULL, {{ b_false }}, {{ b_true }}, {{ b_false }}, '{{ function_name }}导出', NULL, {{ b_false }}, parent_id, NULL, NOW(), NOW()),
|
||||
('{{ function_name }}导入', 3, 6, {{ b_true }}, '{{ permission_prefix }}:import', NULL, NULL, NULL, NULL, NULL, {{ b_false }}, {{ b_true }}, {{ b_false }}, '{{ function_name }}导入', NULL, {{ b_false }}, parent_id, NULL, NOW(), NOW()),
|
||||
('{{ function_name }}批量状态修改', 3, 7, {{ b_true }}, '{{ permission_prefix }}:patch', NULL, NULL, NULL, NULL, NULL, {{ b_false }}, {{ b_true }}, {{ b_false }}, '{{ function_name }}批量状态修改', NULL, {{ b_false }}, parent_id, NULL, NOW(), NOW()),
|
||||
('{{ function_name }}下载导入模板', 3, 8, {{ b_true }}, '{{ permission_prefix }}:download', NULL, NULL, NULL, NULL, NULL, {{ b_false }}, {{ b_true }}, {{ b_false }}, '{{ function_name }}下载导入模板', NULL, {{ b_false }}, parent_id, NULL, NOW(), NOW());
|
||||
|
||||
-- 可选:输出插入的父菜单ID(调试用)
|
||||
RAISE NOTICE '{{ function_name }}菜单创建完成,父菜单ID: %', parent_id;
|
||||
END $$;
|
||||
|
||||
{% else %}
|
||||
-- 未识别的数据库类型,默认按 MySQL 处理
|
||||
INSERT INTO `system_menu` (name, type, {{ order_col }}, status, permission, icon, route_name, route_path, component_path, redirect, hidden, keep_alive, always_show, title, params, affix, parent_id, description, created_at, updated_at)
|
||||
VALUES ('{{ function_name }}', 2, 1, {{ b_true }}, '{{ permission_prefix }}:query', NULL, '{{ business_name|snake_to_camel }}', '/{{ module_name }}/{{ business_name }}', '{{ module_name }}/{{ business_name }}/index', NULL, {{ b_false }}, {{ b_true }}, {{ b_false }}, '{{ function_name }}', NULL, {{ b_false }}, {{ parent_menu_id }}, '{{ function_name }}菜单', now(), now());
|
||||
SELECT @parentId := LAST_INSERT_ID();
|
||||
INSERT INTO `system_menu` (name, type, {{ order_col }}, status, permission, icon, route_name, route_path, component_path, redirect, hidden, keep_alive, always_show, title, params, affix, parent_id, description, created_at, updated_at)
|
||||
VALUES ('{{ function_name }}查询', 3, 1, {{ b_true }}, '{{ permission_prefix }}:query', NULL, NULL, NULL, NULL, NULL, {{ b_false }}, {{ b_true }}, {{ b_false }}, '{{ function_name }}查询', NULL, {{ b_false }}, @parentId, NULL, NOW(), NOW());
|
||||
INSERT INTO `system_menu` (name, type, {{ order_col }}, status, permission, icon, route_name, route_path, component_path, redirect, hidden, keep_alive, always_show, title, params, affix, parent_id, description, created_at, updated_at)
|
||||
VALUES ('{{ function_name }}新增', 3, 2, {{ b_true }}, '{{ permission_prefix }}:create', NULL, NULL, NULL, NULL, NULL, {{ b_false }}, {{ b_true }}, {{ b_false }}, '{{ function_name }}新增', NULL, {{ b_false }}, @parentId, NULL, NOW(), NOW());
|
||||
INSERT INTO `system_menu` (name, type, {{ order_col }}, status, permission, icon, route_name, route_path, component_path, redirect, hidden, keep_alive, always_show, title, params, affix, parent_id, description, created_at, updated_at)
|
||||
VALUES ('{{ function_name }}修改', 3, 3, {{ b_true }}, '{{ permission_prefix }}:update', NULL, NULL, NULL, NULL, NULL, {{ b_false }}, {{ b_true }}, {{ b_false }}, '{{ function_name }}修改', NULL, {{ b_false }}, @parentId, NULL, NOW(), NOW());
|
||||
INSERT INTO `system_menu` (name, type, {{ order_col }}, status, permission, icon, route_name, route_path, component_path, redirect, hidden, keep_alive, always_show, title, params, affix, parent_id, description, created_at, updated_at)
|
||||
VALUES ('{{ function_name }}删除', 3, 4, {{ b_true }}, '{{ permission_prefix }}:delete', NULL, NULL, NULL, NULL, NULL, {{ b_false }}, {{ b_true }}, {{ b_false }}, '{{ function_name }}删除', NULL, {{ b_false }}, @parentId, NULL, NOW(), NOW());
|
||||
INSERT INTO `system_menu` (name, type, {{ order_col }}, status, permission, icon, route_name, route_path, component_path, redirect, hidden, keep_alive, always_show, title, params, affix, parent_id, description, created_at, updated_at)
|
||||
VALUES ('{{ function_name }}导出', 3, 5, {{ b_true }}, '{{ permission_prefix }}:export', NULL, NULL, NULL, NULL, NULL, {{ b_false }}, {{ b_true }}, {{ b_false }}, '{{ function_name }}导出', NULL, {{ b_false }}, @parentId, NULL, NOW(), NOW());
|
||||
INSERT INTO `system_menu` (name, type, {{ order_col }}, status, permission, icon, route_name, route_path, component_path, redirect, hidden, keep_alive, always_show, title, params, affix, parent_id, description, created_at, updated_at)
|
||||
VALUES ('{{ function_name }}导入', 3, 6, {{ b_true }}, '{{ permission_prefix }}:import', NULL, NULL, NULL, NULL, NULL, {{ b_false }}, {{ b_true }}, {{ b_false }}, '{{ function_name }}导入', NULL, {{ b_false }}, @parentId, NULL, NOW(), NOW());
|
||||
INSERT INTO `system_menu` (name, type, {{ order_col }}, status, permission, icon, route_name, route_path, component_path, redirect, hidden, keep_alive, always_show, title, params, affix, parent_id, description, created_at, updated_at)
|
||||
VALUES ('{{ function_name }}批量状态修改', 3, 7, {{ b_true }}, '{{ permission_prefix }}:patch', NULL, NULL, NULL, NULL, NULL, {{ b_false }}, {{ b_true }}, {{ b_false }}, '{{ function_name }}批量状态修改', NULL, {{ b_false }}, @parentId, NULL, NOW(), NOW());
|
||||
INSERT INTO `system_menu` (name, type, {{ order_col }}, status, permission, icon, route_name, route_path, component_path, redirect, hidden, keep_alive, always_show, title, params, affix, parent_id, description, created_at, updated_at)
|
||||
VALUES ('{{ function_name }}下载导入模板', 3, 8, {{ b_true }}, '{{ permission_prefix }}:download', NULL, NULL, NULL, NULL, NULL, {{ b_false }}, {{ b_true }}, {{ b_false }}, '{{ function_name }}下载导入模板', NULL, {{ b_false }}, @parentId, NULL, NOW(), NOW());
|
||||
{% endif %}
|
||||
@@ -0,0 +1,132 @@
|
||||
import request from "@/utils/request";
|
||||
|
||||
const API_PATH = "/{{ module_name }}/{{ business_name|lower }}";
|
||||
|
||||
// 参考 demo.ts 的风格,提供标准的 CRUD 与导入/导出 API(TypeScript)
|
||||
const {{ class_name }}API = {
|
||||
// 列表查询
|
||||
list{{ class_name }}(query: {{ class_name }}PageQuery) {
|
||||
return request<ApiResponse<PageResult<{{ class_name }}Table[]>>>({
|
||||
url: `${API_PATH}/list`,
|
||||
method: "get",
|
||||
params: query,
|
||||
});
|
||||
},
|
||||
|
||||
// 详情查询
|
||||
detail{{ class_name }}(id: number) {
|
||||
return request<ApiResponse<{{ class_name }}Table>>({
|
||||
url: `${API_PATH}/detail/${id}`,
|
||||
method: "get",
|
||||
});
|
||||
},
|
||||
|
||||
// 新增
|
||||
create{{ class_name }}(data: {{ class_name }}Form) {
|
||||
return request<ApiResponse>({
|
||||
url: `${API_PATH}/create`,
|
||||
method: "post",
|
||||
data,
|
||||
});
|
||||
},
|
||||
|
||||
// 修改(带主键)
|
||||
update{{ class_name }}(id: number, data: {{ class_name }}Form) {
|
||||
return request<ApiResponse>({
|
||||
url: `${API_PATH}/update/${id}`,
|
||||
method: "put",
|
||||
data,
|
||||
});
|
||||
},
|
||||
|
||||
// 删除(支持批量)
|
||||
delete{{ class_name }}(ids: number[]) {
|
||||
return request<ApiResponse>({
|
||||
url: `${API_PATH}/delete`,
|
||||
method: "delete",
|
||||
data: ids,
|
||||
});
|
||||
},
|
||||
|
||||
// 导出
|
||||
export{{ class_name }}(query: {{ class_name }}PageQuery) {
|
||||
return request<Blob>({
|
||||
url: `${API_PATH}/export`,
|
||||
method: "post",
|
||||
data: query,
|
||||
responseType: "blob",
|
||||
});
|
||||
},
|
||||
|
||||
// 下载导入模板
|
||||
downloadTemplate{{ class_name }}() {
|
||||
return request<Blob>({
|
||||
url: `${API_PATH}/download/template`,
|
||||
method: "post",
|
||||
responseType: "blob",
|
||||
});
|
||||
},
|
||||
|
||||
// 导入
|
||||
import{{ class_name }}(data: FormData) {
|
||||
return request<ApiResponse>({
|
||||
url: `${API_PATH}/import`,
|
||||
method: "post",
|
||||
data,
|
||||
headers: { "Content-Type": "multipart/form-data" },
|
||||
});
|
||||
},
|
||||
// 批量启用/停用
|
||||
batchAvailable{{ class_name }}(body: { ids: number[]; status: boolean }) {
|
||||
return request<ApiResponse>({
|
||||
url: `${API_PATH}/available/setting`,
|
||||
method: "patch",
|
||||
data: body,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export default {{ class_name }}API;
|
||||
|
||||
// ------------------------------
|
||||
// TS 类型声明
|
||||
// ------------------------------
|
||||
|
||||
export interface {{ class_name }}PageQuery extends PageQuery {
|
||||
{% for column in columns %}
|
||||
{% if column.is_query == "1" and column.query_type != "BETWEEN" %}
|
||||
{{ column.python_field }}?: {{
|
||||
'boolean' if ('status' in (column.python_field|lower)) or (column.html_type == 'radio')
|
||||
else 'number' if column.is_pk == '1'
|
||||
else 'string'
|
||||
}};
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
// 时间范围查询(按示例统一字段名,如需按字段拆分可在页面层处理)
|
||||
start_time?: string;
|
||||
end_time?: string;
|
||||
}
|
||||
|
||||
export interface {{ class_name }}Table {
|
||||
{% for column in columns %}
|
||||
{{ column.python_field }}?: {{
|
||||
'boolean' if ('status' in (column.python_field|lower)) or (column.html_type == 'radio')
|
||||
else 'number' if column.is_pk == '1'
|
||||
else 'string'
|
||||
}};
|
||||
{% endfor %}
|
||||
creator?: creatorType;
|
||||
}
|
||||
|
||||
export interface {{ class_name }}Form {
|
||||
id?: number;
|
||||
{% for column in columns %}
|
||||
{% if column.is_insert == "1" or column.is_edit == "1" %}
|
||||
{{ column.python_field }}?: {{
|
||||
'boolean' if ('status' in (column.python_field|lower)) or (column.html_type == 'radio')
|
||||
else 'number' if column.is_pk == '1'
|
||||
else 'string'
|
||||
}};
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
}
|
||||
@@ -0,0 +1,646 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<!-- 搜索区域 -->
|
||||
<div v-show="visible" class="search-container">
|
||||
<el-form ref="queryFormRef" :model="queryFormData" label-suffix=":" :inline="true" @submit.prevent="handleQuery">
|
||||
{% for column in columns %}
|
||||
{% if column.is_query == "1" %}
|
||||
{% set dict_type = column.dict_type %}
|
||||
{% set column_comment = column.column_comment if column.column_comment else '' %}
|
||||
{% set parentheseIndex = column_comment.find("(") %}
|
||||
{% set comment = column_comment[:parentheseIndex] if parentheseIndex != -1 else column_comment %}
|
||||
|
||||
{% if column.html_type == "input" %}
|
||||
<el-form-item label="{{ comment }}" prop="{{ column.python_field }}">
|
||||
<el-input v-model="queryFormData.{{ column.python_field }}" placeholder="请输入{{ comment }}" clearable />
|
||||
</el-form-item>
|
||||
{% elif (column.html_type == "select" or column.html_type == "radio") and dict_type != "" %}
|
||||
<el-form-item label="{{ comment }}" prop="{{ column.python_field }}">
|
||||
<el-select v-model="queryFormData.{{ column.python_field }}" placeholder="请选择{{ comment }}" style="width: 180px" clearable>
|
||||
<el-option v-for="dict in dictStore.getDictArray('{{ dict_type }}')" :key="dict.dict_value" :label="dict.dict_label" :value="dict.dict_value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
{% elif (column.html_type == "select" or column.html_type == "radio") and dict_type %}
|
||||
<el-form-item label="{{ comment }}" prop="{{ column.python_field }}">
|
||||
<el-select v-model="queryFormData.{{ column.python_field }}" placeholder="请选择{{ comment }}" clearable>
|
||||
<el-option label="请选择字典生成" value="" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
{% elif column.html_type == "datetime" and column.query_type != "BETWEEN" %}
|
||||
<el-form-item label="{{ comment }}" prop="{{ column.python_field }}">
|
||||
<el-date-picker v-model="queryFormData.{{ column.python_field }}" type="date" value-format="YYYY-MM-DD" clearable placeholder="请选择{{ comment }}" />
|
||||
</el-form-item>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<!-- 可选:创建人选择与统一日期范围(展开后显示) -->
|
||||
<el-form-item v-if="isExpand" prop="creator" label="创建人">
|
||||
<UserTableSelect v-model="queryFormData.creator" @confirm-click="handleConfirm" @clear-click="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item v-if="isExpand" prop="start_time" label="创建时间">
|
||||
<DatePicker v-model="dateRange" @update:model-value="handleDateRangeChange" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button v-hasPerm="['{{ module_name }}:{{ business_name }}:query']" type="primary" icon="search" @click="handleQuery">查询</el-button>
|
||||
<el-button v-hasPerm="['{{ module_name }}:{{ business_name }}:query']" icon="refresh" @click="handleResetQuery">重置</el-button>
|
||||
<template v-if="isExpandable">
|
||||
<el-link class="ml-3" type="primary" underline="never" @click="isExpand = !isExpand">
|
||||
{{ '{{' }} isExpand ? "收起" : "展开" {{ '}}' }}
|
||||
<el-icon>
|
||||
<template v-if="isExpand">
|
||||
<ArrowUp />
|
||||
</template>
|
||||
<template v-else>
|
||||
<ArrowDown />
|
||||
</template>
|
||||
</el-icon>
|
||||
</el-link>
|
||||
</template>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<el-card shadow="hover" class="data-table">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>
|
||||
<el-tooltip content="{{ function_name }}列表">
|
||||
<QuestionFilled class="w-4 h-4 mx-1" />
|
||||
</el-tooltip>
|
||||
{{ function_name }}列表
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 功能区域 -->
|
||||
<div class="data-table__toolbar">
|
||||
<div class="data-table__toolbar--left">
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="1.5">
|
||||
<el-button v-hasPerm="['{{ module_name }}:{{ business_name }}:create']" type="success" icon="plus" @click="handleOpenDialog('create')">新增</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button v-hasPerm="['{{ module_name }}:{{ business_name }}:delete']" type="danger" icon="delete" :disabled="selectIds.length === 0" @click="handleDelete(selectIds)">批量删除</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-dropdown v-hasPerm="['{{ module_name }}:{{ business_name }}:batch']" trigger="click">
|
||||
<el-button type="default" :disabled="selectIds.length === 0" icon="ArrowDown">更多</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item icon="Check" @click="handleMoreClick(true)">批量启用</el-dropdown-item>
|
||||
<el-dropdown-item icon="CircleClose" @click="handleMoreClick(false)">批量停用</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
<div class="data-table__toolbar--right">
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="1.5">
|
||||
<el-tooltip content="导入">
|
||||
<el-button v-hasPerm="['{{ module_name }}:{{ business_name }}:import']" type="success" icon="upload" circle @click="handleOpenImportDialog" />
|
||||
</el-tooltip>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-tooltip content="导出">
|
||||
<el-button v-hasPerm="['{{ module_name }}:{{ business_name }}:export']" type="warning" icon="download" circle @click="handleOpenExportsModal" />
|
||||
</el-tooltip>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-tooltip content="搜索显示/隐藏">
|
||||
<el-button v-hasPerm="['*:*:*']" type="info" icon="search" circle @click="visible = !visible" />
|
||||
</el-tooltip>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-tooltip content="刷新">
|
||||
<el-button v-hasPerm="['{{ module_name }}:{{ business_name }}:refresh']" type="primary" icon="refresh" circle @click="handleRefresh" />
|
||||
</el-tooltip>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-popover placement="bottom" trigger="click">
|
||||
<template #reference>
|
||||
<el-button type="danger" icon="operation" circle></el-button>
|
||||
</template>
|
||||
<el-scrollbar max-height="350px">
|
||||
<template v-for="column in tableColumns" :key="column.prop">
|
||||
<el-checkbox v-if="column.prop" v-model="column.show" :label="column.label" />
|
||||
</template>
|
||||
</el-scrollbar>
|
||||
</el-popover>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 表格区域 -->
|
||||
<el-table
|
||||
ref="dataTableRef"
|
||||
v-loading="loading"
|
||||
:data="pageTableData"
|
||||
highlight-current-row
|
||||
class="data-table__content"
|
||||
:height="450"
|
||||
border
|
||||
stripe
|
||||
@selection-change="handleSelectionChange"
|
||||
>
|
||||
<template #empty>
|
||||
<el-empty :image-size="80" description="暂无数据" />
|
||||
</template>
|
||||
<el-table-column v-if="tableColumns.find((col) => col.prop === 'selection')?.show" type="selection" min-width="55" align="center" />
|
||||
<el-table-column v-if="tableColumns.find((col) => col.prop === 'index')?.show" fixed label="序号" min-width="60">
|
||||
<template #default="scope">
|
||||
{{ '{{' }} (queryFormData.page_no - 1) * queryFormData.page_size + scope.$index + 1 {{ '}}' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
{% for column in columns %}
|
||||
{% set python_field = column.python_field %}
|
||||
{% set column_comment = column.column_comment if column.column_comment else '' %}
|
||||
{% set parentheseIndex = column_comment.find("(") %}
|
||||
{% set comment = column_comment[:parentheseIndex] if parentheseIndex != -1 else column_comment %}
|
||||
{% if column.is_list == "1" %}
|
||||
<el-table-column v-if="tableColumns.find((col) => col.prop === '{{ python_field }}')?.show" label="{{ comment }}" prop="{{ python_field }}" min-width="140">
|
||||
{% if python_field == "status" %}
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.status ? 'success' : 'info'">{{ '{{' }} scope.row.status ? '启用' : '停用' {{ '}}' }}</el-tag>
|
||||
</template>
|
||||
{% elif python_field == "creator" %}
|
||||
<template #default="scope">
|
||||
<el-tag>{{ '{{' }} scope.row.creator?.name {{ '}}' }}</el-tag>
|
||||
</template>
|
||||
{% endif %}
|
||||
</el-table-column>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<el-table-column v-if="tableColumns.find(col => col.prop === 'operation')?.show" fixed="right" label="操作" align="center" min-width="180">
|
||||
<template #default="scope">
|
||||
<el-button v-hasPerm="['{{ module_name }}:{{ business_name }}:detail']" type="info" size="small" link icon="document" @click="handleOpenDialog('detail', scope.row.id)">详情</el-button>
|
||||
<el-button v-hasPerm="['{{ module_name }}:{{ business_name }}:update']" type="primary" size="small" link icon="edit" @click="handleOpenDialog('update', scope.row.id)">编辑</el-button>
|
||||
<el-button v-hasPerm="['{{ module_name }}:{{ business_name }}:delete']" type="danger" size="small" link icon="delete" @click="handleDelete([scope.row.id])">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页区域 -->
|
||||
<template #footer>
|
||||
<pagination v-model:total="total" v-model:page="queryFormData.page_no" v-model:limit="queryFormData.page_size" @pagination="loadingData" />
|
||||
</template>
|
||||
</el-card>
|
||||
|
||||
<!-- 弹窗区域 -->
|
||||
<el-dialog v-model="dialogVisible.visible" :title="dialogVisible.title" @close="handleCloseDialog">
|
||||
<!-- 详情 -->
|
||||
<template v-if="dialogVisible.type === 'detail'">
|
||||
<el-descriptions :column="4" border>
|
||||
{% for column in columns %}
|
||||
{% set python_field = column.python_field %}
|
||||
{% set column_comment = column.column_comment if column.column_comment else '' %}
|
||||
{% set parentheseIndex = column_comment.find("(") %}
|
||||
{% set comment = column_comment[:parentheseIndex] if parentheseIndex != -1 else column_comment %}
|
||||
<el-descriptions-item label="{{ comment }}" :span="2">
|
||||
{{ '{' }}{{ '{' }} detailFormData.{{ python_field }} {{ '}' }}{{ '}' }}
|
||||
</el-descriptions-item>
|
||||
{% endfor %}
|
||||
</el-descriptions>
|
||||
</template>
|
||||
<!-- 新增、编辑表单 -->
|
||||
<template v-else>
|
||||
<el-form ref="dataFormRef" :model="formData" :rules="rules" label-suffix=":" label-width="auto" label-position="right">
|
||||
{% for column in columns %}
|
||||
{% if column.is_insert == "1" or column.is_edit == "1" %}
|
||||
{% set dict_type = column.dict_type %}
|
||||
{% set column_comment = column.column_comment if column.column_comment else '' %}
|
||||
{% set parentheseIndex = column_comment.find("(") %}
|
||||
{% set comment = column_comment[:parentheseIndex] if parentheseIndex != -1 else column_comment %}
|
||||
{% set required = 'true' if column.is_nullable == '1' else 'false' %}
|
||||
|
||||
{% if column.python_field == "status" %}
|
||||
<el-form-item label="状态" prop="status" :required="true">
|
||||
<el-radio-group v-model="formData.status">
|
||||
<el-radio :value="true">启用</el-radio>
|
||||
<el-radio :value="false">停用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
{% elif column.html_type == "input" %}
|
||||
<el-form-item label="{{ comment }}" prop="{{ column.python_field }}" :required="{{ required }}">
|
||||
<el-input v-model="formData.{{ column.python_field }}" placeholder="请输入{{ comment }}" />
|
||||
</el-form-item>
|
||||
{% elif column.html_type == "textarea" %}
|
||||
<el-form-item label="{{ comment }}" prop="{{ column.python_field }}" :required="{{ required }}">
|
||||
<el-input v-model="formData.{{ column.python_field }}" type="textarea" placeholder="请输入{{ comment }}" rows="4" />
|
||||
</el-form-item>
|
||||
{% elif (column.html_type == "select" or column.html_type == "radio") and dict_type != "" %}
|
||||
<el-form-item label="{{ comment }}" prop="{{ column.python_field }}" :required="{{ required }}">
|
||||
<el-select v-model="formData.{{ column.python_field }}" placeholder="请选择{{ comment }}">
|
||||
<el-option v-for="dict in dictStore.getDictArray('{{ dict_type }}')" :key="dict.dict_value" :label="dict.dict_label" :value="dict.dict_value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
{% elif (column.html_type == "select" or column.html_type == "radio") and dict_type %}
|
||||
<el-form-item label="{{ comment }}" prop="{{ column.python_field }}" :required="{{ required }}">
|
||||
<el-select v-model="formData.{{ column.python_field }}" placeholder="请选择{{ comment }}">
|
||||
<el-option label="请选择字典生成" value="" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
{% elif column.html_type == "date" %}
|
||||
<el-form-item label="{{ comment }}" prop="{{ column.python_field }}" :required="{{ required }}">
|
||||
<el-date-picker v-model="formData.{{ column.python_field }}" type="date" value-format="YYYY-MM-DD" placeholder="请选择{{ comment }}" />
|
||||
</el-form-item>
|
||||
{% elif column.html_type == "datetime" %}
|
||||
<el-form-item label="{{ comment }}" prop="{{ column.python_field }}" :required="{{ required }}">
|
||||
<el-date-picker v-model="formData.{{ column.python_field }}" type="datetime" value-format="YYYY-MM-DD HH:mm:ss" placeholder="请选择{{ comment }}" />
|
||||
</el-form-item>
|
||||
{% elif column.html_type == "checkbox" %}
|
||||
<el-form-item label="{{ comment }}" prop="{{ column.python_field }}">
|
||||
<el-checkbox v-model="formData.{{ column.python_field }}">{{ comment }}</el-checkbox>
|
||||
</el-form-item>
|
||||
{% elif column.html_type == "imageUpload" %}
|
||||
<el-form-item label="{{ comment }}" prop="{{ column.python_field }}">
|
||||
<SingleImageUpload v-model="formData.{{ column.python_field }}" />
|
||||
</el-form-item>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</el-form>
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="handleCloseDialog">取消</el-button>
|
||||
<el-button v-if="dialogVisible.type !== 'detail'" v-hasPerm="['{{ module_name }}:{{ business_name }}:submit']" type="primary" @click="handleSubmit">确定</el-button>
|
||||
<el-button v-else v-hasPerm="['{{ module_name }}:{{ business_name }}:detail']" type="primary" @click="handleCloseDialog">确定</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 导入弹窗 -->
|
||||
<ImportModal
|
||||
v-model="importDialogVisible"
|
||||
:content-config="curdContentConfig"
|
||||
@upload="handleUpload"
|
||||
/>
|
||||
|
||||
<!-- 导出弹窗 -->
|
||||
<ExportModal
|
||||
v-model="exportsDialogVisible"
|
||||
:content-config="curdContentConfig"
|
||||
:query-params="queryFormData"
|
||||
:page-data="pageTableData"
|
||||
:selection-data="selectionRows"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineOptions({
|
||||
name: "{{ class_name }}",
|
||||
inheritAttrs: false,
|
||||
});
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { ResultEnum } from '@/enums/api/result.enum'
|
||||
import { QuestionFilled, ArrowUp, ArrowDown, Check, CircleClose } from '@element-plus/icons-vue'
|
||||
import { formatToDateTime } from '@/utils/dateUtil'
|
||||
import {{ class_name }}API, { {{ class_name }}PageQuery, {{ class_name }}Table, {{ class_name }}Form } from '@/api/{{ module_name }}/{{ business_name }}'
|
||||
import { useDictStore } from '@/store/index'
|
||||
import SingleImageUpload from '@/components/Upload/SingleImageUpload.vue'
|
||||
import ImportModal from '@/components/CURD/ImportModal.vue'
|
||||
import ExportModal from '@/components/CURD/ExportModal.vue'
|
||||
import DatePicker from '@/components/DatePicker/index.vue'
|
||||
import type { IContentConfig } from '@/components/CURD/types'
|
||||
|
||||
const visible = ref(true)
|
||||
const isExpand = ref(false)
|
||||
const isExpandable = ref(true)
|
||||
|
||||
const queryFormRef = ref()
|
||||
const dataFormRef = ref()
|
||||
const total = ref(0)
|
||||
const selectIds = ref<number[]>([])
|
||||
const selectionRows = ref<{{ class_name }}Table[]>([]);
|
||||
const loading = ref(false)
|
||||
|
||||
// 字典仓库与需要加载的字典类型
|
||||
const dictStore = useDictStore()
|
||||
const dictTypes = [
|
||||
{% for column in columns %}
|
||||
{% if column.dict_type %}
|
||||
'{{ column.dict_type }}',
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
]
|
||||
|
||||
// 分页表单
|
||||
const pageTableData = ref<{{ class_name }}Table[]>([]);
|
||||
|
||||
// 表格列配置(根据列生成,可显隐)
|
||||
const tableColumns = ref([
|
||||
{ prop: 'selection', label: '选择框', show: true },
|
||||
{ prop: 'index', label: '序号', show: true },
|
||||
{% for column in columns %}
|
||||
{% if column.is_list == "1" %}
|
||||
{ prop: '{{ column.python_field }}', label: '{{ column.column_comment or column.python_field }}', show: true },
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{ prop: 'operation', label: '操作', show: true }
|
||||
])
|
||||
|
||||
// 导出列(不含选择/序号/操作)
|
||||
const exportColumns = [
|
||||
{% for column in columns %}
|
||||
{% if column.is_list == "1" %}
|
||||
{ prop: '{{ column.python_field }}', label: '{{ column.column_comment or column.python_field }}' },
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
]
|
||||
|
||||
// 导入/导出配置
|
||||
const curdContentConfig = {
|
||||
permPrefix: '{{ module_name }}:{{ business_name }}',
|
||||
cols: exportColumns as any,
|
||||
importTemplate: () => {{ class_name }}API.downloadTemplate{{ class_name }}(),
|
||||
exportsAction: async (params: any) => {
|
||||
const query: any = { ...params };
|
||||
if (typeof query.status === 'string') {
|
||||
query.status = query.status === 'true';
|
||||
}
|
||||
query.page_no = 1;
|
||||
query.page_size = 9999;
|
||||
const all: any[] = [];
|
||||
while (true) {
|
||||
const res = await {{ class_name }}API.list{{ class_name }}(query)
|
||||
const items = res.data?.data?.items || []
|
||||
const total = res.data?.data?.total || 0
|
||||
all.push(...items)
|
||||
if (all.length >= total || items.length === 0) break
|
||||
query.page_no += 1
|
||||
}
|
||||
return all;
|
||||
},
|
||||
} as unknown as IContentConfig
|
||||
|
||||
// 弹窗状态
|
||||
const dialogVisible = reactive({
|
||||
title: '',
|
||||
visible: false,
|
||||
type: 'create', // 'create' | 'update' | 'detail'
|
||||
})
|
||||
|
||||
// 编辑表单
|
||||
const formData = reactive<{{ class_name }}Form>({
|
||||
id: undefined,
|
||||
{% for column in columns %}
|
||||
{% if column.is_insert == "1" or column.is_edit == "1" %}
|
||||
{{ column.python_field }}: undefined,
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
})
|
||||
|
||||
// 定义初始表单数据常量
|
||||
const initialFormData: {{ class_name }}Form = {
|
||||
id: undefined,
|
||||
{% for column in columns %}
|
||||
{% if column.is_insert == "1" or column.is_edit == "1" %}
|
||||
{{ column.python_field }}: {{ 'true' if column.python_field == 'status' else ('' if column.html_type == 'textarea' else 'undefined') }},
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
}
|
||||
|
||||
// 重置表单
|
||||
async function resetForm() {
|
||||
if (dataFormRef.value) {
|
||||
dataFormRef.value.resetFields();
|
||||
dataFormRef.value.clearValidate();
|
||||
}
|
||||
// 完全重置 formData 为初始状态
|
||||
Object.assign(formData, initialFormData);
|
||||
}
|
||||
|
||||
// 表单验证规则(必填项按 is_nullable 生成)
|
||||
const rules = reactive({
|
||||
{% for column in columns %}
|
||||
{% if column.is_insert == "1" or column.is_edit == "1" %}
|
||||
{% set required = 'true' if column.is_nullable == '1' else 'false' %}
|
||||
{{ column.python_field }}: [
|
||||
{ required: {{ required }}, message: '请输入{{ column.column_comment or column.python_field }}', trigger: 'blur' },
|
||||
],
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
})
|
||||
|
||||
// 详情表单
|
||||
const detailFormData = ref<{{ class_name }}Table>({});
|
||||
|
||||
|
||||
// 统一日期范围
|
||||
const dateRange = ref<[Date, Date] | []>([]);
|
||||
function handleDateRangeChange(range: [Date, Date]) {
|
||||
dateRange.value = range;
|
||||
if (range && range.length === 2) {
|
||||
queryFormData.start_time = formatToDateTime(range[0]);
|
||||
queryFormData.end_time = formatToDateTime(range[1]);
|
||||
} else {
|
||||
queryFormData.start_time = undefined;
|
||||
queryFormData.end_time = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// 查询参数
|
||||
const queryFormData = reactive<{{ class_name }}PageQuery>({
|
||||
page_no: 1,
|
||||
page_size: 10,
|
||||
{% for column in columns %}
|
||||
{% if column.is_query == "1" and column.query_type != "BETWEEN" %}
|
||||
{{ column.python_field }}: undefined,
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
start_time: undefined,
|
||||
end_time: undefined,
|
||||
creator: undefined,
|
||||
})
|
||||
|
||||
// 加载表格数据
|
||||
async function loadingData() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const response = await {{ class_name }}API.list{{ class_name }}(queryFormData);
|
||||
pageTableData.value = response.data.data.items;
|
||||
total.value = response.data.data.total;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 查询(重置页码后获取数据)
|
||||
async function handleQuery() {
|
||||
queryFormData.page_no = 1;
|
||||
loadingData();
|
||||
}
|
||||
|
||||
// 选择创建人后触发查询
|
||||
function handleConfirm() {
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
// 重置查询
|
||||
async function handleResetQuery() {
|
||||
queryFormRef.value.resetFields();
|
||||
queryFormData.page_no = 1;
|
||||
dateRange.value = [];
|
||||
queryFormData.start_time = undefined;
|
||||
queryFormData.end_time = undefined;
|
||||
loadingData();
|
||||
}
|
||||
|
||||
// 行复选框选中项变化
|
||||
function handleSelectionChange(selection: any[]) {
|
||||
selectIds.value = selection.map((item: any) => item.id)
|
||||
selectionRows.value = selection
|
||||
}
|
||||
|
||||
// 关闭弹窗
|
||||
async function handleCloseDialog() {
|
||||
dialogVisible.visible = false;
|
||||
resetForm();
|
||||
}
|
||||
|
||||
// 打开弹窗
|
||||
async function handleOpenDialog(type: 'create' | 'update' | 'detail', id?: number) {
|
||||
dialogVisible.type = type
|
||||
if (id) {
|
||||
const response = await {{ class_name }}API.detail{{ class_name }}(id);
|
||||
if (type === 'detail') {
|
||||
dialogVisible.title = '详情';
|
||||
Object.assign(detailFormData.value, response.data.data);
|
||||
} else if (type === 'update') {
|
||||
dialogVisible.title = '修改';
|
||||
Object.assign(formData, response.data.data);
|
||||
}
|
||||
} else {
|
||||
dialogVisible.title = '新增{{ function_name }}';
|
||||
formData.id = undefined;
|
||||
}
|
||||
dialogVisible.visible = true;
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
async function handleSubmit() {
|
||||
dataFormRef.value.validate(async (valid: any) => {
|
||||
if (valid) {
|
||||
loading.value = true
|
||||
try {
|
||||
const id = formData.id
|
||||
if (id) {
|
||||
await {{ class_name }}API.update{{ class_name }}(id, { id, ...formData });
|
||||
dialogVisible.visible = false;
|
||||
resetForm();
|
||||
handleCloseDialog();
|
||||
handleResetQuery();
|
||||
} else {
|
||||
await {{ class_name }}API.create{{ class_name }}(formData);
|
||||
dialogVisible.visible = false;
|
||||
resetForm();
|
||||
handleCloseDialog();
|
||||
handleResetQuery();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 删除、批量删除
|
||||
async function handleDelete(ids: number[]) {
|
||||
ElMessageBox.confirm('确认删除该项数据?', '警告', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
})
|
||||
.then(async () => {
|
||||
try {
|
||||
loading.value = true;
|
||||
await {{ class_name }}API.delete{{ class_name }}(ids);
|
||||
handleResetQuery()
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
ElMessageBox.close()
|
||||
})
|
||||
}
|
||||
|
||||
// 批量启用/停用
|
||||
async function handleMoreClick(status: boolean) {
|
||||
if (selectIds.value.length) {
|
||||
ElMessageBox.confirm(`确认${status ? '启用' : '停用'}该项数据?`, '警告', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
}).then(async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
await {{ class_name }}API.batchAvailable{{ class_name }}({ ids: selectIds.value, status });
|
||||
handleResetQuery()
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}).catch(() => {
|
||||
ElMessageBox.close()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 导入弹窗显示状态
|
||||
const importDialogVisible = ref(false)
|
||||
// 导出弹窗显示状态
|
||||
const exportsDialogVisible = ref(false)
|
||||
|
||||
// 打开导入弹窗
|
||||
function handleOpenImportDialog() {
|
||||
importDialogVisible.value = true
|
||||
}
|
||||
// 打开导出弹窗
|
||||
function handleOpenExportsModal() {
|
||||
exportsDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 处理上传
|
||||
const handleUpload = async (formData: FormData) => {
|
||||
try {
|
||||
const response = await {{ class_name }}API.import{{ class_name }}(formData);
|
||||
if (response.data.code === ResultEnum.SUCCESS) {
|
||||
ElMessage.success(`${response.data.msg},${response.data.data}`)
|
||||
importDialogVisible.value = false
|
||||
await handleQuery()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 列表刷新
|
||||
async function handleRefresh() {
|
||||
await loadingData()
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
// 预加载字典数据
|
||||
if (dictTypes.length > 0) {
|
||||
await dictStore.getDict(dictTypes)
|
||||
}
|
||||
loadingData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -0,0 +1,228 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import re
|
||||
from typing import List
|
||||
|
||||
from app.common.constant import GenConstant
|
||||
from app.utils.string_util import StringUtil
|
||||
|
||||
from app.api.v1.module_generator.gencode.schema import GenTableOutSchema, GenTableSchema, GenTableColumnSchema
|
||||
|
||||
|
||||
class GenUtils:
|
||||
"""代码生成器工具类"""
|
||||
|
||||
@classmethod
|
||||
def init_table(cls, gen_table: GenTableSchema) -> None:
|
||||
"""
|
||||
初始化表信息
|
||||
|
||||
参数:
|
||||
- gen_table (GenTableSchema): 业务表对象。
|
||||
|
||||
返回:
|
||||
- None
|
||||
"""
|
||||
# 只有当字段为None时才设置默认值
|
||||
gen_table.class_name = cls.convert_class_name(gen_table.table_name or "")
|
||||
gen_table.package_name = 'module_gencode'
|
||||
gen_table.module_name = gen_table.package_name.split('.')[-1]
|
||||
gen_table.business_name = gen_table.table_name
|
||||
gen_table.function_name = re.sub(r'(?:表|测试)', '', gen_table.table_comment or "")
|
||||
|
||||
@classmethod
|
||||
def init_column_field(cls, column: GenTableColumnSchema, table: GenTableOutSchema) -> None:
|
||||
"""
|
||||
初始化列属性字段
|
||||
|
||||
参数:
|
||||
- column (GenTableColumnSchema): 业务表字段对象。
|
||||
- table (GenTableOutSchema): 业务表对象。
|
||||
|
||||
返回:
|
||||
- None
|
||||
"""
|
||||
data_type = cls.get_db_type(column.column_type or "")
|
||||
column_name = column.column_name or ""
|
||||
if not table.id:
|
||||
raise ValueError("业务表ID不能为空")
|
||||
column.table_id = table.id
|
||||
column.python_field = cls.to_camel_case(column_name)
|
||||
# 只有当python_type为None时才设置默认类型
|
||||
column.python_type = StringUtil.get_mapping_value_by_key_ignore_case(GenConstant.DB_TO_PYTHON, data_type)
|
||||
|
||||
if column.column_length is None:
|
||||
column.column_length = ''
|
||||
|
||||
if column.column_default is None:
|
||||
column.column_default = ''
|
||||
|
||||
if column.html_type is None:
|
||||
if cls.arrays_contains(GenConstant.COLUMNTYPE_STR, data_type) or cls.arrays_contains(
|
||||
GenConstant.COLUMNTYPE_TEXT, data_type
|
||||
):
|
||||
# 字符串长度超过500设置为文本域
|
||||
column_length = cls.get_column_length(column.column_type or "")
|
||||
html_type = (
|
||||
GenConstant.HTML_TEXTAREA
|
||||
if column_length >= 500 or cls.arrays_contains(GenConstant.COLUMNTYPE_TEXT, data_type)
|
||||
else GenConstant.HTML_INPUT
|
||||
)
|
||||
column.html_type = html_type
|
||||
elif cls.arrays_contains(GenConstant.COLUMNTYPE_TIME, data_type):
|
||||
column.html_type = GenConstant.HTML_DATETIME
|
||||
elif cls.arrays_contains(GenConstant.COLUMNTYPE_NUMBER, data_type):
|
||||
column.html_type = GenConstant.HTML_INPUT
|
||||
elif column_name.lower().endswith("status"):
|
||||
column.html_type = GenConstant.HTML_RADIO
|
||||
elif column_name.lower().endswith("type") or column_name.lower().endswith("sex"):
|
||||
column.html_type = GenConstant.HTML_SELECT
|
||||
elif column_name.lower().endswith("image"):
|
||||
column.html_type = GenConstant.HTML_IMAGE_UPLOAD
|
||||
elif column_name.lower().endswith("file"):
|
||||
column.html_type = GenConstant.HTML_FILE_UPLOAD
|
||||
elif column_name.lower().endswith("content"):
|
||||
column.html_type = GenConstant.HTML_EDITOR
|
||||
else:
|
||||
column.html_type = GenConstant.HTML_INPUT
|
||||
|
||||
# 只有当is_insert为None时才设置插入字段(默认所有字段都需要插入)
|
||||
if column.is_insert:
|
||||
column.is_insert = GenConstant.REQUIRE
|
||||
else:
|
||||
column.is_insert = False
|
||||
|
||||
# 只有当is_edit为None时才设置编辑字段
|
||||
if not cls.arrays_contains(GenConstant.COLUMNNAME_NOT_EDIT, column_name) and not column.is_pk:
|
||||
column.is_edit = GenConstant.REQUIRE
|
||||
else:
|
||||
column.is_edit = False
|
||||
|
||||
# 只有当is_list为None时才设置列表字段
|
||||
if not cls.arrays_contains(GenConstant.COLUMNNAME_NOT_LIST, column_name) and not column.is_pk:
|
||||
column.is_list = GenConstant.REQUIRE
|
||||
else:
|
||||
column.is_list = False
|
||||
|
||||
# 只有当is_query为None时才设置查询字段
|
||||
if not cls.arrays_contains(GenConstant.COLUMNNAME_NOT_QUERY, column_name) and not column.is_pk:
|
||||
column.is_query = GenConstant.REQUIRE
|
||||
# 直接设置查询类型,因为我们已经确定这是一个查询字段
|
||||
if column_name.lower().endswith('name') or data_type in ['varchar', 'char', 'text']:
|
||||
column.query_type = GenConstant.QUERY_LIKE
|
||||
else:
|
||||
column.query_type = GenConstant.QUERY_EQ
|
||||
else:
|
||||
column.is_query = False
|
||||
column.query_type = None
|
||||
|
||||
@classmethod
|
||||
def arrays_contains(cls, arr, target_value) -> bool:
|
||||
"""
|
||||
检查目标值是否在数组中
|
||||
|
||||
注意:从根本上解决问题,现在确保传入的参数都是正确的类型:
|
||||
- arr 是列表类型,且在GenConstant中定义
|
||||
- target_value 不会是None
|
||||
|
||||
参数:
|
||||
- arr: 数组类型
|
||||
- target_value: 目标值
|
||||
|
||||
返回:
|
||||
- bool: 如果目标值在数组中,返回True;否则返回False
|
||||
"""
|
||||
# 从根本上解决问题,不再需要复杂的防御性检查
|
||||
# 因为现在我们确保传入的arr是GenConstant中定义的列表常量
|
||||
# 并且target_value在调用前已经被处理过不会是None
|
||||
|
||||
# 简单直接地执行包含检查
|
||||
target_str = str(target_value).lower()
|
||||
return any(str(item).lower() == target_str for item in arr)
|
||||
|
||||
@classmethod
|
||||
def convert_class_name(cls, table_name: str) -> str:
|
||||
"""
|
||||
表名转换成 Python 类名
|
||||
|
||||
参数:
|
||||
- table_name (str): 业务表名。
|
||||
|
||||
返回:
|
||||
- str: Python 类名。
|
||||
"""
|
||||
return StringUtil.convert_to_camel_case(table_name)
|
||||
|
||||
@classmethod
|
||||
def replace_first(cls, input_string: str, search_list: List[str]) -> str:
|
||||
"""
|
||||
批量替换前缀
|
||||
|
||||
参数:
|
||||
- input_string (str): 需要被替换的字符串。
|
||||
- search_list (List[str]): 可替换的字符串列表。
|
||||
|
||||
返回:
|
||||
- str: 替换后的字符串。
|
||||
"""
|
||||
for search_string in search_list:
|
||||
if input_string.startswith(search_string):
|
||||
return input_string.replace(search_string, '', 1)
|
||||
return input_string
|
||||
|
||||
@classmethod
|
||||
def get_db_type(cls, column_type: str) -> str:
|
||||
"""
|
||||
获取数据库类型字段
|
||||
|
||||
参数:
|
||||
- column_type (str): 字段类型。
|
||||
|
||||
返回:
|
||||
- str: 数据库类型。
|
||||
"""
|
||||
if '(' in column_type:
|
||||
return column_type.split('(')[0]
|
||||
return column_type
|
||||
|
||||
@classmethod
|
||||
def get_column_length(cls, column_type: str) -> int:
|
||||
"""
|
||||
获取字段长度
|
||||
|
||||
参数:
|
||||
- column_type (str): 字段类型,例如 'varchar(255)' 或 'decimal(10,2)'
|
||||
|
||||
返回:
|
||||
- int: 字段长度(优先取第一个长度值,无法解析时返回0)。
|
||||
"""
|
||||
if '(' in column_type:
|
||||
length = len(column_type.split('(')[1].split(')')[0])
|
||||
return length
|
||||
return 0
|
||||
|
||||
@classmethod
|
||||
def split_column_type(cls, column_type: str) -> List[str]:
|
||||
"""
|
||||
拆分列类型
|
||||
|
||||
参数:
|
||||
- column_type (str): 字段类型。
|
||||
|
||||
返回:
|
||||
- List[str]: 拆分结果。
|
||||
"""
|
||||
if '(' in column_type and ')' in column_type:
|
||||
return column_type.split('(')[1].split(')')[0].split(',')
|
||||
return []
|
||||
|
||||
@classmethod
|
||||
def to_camel_case(cls, text: str) -> str:
|
||||
"""
|
||||
将字符串转换为驼峰命名
|
||||
|
||||
param text: 需要转换的字符串
|
||||
:return: 驼峰命名
|
||||
"""
|
||||
parts = text.split('_')
|
||||
return parts[0] + ''.join(word.capitalize() for word in parts[1:])
|
||||
@@ -0,0 +1,395 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
|
||||
from datetime import datetime
|
||||
from jinja2.environment import Environment
|
||||
from jinja2 import Environment, FileSystemLoader, select_autoescape, Template
|
||||
from typing import List, Any, Set
|
||||
|
||||
from app.common.constant import GenConstant
|
||||
from app.config.path_conf import TEMPLATE_DIR
|
||||
from app.config.setting import settings
|
||||
from app.utils.common_util import CamelCaseUtil, SnakeCaseUtil
|
||||
from app.utils.string_util import StringUtil
|
||||
|
||||
from app.api.v1.module_generator.gencode.schema import GenTableOutSchema, GenTableColumnOutSchema
|
||||
|
||||
|
||||
class Jinja2TemplateUtil:
|
||||
"""
|
||||
模板处理工具类
|
||||
"""
|
||||
|
||||
# 项目路径
|
||||
FRONTEND_PROJECT_PATH = 'frontend'
|
||||
BACKEND_PROJECT_PATH = 'backend'
|
||||
# 默认上级菜单,系统工具
|
||||
DEFAULT_PARENT_MENU_ID = "3"
|
||||
|
||||
# 环境对象
|
||||
_env = None
|
||||
|
||||
@classmethod
|
||||
def get_env(cls):
|
||||
"""
|
||||
获取模板环境对象。
|
||||
|
||||
参数:
|
||||
- 无
|
||||
|
||||
返回:
|
||||
- Environment: Jinja2 环境对象。
|
||||
"""
|
||||
try:
|
||||
if cls._env is None:
|
||||
cls._env = Environment(
|
||||
loader=FileSystemLoader(TEMPLATE_DIR),
|
||||
autoescape=False, # 自动转义HTML
|
||||
trim_blocks=True, # 删除多余的空行
|
||||
lstrip_blocks=True, # 删除行首空格
|
||||
keep_trailing_newline=True, # 保留行尾换行符
|
||||
enable_async=True, # 开启异步支持
|
||||
)
|
||||
cls._env.filters.update(
|
||||
{
|
||||
'camel_to_snake': SnakeCaseUtil.camel_to_snake,
|
||||
'snake_to_camel': CamelCaseUtil.snake_to_camel,
|
||||
'get_sqlalchemy_type': cls.get_sqlalchemy_type,
|
||||
}
|
||||
)
|
||||
return cls._env
|
||||
except Exception as e:
|
||||
raise RuntimeError(f'初始化Jinja2模板引擎失败: {e}')
|
||||
|
||||
@classmethod
|
||||
def get_template(cls, template_path: str) -> Template:
|
||||
"""
|
||||
获取模板。
|
||||
|
||||
参数:
|
||||
- template_path (str): 模板路径。
|
||||
|
||||
返回:
|
||||
- Template: Jinja2 模板对象。
|
||||
|
||||
异常:
|
||||
- TemplateNotFound: 模板未找到时抛出。
|
||||
"""
|
||||
return cls.get_env().get_template(template_path)
|
||||
|
||||
@classmethod
|
||||
def prepare_context(cls, gen_table: GenTableOutSchema) -> dict[str, Any]:
|
||||
"""
|
||||
准备模板变量。
|
||||
|
||||
参数:
|
||||
- gen_table (GenTableOutSchema): 生成表的配置信息。
|
||||
|
||||
返回:
|
||||
- Dict[str, Any]: 模板上下文字典。
|
||||
"""
|
||||
# 处理options为None的情况
|
||||
# if not gen_table.options:
|
||||
# raise ValueError('请先完善生成配置信息')
|
||||
class_name = gen_table.class_name or ''
|
||||
module_name = gen_table.module_name or ''
|
||||
business_name = gen_table.business_name or ''
|
||||
package_name = gen_table.package_name or ''
|
||||
function_name = gen_table.function_name or ''
|
||||
|
||||
context = {
|
||||
'table_name': gen_table.table_name or '',
|
||||
'table_comment': gen_table.table_comment or '',
|
||||
'function_name': function_name if StringUtil.is_not_empty(function_name) else '【请填写功能名称】',
|
||||
'class_name': class_name,
|
||||
'module_name': module_name,
|
||||
'business_name': business_name,
|
||||
'base_package': cls.get_package_prefix(package_name),
|
||||
'package_name': package_name,
|
||||
'datetime': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
|
||||
'pk_column': gen_table.pk_column,
|
||||
'model_import_list': cls.get_model_import_list(gen_table),
|
||||
'schema_import_list': cls.get_schema_import_list(gen_table),
|
||||
'permission_prefix': cls.get_permission_prefix(module_name, business_name),
|
||||
'columns': gen_table.columns or [],
|
||||
'table': gen_table,
|
||||
'dicts': cls.get_dicts(gen_table),
|
||||
'db_type': settings.DATABASE_TYPE,
|
||||
'column_not_add_show': GenConstant.COLUMNNAME_NOT_ADD_SHOW,
|
||||
'column_not_edit_show': GenConstant.COLUMNNAME_NOT_EDIT_SHOW,
|
||||
'parent_menu_id': int(gen_table.parent_menu_id) if gen_table.parent_menu_id is not None else int(cls.DEFAULT_PARENT_MENU_ID),
|
||||
}
|
||||
|
||||
return context
|
||||
|
||||
@classmethod
|
||||
def get_template_list(cls):
|
||||
"""
|
||||
获取模板列表。
|
||||
|
||||
参数:
|
||||
- 无
|
||||
返回:
|
||||
- List[str]: 模板路径列表。
|
||||
"""
|
||||
templates = [
|
||||
'python/controller.py.j2',
|
||||
'python/service.py.j2',
|
||||
'python/crud.py.j2',
|
||||
'python/schema.py.j2',
|
||||
'python/param.py.j2',
|
||||
'python/model.py.j2',
|
||||
'sql/sql.sql.j2',
|
||||
'ts/api.ts.j2',
|
||||
'vue/index.vue.j2',
|
||||
]
|
||||
return templates
|
||||
|
||||
|
||||
@classmethod
|
||||
def get_file_name(cls, template: str, gen_table: GenTableOutSchema):
|
||||
"""
|
||||
根据模板生成文件名。
|
||||
|
||||
参数:
|
||||
- template (str): 模板路径字符串。
|
||||
- gen_table (GenTableOutSchema): 生成表的配置信息。
|
||||
|
||||
返回:
|
||||
- str: 模板生成的文件名。
|
||||
|
||||
异常:
|
||||
- ValueError: 当无法生成有效文件名时抛出。
|
||||
"""
|
||||
module_name = gen_table.module_name or ''
|
||||
business_name = gen_table.business_name or ''
|
||||
|
||||
# 验证必要的参数
|
||||
if not module_name or not business_name:
|
||||
raise ValueError(f"无法为模板 {template} 生成文件名:模块名或业务名未设置")
|
||||
|
||||
# 映射表方式简化
|
||||
template_mapping = {
|
||||
'controller.py.j2': f'{cls.BACKEND_PROJECT_PATH}/app/api/v1/{module_name}/{business_name}/controller.py',
|
||||
'service.py.j2': f'{cls.BACKEND_PROJECT_PATH}/app/api/v1/{module_name}/{business_name}/service.py',
|
||||
'crud.py.j2': f'{cls.BACKEND_PROJECT_PATH}/app/api/v1/{module_name}/{business_name}/crud.py',
|
||||
'model.py.j2': f'{cls.BACKEND_PROJECT_PATH}/app/api/v1/{module_name}/{business_name}/model.py',
|
||||
'param.py.j2': f'{cls.BACKEND_PROJECT_PATH}/app/api/v1/{module_name}/{business_name}/param.py',
|
||||
'schema.py.j2': f'{cls.BACKEND_PROJECT_PATH}/app/api/v1/{module_name}/{business_name}/schema.py',
|
||||
'sql.sql.j2': f'{cls.BACKEND_PROJECT_PATH}/sql/menu/{module_name}/{business_name}.sql',
|
||||
'api.ts.j2': f'{cls.FRONTEND_PROJECT_PATH}/src/api/{module_name}/{business_name}.ts',
|
||||
'index.vue.j2': f'{cls.FRONTEND_PROJECT_PATH}/src/views/{module_name}/{business_name}/index.vue'
|
||||
}
|
||||
|
||||
# 查找匹配的模板路径
|
||||
for key, path in template_mapping.items():
|
||||
if key in template:
|
||||
return path
|
||||
|
||||
# 默认处理
|
||||
template_name = template.split('/')[-1].replace('.j2', '')
|
||||
return f'{cls.BACKEND_PROJECT_PATH}/generated/{template_name}'
|
||||
|
||||
@classmethod
|
||||
def get_package_prefix(cls, package_name: str) -> str:
|
||||
"""
|
||||
获取包前缀。
|
||||
|
||||
参数:
|
||||
- package_name (str): 包名。
|
||||
|
||||
返回:
|
||||
- str: 包前缀。
|
||||
"""
|
||||
# 修复:当包名中不存在'.'时,直接返回原包名
|
||||
return package_name[: package_name.rfind('.')] if '.' in package_name else package_name
|
||||
|
||||
@classmethod
|
||||
def get_schema_import_list(cls, gen_table: GenTableOutSchema):
|
||||
"""
|
||||
获取schema模板导入包列表
|
||||
|
||||
:param gen_table: 生成表的配置信息
|
||||
:return: 导入包列表
|
||||
"""
|
||||
columns = gen_table.columns or []
|
||||
import_list = set()
|
||||
for column in columns:
|
||||
if column.python_type in GenConstant.TYPE_DATE:
|
||||
import_list.add(f'from datetime import {column.python_type}')
|
||||
elif column.python_type == GenConstant.TYPE_DECIMAL:
|
||||
import_list.add('from decimal import Decimal')
|
||||
if gen_table.sub:
|
||||
if gen_table.sub_table and gen_table.sub_table.columns:
|
||||
sub_columns = gen_table.sub_table.columns or []
|
||||
for sub_column in sub_columns:
|
||||
if sub_column.python_type in GenConstant.TYPE_DATE:
|
||||
import_list.add(f'from datetime import {sub_column.python_type}')
|
||||
elif sub_column.python_type == GenConstant.TYPE_DECIMAL:
|
||||
import_list.add('from decimal import Decimal')
|
||||
return cls.merge_same_imports(list(import_list), 'from datetime import')
|
||||
|
||||
@classmethod
|
||||
def get_model_import_list(cls, gen_table: GenTableOutSchema):
|
||||
"""
|
||||
获取do模板导入包列表
|
||||
|
||||
:param gen_table: 生成表的配置信息
|
||||
:return: 导入包列表
|
||||
"""
|
||||
columns = gen_table.columns or []
|
||||
import_list = set()
|
||||
|
||||
for column in columns:
|
||||
if column.column_type:
|
||||
data_type = cls.get_db_type(column.column_type)
|
||||
if data_type in GenConstant.COLUMNTYPE_GEOMETRY:
|
||||
import_list.add('from geoalchemy2 import Geometry')
|
||||
import_list.add(
|
||||
f'from sqlalchemy import {StringUtil.get_mapping_value_by_key_ignore_case(GenConstant.DB_TO_SQLALCHEMY, data_type)}'
|
||||
)
|
||||
if gen_table.sub:
|
||||
import_list.add('from sqlalchemy import ForeignKey')
|
||||
if gen_table.sub_table and gen_table.sub_table.columns:
|
||||
sub_columns = gen_table.sub_table.columns or []
|
||||
for sub_column in sub_columns:
|
||||
if sub_column.column_type:
|
||||
data_type = cls.get_db_type(sub_column.column_type)
|
||||
import_list.add(
|
||||
f'from sqlalchemy import {StringUtil.get_mapping_value_by_key_ignore_case(GenConstant.DB_TO_SQLALCHEMY, data_type)}'
|
||||
)
|
||||
return cls.merge_same_imports(list(import_list), 'from sqlalchemy import')
|
||||
|
||||
@classmethod
|
||||
def get_db_type(cls, column_type: str) -> str:
|
||||
"""
|
||||
获取数据库字段类型。
|
||||
|
||||
参数:
|
||||
- column_type (str): 字段类型字符串。
|
||||
|
||||
返回:
|
||||
- str: 数据库类型(去除长度等修饰)。
|
||||
"""
|
||||
if '(' in column_type:
|
||||
return column_type.split('(')[0]
|
||||
return column_type
|
||||
|
||||
@classmethod
|
||||
def merge_same_imports(cls, imports: List[str], import_start: str) -> List[str]:
|
||||
"""
|
||||
合并相同的导入语句。
|
||||
|
||||
参数:
|
||||
- imports (List[str]): 导入语句列表。
|
||||
- import_start (str): 导入语句的起始字符串。
|
||||
|
||||
返回:
|
||||
- List[str]: 合并后的导入语句列表。
|
||||
"""
|
||||
merged_imports = []
|
||||
_imports = []
|
||||
for import_stmt in imports:
|
||||
if import_stmt.startswith(import_start):
|
||||
imported_items = import_stmt.split('import')[1].strip()
|
||||
_imports.extend(imported_items.split(', '))
|
||||
else:
|
||||
merged_imports.append(import_stmt)
|
||||
|
||||
if _imports:
|
||||
merged_datetime_import = f'{import_start} {", ".join(_imports)}'
|
||||
merged_imports.append(merged_datetime_import)
|
||||
|
||||
return merged_imports
|
||||
|
||||
@classmethod
|
||||
def get_dicts(cls, gen_table: GenTableOutSchema):
|
||||
"""
|
||||
获取字典列表。
|
||||
|
||||
参数:
|
||||
- gen_table (GenTableOutSchema): 生成表的配置信息。
|
||||
|
||||
返回:
|
||||
- str: 以逗号分隔的字典类型字符串。
|
||||
"""
|
||||
columns = gen_table.columns or []
|
||||
dicts = set()
|
||||
cls.add_dicts(dicts, columns)
|
||||
# 处理sub_table为None的情况
|
||||
if gen_table.sub_table is not None:
|
||||
# 处理sub_table.columns为None的情况
|
||||
sub_columns = gen_table.sub_table.columns or []
|
||||
cls.add_dicts(dicts, sub_columns)
|
||||
return ', '.join(dicts)
|
||||
|
||||
@classmethod
|
||||
def add_dicts(cls, dicts: Set[str], columns: List[GenTableColumnOutSchema]):
|
||||
"""
|
||||
添加字典类型到集合。
|
||||
|
||||
参数:
|
||||
- dicts (Set[str]): 字典类型集合。
|
||||
- columns (List[GenTableColumnOutSchema]): 字段列表。
|
||||
|
||||
返回:
|
||||
- Set[str]: 更新后的字典类型集合。
|
||||
"""
|
||||
for column in columns:
|
||||
super_column = column.super_column if column.super_column is not None else '0'
|
||||
dict_type = column.dict_type or ''
|
||||
html_type = column.html_type or ''
|
||||
|
||||
if (
|
||||
not super_column
|
||||
and StringUtil.is_not_empty(dict_type)
|
||||
and StringUtil.equals_any_ignore_case(
|
||||
html_type, [GenConstant.HTML_SELECT, GenConstant.HTML_RADIO, GenConstant.HTML_CHECKBOX]
|
||||
)
|
||||
):
|
||||
dicts.add(f"'{dict_type}'")
|
||||
|
||||
@classmethod
|
||||
def get_permission_prefix(cls, module_name: str | None, business_name: str | None) -> str:
|
||||
"""
|
||||
获取权限前缀。
|
||||
|
||||
参数:
|
||||
- module_name (str | None): 模块名。
|
||||
- business_name (str | None): 业务名。
|
||||
|
||||
返回:
|
||||
- str: 权限前缀字符串。
|
||||
"""
|
||||
return f'{module_name}:{business_name}'
|
||||
|
||||
@classmethod
|
||||
def get_sqlalchemy_type(cls, column):
|
||||
"""
|
||||
获取 SQLAlchemy 类型。
|
||||
|
||||
参数:
|
||||
- column_type (Any): 列类型或包含 `column_type` 属性的对象。
|
||||
|
||||
返回:
|
||||
- str: SQLAlchemy 类型字符串。
|
||||
"""
|
||||
if '(' in column:
|
||||
column_type_list = column.split('(')
|
||||
if column_type_list[0] in GenConstant.COLUMNTYPE_STR:
|
||||
sqlalchemy_type = (
|
||||
StringUtil.get_mapping_value_by_key_ignore_case(
|
||||
GenConstant.DB_TO_SQLALCHEMY, column_type_list[0]
|
||||
)
|
||||
+ '('
|
||||
+ column_type_list[1]
|
||||
)
|
||||
else:
|
||||
sqlalchemy_type = StringUtil.get_mapping_value_by_key_ignore_case(
|
||||
GenConstant.DB_TO_SQLALCHEMY, column_type_list[0]
|
||||
)
|
||||
else:
|
||||
sqlalchemy_type = StringUtil.get_mapping_value_by_key_ignore_case(
|
||||
GenConstant.DB_TO_SQLALCHEMY, column
|
||||
)
|
||||
|
||||
return sqlalchemy_type
|
||||
Reference in New Issue
Block a user