mirror of
https://github.com/fastapiadmin/FastapiAdmin.git
synced 2026-09-21 20:55:14 +00:00
refactor: 重构代码生成模板与项目结构,新增发票PDF生成功能
1. 统一移动代码生成模板到根目录templates目录,删除旧模板文件 2. 调整SQLAlchemy导入顺序,优化代码格式 3. 重构查询参数类,复用BaseQueryParam基础能力 4. 新增发票管理模块:添加weasyprint依赖,实现本地PDF发票渲染 5. 完善邮件、菜单等模块的代码实现与注释 6. 修复SQL查询条件写法,使用is_(True)替代==True 7. 补全模型类的UserMixin、TenantMixin继承与配置
This commit is contained in:
@@ -1 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
@@ -1,243 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import urllib.parse
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Body, Depends, Path, UploadFile
|
||||
from fastapi.responses import JSONResponse, StreamingResponse
|
||||
|
||||
from app.common.response import ResponseSchema, StreamResponse, SuccessResponse
|
||||
from app.core.base_params import PaginationQueryParam
|
||||
from app.core.base_schema import AuthSchema, BatchSetAvailable, PageResultSchema
|
||||
from app.core.dependencies import AuthPermission
|
||||
from app.core.router_class import OperationLogRoute
|
||||
from app.utils.common_util import bytes2file_response
|
||||
|
||||
from .schema import {{ class_name }}CreateSchema, {{ class_name }}OutSchema, {{ class_name }}QueryParam, {{ class_name }}UpdateSchema
|
||||
from .service import {{ class_name }}Service
|
||||
|
||||
{{ class_name }}Router = APIRouter(route_class=OperationLogRoute, prefix="/{{ module_name }}", tags=["{{ function_name }}模块"])
|
||||
|
||||
|
||||
@{{ class_name }}Router.get(
|
||||
"/detail/{id}",
|
||||
summary="获取{{ function_name }}详情",
|
||||
response_model=ResponseSchema[{{ class_name }}OutSchema],
|
||||
)
|
||||
async def get_obj_detail_controller(
|
||||
id: Annotated[int, Path(description="{{ function_name }}ID")],
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["{{ permission_prefix }}:detail"]))],
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
获取{{ function_name }}详情
|
||||
|
||||
参数:
|
||||
- id (int): {{ function_name }}ID
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
|
||||
返回:
|
||||
- JSONResponse: 包含{{ function_name }}详情的JSON响应
|
||||
"""
|
||||
result_dict = await {{ class_name }}Service.detail_service(id=id, auth=auth)
|
||||
return SuccessResponse(data=result_dict, msg="获取{{ function_name }}详情成功")
|
||||
|
||||
|
||||
@{{ class_name }}Router.get(
|
||||
"/list",
|
||||
summary="分页查询{{ function_name }}",
|
||||
response_model=ResponseSchema[PageResultSchema[{{ class_name }}OutSchema]],
|
||||
)
|
||||
async def get_obj_list_controller(
|
||||
page: Annotated[PaginationQueryParam, Depends()],
|
||||
search: Annotated[{{ class_name }}QueryParam, Depends()],
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["{{ permission_prefix }}:query"]))],
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
查询{{ function_name }}列表
|
||||
|
||||
参数:
|
||||
- page (PaginationQueryParam): 分页查询参数
|
||||
- search ({{ class_name }}QueryParam): 查询参数
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
|
||||
返回:
|
||||
- JSONResponse: 包含{{ function_name }}列表分页信息的JSON响应
|
||||
"""
|
||||
result_dict = await {{ class_name }}Service.page_service(
|
||||
auth=auth,
|
||||
page_no=page.page_no,
|
||||
page_size=page.page_size,
|
||||
search=search,
|
||||
order_by=page.order_by,
|
||||
)
|
||||
return SuccessResponse(data=result_dict, msg="查询{{ function_name }}列表成功")
|
||||
|
||||
|
||||
@{{ class_name }}Router.post(
|
||||
"/create",
|
||||
summary="创建{{ function_name }}",
|
||||
response_model=ResponseSchema[{{ class_name }}OutSchema],
|
||||
)
|
||||
async def create_obj_controller(
|
||||
data: {{ class_name }}CreateSchema,
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["{{ permission_prefix }}:create"]))],
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
创建{{ function_name }}
|
||||
|
||||
参数:
|
||||
- data ({{ class_name }}CreateSchema): {{ function_name }}创建模型
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
|
||||
返回:
|
||||
- JSONResponse: 包含创建{{ function_name }}详情的JSON响应
|
||||
"""
|
||||
result_dict = await {{ class_name }}Service.create_service(auth=auth, data=data)
|
||||
return SuccessResponse(data=result_dict, msg="创建{{ function_name }}成功")
|
||||
|
||||
|
||||
@{{ class_name }}Router.put(
|
||||
"/update/{id}",
|
||||
summary="修改{{ function_name }}",
|
||||
response_model=ResponseSchema[{{ class_name }}OutSchema],
|
||||
)
|
||||
async def update_obj_controller(
|
||||
data: {{ class_name }}UpdateSchema,
|
||||
id: Annotated[int, Path(description="{{ function_name }}ID")],
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["{{ permission_prefix }}:update"]))],
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
修改{{ function_name }}
|
||||
|
||||
参数:
|
||||
- data ({{ class_name }}UpdateSchema): {{ function_name }}更新模型
|
||||
- id (int): {{ function_name }}ID
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
|
||||
返回:
|
||||
- JSONResponse: 包含修改{{ function_name }}详情的JSON响应
|
||||
"""
|
||||
result_dict = await {{ class_name }}Service.update_service(auth=auth, id=id, data=data)
|
||||
return SuccessResponse(data=result_dict, msg="修改{{ function_name }}成功")
|
||||
|
||||
|
||||
@{{ class_name }}Router.delete(
|
||||
"/delete",
|
||||
summary="删除{{ function_name }}",
|
||||
response_model=ResponseSchema[None],
|
||||
)
|
||||
async def delete_obj_controller(
|
||||
ids: Annotated[list[int], Body(description="ID列表")],
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["{{ permission_prefix }}:delete"]))],
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
删除{{ function_name }}
|
||||
|
||||
参数:
|
||||
- ids (list[int]): {{ function_name }}ID列表
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
|
||||
返回:
|
||||
- JSONResponse: 包含删除{{ function_name }}详情的JSON响应
|
||||
"""
|
||||
await {{ class_name }}Service.delete_service(auth=auth, ids=ids)
|
||||
return SuccessResponse(msg="删除{{ function_name }}成功")
|
||||
|
||||
|
||||
@{{ class_name }}Router.patch(
|
||||
"/available/setting",
|
||||
summary="批量修改{{ function_name }}状态",
|
||||
response_model=ResponseSchema[None],
|
||||
)
|
||||
async def batch_set_available_obj_controller(
|
||||
data: BatchSetAvailable,
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["{{ permission_prefix }}:patch"]))],
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
批量修改{{ function_name }}状态
|
||||
|
||||
参数:
|
||||
- data (BatchSetAvailable): 批量修改{{ function_name }}状态模型
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
|
||||
返回:
|
||||
- JSONResponse: 包含批量修改{{ function_name }}状态详情的JSON响应
|
||||
"""
|
||||
await {{ class_name }}Service.set_available_service(auth=auth, data=data)
|
||||
return SuccessResponse(msg="批量修改{{ function_name }}状态成功")
|
||||
|
||||
|
||||
@{{ class_name }}Router.post(
|
||||
"/export",
|
||||
summary="导出{{ function_name }}",
|
||||
)
|
||||
async def export_obj_list_controller(
|
||||
search: Annotated[{{ class_name }}QueryParam, Depends()],
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["{{ permission_prefix }}:export"]))],
|
||||
) -> StreamingResponse:
|
||||
"""
|
||||
导出{{ function_name }}
|
||||
|
||||
参数:
|
||||
- search ({{ class_name }}QueryParam): 查询参数
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
|
||||
返回:
|
||||
- StreamingResponse: 包含{{ function_name }}列表的Excel文件流响应
|
||||
"""
|
||||
result_dict_list = await {{ class_name }}Service.list_service(search=search, auth=auth)
|
||||
export_result = await {{ class_name }}Service.batch_export_service(obj_list=result_dict_list)
|
||||
|
||||
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 }}",
|
||||
response_model=ResponseSchema[str],
|
||||
)
|
||||
async def import_obj_list_controller(
|
||||
file: UploadFile,
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["{{ permission_prefix }}:import"]))],
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
导入{{ function_name }}
|
||||
|
||||
参数:
|
||||
- file (UploadFile): 导入的Excel文件
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
|
||||
返回:
|
||||
- JSONResponse: 包含导入{{ function_name }}详情的JSON响应
|
||||
"""
|
||||
batch_import_result = await {{ class_name }}Service.batch_import_service(
|
||||
file=file, auth=auth, update_support=True
|
||||
)
|
||||
return SuccessResponse(data=batch_import_result, msg="导入{{ function_name }}成功")
|
||||
|
||||
|
||||
@{{ class_name }}Router.post(
|
||||
"/download/template",
|
||||
summary="获取{{ function_name }}导入模板",
|
||||
dependencies=[Depends(AuthPermission(["{{ permission_prefix }}:download"]))],
|
||||
)
|
||||
async def export_obj_template_controller() -> StreamingResponse:
|
||||
"""
|
||||
获取{{ function_name }}导入模板
|
||||
|
||||
返回:
|
||||
- StreamingResponse: 包含{{ function_name }}导入模板的Excel文件流响应
|
||||
"""
|
||||
import_template_result = await {{ class_name }}Service.import_template_download_service()
|
||||
|
||||
return StreamResponse(
|
||||
data=bytes2file_response(import_template_result),
|
||||
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
headers={
|
||||
"Content-Disposition": f"attachment; filename={urllib.parse.quote('{{ function_name }}导入模板.xlsx')}",
|
||||
"Access-Control-Expose-Headers": "Content-Disposition",
|
||||
},
|
||||
)
|
||||
@@ -1,19 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
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 {{ 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)
|
||||
@@ -1,46 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
{% for model_import in model_import_list %}
|
||||
{{ model_import }}
|
||||
{% endfor %}
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.base_model import ModelMixin, TenantMixin, UserMixin
|
||||
{% if table.sub and not is_sub_entity %}
|
||||
from ..{{ sub_module_name }}.model import {{ sub_model_class_name }}
|
||||
{% endif %}
|
||||
|
||||
|
||||
class {{ class_name }}Model(ModelMixin, TenantMixin, UserMixin):
|
||||
"""
|
||||
{{ function_name }}表
|
||||
"""
|
||||
__tablename__: str = '{{ table_name }}'
|
||||
__table_args__: dict[str, str] = {'comment': '{{ function_name }}'}
|
||||
__loader_options__: list[str] = ["created_by", "updated_by", "deleted_by"{% if table.sub and not is_sub_entity %}, "{{ sub_rel_list_name }}"{% endif %}]
|
||||
|
||||
{% if not is_sub_entity %}
|
||||
{% for column in columns %}
|
||||
{% if column.column_name not in ['id', 'uuid', 'tenant_id', 'status', 'description', 'created_time', 'updated_time', 'created_id', 'updated_id', 'is_deleted', 'deleted_time', 'deleted_id'] %}
|
||||
{% set sqlalchemy_type = column|get_sqlalchemy_type %}
|
||||
{{ column.column_name }}: Mapped[{{ column.python_type }} | None] = mapped_column({{ sqlalchemy_type }}, {% if column.is_pk %}primary_key=True, {% endif %}{% if column.is_increment %}autoincrement=True, {% endif %}{% if (not column.is_nullable) or column.is_pk %}nullable=False{% else %}nullable=True{% endif %}, comment='{{ column.column_comment }}')
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% if table.sub %}
|
||||
{{ sub_rel_list_name }} = relationship('{{ sub_model_class_name }}', back_populates='{{ parent_rel_name }}')
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% for column in columns %}
|
||||
{% if column.column_name not in ['id', 'uuid', 'tenant_id', 'status', 'description', 'created_time', 'updated_time', 'created_id', 'updated_id', 'is_deleted', 'deleted_time', 'deleted_id'] %}
|
||||
{% set sqlalchemy_type = column|get_sqlalchemy_type %}
|
||||
{% if column.column_name == sub_table_fk_name %}
|
||||
{{ column.column_name }}: Mapped[{{ column.python_type }} | None] = mapped_column({{ sqlalchemy_type }}, ForeignKey('{{ parent_table_name }}.{{ parent_pk_column_name }}', ondelete='CASCADE'), {% if column.is_pk %}primary_key=True, {% endif %}{% if column.is_increment %}autoincrement=True, {% endif %}{% if (not column.is_nullable) or column.is_pk %}nullable=False{% else %}nullable=True{% endif %}, comment='{{ column.column_comment }}')
|
||||
{% else %}
|
||||
{{ column.column_name }}: Mapped[{{ column.python_type }} | None] = mapped_column({{ sqlalchemy_type }}, {% if column.is_pk %}primary_key=True, {% endif %}{% if column.is_increment %}autoincrement=True, {% endif %}{% if (not column.is_nullable) or column.is_pk %}nullable=False{% else %}nullable=True{% endif %}, comment='{{ column.column_comment }}')
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{{ parent_rel_name }} = relationship('{{ parent_model_class_name }}', back_populates='{{ parent_list_rel_name }}')
|
||||
{% endif %}
|
||||
@@ -1,87 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
{% if table.sub %}
|
||||
from typing import List
|
||||
{% endif %}
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
from fastapi import Query
|
||||
{% for import_stmt in schema_import_list %}
|
||||
{{ import_stmt }}
|
||||
{% endfor %}
|
||||
{# DateTimeStr 由 schema_import_list 在存在 created_time/updated_time 列时注入 #}
|
||||
from app.common.enums import QueueEnum
|
||||
from app.core.base_schema import BaseSchema, UserBySchema
|
||||
|
||||
class {{ class_name }}CreateSchema(BaseModel):
|
||||
"""
|
||||
{{ function_name }}新增模型
|
||||
"""
|
||||
{% for column in columns %}
|
||||
{% if column.column_name not in ['uuid', 'created_time', 'updated_time', 'created_id', 'updated_id', 'is_deleted', 'deleted_time', 'deleted_id'] and column.column_name != pk_column_name %}
|
||||
{% if column.column_name == 'status' %}
|
||||
{{ column.column_name }}: {{ column.python_type }} = Field(default="0", description='{{ column.column_comment }}')
|
||||
{% elif column.column_name == 'description' %}
|
||||
{{ column.column_name }}: str | None = Field(default=None, max_length=255, description='{{ column.column_comment }}')
|
||||
{% else %}
|
||||
{{ column.column_name }}: {{ column.python_type }} = Field(default=..., description='{{ column.column_comment }}')
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
|
||||
class {{ class_name }}UpdateSchema({{ class_name }}CreateSchema):
|
||||
"""
|
||||
{{ function_name }}更新模型
|
||||
"""
|
||||
...
|
||||
|
||||
|
||||
class {{ class_name }}OutSchema({{ class_name }}CreateSchema, BaseSchema, UserBySchema):
|
||||
"""
|
||||
{{ function_name }}响应模型
|
||||
"""
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class {{ class_name }}QueryParam:
|
||||
"""{{ function_name }}查询参数"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
{% for column in columns %}
|
||||
{% if column.is_query and column.query_type == 'LIKE' %}
|
||||
{{ column.column_name }}: str | None = Query(None, description="{{ column.column_comment }}"),
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% for column in columns %}
|
||||
{% if column.is_query and column.query_type == 'EQ' and column.column_name not in ['created_time', 'updated_time'] %}
|
||||
{{ column.column_name }}: {{ column.python_type }} | None = Query(None, description="{{ column.column_comment }}"),
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if 'created_time' in table_column_names %}
|
||||
created_time: list[DateTimeStr] | None = Query(None, description="创建时间范围", examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"]),
|
||||
{% endif %}
|
||||
{% if 'updated_time' in table_column_names %}
|
||||
updated_time: list[DateTimeStr] | None = Query(None, description="更新时间范围", examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"]),
|
||||
{% endif %}
|
||||
) -> None:
|
||||
{% for column in columns %}
|
||||
{% if column.is_query and column.query_type == 'LIKE' %}
|
||||
# 模糊查询字段
|
||||
self.{{ column.column_name }} = (QueueEnum.like.value, {{ column.column_name }})
|
||||
{% elif column.is_query and column.query_type == 'EQ' and column.column_name not in ['created_time', 'updated_time'] %}
|
||||
# 精确查询字段
|
||||
if {{ column.column_name }} is not None:
|
||||
self.{{ column.column_name }} = (QueueEnum.eq.value, {{ column.column_name }})
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if 'created_time' in table_column_names %}
|
||||
# 时间范围查询
|
||||
if created_time and len(created_time) == 2:
|
||||
self.created_time = (QueueEnum.between.value, (created_time[0], created_time[1]))
|
||||
{% endif %}
|
||||
{% if 'updated_time' in table_column_names %}
|
||||
if updated_time and len(updated_time) == 2:
|
||||
self.updated_time = (QueueEnum.between.value, (updated_time[0], updated_time[1]))
|
||||
{% endif %}
|
||||
{# created_id / updated_id 若为 EQ 查询列,已在上方 query_type 循环中处理 #}
|
||||
@@ -1,323 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import io
|
||||
import pandas as pd
|
||||
from fastapi import UploadFile
|
||||
|
||||
from app.api.v1.module_system.auth.schema import AuthSchema
|
||||
from app.core.base_schema import BatchSetAvailable
|
||||
from app.core.exceptions import CustomException
|
||||
from app.utils.excel_util import ExcelUtil
|
||||
|
||||
from .crud import {{ class_name }}CRUD
|
||||
from .schema import (
|
||||
{{ class_name }}CreateSchema,
|
||||
{{ class_name }}UpdateSchema,
|
||||
{{ class_name }}OutSchema,
|
||||
{{ class_name }}QueryParam
|
||||
)
|
||||
|
||||
class {{ class_name }}Service:
|
||||
"""
|
||||
{{ function_name }}服务层
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
async def detail_{{ business_name_slug }}_service(cls, auth: AuthSchema, id: int) -> dict:
|
||||
"""
|
||||
详情
|
||||
|
||||
参数:
|
||||
- auth: AuthSchema - 认证信息
|
||||
- id: int - 数据ID
|
||||
|
||||
返回:
|
||||
- dict - 数据详情
|
||||
"""
|
||||
obj = await {{ class_name }}CRUD(auth).get(id=id)
|
||||
if not obj:
|
||||
raise CustomException(msg="该数据不存在")
|
||||
return {{ class_name }}OutSchema.model_validate(obj).model_dump()
|
||||
|
||||
@classmethod
|
||||
async def list_{{ business_name_slug }}_service(cls, auth: AuthSchema, search: {{ class_name }}QueryParam | None = None, order_by: list[dict] | None = None) -> list[dict]:
|
||||
"""
|
||||
列表查询
|
||||
|
||||
参数:
|
||||
- auth: AuthSchema - 认证信息
|
||||
- search: {{ class_name }}QueryParam | None - 查询参数
|
||||
- order_by: list[dict] | None - 排序参数
|
||||
|
||||
返回:
|
||||
- list[dict] - 数据列表
|
||||
"""
|
||||
search_dict = search.__dict__ if search else None
|
||||
obj_list = await {{ class_name }}CRUD(auth).list(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_slug }}_service(cls, auth: AuthSchema, page_no: int, page_size: int, search: {{ class_name }}QueryParam | None = None, order_by: list[dict] | None = None) -> dict:
|
||||
"""
|
||||
分页查询(数据库分页)
|
||||
|
||||
参数:
|
||||
- auth: AuthSchema - 认证信息
|
||||
- page_no: int - 页码
|
||||
- page_size: int - 每页数量
|
||||
- search: {{ class_name }}QueryParam | None - 查询参数
|
||||
- order_by: list[dict] | None - 排序参数
|
||||
|
||||
返回:
|
||||
- dict - 分页查询结果
|
||||
"""
|
||||
search_dict = search.__dict__ if search else {}
|
||||
order_by_list = order_by or [{'{{ pk_column_name }}': 'asc'}]
|
||||
offset = (page_no - 1) * page_size
|
||||
result = await {{ class_name }}CRUD(auth).page(
|
||||
offset=offset,
|
||||
limit=page_size,
|
||||
order_by=order_by_list,
|
||||
search=search_dict,
|
||||
out_schema={{ class_name }}OutSchema
|
||||
)
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
async def create_{{ business_name_slug }}_service(cls, auth: AuthSchema, data: {{ class_name }}CreateSchema) -> dict:
|
||||
"""
|
||||
创建
|
||||
|
||||
参数:
|
||||
- auth: AuthSchema - 认证信息
|
||||
- data: {{ class_name }}CreateSchema - 创建数据
|
||||
|
||||
返回:
|
||||
- dict - 创建结果
|
||||
"""
|
||||
{% for column in columns %}
|
||||
{% if column.is_unique %}
|
||||
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(data=data)
|
||||
return {{ class_name }}OutSchema.model_validate(obj).model_dump()
|
||||
|
||||
@classmethod
|
||||
async def update_{{ business_name_slug }}_service(cls, auth: AuthSchema, id: int, data: {{ class_name }}UpdateSchema) -> dict:
|
||||
"""
|
||||
更新
|
||||
|
||||
参数:
|
||||
- auth: AuthSchema - 认证信息
|
||||
- id: int - 数据ID
|
||||
- data: {{ class_name }}UpdateSchema - 更新数据
|
||||
|
||||
返回:
|
||||
- dict - 更新结果
|
||||
"""
|
||||
# 检查数据是否存在
|
||||
obj = await {{ class_name }}CRUD(auth).get(id=id)
|
||||
if not obj:
|
||||
raise CustomException(msg='更新失败,该数据不存在')
|
||||
|
||||
# 检查唯一性约束
|
||||
{% for column in columns %}
|
||||
{% if column.is_unique %}
|
||||
exist_obj = await {{ class_name }}CRUD(auth).get({{ column.column_name }}=data.{{ column.column_name }})
|
||||
if exist_obj and getattr(exist_obj, '{{ pk_column_name }}') != id:
|
||||
raise CustomException(msg='更新失败,{{ column.column_comment }}重复')
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
obj = await {{ class_name }}CRUD(auth).update(id=id, data=data)
|
||||
return {{ class_name }}OutSchema.model_validate(obj).model_dump()
|
||||
|
||||
@classmethod
|
||||
async def delete_{{ business_name_slug }}_service(cls, auth: AuthSchema, ids: list[int]) -> None:
|
||||
"""
|
||||
删除
|
||||
|
||||
参数:
|
||||
- auth: AuthSchema - 认证信息
|
||||
- ids: list[int] - 数据ID列表
|
||||
|
||||
返回:
|
||||
- None
|
||||
"""
|
||||
if len(ids) < 1:
|
||||
raise CustomException(msg='删除失败,删除对象不能为空')
|
||||
for id in ids:
|
||||
obj = await {{ class_name }}CRUD(auth).get(id=id)
|
||||
if not obj:
|
||||
raise CustomException(msg=f'删除失败,ID为{id}的数据不存在')
|
||||
await {{ class_name }}CRUD(auth).delete(ids=ids)
|
||||
|
||||
@classmethod
|
||||
async def set_available_{{ business_name_slug }}_service(cls, auth: AuthSchema, data: BatchSetAvailable) -> None:
|
||||
"""
|
||||
批量设置状态
|
||||
|
||||
参数:
|
||||
- auth: AuthSchema - 认证信息
|
||||
- data: BatchSetAvailable - 批量设置状态数据
|
||||
|
||||
返回:
|
||||
- None
|
||||
"""
|
||||
await {{ class_name }}CRUD(auth).set(ids=data.ids, status=data.status)
|
||||
|
||||
@classmethod
|
||||
async def batch_export_{{ business_name_slug }}_service(cls, obj_list: list[dict]) -> bytes:
|
||||
"""
|
||||
批量导出
|
||||
|
||||
参数:
|
||||
- obj_list: list[dict] - 数据列表
|
||||
|
||||
返回:
|
||||
- bytes - 导出的Excel文件内容
|
||||
"""
|
||||
mapping_dict = {
|
||||
{% for column in columns %}
|
||||
'{{ column.column_name }}': '{{ column.column_comment }}',
|
||||
{% endfor %}
|
||||
}
|
||||
# 复制数据并转换状态
|
||||
data = obj_list.copy()
|
||||
for item in data:
|
||||
# 处理状态
|
||||
item["status"] = "启用" if item.get("status") == 0 else "停用"
|
||||
# 处理创建者
|
||||
creator_info = item.get("created_id")
|
||||
if isinstance(creator_info, dict):
|
||||
item["created_id"] = creator_info.get("name", "未知")
|
||||
else:
|
||||
item["created_id"] = "未知"
|
||||
|
||||
return ExcelUtil.export_list2excel(list_data=data, mapping_dict=mapping_dict)
|
||||
|
||||
@classmethod
|
||||
async def batch_import_{{ business_name_slug }}_service(cls, auth: AuthSchema, file: UploadFile, update_support: bool = False) -> str:
|
||||
"""
|
||||
批量导入
|
||||
|
||||
参数:
|
||||
- auth: AuthSchema - 认证信息
|
||||
- file: UploadFile - 上传的Excel文件
|
||||
- update_support: bool - 是否支持更新存在数据
|
||||
|
||||
返回:
|
||||
- str - 导入结果信息
|
||||
"""
|
||||
header_dict = {
|
||||
{% for column in columns %}
|
||||
'{{ column.column_comment }}': '{{ column.column_name }}',
|
||||
{% endfor %}
|
||||
}
|
||||
|
||||
try:
|
||||
# 读取Excel文件
|
||||
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.is_nullable is false and column.is_pk is false %}
|
||||
errors = []
|
||||
missing_rows = df[df['{{ column.column_name }}'].isnull()].index.tolist()
|
||||
if missing_rows:
|
||||
field_name = [k for k,v in header_dict.items() if v == '{{ column.column_name }}'][0]
|
||||
rows_str = "、".join([str(i+1) for i in missing_rows])
|
||||
errors.append(f"{field_name}不能为空,第{rows_str}行")
|
||||
if errors:
|
||||
raise CustomException(msg=f"导入失败,以下行缺少必要字段:\n{'; '.join(errors)}")
|
||||
{% 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 %}
|
||||
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=getattr(exists_obj, '{{ pk_column_name }}'), 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(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:
|
||||
logger.error(f"批量导入失败: {str(e)}")
|
||||
raise CustomException(msg=f"导入失败: {str(e)}")
|
||||
|
||||
@classmethod
|
||||
async def import_template_download_{{ business_name_slug }}_service(cls) -> bytes:
|
||||
"""
|
||||
下载导入模板
|
||||
|
||||
返回:
|
||||
- bytes - Excel文件的二进制数据
|
||||
"""
|
||||
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
|
||||
)
|
||||
@@ -1,126 +0,0 @@
|
||||
import { request } from "@utils";
|
||||
|
||||
// API 前缀来自分系统包 module_xxx → /xxx
|
||||
// 对齐 module_example/demo:业务接口固定为 /{prefix}/{module_name}
|
||||
const API_PATH = "/{{ api_route_prefix }}/{{ module_name }}";
|
||||
|
||||
const {{ class_name }}API = {
|
||||
get{{ class_name }}List(query: {{ class_name }}PageQuery) {
|
||||
return request<ApiResponse<PageResult<{{ class_name }}Table>>>({
|
||||
url: `${API_PATH}/list`,
|
||||
method: "get",
|
||||
params: query,
|
||||
});
|
||||
},
|
||||
|
||||
get{{ class_name }}Detail(query: number) {
|
||||
return request<ApiResponse<{{ class_name }}Table>>({
|
||||
url: `${API_PATH}/detail/${query}`,
|
||||
method: "get",
|
||||
});
|
||||
},
|
||||
|
||||
create{{ class_name }}(body: {{ class_name }}Form) {
|
||||
return request<ApiResponse>({
|
||||
url: `${API_PATH}/create`,
|
||||
method: "post",
|
||||
data: body,
|
||||
});
|
||||
},
|
||||
|
||||
update{{ class_name }}(id: number, body: {{ class_name }}Form) {
|
||||
return request<ApiResponse>({
|
||||
url: `${API_PATH}/update/${id}`,
|
||||
method: "put",
|
||||
data: body,
|
||||
});
|
||||
},
|
||||
|
||||
delete{{ class_name }}(body: number[]) {
|
||||
return request<ApiResponse>({
|
||||
url: `${API_PATH}/delete`,
|
||||
method: "delete",
|
||||
data: body,
|
||||
});
|
||||
},
|
||||
|
||||
batch{{ class_name }}(body: BatchType) {
|
||||
return request<ApiResponse>({
|
||||
url: `${API_PATH}/available/setting`,
|
||||
method: "patch",
|
||||
data: body,
|
||||
});
|
||||
},
|
||||
|
||||
export{{ class_name }}(body: {{ class_name }}PageQuery) {
|
||||
return request<Blob>({
|
||||
url: `${API_PATH}/export`,
|
||||
method: "post",
|
||||
data: body,
|
||||
responseType: "blob",
|
||||
});
|
||||
},
|
||||
|
||||
downloadTemplate{{ class_name }}() {
|
||||
return request<ApiResponse>({
|
||||
url: `${API_PATH}/download/template`,
|
||||
method: "post",
|
||||
responseType: "blob",
|
||||
});
|
||||
},
|
||||
|
||||
import{{ class_name }}(body: FormData) {
|
||||
return request<ApiResponse>({
|
||||
url: `${API_PATH}/import`,
|
||||
method: "post",
|
||||
data: body,
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data",
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export default {{ class_name }}API;
|
||||
|
||||
// ------------------------------
|
||||
// TS 类型声明
|
||||
// ------------------------------
|
||||
|
||||
/** 列表查询参数 */
|
||||
export interface {{ class_name }}PageQuery extends PageQuery {
|
||||
{% for column in columns %}
|
||||
{# 主键列默认不参与查询 #}
|
||||
{% if column.is_query and column.column_name != pk_column_name and column.column_name not in ['created_time', 'updated_time'] %}
|
||||
{{ column.column_name }}?: {{
|
||||
'string' if column.query_type == 'LIKE'
|
||||
else (column.python_type | python_to_ts_type)
|
||||
}};
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
created_time?: string[];
|
||||
updated_time?: string[];
|
||||
created_id?: number;
|
||||
updated_id?: number;
|
||||
}
|
||||
|
||||
/** 列表展示项 */
|
||||
export interface {{ class_name }}Table extends BaseType {
|
||||
{% for column in columns %}
|
||||
{% if column.column_name not in ['id', 'uuid', 'status', 'description', 'created_time', 'updated_time'] and column.column_name != pk_column_name %}
|
||||
{{ column.column_name }}?: {{ column.python_type | python_to_ts_type }};
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
created_by?: CommonType;
|
||||
updated_by?: CommonType;
|
||||
deleted_by?: CommonType;
|
||||
}
|
||||
|
||||
/** 新增/修改表单参数 */
|
||||
export interface {{ class_name }}Form extends BaseFormType {
|
||||
{% for column in columns %}
|
||||
{% if (column.is_insert or column.is_edit) and column.column_name not in ['uuid', 'status', 'description', 'created_time', 'updated_time', 'created_id', 'updated_id'] and column.column_name != pk_column_name %}
|
||||
{{ column.column_name }}?: {{ column.python_type | python_to_ts_type }};
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
}
|
||||
@@ -1,826 +0,0 @@
|
||||
{# =============================================================================
|
||||
# {{ function_name }} 页面
|
||||
# 架构:FaSearchBarWithAudit + FaTableHeader + FaTable + FaDialog
|
||||
# 与 module_example/demo 完全对齐
|
||||
# 页面路由:/{{ menu_route_first_segment }}/{{ module_name }}
|
||||
# ============================================================================= #}
|
||||
<template>
|
||||
<div class="fa-full-height">
|
||||
<FaSearchBarWithAudit
|
||||
v-show="showSearchBar"
|
||||
ref="searchBarRef"
|
||||
v-model="searchForm"
|
||||
:items="businessSearchItems"
|
||||
:rules="searchBarRules"
|
||||
:is-expand="false"
|
||||
:show-expand="true"
|
||||
:show-reset="true"
|
||||
:show-search="true"
|
||||
:disabled-search="false"
|
||||
:default-expanded="false"
|
||||
@search="handleSearch"
|
||||
@reset="onResetSearch"
|
||||
/>
|
||||
|
||||
<ElCard class="fa-table-card" :style="{ 'margin-top': showSearchBar ? '12px' : '0' }">
|
||||
<FaTableHeader
|
||||
v-model:columns="columnChecks"
|
||||
v-model:showSearchBar="showSearchBar"
|
||||
:loading="loading"
|
||||
@refresh="refreshData"
|
||||
>
|
||||
<template #left>
|
||||
<FaTableHeaderLeft
|
||||
:remove-ids="selectedIds"
|
||||
:perm-create="['{{ permission_prefix }}:create']"
|
||||
:perm-import="['{{ permission_prefix }}:import']"
|
||||
:perm-export="['{{ permission_prefix }}:export']"
|
||||
:perm-delete="['{{ permission_prefix }}:delete']"
|
||||
:perm-patch="['{{ permission_prefix }}:patch']"
|
||||
:delete-loading="batchDeleting"
|
||||
@add="openEditDialog('add')"
|
||||
@import="openImport"
|
||||
@export="openExport"
|
||||
@delete="handleBatchDelete"
|
||||
@more="runBatchStatus"
|
||||
/>
|
||||
</template>
|
||||
</FaTableHeader>
|
||||
|
||||
<FaTable
|
||||
ref="faTableRef"
|
||||
:loading="loading"
|
||||
:data="data"
|
||||
:columns="columns"
|
||||
:pagination="pagination"
|
||||
@selection-change="onTableSelectionChange"
|
||||
@pagination:size-change="handleSizeChange"
|
||||
@pagination:current-change="handleCurrentChange"
|
||||
/>
|
||||
</ElCard>
|
||||
|
||||
<FaDialog
|
||||
v-model="dialogVisible.visible"
|
||||
:title="dialogVisible.title"
|
||||
width="920px"
|
||||
dialog-class="crud-embed-dialog"
|
||||
modal-class="crud-embed-dialog"
|
||||
:form-mode="dialogVisible.type"
|
||||
:confirm-loading="submitLoading"
|
||||
@cancel="handleCloseDialog"
|
||||
@confirm="dialogVisible.type === 'detail' ? handleCloseDialog() : handleSubmit()"
|
||||
>
|
||||
<template v-if="dialogVisible.type === 'detail'">
|
||||
<FaDescriptions
|
||||
:column="4"
|
||||
:data="detailFormData"
|
||||
:items="detailItems"
|
||||
max-height="70vh"
|
||||
/>
|
||||
</template>
|
||||
<template v-else>
|
||||
<FaForm
|
||||
:key="formRenderKey"
|
||||
scrollbar
|
||||
max-height="70vh"
|
||||
ref="dataFormRef"
|
||||
v-model="formData"
|
||||
:items="dialogFormItems"
|
||||
:rules="rules"
|
||||
label-suffix=":"
|
||||
:label-width="100"
|
||||
label-position="right"
|
||||
:span="24"
|
||||
:gutter="16"
|
||||
:show-reset="false"
|
||||
:show-submit="false"
|
||||
class="crud-dialog-art-form"
|
||||
/>
|
||||
{% if table.sub %}
|
||||
<ElDivider>{{ table.sub_table.function_name }}列表</ElDivider>
|
||||
<div class="sub-table-section">
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<ElButton size="small" type="primary" @click="addSubRow">
|
||||
新增{{ table.sub_table.function_name }}
|
||||
</ElButton>
|
||||
</div>
|
||||
<ElTable :data="subTableData" size="small" border>
|
||||
{% for col in table.sub_table.columns %}
|
||||
{% if col.is_list and col.column_name != table.sub_table.pk_column.column_name %}
|
||||
{% set col_comment = col.column_comment if col.column_comment else col.column_name %}
|
||||
<ElTableColumn prop="{{ col.column_name }}" label="{{ col_comment }}" />
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<ElTableColumn label="操作" width="80" fixed="right">
|
||||
<template #default="{ $index }">
|
||||
<ElButton type="danger" size="small" link @click="removeSubRow($index)">
|
||||
删除
|
||||
</ElButton>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</ElTable>
|
||||
</div>
|
||||
{% endif %}
|
||||
</ElScrollbar>
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer" style="padding-right: var(--el-dialog-padding-primary)">
|
||||
<ElButton @click="handleCloseDialog">取消</ElButton>
|
||||
<ElButton v-if="dialogVisible.type !== 'detail'" type="primary" @click="handleSubmit">
|
||||
确定
|
||||
</ElButton>
|
||||
<ElButton v-else type="primary" @click="handleCloseDialog">确定</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</FaDialog>
|
||||
|
||||
<FaImportDialog
|
||||
v-model="importVisible"
|
||||
:content-config="importContentConfig"
|
||||
default-template-file-name="{{ module_name }}_import_template.xlsx"
|
||||
@upload="handleCrudImportUpload"
|
||||
/>
|
||||
|
||||
<FaExportDialog
|
||||
v-model="exportVisible"
|
||||
:content-config="exportContentConfig"
|
||||
:query-params="exportQueryParams"
|
||||
:page-data="data"
|
||||
:selection-data="selectedRows"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { h, computed, ref, reactive, onMounted } from "vue";
|
||||
import { useAuth } from "@/hooks/core/useAuth";
|
||||
import { renderTableOperationCell, type TableOperationAction } from "@/utils/table";
|
||||
import { useTable } from "@/hooks/core/useTable";
|
||||
import { useImportExport } from "@/hooks/core/useImportExport";
|
||||
import { useCrudDialog } from "@/hooks/core/useCrudDialog";
|
||||
import { useTableSelection } from "@/hooks/core/useTableSelection";
|
||||
import { cleanEmptyArrayParams, stripPaginationParams } from "@/utils/query";
|
||||
import type { IContentConfig, IObject } from "@/components/modal/types";
|
||||
import type { AuditSearchFormParams } from "@/components/forms/fa-search-bar/auditSearchFormItems";
|
||||
import type { FormItem } from "@/components/forms/fa-form/index.vue";
|
||||
import type { ColumnOption } from "@/types/component";
|
||||
import {{ class_name }}API, {
|
||||
type {{ class_name }}Form,
|
||||
type {{ class_name }}PageQuery,
|
||||
type {{ class_name }}Table,
|
||||
} from "@/api/{{ package_name }}/{{ module_name }}";
|
||||
import { ElTag, ElMessage } from "element-plus";
|
||||
import { useDictStore } from "@/stores/modules/dict";
|
||||
import { ResultEnum } from "@/enums/api/result.enum";
|
||||
|
||||
defineOptions({
|
||||
name: "{{ class_name }}",
|
||||
inheritAttrs: false,
|
||||
});
|
||||
|
||||
const { hasAuth } = useAuth();
|
||||
const dictStore = useDictStore();
|
||||
|
||||
type {{ class_name }}SearchFormParams = {
|
||||
{% for column in columns %}
|
||||
{% if column.is_query and column.column_name not in ['created_time', 'updated_time', 'created_id', 'updated_id'] and column.column_name != pk_column_name %}
|
||||
{{ column.column_name }}?: string;
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
} & AuditSearchFormParams;
|
||||
|
||||
function normalize{{ class_name }}Query(params: Record<string, unknown>): {{ class_name }}PageQuery {
|
||||
const p = { ...params } as Record<string, unknown>;
|
||||
if (Array.isArray(p.created_time) && p.created_time.length === 0) p.created_time = undefined;
|
||||
if (Array.isArray(p.updated_time) && p.updated_time.length === 0) p.updated_time = undefined;
|
||||
return p as unknown as {{ class_name }}PageQuery;
|
||||
}
|
||||
|
||||
const searchForm = ref<{{ class_name }}SearchFormParams>({
|
||||
{% for column in columns %}
|
||||
{% if column.is_query and column.column_name not in ['created_time', 'updated_time', 'created_id', 'updated_id'] and column.column_name != pk_column_name %}
|
||||
{{ column.column_name }}: undefined,
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
created_id: undefined,
|
||||
updated_id: undefined,
|
||||
created_time: [],
|
||||
updated_time: [],
|
||||
});
|
||||
|
||||
/** 搜索区域默认展开展示 */
|
||||
const showSearchBar = ref(true);
|
||||
|
||||
const searchBarRef = ref<InstanceType<typeof FaSearchBarWithAudit> | null>(null);
|
||||
const searchBarRules: Record<string, unknown> = {};
|
||||
|
||||
const statusOptions = ref([
|
||||
{ label: "启用", value: 0 },
|
||||
{ label: "停用", value: 1 },
|
||||
]);
|
||||
|
||||
/** 业务搜索项(审计四字段由 FaSearchBarWithAudit 自动追加) */
|
||||
const businessSearchItems = computed(() => [
|
||||
{% for column in columns %}
|
||||
{# 主键列默认不参与搜索 #}
|
||||
{% if column.is_query and column.column_name not in ['created_time', 'updated_time', 'created_id', 'updated_id'] and column.column_name != pk_column_name %}
|
||||
{% 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.column_name == "status" %}
|
||||
{
|
||||
label: "状态",
|
||||
key: "status",
|
||||
type: "select",
|
||||
props: {
|
||||
placeholder: "请选择状态",
|
||||
options: statusOptions.value,
|
||||
clearable: true,
|
||||
},
|
||||
span: 6,
|
||||
},
|
||||
{% elif column.html_type == "input" %}
|
||||
{
|
||||
label: "{{ comment }}",
|
||||
key: "{{ column.column_name }}",
|
||||
type: "input",
|
||||
placeholder: "请输入{{ comment }}",
|
||||
clearable: true,
|
||||
span: 6,
|
||||
},
|
||||
{% elif (column.html_type == "select" or column.html_type == "radio") and dict_type != "" %}
|
||||
{
|
||||
label: "{{ comment }}",
|
||||
key: "{{ column.column_name }}",
|
||||
type: "select",
|
||||
props: {
|
||||
placeholder: "请选择{{ comment }}",
|
||||
options: [],
|
||||
clearable: true,
|
||||
},
|
||||
span: 6,
|
||||
},
|
||||
{% elif column.html_type == "datetime" %}
|
||||
{
|
||||
label: "{{ comment }}",
|
||||
key: "{{ column.column_name }}",
|
||||
type: "date-picker",
|
||||
props: {
|
||||
type: "date",
|
||||
valueFormat: "YYYY-MM-DD",
|
||||
clearable: true,
|
||||
placeholder: "请选择{{ comment }}",
|
||||
},
|
||||
span: 6,
|
||||
},
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
]);
|
||||
|
||||
const faTableRef = ref<{ elTableRef?: { clearSelection: () => void } } | null>(null);
|
||||
const selectedRows = ref<{{ class_name }}Table[]>([]);
|
||||
const selectedIds = computed(() =>
|
||||
selectedRows.value.map((r) => r.id).filter((id): id is number => id != null && !Number.isNaN(id))
|
||||
);
|
||||
const batchDeleting = ref(false);
|
||||
|
||||
function onTableSelectionChange(rows: {{ class_name }}Table[]) {
|
||||
selectedRows.value = rows;
|
||||
}
|
||||
|
||||
const PK = "{{ pk_column_name }}" as const;
|
||||
|
||||
const {
|
||||
columns,
|
||||
columnChecks,
|
||||
data,
|
||||
loading,
|
||||
pagination,
|
||||
searchParams,
|
||||
getData,
|
||||
replaceSearchParams,
|
||||
resetSearchParams,
|
||||
handleSizeChange,
|
||||
handleCurrentChange,
|
||||
refreshData,
|
||||
refreshCreate,
|
||||
refreshUpdate,
|
||||
refreshRemove,
|
||||
} = useTable({
|
||||
core: {
|
||||
apiFn: {{ class_name }}API.get{{ class_name }}List,
|
||||
apiParams: {
|
||||
page_no: 1,
|
||||
page_size: 10,
|
||||
},
|
||||
columnsFactory: (): ColumnOption<{{ class_name }}Table>[] => [
|
||||
{ type: "selection", width: 48, fixed: "left" },
|
||||
{% for column in columns %}
|
||||
{% if column.is_list and column.column_name != pk_column_name %}
|
||||
{% 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.column_name == "status" %}
|
||||
{
|
||||
prop: "status",
|
||||
label: "状态",
|
||||
width: 88,
|
||||
formatter: (row: {{ class_name }}Table) => {
|
||||
const ok = row.status === 0;
|
||||
const cfg = ok
|
||||
? { type: "success" as const, text: "启用" }
|
||||
: { type: "info" as const, text: "停用" };
|
||||
return h(ElTag, { type: cfg.type }, () => cfg.text);
|
||||
},
|
||||
},
|
||||
{% elif column.column_name == "created_id" %}
|
||||
{
|
||||
prop: "created_by",
|
||||
label: "{{ comment }}",
|
||||
minWidth: 100,
|
||||
formatter: (row: {{ class_name }}Table) => row.created_by?.name ?? "—",
|
||||
},
|
||||
{% elif column.column_name == "updated_id" %}
|
||||
{
|
||||
prop: "updated_by",
|
||||
label: "{{ comment }}",
|
||||
minWidth: 100,
|
||||
formatter: (row: {{ class_name }}Table) => row.updated_by?.name ?? "—",
|
||||
},
|
||||
{% elif column.column_name == "created_time" %}
|
||||
{ prop: "created_time", label: "{{ comment }}", width: 168, showOverflowTooltip: true },
|
||||
{% elif column.column_name == "updated_time" %}
|
||||
{ prop: "updated_time", label: "{{ comment }}", width: 168, showOverflowTooltip: true },
|
||||
{% elif column.python_type == "bool" or column.html_type == "checkbox" %}
|
||||
{
|
||||
prop: "{{ column.column_name }}",
|
||||
label: "{{ comment }}",
|
||||
width: 80,
|
||||
formatter: (row: {{ class_name }}Table) =>
|
||||
h(ElTag, { type: row.{{ column.column_name }} ? "success" : "danger" }, () =>
|
||||
row.{{ column.column_name }} ? "是" : "否"
|
||||
),
|
||||
},
|
||||
{% elif column.html_type == "imageUpload" %}
|
||||
{
|
||||
prop: "{{ column.column_name }}",
|
||||
label: "{{ comment }}",
|
||||
minWidth: 120,
|
||||
formatter: (row: {{ class_name }}Table) => {
|
||||
if (!row.{{ column.column_name }}) return h("span", { class: "text-g-400" }, "—");
|
||||
return h("el-image", {
|
||||
src: row.{{ column.column_name }},
|
||||
style: "width: 48px; height: 48px; border-radius: 4px; object-fit: cover;",
|
||||
fit: "cover",
|
||||
previewSrcList: [row.{{ column.column_name }}],
|
||||
hideOnClickModal: true,
|
||||
});
|
||||
},
|
||||
},
|
||||
{% elif (column.html_type == "select" or column.html_type == "radio") and column.dict_type %}
|
||||
{# 字典列在列配置中用 prop 占位,运行时由 dictStore 动态替换 #}
|
||||
{ prop: "{{ column.column_name }}", label: "{{ comment }}", minWidth: 120, showOverflowTooltip: true },
|
||||
{% else %}
|
||||
{ prop: "{{ column.column_name }}", label: "{{ comment }}", minWidth: 120, showOverflowTooltip: true },
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{
|
||||
prop: "operation",
|
||||
label: "操作",
|
||||
width: 220,
|
||||
fixed: "right",
|
||||
align: "right",
|
||||
formatter: (row: {{ class_name }}Table) => formatOperationCell(row),
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
/** 供 FaImportDialog / FaExportDialog 的列配置 */
|
||||
const crudCols = computed(() =>
|
||||
columns.value.map((c: ColumnOption<{{ class_name }}Table>) => {
|
||||
const t = (c as { type?: string }).type;
|
||||
return {
|
||||
prop: c.prop,
|
||||
label: c.label,
|
||||
type: t === "selection" ? ("selection" as const) : ("default" as const),
|
||||
show: true,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
const exportQueryParams = computed(() => {
|
||||
const sp = { ...(searchParams as object) } as Record<string, unknown>;
|
||||
delete sp.current;
|
||||
delete sp.size;
|
||||
delete sp.page_no;
|
||||
delete sp.page_size;
|
||||
return normalize{{ class_name }}Query(sp);
|
||||
});
|
||||
|
||||
const importContentConfig = computed<IContentConfig>(() => ({
|
||||
permPrefix: "{{ permission_prefix }}",
|
||||
cols: crudCols.value,
|
||||
indexAction: async () => ({}),
|
||||
importTemplate: () => {{ class_name }}API.downloadTemplate{{ class_name }}(),
|
||||
}));
|
||||
|
||||
const exportContentConfig = computed(() => ({
|
||||
permPrefix: "{{ permission_prefix }}",
|
||||
cols: crudCols.value,
|
||||
exportsBlobAction: async (params: IObject) => {
|
||||
const merged = normalize{{ class_name }}Query({
|
||||
...(exportQueryParams.value as unknown as Record<string, unknown>),
|
||||
...params,
|
||||
} as Record<string, unknown>);
|
||||
const res = await {{ class_name }}API.export{{ class_name }}(merged as {{ class_name }}PageQuery);
|
||||
return res.data as Blob;
|
||||
},
|
||||
}));
|
||||
|
||||
const { dialogVisible } = useCrudDialog();
|
||||
|
||||
const detailFormData = ref<{{ class_name }}Table>({});
|
||||
|
||||
const detailItems: import("@/components/others/fa-descriptions/index.vue").DescriptionsItem[] = [
|
||||
{% for column in columns %}
|
||||
{% if column.is_list %}
|
||||
{% 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.column_name == 'status' %}
|
||||
{ label: "状态", prop: "status", tag: { map: { "0": { type: "success", text: "启用" }, "1": { type: "danger", text: "停用" } } } },
|
||||
{% elif column.column_name == 'created_id' %}
|
||||
{ label: "创建人", prop: "created_by.name" },
|
||||
{% elif column.column_name == 'updated_id' %}
|
||||
{ label: "更新人", prop: "updated_by.name" },
|
||||
{% elif column.column_name == 'created_time' %}
|
||||
{ label: "创建时间", prop: "created_time" },
|
||||
{% elif column.column_name == 'updated_time' %}
|
||||
{ label: "更新时间", prop: "updated_time" },
|
||||
{% else %}
|
||||
{ label: "{{ comment }}", prop: "{{ column.column_name }}" },
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
];
|
||||
|
||||
const formData = ref<{{ class_name }}Form>({
|
||||
{% for column in columns %}
|
||||
{% if column.is_insert or column.is_edit %}
|
||||
{% if column.column_name not in ['uuid', 'created_time', 'updated_time', 'created_id', 'updated_id', 'is_deleted', 'deleted_time', 'deleted_id', 'tenant_id'] and column.column_name != pk_column_name %}
|
||||
{% if column.column_name == "status" %}
|
||||
{{ column.column_name }}: "0",
|
||||
{% elif column.python_type == "bool" or column.html_type == "switch" %}
|
||||
{{ column.column_name }}: false,
|
||||
{% else %}
|
||||
{{ column.column_name }}: undefined,
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
});
|
||||
|
||||
const rules = reactive({
|
||||
{% for column in columns %}
|
||||
{% if column.is_insert or column.is_edit %}
|
||||
{% if column.column_name not in ['uuid', 'created_time', 'updated_time', 'created_id', 'updated_id', 'is_deleted', 'deleted_time', 'deleted_id', 'tenant_id'] and column.column_name != pk_column_name %}
|
||||
{{ column.column_name }}: [{ required: {% if not column.is_nullable %}true{% else %}false{% endif %}, message: "请填写{{ column.column_comment or column.column_name }}", trigger: "blur" }],
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
});
|
||||
|
||||
const dialogFormItems = computed<FormItem[]>(() => [
|
||||
{% for column in columns %}
|
||||
{% if column.is_insert or column.is_edit %}
|
||||
{% 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.column_name not in ['uuid', 'created_time', 'updated_time', 'created_id', 'updated_id', 'is_deleted', 'deleted_time', 'deleted_id', 'tenant_id'] and column.column_name != pk_column_name %}
|
||||
{% if column.column_name == "status" %}
|
||||
{ key: "status", label: "状态", type: "radio", props: { options: [{ label: "启用", value: 0 }, { label: "停用", value: 1 }] } },
|
||||
{% elif column.column_name == "description" %}
|
||||
{ key: "description", label: "描述", type: "textarea", placeholder: "请输入描述" },
|
||||
{% elif column.python_type == "int" or column.python_type == "float" or column.python_type == "Decimal" %}
|
||||
{ key: "{{ column.column_name }}", label: "{{ comment }}", type: "input-number", placeholder: "请输入{{ comment }}"{% if column.python_type == "float" or column.python_type == "Decimal" %}, props: { step: 0.01, precision: 2 }{% endif %} },
|
||||
{% elif column.html_type == "input" %}
|
||||
{ key: "{{ column.column_name }}", label: "{{ comment }}", type: "input", placeholder: "请输入{{ comment }}" },
|
||||
{% elif column.html_type == "textarea" %}
|
||||
{ key: "{{ column.column_name }}", label: "{{ comment }}", type: "textarea", placeholder: "请输入{{ comment }}" },
|
||||
{% elif dict_type != "" %}
|
||||
{ key: "{{ column.column_name }}", label: "{{ comment }}", type: "select", placeholder: "请选择{{ comment }}", options: dictStore.getDictArray("{{ dict_type }}").map(d => ({ label: d.dict_label, value: d.dict_value })) },
|
||||
{% elif column.html_type == "select" or column.html_type == "radio" %}
|
||||
{ key: "{{ column.column_name }}", label: "{{ comment }}", type: "select", placeholder: "请选择{{ comment }}" },
|
||||
{% elif column.html_type == "date" %}
|
||||
{ key: "{{ column.column_name }}", label: "{{ comment }}", type: "date", placeholder: "请选择{{ comment }}" },
|
||||
{% elif column.html_type == "datetime" %}
|
||||
{ key: "{{ column.column_name }}", label: "{{ comment }}", type: "datetime", placeholder: "请选择{{ comment }}" },
|
||||
{% elif column.html_type == "switch" %}
|
||||
{ key: "{{ column.column_name }}", label: "{{ comment }}", type: "switch" },
|
||||
{% elif column.html_type == "checkbox" %}
|
||||
{ key: "{{ column.column_name }}", label: "{{ comment }}", type: "checkbox" },
|
||||
{% else %}
|
||||
{ key: "{{ column.column_name }}", label: "{{ comment }}", type: "input", placeholder: "请输入{{ comment }}" },
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
]);
|
||||
|
||||
const dataFormRef = ref<{
|
||||
resetFields: () => void;
|
||||
clearValidate: () => void;
|
||||
validate: (cb: (valid: boolean) => void) => void;
|
||||
} | null>(null);
|
||||
const submitLoading = ref(false);
|
||||
const formRenderKey = ref(0);
|
||||
|
||||
const { importVisible, exportVisible, openImport, openExport } = useImportExport();
|
||||
|
||||
const initialFormData: {{ class_name }}Form = {
|
||||
{% for column in columns %}
|
||||
{% if column.is_insert or column.is_edit %}
|
||||
{% if column.column_name not in ['uuid', 'created_time', 'updated_time', 'created_id', 'updated_id', 'is_deleted', 'deleted_time', 'deleted_id', 'tenant_id'] and column.column_name != pk_column_name %}
|
||||
{% if column.column_name == "status" %}
|
||||
{{ column.column_name }}: "0",
|
||||
{% elif column.python_type == "bool" or column.html_type == "switch" %}
|
||||
{{ column.column_name }}: false,
|
||||
{% else %}
|
||||
{{ column.column_name }}: undefined,
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
};
|
||||
|
||||
const handleSearch = async (params: {{ class_name }}SearchFormParams) => {
|
||||
await searchBarRef.value?.validate();
|
||||
replaceSearchParams({
|
||||
{% for column in columns %}
|
||||
{% if column.is_query and column.column_name not in ['created_time', 'updated_time', 'created_id', 'updated_id'] and column.column_name != pk_column_name %}
|
||||
{{ column.column_name }}: params.{{ column.column_name }},
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
created_id: params.created_id ?? undefined,
|
||||
updated_id: params.updated_id ?? undefined,
|
||||
created_time:
|
||||
Array.isArray(params.created_time) && params.created_time.length === 2
|
||||
? params.created_time
|
||||
: undefined,
|
||||
updated_time:
|
||||
Array.isArray(params.updated_time) && params.updated_time.length === 2
|
||||
? params.updated_time
|
||||
: undefined,
|
||||
} as Record<string, unknown>);
|
||||
getData();
|
||||
};
|
||||
|
||||
const onResetSearch = async () => {
|
||||
searchForm.value = {
|
||||
{% for column in columns %}
|
||||
{% if column.is_query and column.column_name not in ['created_time', 'updated_time', 'created_id', 'updated_id'] and column.column_name != pk_column_name %}
|
||||
{{ column.column_name }}: undefined,
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
created_id: undefined,
|
||||
updated_id: undefined,
|
||||
created_time: [],
|
||||
updated_time: [],
|
||||
};
|
||||
await resetSearchParams();
|
||||
};
|
||||
|
||||
function buildRowActions(row: {{ class_name }}Table): TableOperationAction[] {
|
||||
const all: TableOperationAction[] = [
|
||||
{
|
||||
key: "detail",
|
||||
label: "详情",
|
||||
artType: "view",
|
||||
perm: "{{ permission_prefix }}:detail",
|
||||
run: () => void openDetailDialog(row),
|
||||
},
|
||||
{
|
||||
key: "edit",
|
||||
label: "编辑",
|
||||
artType: "edit",
|
||||
icon: "ri:edit-2-line",
|
||||
perm: "{{ permission_prefix }}:update",
|
||||
run: () => void openEditDialog("edit", row),
|
||||
},
|
||||
{
|
||||
key: "delete",
|
||||
label: "删除",
|
||||
artType: "delete",
|
||||
icon: "ri:delete-bin-4-line",
|
||||
perm: "{{ permission_prefix }}:delete",
|
||||
run: () => deleteRow(row),
|
||||
},
|
||||
];
|
||||
return all.filter((a) => a.perm != null && hasAuth(a.perm));
|
||||
}
|
||||
|
||||
function formatOperationCell(row: {{ class_name }}Table) {
|
||||
return renderTableOperationCell(buildRowActions(row), {
|
||||
wrapperClass: "inline-flex flex-wrap items-center justify-end gap-1",
|
||||
});
|
||||
}
|
||||
|
||||
async function openDetailDialog(row: {{ class_name }}Table) {
|
||||
if (!row[PK]) return;
|
||||
const response = await {{ class_name }}API.get{{ class_name }}Detail(row[PK] as number);
|
||||
dialogVisible.type = "detail";
|
||||
dialogVisible.title = "详情";
|
||||
detailFormData.value = response.data.data ?? { ...row };
|
||||
{% if table.sub %}
|
||||
subTableData.value = (response.data.data as any)?.{{ sub_rel_list_name }} ?? [];
|
||||
{% endif %}
|
||||
dialogVisible.visible = true;
|
||||
}
|
||||
|
||||
async function openEditDialog(type: "add" | "edit", row?: {{ class_name }}Table) {
|
||||
dialogVisible.type = type === "add" ? "create" : "update";
|
||||
if (type === "add") {
|
||||
dialogVisible.title = "新增{{ function_name }}";
|
||||
Object.assign(formData.value, initialFormData);
|
||||
formData.value[PK] = undefined;
|
||||
{% if table.sub %}
|
||||
subTableData.value = [];
|
||||
{% endif %}
|
||||
formRenderKey.value += 1;
|
||||
} else if (row?.[PK]) {
|
||||
dialogVisible.title = "修改";
|
||||
formRenderKey.value += 1;
|
||||
const response = await {{ class_name }}API.get{{ class_name }}Detail(row[PK] as number);
|
||||
Object.assign(formData.value, response.data.data);
|
||||
{% if table.sub %}
|
||||
subTableData.value = (response.data.data as any)?.{{ sub_rel_list_name }} ?? [];
|
||||
{% endif %}
|
||||
}
|
||||
dialogVisible.visible = true;
|
||||
}
|
||||
|
||||
async function resetForm() {
|
||||
if (dataFormRef.value) {
|
||||
dataFormRef.value.resetFields();
|
||||
dataFormRef.value.clearValidate();
|
||||
}
|
||||
Object.assign(formData.value, initialFormData);
|
||||
}
|
||||
|
||||
async function handleCloseDialog() {
|
||||
dialogVisible.visible = false;
|
||||
await resetForm();
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
dataFormRef.value?.validate(async (valid: boolean) => {
|
||||
if (!valid) return;
|
||||
const submitData = { ...formData.value };
|
||||
const id = formData.value[PK] as number | undefined;
|
||||
try {
|
||||
submitLoading.value = true;
|
||||
if (id) {
|
||||
await {{ class_name }}API.update{{ class_name }}(id, { [PK]: id, ...submitData });
|
||||
await refreshUpdate();
|
||||
} else {
|
||||
await {{ class_name }}API.create{{ class_name }}(submitData);
|
||||
await refreshCreate();
|
||||
}
|
||||
dialogVisible.visible = false;
|
||||
await resetForm();
|
||||
} catch (error: unknown) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
submitLoading.value = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const deleteRow = async (row: {{ class_name }}Table) => {
|
||||
if (!row[PK]) return;
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定删除该{{ function_name }}吗?此操作不可恢复!`,
|
||||
"删除确认",
|
||||
{
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning",
|
||||
}
|
||||
);
|
||||
await {{ class_name }}API.delete{{ class_name }}([row[PK] as number]);
|
||||
ElMessage.success("删除成功");
|
||||
faTableRef.value?.elTableRef?.clearSelection();
|
||||
await refreshRemove();
|
||||
} catch {
|
||||
ElMessage.info("已取消删除");
|
||||
}
|
||||
};
|
||||
|
||||
{% if table.sub %}
|
||||
const subTableData = ref<any[]>([]);
|
||||
|
||||
function addSubRow() {
|
||||
subTableData.value.push({
|
||||
{% for col in table.sub_table.columns %}
|
||||
{% if col.column_name not in ['id', 'uuid', 'status', 'description', 'created_time', 'updated_time', 'created_id', 'updated_id', 'is_deleted', 'deleted_time', 'deleted_id'] and col.column_name != table.sub_table.pk_column.column_name %}
|
||||
{{ col.column_name }}: undefined,
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
});
|
||||
}
|
||||
|
||||
function removeSubRow(index: number) {
|
||||
subTableData.value.splice(index, 1);
|
||||
}
|
||||
{% endif %}
|
||||
|
||||
async function handleBatchDelete() {
|
||||
const ids = selectedIds.value;
|
||||
if (ids.length === 0) return;
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定删除选中的 ${ids.length} 条数据吗?此操作不可恢复!`,
|
||||
"批量删除",
|
||||
{
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning",
|
||||
}
|
||||
);
|
||||
batchDeleting.value = true;
|
||||
await {{ class_name }}API.delete{{ class_name }}(ids);
|
||||
ElMessage.success("删除成功");
|
||||
faTableRef.value?.elTableRef?.clearSelection();
|
||||
await refreshRemove();
|
||||
} catch {
|
||||
ElMessage.info("已取消删除");
|
||||
} finally {
|
||||
batchDeleting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function runBatchStatus(status: string) {
|
||||
const ids = selectedIds.value;
|
||||
if (ids.length === 0) {
|
||||
ElMessage.warning("请先在列表中勾选数据");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确认对选中的 ${ids.length} 条数据${status === "0" ? "启用" : "停用"}?`,
|
||||
"批量设置",
|
||||
{ confirmButtonText: "确定", cancelButtonText: "取消", type: "warning" }
|
||||
);
|
||||
await {{ class_name }}API.batch{{ class_name }}({ ids, status });
|
||||
ElMessage.success("操作成功");
|
||||
faTableRef.value?.elTableRef?.clearSelection();
|
||||
await refreshData();
|
||||
} catch {
|
||||
// 用户取消
|
||||
}
|
||||
}
|
||||
|
||||
async function handleCrudImportUpload(uploadFormData: FormData) {
|
||||
try {
|
||||
const res = await {{ class_name }}API.import{{ class_name }}(uploadFormData);
|
||||
if (res.data.code !== ResultEnum.SUCCESS) {
|
||||
ElMessage.error(res.data.msg || "导入失败");
|
||||
return;
|
||||
}
|
||||
ElMessage.success(res.data.msg || "导入成功");
|
||||
importVisible.value = false;
|
||||
await refreshData();
|
||||
} catch (error) {
|
||||
console.error("[Import]", error);
|
||||
ElMessage.error("导入失败");
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
{% for column in columns %}
|
||||
{% if column.is_query and column.dict_type and (column.html_type == "select" or column.html_type == "radio") %}
|
||||
(() => {
|
||||
const item = businessSearchItems.value.find((i) => i.key === "{{ column.column_name }}");
|
||||
if (item && "props" in item && item.props) {
|
||||
(item.props as Record<string, unknown>).options = dictStore.getDictArray("{{ column.dict_type }}").map(
|
||||
(d: { dict_label: string; dict_value: string }) => ({
|
||||
label: d.dict_label,
|
||||
value: d.dict_value,
|
||||
})
|
||||
);
|
||||
}
|
||||
})();
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
getData();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
@@ -1,331 +0,0 @@
|
||||
import re
|
||||
|
||||
from app.common.constant import GenConstant
|
||||
from app.plugin.module_generator.gencode.schema import (
|
||||
GenTableColumnSchema,
|
||||
GenTableOutSchema,
|
||||
GenTableSchema,
|
||||
)
|
||||
from app.utils.string_util import StringUtil
|
||||
|
||||
|
||||
class GenUtils:
|
||||
"""代码生成器工具类"""
|
||||
|
||||
@classmethod
|
||||
def init_table(cls, gen_table: GenTableSchema) -> None:
|
||||
"""
|
||||
初始化表信息
|
||||
|
||||
参数:
|
||||
- gen_table (GenTableSchema): 业务表对象。
|
||||
|
||||
返回:
|
||||
- None
|
||||
"""
|
||||
gen_table.class_name = cls.convert_class_name(gen_table.table_name or "")
|
||||
# 导入时给出“可用默认值”,减少用户点开后看到空表单:
|
||||
# - module_name:从表名推导(去掉 gen_/tb_ 前缀)
|
||||
# - package_name:默认 module_{module_name}(仍可在前端改)
|
||||
if gen_table.business_name is None:
|
||||
gen_table.business_name = gen_table.table_name
|
||||
if gen_table.module_name is None or not str(gen_table.module_name).strip():
|
||||
tn = (gen_table.table_name or "").strip().lower()
|
||||
if tn.startswith("gen_"):
|
||||
tn = tn[4:]
|
||||
elif tn.startswith("tb_"):
|
||||
tn = tn[3:]
|
||||
tn = re.sub(r"[^a-z0-9_]+", "_", tn)
|
||||
tn = re.sub(r"_+", "_", tn).strip("_") or "module"
|
||||
gen_table.module_name = tn
|
||||
|
||||
if gen_table.package_name is None or not str(gen_table.package_name).strip():
|
||||
mn = (gen_table.module_name or "").strip()
|
||||
if mn:
|
||||
gen_table.package_name = mn if mn.startswith("module_") else f"module_{mn}"
|
||||
|
||||
fn = re.sub(r"(?:表|测试)", "", gen_table.table_comment or "")
|
||||
fn = (fn or "").strip()
|
||||
if not fn:
|
||||
# 表注释为空时:用表名兜底,至少不为空
|
||||
fn = (gen_table.table_name or "").strip()
|
||||
gen_table.function_name = fn
|
||||
|
||||
@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 table.id is None:
|
||||
raise ValueError("业务表ID不能为空")
|
||||
column.table_id = table.id
|
||||
column.python_field = cls.to_camel_case(column_name)
|
||||
|
||||
# 特殊处理几何类型,根据数据库类型选择不同的映射
|
||||
from app.config.setting import settings
|
||||
|
||||
if data_type in [
|
||||
"point",
|
||||
"line",
|
||||
"linestring",
|
||||
"polygon",
|
||||
"multipoint",
|
||||
"multilinestring",
|
||||
"multipolygon",
|
||||
"geometrycollection",
|
||||
"geometry",
|
||||
]:
|
||||
if settings.DATABASE_TYPE == "mysql":
|
||||
column.python_type = "bytes"
|
||||
elif settings.DATABASE_TYPE == "postgres":
|
||||
column.python_type = "list"
|
||||
else:
|
||||
# 只有当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:
|
||||
# 先按“字段名语义”推断(优先级高于通用字符串规则)
|
||||
lower_name = column_name.lower()
|
||||
if lower_name.endswith("status"):
|
||||
column.html_type = GenConstant.HTML_RADIO
|
||||
elif lower_name.endswith("type") or lower_name.endswith("sex"):
|
||||
column.html_type = GenConstant.HTML_SELECT
|
||||
elif lower_name.endswith("image"):
|
||||
column.html_type = GenConstant.HTML_IMAGE_UPLOAD
|
||||
elif lower_name.endswith("file"):
|
||||
column.html_type = GenConstant.HTML_FILE_UPLOAD
|
||||
elif lower_name.endswith("content"):
|
||||
column.html_type = GenConstant.HTML_EDITOR
|
||||
# 再按“数据类型”推断
|
||||
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 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 "")
|
||||
column.html_type = GenConstant.HTML_TEXTAREA if column_length >= 500 or cls.arrays_contains(GenConstant.COLUMNTYPE_TEXT, data_type) else GenConstant.HTML_INPUT
|
||||
else:
|
||||
column.html_type = GenConstant.HTML_INPUT
|
||||
|
||||
# 默认新增字段:非主键且不在“新增不展示”黑名单中
|
||||
# 说明:schema 默认值可能为 True/False;仅当调用方未显式配置时才做推断
|
||||
if column.is_insert is None:
|
||||
column.is_insert = bool((not column.is_pk) and (not cls.arrays_contains(GenConstant.COLUMNNAME_NOT_ADD_SHOW, column_name)))
|
||||
|
||||
# 默认编辑字段:非主键且不在“不编辑”黑名单中
|
||||
if column.is_edit is None:
|
||||
column.is_edit = bool((not cls.arrays_contains(GenConstant.COLUMNNAME_NOT_EDIT, column_name)) and (not column.is_pk))
|
||||
|
||||
# 默认列表字段:非主键且不在“不列表显示”黑名单中
|
||||
if column.is_list is None:
|
||||
column.is_list = bool((not cls.arrays_contains(GenConstant.COLUMNNAME_NOT_LIST, column_name)) and (not column.is_pk))
|
||||
|
||||
# 默认查询字段:非主键且不在“不查询”黑名单中
|
||||
if column.is_query is None:
|
||||
column.is_query = bool((not cls.arrays_contains(GenConstant.COLUMNNAME_NOT_QUERY, column_name)) and (not column.is_pk))
|
||||
|
||||
# 查询类型:仅当开启查询且 query_type 未显式配置时推断
|
||||
if column.is_query:
|
||||
if column.query_type is None:
|
||||
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.query_type = None
|
||||
|
||||
# 主键强约束:无论默认推断/历史配置如何,主键列不应出现在新增/编辑/列表/查询
|
||||
if bool(column.is_pk):
|
||||
column.is_insert = False
|
||||
column.is_edit = False
|
||||
column.is_list = False
|
||||
column.is_query = False
|
||||
column.query_type = None
|
||||
|
||||
@classmethod
|
||||
def arrays_contains(cls, arr: list, target_value: str) -> bool:
|
||||
"""
|
||||
检查目标值是否在数组中
|
||||
|
||||
注意:从根本上解决问题,现在确保传入的参数都是正确的类型:
|
||||
- arr 是列表类型,且在GenConstant中定义
|
||||
- target_value 不会是None
|
||||
|
||||
参数:
|
||||
- arr: 数组类型
|
||||
- target_value: 目标值
|
||||
|
||||
返回:
|
||||
- bool: 如果目标值在数组中,返回True;否则返回False
|
||||
"""
|
||||
# 从根本上解决问题,不再需要复杂的防御性检查
|
||||
# 因为现在我们确保传入的arr是GenConstant中定义的列表常量
|
||||
# 并且target_value在调用前已经被处理过不会是None
|
||||
|
||||
# 移除 COLLATE 子句和 UNSIGNED 标记(不区分大小写)
|
||||
target_str = str(target_value)
|
||||
|
||||
# 移除 COLLATE 子句
|
||||
collate_pattern = re.compile(r"\s+COLLATE\s+", re.IGNORECASE)
|
||||
if collate_pattern.search(target_str):
|
||||
target_str = collate_pattern.split(target_str)[0].strip()
|
||||
|
||||
# 移除 UNSIGNED 标记
|
||||
unsigned_pattern = re.compile(r"\s+UNSIGNED", re.IGNORECASE)
|
||||
if unsigned_pattern.search(target_str):
|
||||
target_str = unsigned_pattern.sub("", target_str).strip()
|
||||
|
||||
# 转换为小写进行比较
|
||||
target_str = target_str.lower()
|
||||
|
||||
# 对于包含括号的类型(如TINYINT(1)),需要特殊处理
|
||||
# 先获取基本类型名称(不含括号)用于比较
|
||||
target_base_type = target_str.split("(")[0] if "(" in target_str else target_str
|
||||
|
||||
for item in arr:
|
||||
item_str = str(item).lower()
|
||||
item_base_type = item_str.split("(")[0] if "(" in item_str else item_str
|
||||
if target_base_type == item_base_type:
|
||||
return True
|
||||
return False
|
||||
|
||||
@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: 数据库类型。
|
||||
"""
|
||||
# 移除 COLLATE 子句(处理带引号和不带引号的情况,不区分大小写)
|
||||
collate_pattern = re.compile(r"\s+COLLATE\s+", re.IGNORECASE)
|
||||
if collate_pattern.search(column_type):
|
||||
column_type = collate_pattern.split(column_type)[0].strip()
|
||||
|
||||
# 移除 UNSIGNED 标记(不区分大小写)
|
||||
unsigned_pattern = re.compile(r"\s+UNSIGNED", re.IGNORECASE)
|
||||
if unsigned_pattern.search(column_type):
|
||||
column_type = unsigned_pattern.sub("", column_type).strip()
|
||||
|
||||
# 特殊处理tinyint(1),映射为boolean
|
||||
if column_type.lower().startswith("tinyint(1)"):
|
||||
return "boolean"
|
||||
|
||||
# 处理PostgreSQL数组类型(如 integer[], text[] 或 ARRAY[INTEGER])
|
||||
if "[]" in column_type or column_type.upper().startswith("ARRAY["):
|
||||
return "array"
|
||||
|
||||
# 提取基本类型:
|
||||
# - 去掉括号参数:varchar(64) -> varchar
|
||||
# - 去掉空格后的修饰:timestamp without time zone -> timestamp
|
||||
# - 统一小写
|
||||
base = column_type.split("(", 1)[0].strip()
|
||||
if not base:
|
||||
return ""
|
||||
base = base.split(None, 1)[0].strip()
|
||||
return base.lower()
|
||||
|
||||
@classmethod
|
||||
def get_column_length(cls, column_type: str) -> int:
|
||||
"""
|
||||
获取字段长度
|
||||
|
||||
参数:
|
||||
- column_type (str): 字段类型,例如 'varchar(255)' 或 'decimal(10,2)'
|
||||
|
||||
返回:
|
||||
- int: 字段长度(优先取第一个长度值,无法解析时返回0)。
|
||||
"""
|
||||
if not column_type:
|
||||
return 0
|
||||
if "(" not in column_type or ")" not in column_type:
|
||||
return 0
|
||||
|
||||
# 形如 varchar(255) / decimal(10,2) / numeric(20, 0)
|
||||
inner = column_type.split("(", 1)[1].split(")", 1)[0].strip()
|
||||
if not inner:
|
||||
return 0
|
||||
|
||||
first = inner.split(",", 1)[0].strip()
|
||||
try:
|
||||
return int(first)
|
||||
except ValueError:
|
||||
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:
|
||||
"""
|
||||
将字符串转换为驼峰命名
|
||||
|
||||
参数:
|
||||
- text (str): 需要转换的字符串
|
||||
|
||||
返回:
|
||||
- str: 驼峰命名
|
||||
"""
|
||||
parts = text.split("_")
|
||||
return parts[0] + "".join(word.capitalize() for word in parts[1:])
|
||||
@@ -1,794 +0,0 @@
|
||||
import re
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
from jinja2 import Environment, FileSystemLoader, Template
|
||||
|
||||
from app.common.constant import GenConstant
|
||||
from app.config.path_conf import TEMPLATE_DIR
|
||||
from app.config.setting import settings
|
||||
from app.plugin.module_generator.gencode.schema import (
|
||||
GenTableColumnOutSchema,
|
||||
GenTableOutSchema,
|
||||
)
|
||||
from app.plugin.module_generator.gencode.tools.gen_util import GenUtils
|
||||
from app.utils.common_util import CamelCaseUtil, SnakeCaseUtil
|
||||
from app.utils.string_util import StringUtil
|
||||
|
||||
|
||||
class Jinja2TemplateUtil:
|
||||
"""
|
||||
模板处理工具类
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def normalize_db_column_type_for_mapping(cls, column_type: str | None) -> str:
|
||||
"""
|
||||
与 ``GenUtils.get_db_type`` 一致地去掉 COLLATE / UNSIGNED,便于与 ``DB_TO_SQLALCHEMY`` 键匹配。
|
||||
|
||||
参数:
|
||||
- column_type (str | None): 原始列类型字符串。
|
||||
|
||||
返回:
|
||||
- str: 规范化后的类型片段;空输入返回空字符串。
|
||||
"""
|
||||
ct = (column_type or "").strip()
|
||||
if not ct:
|
||||
return ""
|
||||
collate_pattern = re.compile(r"\s+COLLATE\s+", re.IGNORECASE)
|
||||
if collate_pattern.search(ct):
|
||||
ct = collate_pattern.split(ct)[0].strip()
|
||||
unsigned_pattern = re.compile(r"\s+UNSIGNED", re.IGNORECASE)
|
||||
if unsigned_pattern.search(ct):
|
||||
ct = unsigned_pattern.sub("", ct).strip()
|
||||
return ct
|
||||
|
||||
# 项目路径
|
||||
FRONTEND_PROJECT_PATH = "frontend"
|
||||
BACKEND_PROJECT_PATH = "backend"
|
||||
|
||||
# 环境对象
|
||||
_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,
|
||||
"python_to_ts_type": cls.python_type_to_ts_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 business_name_to_slug(cls, business_name: str | None) -> str:
|
||||
"""
|
||||
业务路径可含斜杠(如 ``demo/subdir``)用于目录与路由前缀;
|
||||
Python 函数/方法名仅使用最后一段并规范为合法 snake_case 片段。
|
||||
|
||||
参数:
|
||||
- business_name (str | None): 业务路径或名称。
|
||||
|
||||
返回:
|
||||
- str: 用于 Python 标识的 slug,默认 entity。
|
||||
"""
|
||||
s = (business_name or "").strip().strip("/")
|
||||
if not s:
|
||||
return "entity"
|
||||
if "/" in s:
|
||||
s = s.split("/")[-1]
|
||||
s = re.sub(r"[^a-zA-Z0-9_]", "_", s)
|
||||
if not s:
|
||||
return "entity"
|
||||
if s[0].isdigit():
|
||||
s = "_" + s
|
||||
return s
|
||||
|
||||
@classmethod
|
||||
def business_name_to_path(cls, business_name: str | None) -> str:
|
||||
"""把 business_name 规范为可用于目录/路由的多段路径(保留 `/`)。
|
||||
|
||||
约定:`business_name` 允许 `a/b/c` 表示多级菜单目录。
|
||||
- 目录/路由:使用完整多段
|
||||
- 文件名/route_name:使用最后一段 slug(见 `business_name_to_slug`)
|
||||
|
||||
参数:
|
||||
- business_name (str | None): 业务路径或名称。
|
||||
|
||||
返回:
|
||||
- str: 多段路径字符串(小写 slug),默认 entity。
|
||||
"""
|
||||
s = (business_name or "").strip().strip("/")
|
||||
if not s:
|
||||
return "entity"
|
||||
# 每段都做一次轻度规范(与 schema 的 slug 规则一致:a-z0-9_)
|
||||
segs = []
|
||||
for raw in [p for p in s.split("/") if p]:
|
||||
seg = re.sub(r"[^a-zA-Z0-9_]", "_", raw).lower()
|
||||
seg = re.sub(r"_+", "_", seg).strip("_") or "entity"
|
||||
if seg[0].isdigit():
|
||||
seg = "_" + seg
|
||||
segs.append(seg)
|
||||
return "/".join(segs) if segs else "entity"
|
||||
|
||||
@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 ""
|
||||
package_name = (gen_table.package_name or "").strip()
|
||||
module_name = (gen_table.module_name or "").strip()
|
||||
business_name = (gen_table.business_name or "").strip()
|
||||
function_name = gen_table.function_name or ""
|
||||
|
||||
# 生成规则(对齐 module_example/demo):
|
||||
# - 分系统根:package_name = module_xxx
|
||||
# - 目录固定为:module_xxx / module_name(不再额外使用业务名作为目录层级)
|
||||
# - 权限前缀固定为:module_xxx:module_name(操作在模板里再拼 :query/:create...)
|
||||
business_path = cls.business_name_to_path(business_name)
|
||||
business_name_slug = cls.business_name_to_slug(business_name)
|
||||
permission_prefix = ":".join([s for s in [package_name, module_name] if s])
|
||||
api_route_prefix = cls.get_api_route_prefix(package_name)
|
||||
|
||||
_cols = gen_table.columns or []
|
||||
table_column_names = frozenset(c.column_name for c in _cols if getattr(c, "column_name", None))
|
||||
|
||||
sub_class_name = ""
|
||||
sub_model_class_name = ""
|
||||
sub_rel_list_name = ""
|
||||
parent_rel_name = ""
|
||||
if gen_table.sub and gen_table.sub_table:
|
||||
st = gen_table.sub_table
|
||||
scn = (st.class_name or GenUtils.convert_class_name(gen_table.sub_table_name or "")).strip()
|
||||
sub_class_name = scn
|
||||
sub_model_class_name = f"{scn}Model"
|
||||
sub_rel_list_name = f"{SnakeCaseUtil.camel_to_snake(scn)}_list"
|
||||
parent_rel_name = SnakeCaseUtil.camel_to_snake(gen_table.class_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,
|
||||
"business_path": business_path,
|
||||
"business_file": business_name_slug,
|
||||
"business_name_slug": business_name_slug,
|
||||
"base_package": cls.get_package_prefix(package_name),
|
||||
"package_name": package_name,
|
||||
"menu_route_first_segment": cls.get_menu_route_first_segment(gen_table),
|
||||
"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": permission_prefix,
|
||||
"api_route_prefix": api_route_prefix,
|
||||
"columns": gen_table.columns or [],
|
||||
"table_column_names": table_column_names,
|
||||
"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 else None,
|
||||
"is_sub_entity": False,
|
||||
"sub_class_name": sub_class_name,
|
||||
"sub_model_class_name": sub_model_class_name,
|
||||
"sub_module_name": (gen_table.sub_table.module_name if gen_table.sub and gen_table.sub_table else ""),
|
||||
"sub_rel_list_name": sub_rel_list_name,
|
||||
"parent_rel_name": parent_rel_name,
|
||||
"parent_list_rel_name": "",
|
||||
"parent_table_name": "",
|
||||
"parent_model_class_name": "",
|
||||
# 数据表实际主键列名(用于生成前端行键等;ModelMixin 仍默认带 id 字段)
|
||||
"pk_column_name": (gen_table.pk_column.column_name if gen_table.pk_column else None) or "id",
|
||||
"parent_pk_column_name": (gen_table.pk_column.column_name if gen_table.pk_column else None) or "id",
|
||||
"sub_table_fk_name": "",
|
||||
}
|
||||
|
||||
return context
|
||||
|
||||
@classmethod
|
||||
def get_menu_route_first_segment(cls, gen_table: GenTableOutSchema) -> str:
|
||||
"""
|
||||
前端页面路由首段(与写入菜单 ``route_path`` 第一段一致):始终为 ``module_xxx``。
|
||||
|
||||
懒加载 ``GenTableService`` 避免与 ``service`` 模块循环依赖。
|
||||
|
||||
参数:
|
||||
- gen_table (GenTableOutSchema): 生成表配置。
|
||||
|
||||
返回:
|
||||
- str: 路由首段(module_xxx)。
|
||||
"""
|
||||
from app.plugin.module_generator.gencode.service import GenTableService
|
||||
|
||||
pid = int(gen_table.parent_menu_id) if gen_table.parent_menu_id is not None else None
|
||||
return GenTableService._menu_route_first_segment(
|
||||
pid,
|
||||
gen_table.package_name or "",
|
||||
gen_table.module_name,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def prepare_sub_render_context(cls, parent: GenTableOutSchema, sub: GenTableOutSchema) -> dict[str, Any]:
|
||||
"""
|
||||
子表业务代码渲染上下文(与主表同模块、独立业务目录)。
|
||||
|
||||
参数:
|
||||
- parent (GenTableOutSchema): 主表配置。
|
||||
- sub (GenTableOutSchema): 子表配置。
|
||||
|
||||
返回:
|
||||
- dict[str, Any]: 子表模板上下文字典。
|
||||
"""
|
||||
ctx = cls.prepare_context(sub)
|
||||
scn = (sub.class_name or GenUtils.convert_class_name(sub.table_name or "")).strip()
|
||||
ctx["is_sub_entity"] = True
|
||||
ctx["parent_class_name"] = parent.class_name or ""
|
||||
ctx["parent_model_class_name"] = f"{parent.class_name}Model"
|
||||
ctx["parent_table_name"] = parent.table_name or ""
|
||||
ctx["parent_pk_column_name"] = (parent.pk_column.column_name if parent.pk_column else None) or "id"
|
||||
ctx["parent_rel_name"] = SnakeCaseUtil.camel_to_snake(parent.class_name or "parent")
|
||||
ctx["parent_list_rel_name"] = f"{SnakeCaseUtil.camel_to_snake(scn)}_list"
|
||||
ctx["sub_table_fk_name"] = (parent.sub_table_fk_name or "").strip()
|
||||
ctx["model_import_list"] = cls.get_model_import_list(sub, is_sub_entity=True)
|
||||
ctx["schema_import_list"] = cls.get_schema_import_list(sub)
|
||||
return ctx
|
||||
|
||||
@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/model.py.j2",
|
||||
"python/__init__.py.j2",
|
||||
"ts/api.ts.j2",
|
||||
"vue/index.vue.j2",
|
||||
]
|
||||
return templates
|
||||
|
||||
@classmethod
|
||||
def get_sub_table_template_list(cls):
|
||||
"""
|
||||
获取子表模板列表(仅 model / schema / __init__,不含 controller/service/crud/vue/api)。
|
||||
|
||||
参数:
|
||||
- 无
|
||||
|
||||
返回:
|
||||
- List[str]: 子表模板路径列表。
|
||||
"""
|
||||
return [
|
||||
"python/model.py.j2",
|
||||
"python/schema.py.j2",
|
||||
"python/__init__.py.j2",
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def get_file_name(cls, template: str, gen_table: GenTableOutSchema):
|
||||
"""
|
||||
根据模板生成文件名。
|
||||
|
||||
参数:
|
||||
- template (str): 模板路径字符串。
|
||||
- gen_table (GenTableOutSchema): 生成表的配置信息。
|
||||
|
||||
返回:
|
||||
- str: 模板生成的文件名。
|
||||
|
||||
异常:
|
||||
- ValueError: 当无法生成有效文件名时抛出。
|
||||
"""
|
||||
package_name = (gen_table.package_name or "").strip()
|
||||
module_name = (gen_table.module_name or "").strip()
|
||||
|
||||
if not package_name:
|
||||
raise ValueError(f"无法为模板 {template} 生成文件名:包名未设置")
|
||||
if not module_name:
|
||||
raise ValueError(f"无法为模板 {template} 生成文件名:模块名未设置")
|
||||
|
||||
# 目录固定为:module_xxx/{module_name}
|
||||
backend_base = f"{cls.BACKEND_PROJECT_PATH}/app/plugin/{package_name}"
|
||||
frontend_view_base = f"{cls.FRONTEND_PROJECT_PATH}/src/views/{package_name}"
|
||||
frontend_api_base = f"{cls.FRONTEND_PROJECT_PATH}/src/api/{package_name}"
|
||||
|
||||
backend_dir = f"{backend_base}/{module_name}"
|
||||
view_dir = f"{frontend_view_base}/{module_name}"
|
||||
api_path = f"{frontend_api_base}/{module_name}.ts"
|
||||
|
||||
template_mapping = {
|
||||
"controller.py.j2": f"{backend_dir}/controller.py",
|
||||
"service.py.j2": f"{backend_dir}/service.py",
|
||||
"crud.py.j2": f"{backend_dir}/crud.py",
|
||||
"schema.py.j2": f"{backend_dir}/schema.py",
|
||||
"model.py.j2": f"{backend_dir}/model.py",
|
||||
"__init__.py.j2": f"{backend_dir}/__init__.py",
|
||||
"api.ts.j2": api_path,
|
||||
"index.vue.j2": f"{view_dir}/index.vue",
|
||||
}
|
||||
|
||||
# 查找匹配的模板路径
|
||||
for key, path in template_mapping.items():
|
||||
if key in template:
|
||||
return path
|
||||
|
||||
# 遍历完所有映射都没找到匹配项,才抛出异常
|
||||
raise ValueError(f"未找到模板 '{template}' 的路径映射")
|
||||
|
||||
@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 模板所需的 Python 导入语句集合。
|
||||
|
||||
参数:
|
||||
- gen_table (GenTableOutSchema): 生成表配置(含主子表列)。
|
||||
|
||||
返回:
|
||||
- set[str]: 导入语句字符串集合。
|
||||
"""
|
||||
columns = gen_table.columns or []
|
||||
import_list = set()
|
||||
has_datetime_import = False
|
||||
has_date_import = False
|
||||
has_time_import = False
|
||||
has_datetime_str = False
|
||||
has_date_str = False
|
||||
has_time_str = False
|
||||
|
||||
for column in columns:
|
||||
# 处理datetime类型的导入
|
||||
if column.python_type and column.python_type in GenConstant.TYPE_DATE:
|
||||
if column.python_type == "datetime":
|
||||
has_datetime_import = True
|
||||
elif column.python_type == "date":
|
||||
has_date_import = True
|
||||
elif column.python_type == "time":
|
||||
has_time_import = True
|
||||
elif column.python_type == GenConstant.TYPE_DECIMAL:
|
||||
import_list.add("from decimal import Decimal")
|
||||
|
||||
# 检查是否需要DateTimeStr、DateStr、TimeStr
|
||||
if column.column_name == "created_time" or column.column_name == "updated_time":
|
||||
has_datetime_str = True
|
||||
|
||||
if gen_table.sub and gen_table.sub_table and gen_table.sub_table.columns:
|
||||
sub_columns = gen_table.sub_table.columns or []
|
||||
for sub_column in sub_columns:
|
||||
# 处理datetime类型的导入
|
||||
if sub_column.python_type and sub_column.python_type in GenConstant.TYPE_DATE:
|
||||
if sub_column.python_type == "datetime":
|
||||
has_datetime_import = True
|
||||
elif sub_column.python_type == "date":
|
||||
has_date_import = True
|
||||
elif sub_column.python_type == "time":
|
||||
has_time_import = True
|
||||
elif sub_column.python_type == GenConstant.TYPE_DECIMAL:
|
||||
import_list.add("from decimal import Decimal")
|
||||
|
||||
# 添加datetime导入
|
||||
if has_datetime_import:
|
||||
import_list.add("from datetime import datetime")
|
||||
if has_date_import:
|
||||
import_list.add("from datetime import date")
|
||||
if has_time_import:
|
||||
import_list.add("from datetime import time")
|
||||
|
||||
# 添加validator导入
|
||||
if has_datetime_str:
|
||||
import_list.add("from app.core.validator import DateTimeStr")
|
||||
if has_date_str:
|
||||
import_list.add("from app.core.validator import DateStr")
|
||||
if has_time_str:
|
||||
import_list.add("from app.core.validator import TimeStr")
|
||||
|
||||
return import_list
|
||||
|
||||
@classmethod
|
||||
def get_model_import_list(cls, gen_table: GenTableOutSchema, *, is_sub_entity: bool = False) -> list[str]:
|
||||
"""
|
||||
获取 model 模板所需的 Python 导入语句列表(含合并后的 sqlalchemy 导入)。
|
||||
|
||||
参数:
|
||||
- gen_table (GenTableOutSchema): 生成表配置。
|
||||
- is_sub_entity (bool): 是否为子表独立生成(含外键与 relationship)。
|
||||
|
||||
返回:
|
||||
- list[str]: 导入语句列表。
|
||||
"""
|
||||
columns = gen_table.columns or []
|
||||
import_list = set()
|
||||
has_datetime_import = False
|
||||
has_date_import = False
|
||||
has_time_import = False
|
||||
|
||||
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)}")
|
||||
# 处理datetime类型的导入
|
||||
if column.python_type and column.python_type in GenConstant.TYPE_DATE:
|
||||
if column.python_type == "datetime":
|
||||
has_datetime_import = True
|
||||
elif column.python_type == "date":
|
||||
has_date_import = True
|
||||
elif column.python_type == "time":
|
||||
has_time_import = True
|
||||
# 处理Decimal类型的导入
|
||||
elif column.python_type == GenConstant.TYPE_DECIMAL:
|
||||
import_list.add("from decimal import Decimal")
|
||||
if gen_table.sub or is_sub_entity:
|
||||
import_list.add("from sqlalchemy import ForeignKey")
|
||||
if gen_table.sub and not is_sub_entity and 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)}")
|
||||
# 处理datetime类型的导入
|
||||
if sub_column.python_type and sub_column.python_type in GenConstant.TYPE_DATE:
|
||||
if sub_column.python_type == "datetime":
|
||||
has_datetime_import = True
|
||||
elif sub_column.python_type == "date":
|
||||
has_date_import = True
|
||||
elif sub_column.python_type == "time":
|
||||
has_time_import = True
|
||||
# 处理Decimal类型的导入
|
||||
elif sub_column.python_type == GenConstant.TYPE_DECIMAL:
|
||||
import_list.add("from decimal import Decimal")
|
||||
|
||||
# 添加datetime导入
|
||||
if has_datetime_import:
|
||||
import_list.add("from datetime import datetime")
|
||||
if has_date_import:
|
||||
import_list.add("from datetime import date")
|
||||
if has_time_import:
|
||||
import_list.add("from datetime import time")
|
||||
|
||||
merged = cls.merge_same_imports(list(import_list), "from sqlalchemy import")
|
||||
if gen_table.sub or is_sub_entity:
|
||||
merged.append("from sqlalchemy.orm import relationship")
|
||||
return merged
|
||||
|
||||
@classmethod
|
||||
def get_db_type(cls, column_type: str) -> str:
|
||||
"""
|
||||
获取数据库字段类型。
|
||||
|
||||
参数:
|
||||
- column_type (str): 字段类型字符串。
|
||||
|
||||
返回:
|
||||
- str: 数据库类型(去除长度等修饰)。
|
||||
"""
|
||||
# 移除 COLLATE 子句(处理带引号和不带引号的情况,不区分大小写)
|
||||
collate_pattern = re.compile(r"\s+COLLATE\s+", re.IGNORECASE)
|
||||
if collate_pattern.search(column_type):
|
||||
column_type = collate_pattern.split(column_type)[0].strip()
|
||||
|
||||
# 移除 UNSIGNED 标记(不区分大小写)
|
||||
unsigned_pattern = re.compile(r"\s+UNSIGNED", re.IGNORECASE)
|
||||
if unsigned_pattern.search(column_type):
|
||||
column_type = unsigned_pattern.sub("", column_type).strip()
|
||||
|
||||
# 处理PostgreSQL数组类型(如 integer[], text[])
|
||||
if "[]" in column_type:
|
||||
return "array"
|
||||
|
||||
# 提取基本类型
|
||||
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_:
|
||||
# 去重并过滤空字符串,然后用逗号连接
|
||||
unique_imports = [item for item in imports_ if item]
|
||||
if len(unique_imports) > 0:
|
||||
merged_datetime_import = f"{import_start} {', '.join(unique_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]) -> None:
|
||||
"""
|
||||
添加字典类型到集合。
|
||||
|
||||
参数:
|
||||
- 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: 权限前缀字符串。
|
||||
"""
|
||||
mn = (module_name or "").strip()
|
||||
bn = (business_name or "").strip().replace("/", ":")
|
||||
if not bn:
|
||||
return mn
|
||||
return f"{mn}:{bn}"
|
||||
|
||||
@classmethod
|
||||
def python_type_to_ts_type(cls, python_type: str | None) -> str:
|
||||
"""
|
||||
将列上的 Python 类型(`get_db_type` + `DB_TO_PYTHON` 映射结果)转为前端 TS 类型片段。
|
||||
|
||||
与 JSON 序列化习惯一致:Decimal、日期时间多为字符串;dict/list 用宽松类型。
|
||||
|
||||
参数:
|
||||
- python_type (str | None): Python 类型名。
|
||||
|
||||
返回:
|
||||
- str: 前端 TypeScript 类型片段。
|
||||
"""
|
||||
if not python_type or not str(python_type).strip():
|
||||
return "string"
|
||||
p = str(python_type).strip()
|
||||
mapping: dict[str, str] = {
|
||||
"int": "number",
|
||||
"float": "number",
|
||||
"bool": "boolean",
|
||||
"Decimal": "string",
|
||||
"date": "string",
|
||||
"time": "string",
|
||||
"datetime": "string",
|
||||
"timedelta": "string",
|
||||
"dict": "Record<string, unknown>",
|
||||
"list": "unknown[]",
|
||||
"bytes": "string",
|
||||
"str": "string",
|
||||
}
|
||||
return mapping.get(p, "string")
|
||||
|
||||
@classmethod
|
||||
def get_api_route_prefix(cls, module_name: str | None) -> str:
|
||||
"""
|
||||
获取前端 API 路径首段,与 `discover` 中插件路由前缀一致(`module_xxx` → `xxx`)。
|
||||
|
||||
参数:
|
||||
- module_name (str | None): 模块名,如 ``module_example``。
|
||||
|
||||
返回:
|
||||
- str: 路由前缀,如 ``example``。
|
||||
"""
|
||||
if not module_name:
|
||||
return ""
|
||||
if module_name.startswith("module_"):
|
||||
return module_name[7:]
|
||||
return module_name
|
||||
|
||||
@classmethod
|
||||
def get_sqlalchemy_type(cls, column: Any) -> str:
|
||||
"""
|
||||
获取 SQLAlchemy 类型。
|
||||
|
||||
参数:
|
||||
- column (Any): 列对象或列类型字符串。
|
||||
|
||||
返回:
|
||||
- str: SQLAlchemy 类型字符串。
|
||||
"""
|
||||
# 获取column_type和column_length
|
||||
column_type = column
|
||||
column_length = None
|
||||
|
||||
# 检查是否是对象
|
||||
if hasattr(column, "column_type"):
|
||||
column_type = column.column_type or ""
|
||||
column_length = column.column_length or None
|
||||
|
||||
column_type = cls.normalize_db_column_type_for_mapping(column_type)
|
||||
|
||||
# MySQL:仅 tinyint(1) 映射为 Boolean;其余 tinyint 走 SmallInteger(见 GenConstant.DB_TO_SQLALCHEMY)
|
||||
ct_norm = (column_type or "").strip()
|
||||
if settings.DATABASE_TYPE != "postgres" and ct_norm:
|
||||
ct_lower = ct_norm.lower()
|
||||
if ct_lower.startswith("tinyint(1)"):
|
||||
return "Boolean"
|
||||
|
||||
# 首先尝试匹配完整类型(包括括号)
|
||||
sqlalchemy_type = StringUtil.get_mapping_value_by_key_ignore_case(GenConstant.DB_TO_SQLALCHEMY, column_type)
|
||||
|
||||
# 特殊处理PostgreSQL类型
|
||||
if settings.DATABASE_TYPE == "postgres":
|
||||
if column_type.upper() == "BOOLEAN":
|
||||
return "Boolean"
|
||||
elif column_type.upper() == "REAL":
|
||||
return "Float"
|
||||
elif column_type.upper() == "DOUBLE PRECISION":
|
||||
return "Float"
|
||||
elif column_type.upper() == "TIMESTAMP":
|
||||
return "DateTime"
|
||||
elif column_type.upper() == "JSONB":
|
||||
return "JSONB"
|
||||
elif column_type.upper() == "UUID":
|
||||
return "Uuid"
|
||||
elif column_type.upper() == "BYTEA":
|
||||
return "LargeBinary"
|
||||
|
||||
# get_mapping_value_by_key_ignore_case 未命中时返回 "",须与 None 同样视为未匹配
|
||||
if not sqlalchemy_type and "(" in column_type:
|
||||
# 如果没有匹配到,再尝试剥离括号
|
||||
column_type_list = column_type.split("(")
|
||||
col_type = column_type_list[0]
|
||||
# 将 'character' 映射为 'char' 以匹配常量定义
|
||||
if col_type.lower() == "character":
|
||||
col_type = "char"
|
||||
sqlalchemy_type = StringUtil.get_mapping_value_by_key_ignore_case(GenConstant.DB_TO_SQLALCHEMY, col_type)
|
||||
# 如果是字符串类型且包含括号参数,保持原参数
|
||||
if sqlalchemy_type in ["String", "CHAR"]:
|
||||
sqlalchemy_type += "(" + column_type_list[1]
|
||||
# 如果是Numeric或DECIMAL类型且包含括号参数,保持原参数
|
||||
elif sqlalchemy_type in ["Numeric", "DECIMAL"]:
|
||||
sqlalchemy_type += "(" + column_type_list[1]
|
||||
elif not sqlalchemy_type:
|
||||
# 处理没有括号的类型
|
||||
col_type = column_type
|
||||
# 将 'character' 映射为 'char' 以匹配常量定义
|
||||
if col_type.lower() == "character":
|
||||
col_type = "char"
|
||||
sqlalchemy_type = StringUtil.get_mapping_value_by_key_ignore_case(GenConstant.DB_TO_SQLALCHEMY, col_type)
|
||||
# 如果是字符串类型且没有指定长度,使用column_length或默认255
|
||||
if sqlalchemy_type in ["String", "CHAR"]:
|
||||
length = column_length if column_length and column_length.isdigit() else "255"
|
||||
sqlalchemy_type += f"({length})"
|
||||
else:
|
||||
# 对于已经匹配到的类型,如果是字符串类型且column有长度信息,添加长度
|
||||
if sqlalchemy_type in ["String", "CHAR"] and "(" not in sqlalchemy_type:
|
||||
# 检查column_length是否有效
|
||||
length = column_length if column_length and column_length.isdigit() else "255"
|
||||
sqlalchemy_type += f"({length})"
|
||||
|
||||
# 如果没有找到匹配的类型,使用String(column_length)或String(255)作为默认类型
|
||||
if not sqlalchemy_type:
|
||||
length = column_length if column_length and column_length.isdigit() else "255"
|
||||
sqlalchemy_type = f"String({length})"
|
||||
return sqlalchemy_type
|
||||
Reference in New Issue
Block a user