refactor: 大规模代码整理与功能优化

1. 重构后端API路由、CRUD与模块结构,整合日志管理,移除废弃demo代码
2. 优化前端组件类型定义、样式与路由配置,修复权限判断逻辑
3. 调整默认排序规则、滚动条样式与工具类函数,更新依赖与配置文件
4. 修复多处类型不匹配与默认值问题,完善表单与菜单验证逻辑
This commit is contained in:
zhangtao
2026-06-17 01:56:31 +08:00
parent 17b3cd0a4c
commit 73f2823692
500 changed files with 40763 additions and 26616 deletions
@@ -3,11 +3,11 @@ from typing import Annotated
from fastapi import APIRouter, Body, Depends, Path
from fastapi.responses import JSONResponse
from app.api.v1.module_system.auth.schema import AuthSchema
from app.common.response import ResponseSchema, StreamResponse, SuccessResponse
from app.core.base_params import PaginationQueryParam
from app.core.base_schema import AuthSchema, PageResultSchema
from app.core.dependencies import AuthPermission
from app.core.logger import log
from app.core.logger import logger
from app.core.router_class import OperationLogRoute
from app.utils.common_util import bytes2file_response
@@ -21,13 +21,12 @@ from .schema import (
)
from .service import GenTableService
GenRouter = APIRouter(route_class=OperationLogRoute, prefix="/gencode", tags=["代码生成模块"])
GenRouter = APIRouter(route_class=OperationLogRoute, prefix="/gencode", tags=["开发工具/代码生成"])
@GenRouter.get(
"/list",
summary="查询代码生成业务表列表",
description="查询代码生成业务表列表",
response_model=ResponseSchema[list[GenTableOutSchema]],
)
async def gen_table_list_controller(
@@ -56,15 +55,13 @@ async def gen_table_list_controller(
search=search,
order_by=order_by,
)
log.info("获取代码生成业务表列表成功")
return SuccessResponse(data=result_dict, msg="获取代码生成业务表列表成功")
@GenRouter.get(
"/db/list",
summary="查询数据库表列表",
description="查询数据库表列表",
response_model=ResponseSchema[list[GenDBTableSchema]],
response_model=ResponseSchema[PageResultSchema[GenDBTableSchema]],
)
async def get_gen_db_table_list_controller(
page: Annotated[PaginationQueryParam, Depends()],
@@ -89,14 +86,12 @@ async def get_gen_db_table_list_controller(
page_size=page.page_size,
search=search,
)
log.info("获取数据库表列表成功")
return SuccessResponse(data=result_dict, msg="获取数据库表列表成功")
@GenRouter.post(
"/import",
summary="导入表结构",
description="导入表结构",
response_model=ResponseSchema[bool],
)
async def import_gen_table_controller(
@@ -120,14 +115,12 @@ async def import_gen_table_controller(
auth, table_names
)
result = await GenTableService.import_gen_table_service(auth, add_gen_table_list)
log.info("导入表结构成功")
return SuccessResponse(msg="导入表结构成功", data=result)
@GenRouter.get(
"/detail/{table_id}",
summary="获取业务表详细信息",
description="获取业务表详细信息",
response_model=ResponseSchema[GenTableOutSchema],
)
async def gen_table_detail_controller(
@@ -145,14 +138,12 @@ async def gen_table_detail_controller(
- JSONResponse: 包含业务表详细信息的JSON响应
"""
gen_table_detail_result = await GenTableService.get_gen_table_detail_service(auth, table_id)
log.info(f"获取table_id为{table_id}的信息成功")
return SuccessResponse(data=gen_table_detail_result, msg="获取业务表详细信息成功")
@GenRouter.post(
"/create",
summary="创建表结构",
description="创建表结构",
response_model=ResponseSchema[bool],
)
async def create_table_controller(
@@ -173,14 +164,12 @@ async def create_table_controller(
- JSONResponse: 包含创建结果的JSON响应
"""
result = await GenTableService.create_table_service(auth, body.sql)
log.info("创建表结构成功")
return SuccessResponse(msg="创建表结构成功", data=result)
@GenRouter.put(
"/update/{table_id}",
summary="编辑业务表信息",
description="编辑业务表信息",
response_model=ResponseSchema[GenTableOutSchema],
)
async def update_gen_table_controller(
@@ -203,14 +192,12 @@ async def update_gen_table_controller(
- JSONResponse: 包含编辑结果的JSON响应
"""
result_dict = await GenTableService.update_gen_table_service(auth, data, table_id)
log.info("编辑业务表信息成功")
return SuccessResponse(data=result_dict, msg="编辑业务表信息成功")
@GenRouter.delete(
"/delete",
summary="删除业务表信息",
description="删除业务表信息",
response_model=ResponseSchema[None],
)
async def delete_gen_table_controller(
@@ -231,14 +218,12 @@ async def delete_gen_table_controller(
- JSONResponse: 包含删除结果的JSON响应
"""
result = await GenTableService.delete_gen_table_service(auth, ids)
log.info("删除业务表信息成功")
return SuccessResponse(msg="删除业务表信息成功", data=result)
@GenRouter.patch(
"/batch/output",
summary="批量生成代码",
description="批量生成代码",
)
async def batch_gen_code_controller(
table_names: Annotated[list[str], Body(description="表名列表")],
@@ -254,19 +239,21 @@ async def batch_gen_code_controller(
返回:
- StreamResponse: 包含批量生成代码的ZIP文件流响应
"""
batch_gen_code_result = await GenTableService.batch_gen_code_service(auth, table_names)
log.info(f"批量生成代码成功,表名列表:{table_names}")
batch_gen_code_result, failed_tables = await GenTableService.batch_gen_code_service(auth, table_names)
headers = {"Content-Disposition": "attachment; filename=code.zip"}
if failed_tables:
logger.warning(f"批量生成代码部分失败,跳过表: {failed_tables}")
headers["X-Skipped-Tables"] = ",".join(failed_tables)
return StreamResponse(
data=bytes2file_response(batch_gen_code_result),
media_type="application/zip",
headers={"Content-Disposition": "attachment; filename=code.zip"},
headers=headers,
)
@GenRouter.post(
"/output/{table_name}",
summary="生成代码到指定路径",
description="生成代码到指定路径",
response_model=ResponseSchema[bool],
)
async def gen_code_local_controller(
@@ -284,15 +271,13 @@ async def gen_code_local_controller(
- JSONResponse: 包含生成结果的JSON响应
"""
result = await GenTableService.generate_code_service(auth, table_name)
log.info(f"生成代码,表名:{table_name},到指定路径成功")
return SuccessResponse(msg="生成代码到指定路径成功", data=result)
@GenRouter.get(
"/preview/{table_id}",
summary="预览代码",
description="预览代码",
response_model=ResponseSchema[dict],
response_model=ResponseSchema[GenTableOutSchema],
)
async def preview_code_controller(
table_id: Annotated[int, Path(description="业务表ID")],
@@ -309,14 +294,12 @@ async def preview_code_controller(
- JSONResponse: 包含预览代码的JSON响应
"""
preview_code_result = await GenTableService.preview_code_service(auth, table_id)
log.info(f"预览代码,表id{table_id},成功")
return SuccessResponse(data=preview_code_result, msg="预览代码成功")
@GenRouter.post(
"/sync_db/{table_name}",
summary="同步数据库",
description="同步数据库",
response_model=ResponseSchema[None],
)
async def sync_db_controller(
@@ -334,14 +317,12 @@ async def sync_db_controller(
- JSONResponse: 包含同步数据库结果的JSON响应
"""
result = await GenTableService.sync_db_service(auth, table_name)
log.info(f"同步数据库,表名:{table_name},成功")
return SuccessResponse(msg="同步数据库成功", data=result)
@GenRouter.get(
"/sync_db/preview/{table_name}",
summary="同步数据库差异预览",
description="同步数据库前差异预览(主表 + 可选子表),不落库",
response_model=ResponseSchema[GenSyncPreviewSchema],
)
async def sync_db_preview_controller(
@@ -1,12 +1,13 @@
import asyncio
from collections.abc import Sequence
from typing import TYPE_CHECKING
from sqlalchemy import Inspector, inspect, select, text
from app.api.v1.module_system.auth.schema import AuthSchema
from app.config.setting import settings
from app.core.base_crud import CRUDBase
from app.core.logger import log
from app.core.base_schema import AuthSchema
from app.core.logger import logger
from .model import GenTableColumnModel, GenTableModel
from .schema import (
@@ -166,7 +167,7 @@ class GenTableCRUD(CRUDBase[GenTableModel, GenTableSchema, GenTableSchema]):
)
table_comment = comment or ""
except Exception as e:
log.warning(f"获取表 {table_name} 的注释失败: {e}")
logger.warning(f"获取表 {table_name} 的注释失败: {e}")
table_comment = ""
# 统一处理 search 为 None 的情况,避免重复判断
@@ -329,21 +330,42 @@ class GenTableCRUD(CRUDBase[GenTableModel, GenTableSchema, GenTableSchema]):
返回:
- list[GenDBTableSchema]: 数据库表信息对象列表。
"""
# 处理空列表情况
if not table_names:
return []
# 调用get_db_table_list获取所有表信息
all_tables = await self.get_db_table_list()
# 过滤出指定名称的表
table_names_set = set(table_names) # 转换为集合以提高查找效率
filtered_tables = [
GenDBTableSchema(**table)
for table in all_tables
if table["table_name"] in table_names_set
]
database_name = settings.DATABASE_NAME
database_type = settings.DATABASE_TYPE
return filtered_tables
from app.core.database import engine
inspector: Inspector = inspect(engine)
all_table_names = set(inspector.get_table_names())
results = []
for table_name in table_names:
if table_name not in all_table_names:
continue
try:
table_comment = inspector.get_table_comment(table_name)
comment = (
table_comment.get("text", "")
if isinstance(table_comment, dict)
else (table_comment or "")
)
except Exception as e:
logger.warning(f"获取表 {table_name} 的注释失败: {e}")
comment = ""
results.append(
GenDBTableSchema(
database_name=database_name,
table_name=table_name,
table_type=database_type,
table_comment=comment or "",
)
)
return results
async def check_table_exists(self, table_name: str) -> bool:
"""
@@ -384,7 +406,7 @@ class GenTableCRUD(CRUDBase[GenTableModel, GenTableSchema, GenTableSchema]):
)
return comment or ""
except Exception as e:
log.warning(f"获取表 {table_name} 的注释失败: {e}")
logger.warning(f"获取表 {table_name} 的注释失败: {e}")
return ""
async def execute_sql(self, sql: str) -> bool:
@@ -402,7 +424,7 @@ class GenTableCRUD(CRUDBase[GenTableModel, GenTableSchema, GenTableSchema]):
await self.auth.db.execute(text(sql))
return True
except Exception as e:
log.error(f"执行SQL时发生错误: {e}")
logger.error(f"执行SQL时发生错误: {e}")
return False
@@ -470,11 +492,8 @@ class GenTableColumnCRUD(CRUDBase[GenTableColumnModel, GenTableColumnSchema, Gen
# 判断是否为自增列(基于数据库类型和列类型)
is_increment = column.get("autoincrement", False) in (True, "auto")
# 获取列长度(如果适用)
column_length = None
# 使用getattr安全地获取length属性,避免访问不存在时抛出AttributeError
column_length = getattr(column["type"], "length", None)
if column_length is not None:
column_length = str(getattr(column["type"], "length", ""))
col_len = getattr(column["type"], "length", None)
column_length = str(col_len) if col_len is not None else ""
# 构造列信息字典
column_info = {
@@ -557,9 +576,11 @@ class GenTableColumnCRUD(CRUDBase[GenTableColumnModel, GenTableColumnSchema, Gen
raise ValueError("数据表名称不能为空")
try:
# 直接调用同步方法获取列信息
columns_info = GenTableColumnCRUD._sync_get_table_columns(
settings.DATABASE_TYPE, table_name
# 在线程池中执行同步 inspect 操作,避免阻塞事件循环
columns_info = await asyncio.to_thread(
GenTableColumnCRUD._sync_get_table_columns,
settings.DATABASE_TYPE,
table_name,
)
# 转换为GenTableColumnOutSchema对象列表
@@ -567,7 +588,7 @@ class GenTableColumnCRUD(CRUDBase[GenTableColumnModel, GenTableColumnSchema, Gen
return columns_list
except Exception as e:
log.error(f"获取表{table_name}的字段列表时出错: {e!s}")
logger.error(f"获取表{table_name}的字段列表时出错: {e!s}")
# 确保即使出错也返回空列表而不是None
raise
@@ -614,9 +635,7 @@ class GenTableColumnCRUD(CRUDBase[GenTableColumnModel, GenTableColumnSchema, Gen
返回:
- GenTableColumnModel | None: 业务表字段列表信息对象。
"""
# 将对象转换为字典,避免SQLAlchemy直接操作对象时出现的状态问题
data_dict = data.model_dump(exclude_unset=True)
return await self.update(id=id, data=data_dict)
return await self.update(id=id, data=data)
async def delete_gen_table_column_by_table_id_crud(self, table_ids: list[int]) -> None:
"""根据业务表ID批量删除业务表字段。
@@ -94,7 +94,7 @@ class GenTableSchema(BaseModel):
default=None,
description=(
"功能子目录/路由段;导入时默认表名;同 module_name 下多表须不同。"
"可含斜杠表示嵌套,参考 module_exampledemo、demo/demo01、gen_demo02"
"可含斜杠表示嵌套,参考 module_exampledemo、demo/subdir、gen_demo。"
),
)
function_name: str | None = Field(default=None, description="生成功能名")
@@ -223,7 +223,7 @@ class GenTableSchema(BaseModel):
@classmethod
def normalize_business_name(cls, v: str | None) -> str | None:
"""
业务名允许多段(如 demo/demo01);统一按 slug 规范。
业务名允许多段(如 demo/subdir);统一按 slug 规范。
参数:
- v (str | None): 原始业务名。
@@ -19,12 +19,13 @@ from sqlglot.expressions import (
Update,
)
from app.api.v1.module_system.auth.schema import AuthSchema
from app.common.constant import GenConstant
from app.common.enums import QueueEnum
from app.config.path_conf import BASE_DIR
from app.config.setting import settings
from app.core.base_schema import AuthSchema
from app.core.exceptions import CustomException
from app.core.logger import log
from app.core.logger import logger
from .crud import GenTableColumnCRUD, GenTableCRUD
from .schema import (
@@ -62,7 +63,7 @@ def handle_service_exception(func: Callable) -> Callable:
return wrapper
_MENU_TYPE_CATALOG = 1 # 与 sys_menu.type、前端 MenuTypeEnum.CATALOG 一致
_MENU_TYPE_CATALOG = 1 # 与 platform_menu.type、前端 MenuTypeEnum.CATALOG 一致
_MENU_TYPE_MENU = 2
@@ -82,9 +83,9 @@ class GenTableService:
pn = (package_name or "").strip()
# 1) 选择上级目录:从上级菜单 route_path 第一段推断 module_xxx
if parent_catalog_id is not None:
from app.api.v1.module_system.menu.crud import MenuCRUD
from app.api.v1.module_platform.menu.crud import MenuCRUD
m = await MenuCRUD(auth).get_by_id_crud(parent_catalog_id)
m = await MenuCRUD(auth).get(id=parent_catalog_id)
if not m:
raise CustomException(msg="上级菜单不存在")
route_path = (getattr(m, "route_path", None) or "").strip()
@@ -110,9 +111,9 @@ class GenTableService:
"""上级菜单仅允许目录:与前端树只展示目录一致,避免挂到菜单/按钮下。"""
if parent_menu_id is None:
return
from app.api.v1.module_system.menu.crud import MenuCRUD
from app.api.v1.module_platform.menu.crud import MenuCRUD
m = await MenuCRUD(auth).get_by_id_crud(parent_menu_id)
m = await MenuCRUD(auth).get(id=parent_menu_id)
if not m:
raise CustomException(msg="上级菜单不存在")
if m.type != _MENU_TYPE_CATALOG:
@@ -161,7 +162,7 @@ class GenTableService:
business_name: str,
) -> int:
"""创建或复用 type=1 模块目录;固定为「目录 → 菜单 → 按钮」中的第一层目录。"""
from app.api.v1.module_system.menu.schema import MenuCreateSchema
from app.api.v1.module_platform.menu.schema import MenuCreateSchema
from app.utils.common_util import CamelCaseUtil
pn = (package_name or "").strip()
@@ -176,10 +177,10 @@ class GenTableService:
)
else:
existing = await menu_crud.get(
name=dir_key, type=_MENU_TYPE_CATALOG, parent_id=("None", None)
name=dir_key, type=_MENU_TYPE_CATALOG, parent_id=(QueueEnum.none.value, None)
)
if existing:
log.info(
logger.info(
f"代码生成:复用模块目录菜单 id={existing.id} name={dir_key!r} parent={parent_catalog_id!r}"
)
return int(existing.id)
@@ -212,7 +213,7 @@ class GenTableService:
description="模块目录(代码生成)",
)
)
log.info(
logger.info(
f"代码生成:新建模块目录菜单 id={created.id} name={dir_key!r} under_parent={parent_catalog_id!r}"
)
return int(created.id)
@@ -241,7 +242,7 @@ class GenTableService:
@classmethod
@handle_service_exception
async def get_gen_table_detail_service(cls, auth: AuthSchema, table_id: int) -> dict:
async def get_gen_table_detail_service(cls, auth: AuthSchema, table_id: int) -> GenTableOutSchema:
"""获取详细信息。
参数:
@@ -252,7 +253,7 @@ class GenTableService:
- dict: 包含业务表详细信息的字典。
"""
gen_table = await cls.get_gen_table_by_id_service(auth, table_id)
return gen_table.model_dump()
return gen_table
@classmethod
@handle_service_exception
@@ -270,7 +271,7 @@ class GenTableService:
- list[dict]: 包含业务表列表信息的字典列表。
"""
gen_table_list_result = await GenTableCRUD(auth=auth).get_gen_table_list(search)
return [GenTableOutSchema.model_validate(obj).model_dump() for obj in gen_table_list_result]
return [GenTableOutSchema.model_validate(obj) for obj in gen_table_list_result]
@classmethod
@handle_service_exception
@@ -504,7 +505,7 @@ class GenTableService:
if not isinstance(sql_statement, (Create, Comment, Alter)):
continue
exc_sql = sql_statement.sql(dialect=settings.DATABASE_TYPE)
log.info(f"执行SQL语句: {exc_sql}")
logger.info(f"执行SQL语句: {exc_sql}")
# ALTER 仅允许添加外键约束,避免任意 ALTER 带来的破坏性
if isinstance(sql_statement, Alter):
@@ -524,6 +525,15 @@ class GenTableService:
)
if not await gen_table_crud.execute_sql(exc_sql):
raise CustomException(msg=f"执行SQL语句 {exc_sql} 失败,请检查数据库")
# 建表成功后自动导入到代码生成模块
if table_names:
gen_table_list = await cls.get_gen_db_table_list_by_name_service(
auth, table_names
)
if gen_table_list:
await cls.import_gen_table_service(auth, gen_table_list)
return True
except Exception as e:
@@ -533,7 +543,7 @@ class GenTableService:
@handle_service_exception
async def update_gen_table_service(
cls, auth: AuthSchema, data: GenTableSchema, table_id: int
) -> dict[str, Any]:
) -> GenTableOutSchema:
"""编辑业务表信息。
参数:
@@ -555,21 +565,54 @@ class GenTableService:
if not result:
raise CustomException(msg="更新业务表信息失败")
# 处理data.columns为None的情况
if data.columns:
if data.columns is not None:
db_columns = await GenTableColumnCRUD(auth).list_gen_table_column_crud(
search={"table_id": table_id}
)
db_column_map = {c.column_name: c for c in db_columns if c.column_name}
submitted_names = {
c.column_name
for c in data.columns
if hasattr(c, "column_name") and c.column_name
}
for gen_table_column in data.columns:
# 确保column有id字段
if hasattr(gen_table_column, "id") and gen_table_column.id:
column_schema = GenTableColumnSchema(**gen_table_column.model_dump())
await GenTableColumnCRUD(auth).update_gen_table_column_crud(
gen_table_column.id, column_schema
col_id = getattr(gen_table_column, "id", None)
col_name = getattr(gen_table_column, "column_name", None)
if col_id and col_name and col_name in db_column_map:
# 只更新前端实际修改的字段(利用 Pydantic model_fields_set
update_data = gen_table_column.model_dump(
exclude_unset=True, exclude={"id", "super_column"}
)
if update_data:
await GenTableColumnCRUD(auth).update(
id=col_id, data=update_data
)
else:
# 新增列:前端新增但库中无对应记录
column_schema = GenTableColumnSchema(
table_id=table_id,
**gen_table_column.model_dump(
exclude={"id", "super_column"}
),
)
GenUtils.init_column_field(column_schema, gen_table_info)
await GenTableColumnCRUD(auth).create_gen_table_column_crud(
column_schema
)
# 删除前端已移除的列
for db_name, db_col in db_column_map.items():
if db_name not in submitted_names:
db_id = getattr(db_col, "id", None)
if db_id:
await GenTableColumnCRUD(auth).delete(ids=[db_id])
# 重新获取带有预加载关系的对象,避免懒加载导致的MissingGreenlet错误
updated_gen_table = await GenTableCRUD(auth).get_gen_table_by_id(table_id)
out = GenTableOutSchema.model_validate(updated_gen_table)
await cls.set_pk_column(out)
await cls.hydrate_sub_table(auth, out)
return out.model_dump()
return out
except CustomException:
raise
except Exception as e:
@@ -642,7 +685,7 @@ class GenTableService:
table_out = GenTableOutSchema.model_validate(gen_table)
result.append(table_out)
except Exception as e:
log.error(f"转换业务表时出错: {e!s}")
logger.error(f"转换业务表时出错: {e!s}")
continue
return result
@@ -687,7 +730,7 @@ class GenTableService:
out_key = Jinja2TemplateUtil.get_file_name(template, gen_table)
preview_code_result[out_key] = render_content
except Exception as e:
log.error(f"渲染模板 {template} 时出错: {e!s}")
logger.error(f"渲染模板 {template} 时出错: {e!s}")
out_key = Jinja2TemplateUtil.get_file_name(template, gen_table)
preview_code_result[out_key] = f"渲染错误: {e!s}"
if gen_table.sub and gen_table.sub_table:
@@ -700,7 +743,7 @@ class GenTableService:
out_key = Jinja2TemplateUtil.get_file_name(template, sub_table)
preview_code_result[out_key] = render_content
except Exception as e:
log.error(f"渲染子表模板 {template} 时出错: {e!s}")
logger.error(f"渲染子表模板 {template} 时出错: {e!s}")
out_key = Jinja2TemplateUtil.get_file_name(template, sub_table)
preview_code_result[out_key] = f"渲染错误: {e!s}"
return preview_code_result
@@ -733,8 +776,8 @@ class GenTableService:
render_info = await cls.__get_gen_render_info(auth, table_name)
gen_table_schema: GenTableOutSchema = render_info[3]
from app.api.v1.module_system.menu.crud import MenuCRUD
from app.api.v1.module_system.menu.schema import MenuCreateSchema
from app.api.v1.module_platform.menu.crud import MenuCRUD
from app.api.v1.module_platform.menu.schema import MenuCreateSchema
from app.utils.common_util import CamelCaseUtil
# 按“上级目录”规则矫正最终包名(分系统根)
@@ -748,12 +791,54 @@ class GenTableService:
if not mn:
raise CustomException(msg="模块名不能为空")
permission_prefix = ":".join([s for s in [pn, mn] if s])
# 创建菜单 CRUD 实例
menu_crud = MenuCRUD(auth)
if not gen_table_schema.function_name:
raise CustomException(msg="功能名称不能为空")
if not gen_table_schema.package_name:
raise CustomException(msg="包名不能为空")
# 1. 先写代码文件(风险最高,放最前,失败不产生菜单孤儿数据)
async def _write_templates(
templates: list[str], ctx: dict[str, Any], table_schema: GenTableOutSchema
) -> None:
for template in templates:
try:
render_content = await env.get_template(template).render_async(**ctx)
file_name = Jinja2TemplateUtil.get_file_name(template, table_schema)
full_path = BASE_DIR.parent.joinpath(file_name)
gen_path = str(full_path)
if not gen_path:
raise CustomException(msg="【代码生成】生成路径为空")
os.makedirs(os.path.dirname(gen_path), exist_ok=True)
await anyio.Path(gen_path).write_text(render_content, encoding="utf-8")
# Python 插件目录需保证包层级可导入:为分系统/模块目录补齐 __init__.py
pn_inner = (table_schema.package_name or "").strip()
mn_inner = (table_schema.module_name or "").strip()
if pn_inner and mn_inner:
plugin_base = BASE_DIR.parent.joinpath(f"backend/app/plugin/{pn_inner}")
module_base = plugin_base.joinpath(mn_inner)
for d in (plugin_base, module_base):
init_path = d.joinpath("__init__.py")
if not init_path.exists():
os.makedirs(str(d), exist_ok=True)
await anyio.Path(str(init_path)).write_text(
"# -*- coding: utf-8 -*-", encoding="utf-8"
)
except Exception as e:
raise CustomException(
msg=f"渲染模板失败,表名:{table_schema.table_name},详细错误信息:{e!s}"
)
await _write_templates(render_info[0], render_info[2], gen_table_schema)
if gen_table_schema.sub and gen_table_schema.sub_table:
gen_table_schema.sub_table.package_name = gen_table_schema.package_name
sub_ctx = Jinja2TemplateUtil.prepare_sub_render_context(
gen_table_schema, gen_table_schema.sub_table
)
sub_templates = Jinja2TemplateUtil.get_sub_table_template_list()
await _write_templates(sub_templates, sub_ctx, gen_table_schema.sub_table)
# 2. 代码成功写入后,再创建菜单(避免失败时产生孤儿菜单数据)
menu_crud = MenuCRUD(auth)
await cls._assert_parent_menu_is_catalog(auth, gen_table_schema.parent_menu_id)
# 1. 目录 + 菜单 + 按钮:先取/建模块目录(名称规则见 _catalog_menu_dir_key
dir_menu_id = await cls._get_or_create_package_directory_menu(
@@ -859,7 +944,14 @@ class GenTableService:
},
]
for button in buttons:
# 检查按钮权限是否已存在
existing_btn = await menu_crud.get(
permission=button["permission"],
type=3,
parent_id=parent_menu.id,
)
if existing_btn:
logger.info(f"按钮权限已存在,跳过创建: {button['permission']}")
continue
await menu_crud.create(
MenuCreateSchema(
name=button["name"],
@@ -882,58 +974,14 @@ class GenTableService:
description=f"{gen_table_schema.function_name}功能按钮",
)
)
log.info(f"成功创建按钮权限: {button['name']}")
log.info(f"成功创建{gen_table_schema.function_name}菜单及按钮权限")
# 2. 菜单创建成功后,再生成页面代码(主表 + 可选子表)
async def _write_templates(
templates: list[str], ctx: dict[str, Any], table_schema: GenTableOutSchema
) -> None:
for template in templates:
try:
render_content = await env.get_template(template).render_async(**ctx)
file_name = Jinja2TemplateUtil.get_file_name(template, table_schema)
full_path = BASE_DIR.parent.joinpath(file_name)
gen_path = str(full_path)
if not gen_path:
raise CustomException(msg="【代码生成】生成路径为空")
os.makedirs(os.path.dirname(gen_path), exist_ok=True)
await anyio.Path(gen_path).write_text(render_content, encoding="utf-8")
# Python 插件目录需保证包层级可导入:为分系统/模块目录补齐 __init__.py
# 生成规则固定为 backend/app/plugin/{module_xxx}/{module_name}/...
pn = (table_schema.package_name or "").strip()
mn = (table_schema.module_name or "").strip()
if pn and mn:
plugin_base = BASE_DIR.parent.joinpath(f"backend/app/plugin/{pn}")
module_base = plugin_base.joinpath(mn)
for d in (plugin_base, module_base):
init_path = d.joinpath("__init__.py")
if not init_path.exists():
os.makedirs(str(d), exist_ok=True)
await anyio.Path(str(init_path)).write_text(
"# -*- coding: utf-8 -*-", encoding="utf-8"
)
except Exception as e:
raise CustomException(
msg=f"渲染模板失败,表名:{table_schema.table_name},详细错误信息:{e!s}"
)
await _write_templates(render_info[0], render_info[2], gen_table_schema)
if gen_table_schema.sub and gen_table_schema.sub_table:
# 子表与主表同分系统,使用自己的模块名,实现同级目录
gen_table_schema.sub_table.package_name = gen_table_schema.package_name
# 确保子表使用自己的模块名,与主表同级目录
sub_ctx = Jinja2TemplateUtil.prepare_sub_render_context(
gen_table_schema, gen_table_schema.sub_table
)
sub_templates = Jinja2TemplateUtil.get_sub_table_template_list()
await _write_templates(sub_templates, sub_ctx, gen_table_schema.sub_table)
logger.info(f"成功创建按钮权限: {button['name']}")
logger.info(f"成功创建{gen_table_schema.function_name}菜单及按钮权限")
return True
@classmethod
@handle_service_exception
async def batch_gen_code_service(cls, auth: AuthSchema, table_names: list[str]) -> bytes:
async def batch_gen_code_service(cls, auth: AuthSchema, table_names: list[str]) -> tuple[bytes, list[str]]:
"""
批量生成代码并打包为ZIP。
- 备注:内存生成并压缩,兼容多模板类型;供下载使用。
@@ -950,6 +998,7 @@ class GenTableService:
raise CustomException(msg="表名列表不能为空")
zip_buffer = io.BytesIO()
file_count = 0
failed_tables: list[str] = []
with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as zip_file:
for table_name in valid_names:
try:
@@ -978,7 +1027,8 @@ class GenTableService:
zip_file.writestr(out_path, render_content)
file_count += 1
except Exception as e:
log.error(f"批量生成代码时处理表 {table_name} 出错: {e!s}")
logger.error(f"批量生成代码时处理表 {table_name} 出错: {e!s}")
failed_tables.append(table_name)
# 继续处理其他表,不中断整个过程
continue
zip_data = zip_buffer.getvalue()
@@ -987,7 +1037,7 @@ class GenTableService:
raise CustomException(
msg="未能生成任何代码文件:请检查所选表是否存在于代码生成配置中,或主子表、字段配置是否正确"
)
return zip_data
return zip_data, failed_tables
@classmethod
@handle_service_exception
@@ -1175,7 +1225,7 @@ class GenTableService:
sub_name_raw
)
except Exception as e:
log.warning(f"获取子表 {sub_name_raw} 字段失败: {e!s}")
logger.warning(f"获取子表 {sub_name_raw} 字段失败: {e!s}")
gen_table.sub = False
gen_table.sub_table = None
gen_table.master_sub_hint = f"无法读取子表结构:{e!s}"
@@ -1280,7 +1330,7 @@ class GenTableService:
@classmethod
@handle_service_exception
async def sync_db_preview_service(cls, auth: AuthSchema, table_name: str) -> dict[str, Any]:
async def sync_db_preview_service(cls, auth: AuthSchema, table_name: str) -> GenSyncPreviewSchema:
"""
同步数据库前差异预览(主表 + 可选子表)。
@@ -1341,7 +1391,7 @@ class GenTableService:
unchanged=s_unchanged,
)
return preview.model_dump()
return preview
@classmethod
def _assert_master_sub_config_valid(cls, gen_table: GenTableOutSchema) -> None:
@@ -1437,7 +1487,7 @@ class GenTableColumnService:
"table_id": table_id
})
result = [
GenTableColumnOutSchema.model_validate(gen_table_column).model_dump()
GenTableColumnOutSchema.model_validate(gen_table_column)
for gen_table_column in gen_table_column_list_result
]
return result
@@ -1,239 +1,243 @@
# -*- coding: utf-8 -*-
import urllib.parse
from typing import Annotated
from fastapi import APIRouter, Depends, UploadFile, Body, Path, Query
from fastapi.responses import StreamingResponse, JSONResponse
from fastapi import APIRouter, Body, Depends, Path, UploadFile
from fastapi.responses import JSONResponse, StreamingResponse
from app.common.response import SuccessResponse, StreamResponse
from app.core.dependencies import AuthPermission
from app.api.v1.module_system.auth.schema import AuthSchema
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 app.core.logger import log
from app.core.base_schema import BatchSetAvailable
from .schema import {{ class_name }}CreateSchema, {{ class_name }}OutSchema, {{ class_name }}QueryParam, {{ class_name }}UpdateSchema
from .service import {{ class_name }}Service
from .schema import {{ class_name }}CreateSchema, {{ class_name }}UpdateSchema, {{ class_name }}QueryParam
# 动态路由容器前缀由 module_xxx 决定(discover: module_xxx -> /xxx
# 对齐 module_example/demo:业务路由前缀固定为 /{module_name}
{{ class_name }}Router = APIRouter(prefix='/{{ module_name }}', tags=["{{ function_name }}模块"])
{{ class_name }}Router = APIRouter(route_class=OperationLogRoute, prefix="/{{ module_name }}", tags=["{{ function_name }}模块"])
@{{ class_name }}Router.get(
"/detail/{id}",
summary="获取{{ function_name }}详情",
description="获取{{ function_name }}详情"
response_model=ResponseSchema[{{ class_name }}OutSchema],
)
async def get_{{ business_name_slug }}_detail_controller(
id: int = Path(..., description="ID"),
auth: AuthSchema = Depends(AuthPermission(["{{ permission_prefix }}:query"]))
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 }}详情接口
获取{{ function_name }}详情
参数:
- id: int - 数据ID
- auth: AuthSchema - 认证信息
- id (int): {{ function_name }}ID
- auth (AuthSchema): 认证信息模型
返回:
- JSONResponse - 包含{{ function_name }}详情的JSON响应
- JSONResponse: 包含{{ function_name }}详情的JSON响应
"""
result_dict = await {{ class_name }}Service.detail_{{ business_name_slug }}_service(auth=auth, id=id)
log.info(f"获取{{ function_name }}详情成功 {id}")
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 }}列表",
description="查询{{ function_name }}列表"
summary="分页查询{{ function_name }}",
response_model=ResponseSchema[PageResultSchema[{{ class_name }}OutSchema]],
)
async def get_{{ business_name_slug }}_list_controller(
page: PaginationQueryParam = Depends(),
search: {{ class_name }}QueryParam = Depends(),
auth: AuthSchema = Depends(AuthPermission(["{{ permission_prefix }}:query"]))
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 }}列表接口(数据库分页)
查询{{ function_name }}列表
参数:
- page: PaginationQueryParam - 分页参数
- search: {{ class_name }}QueryParam - 查询参数
- auth: AuthSchema - 认证信息
- page (PaginationQueryParam): 分页查询参数
- search ({{ class_name }}QueryParam): 查询参数
- auth (AuthSchema): 认证信息模型
返回:
- JSONResponse - 包含{{ function_name }}列表的JSON响应
- JSONResponse: 包含{{ function_name }}列表分页信息的JSON响应
"""
result_dict = await {{ class_name }}Service.page_{{ business_name_slug }}_service(
result_dict = await {{ class_name }}Service.page_service(
auth=auth,
page_no=page.page_no if page.page_no is not None else 1,
page_size=page.page_size if page.page_size is not None else 10,
page_no=page.page_no,
page_size=page.page_size,
search=search,
order_by=page.order_by
order_by=page.order_by,
)
log.info("查询{{ function_name }}列表成功")
return SuccessResponse(data=result_dict, msg="查询{{ function_name }}列表成功")
@{{ class_name }}Router.post(
"/create",
summary="创建{{ function_name }}",
description="创建{{ function_name }}"
response_model=ResponseSchema[{{ class_name }}OutSchema],
)
async def create_{{ business_name_slug }}_controller(
async def create_obj_controller(
data: {{ class_name }}CreateSchema,
auth: AuthSchema = Depends(AuthPermission(["{{ permission_prefix }}:create"]))
auth: Annotated[AuthSchema, Depends(AuthPermission(["{{ permission_prefix }}:create"]))],
) -> JSONResponse:
"""
创建{{ function_name }}接口
创建{{ function_name }}
参数:
- data: {{ class_name }}CreateSchema - 创建数据
- auth: AuthSchema - 认证信息
- data ({{ class_name }}CreateSchema): {{ function_name }}创建模型
- auth (AuthSchema): 认证信息模型
返回:
- JSONResponse - 包含创建{{ function_name }}结果的JSON响应
- JSONResponse: 包含创建{{ function_name }}详情的JSON响应
"""
result_dict = await {{ class_name }}Service.create_{{ business_name_slug }}_service(auth=auth, data=data)
log.info("创建{{ function_name }}成功")
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 }}",
description="修改{{ function_name }}"
response_model=ResponseSchema[{{ class_name }}OutSchema],
)
async def update_{{ business_name_slug }}_controller(
async def update_obj_controller(
data: {{ class_name }}UpdateSchema,
id: int = Path(..., description="ID"),
auth: AuthSchema = Depends(AuthPermission(["{{ permission_prefix }}:update"]))
id: Annotated[int, Path(description="{{ function_name }}ID")],
auth: Annotated[AuthSchema, Depends(AuthPermission(["{{ permission_prefix }}:update"]))],
) -> JSONResponse:
"""
修改{{ function_name }}接口
修改{{ function_name }}
参数:
- id: int - 数据ID
- data: {{ class_name }}UpdateSchema - 更新数据
- auth: AuthSchema - 认证信息
- data ({{ class_name }}UpdateSchema): {{ function_name }}更新模型
- id (int): {{ function_name }}ID
- auth (AuthSchema): 认证信息模型
返回:
- JSONResponse - 包含修改{{ function_name }}结果的JSON响应
- JSONResponse: 包含修改{{ function_name }}详情的JSON响应
"""
result_dict = await {{ class_name }}Service.update_{{ business_name_slug }}_service(auth=auth, id=id, data=data)
log.info("修改{{ function_name }}成功")
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 }}",
description="删除{{ function_name }}"
response_model=ResponseSchema[None],
)
async def delete_{{ business_name_slug }}_controller(
ids: list[int] = Body(..., description="ID列表"),
auth: AuthSchema = Depends(AuthPermission(["{{ permission_prefix }}:delete"]))
async def delete_obj_controller(
ids: Annotated[list[int], Body(description="ID列表")],
auth: Annotated[AuthSchema, Depends(AuthPermission(["{{ permission_prefix }}:delete"]))],
) -> JSONResponse:
"""
删除{{ function_name }}接口
删除{{ function_name }}
参数:
- ids: list[int] - 数据ID列表
- auth: AuthSchema - 认证信息
- ids (list[int]): {{ function_name }}ID列表
- auth (AuthSchema): 认证信息模型
返回:
- JSONResponse - 包含删除{{ function_name }}结果的JSON响应
- JSONResponse: 包含删除{{ function_name }}详情的JSON响应
"""
await {{ class_name }}Service.delete_{{ business_name_slug }}_service(auth=auth, ids=ids)
log.info(f"删除{{ function_name }}成功: {ids}")
await {{ class_name }}Service.delete_service(auth=auth, ids=ids)
return SuccessResponse(msg="删除{{ function_name }}成功")
@{{ class_name }}Router.patch(
"/status/batch",
"/available/setting",
summary="批量修改{{ function_name }}状态",
description="批量修改{{ function_name }}状态"
response_model=ResponseSchema[None],
)
async def batch_set_available_{{ business_name_slug }}_controller(
async def batch_set_available_obj_controller(
data: BatchSetAvailable,
auth: AuthSchema = Depends(AuthPermission(["{{ permission_prefix }}:patch"]))
auth: Annotated[AuthSchema, Depends(AuthPermission(["{{ permission_prefix }}:patch"]))],
) -> JSONResponse:
"""
批量修改{{ function_name }}状态接口
批量修改{{ function_name }}状态
参数:
- data: BatchSetAvailable - 批量修改状态数据
- auth: AuthSchema - 认证信息
- data (BatchSetAvailable): 批量修改{{ function_name }}状态模型
- auth (AuthSchema): 认证信息模型
返回:
- JSONResponse - 包含批量修改{{ function_name }}状态结果的JSON响应
- JSONResponse: 包含批量修改{{ function_name }}状态详情的JSON响应
"""
await {{ class_name }}Service.set_available_{{ business_name_slug }}_service(auth=auth, data=data)
log.info(f"批量修改{{ function_name }}状态成功: {data.ids}")
await {{ class_name }}Service.set_available_service(auth=auth, data=data)
return SuccessResponse(msg="批量修改{{ function_name }}状态成功")
@{{ class_name }}Router.post(
'/export',
"/export",
summary="导出{{ function_name }}",
description="导出{{ function_name }}"
)
async def export_{{ business_name_slug }}_list_controller(
search: {{ class_name }}QueryParam = Depends(),
auth: AuthSchema = Depends(AuthPermission(["{{ permission_prefix }}:export"]))
async def export_obj_list_controller(
search: Annotated[{{ class_name }}QueryParam, Depends()],
auth: Annotated[AuthSchema, Depends(AuthPermission(["{{ permission_prefix }}:export"]))],
) -> StreamingResponse:
"""
导出{{ function_name }}接口
导出{{ function_name }}
参数:
- search: {{ class_name }}QueryParam - 查询参数
- auth: AuthSchema - 认证信息
- search ({{ class_name }}QueryParam): 查询参数
- auth (AuthSchema): 认证信息模型
返回:
- StreamingResponse - 包含导出{{ function_name }}数据的流式响应
- StreamingResponse: 包含{{ function_name }}列表的Excel文件流响应
"""
result_dict_list = await {{ class_name }}Service.list_{{ business_name_slug }}_service(search=search, auth=auth)
export_result = await {{ class_name }}Service.batch_export_{{ business_name_slug }}_service(obj_list=result_dict_list)
log.info('导出{{ function_name }}成功')
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'}
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
headers={"Content-Disposition": "attachment; filename={{ table_name }}.xlsx"},
)
@{{ class_name }}Router.post(
'/import',
summary="导入{{ function_name }}",
description="导入{{ function_name }}"
)
async def import_{{ business_name_slug }}_list_controller(
file: UploadFile,
auth: 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_{{ business_name_slug }}_service(file=file, auth=auth, update_support=True)
log.info("导入{{ function_name }}成功")
return SuccessResponse(data=batch_import_result, msg="导入{{ function_name }}成功")
@{{ class_name }}Router.post(
'/download/template',
summary="获取{{ function_name }}导入模板",
description="获取{{ function_name }}导入模板",
dependencies=[Depends(AuthPermission(["{{ permission_prefix }}:download"]))]
"/import",
summary="导入{{ function_name }}",
response_model=ResponseSchema[str],
)
async def export_{{ business_name_slug }}_template_controller() -> StreamingResponse:
async def import_obj_list_controller(
file: UploadFile,
auth: Annotated[AuthSchema, Depends(AuthPermission(["{{ permission_prefix }}:import"]))],
) -> JSONResponse:
"""
获取{{ function_name }}导入模板接口
导入{{ function_name }}
参数:
- file (UploadFile): 导入的Excel文件
- auth (AuthSchema): 认证信息模型
返回:
- StreamingResponse - 包含{{ function_name }}导入模板的流式响应
- JSONResponse: 包含导入{{ function_name }}详情的JSON响应
"""
import_template_result = await {{ class_name }}Service.import_template_download_{{ business_name_slug }}_service()
log.info('获取{{ function_name }}导入模板成功')
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': 'attachment; filename={{ table_name }}_template.xlsx'}
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,11 +1,9 @@
# -*- coding: utf-8 -*-
from typing import Sequence
from app.core.base_crud import CRUDBase
from app.api.v1.module_system.auth.schema import AuthSchema
from .model import {{ class_name }}Model
from .schema import {{ class_name }}CreateSchema, {{ class_name }}UpdateSchema, {{ class_name }}OutSchema
from .schema import {{ class_name }}CreateSchema, {{ class_name }}UpdateSchema
class {{ class_name }}CRUD(CRUDBase[{{ class_name }}Model, {{ class_name }}CreateSchema, {{ class_name }}UpdateSchema]):
@@ -14,110 +12,8 @@ class {{ class_name }}CRUD(CRUDBase[{{ class_name }}Model, {{ class_name }}Creat
def __init__(self, auth: AuthSchema) -> None:
"""
初始化CRUD数据层
参数:
- auth (AuthSchema): 认证信息模型
"""
super().__init__(model={{ class_name }}Model, auth=auth)
async def get_by_id_{{ business_name_slug }}_crud(self, id: int, preload: list | None = None) -> {{ class_name }}Model | None:
"""
详情
参数:
- id (int): 对象ID
- preload (list | None): 预加载关系,未提供时使用模型默认项
返回:
- {{ class_name }}Model | None: 模型实例或None
"""
return await self.get(id=id, preload=preload)
async def list_{{ business_name_slug }}_crud(self, search: dict | None = None, order_by: list[dict] | None = None, preload: list | None = None) -> Sequence[{{ class_name }}Model]:
"""
列表查询
参数:
- search (dict | None): 查询参数
- order_by (list[dict] | None): 排序参数,未提供时使用模型默认项
- preload (list | None): 预加载关系,未提供时使用模型默认项
返回:
- Sequence[{{ class_name }}Model]: 模型实例序列
"""
return await self.list(search=search, order_by=order_by, preload=preload)
async def create_{{ business_name_slug }}_crud(self, data: {{ class_name }}CreateSchema) -> {{ class_name }}Model | None:
"""
创建
参数:
- data ({{ class_name }}CreateSchema): 创建模型
返回:
- {{ class_name }}Model | None: 模型实例或None
"""
return await self.create(data=data)
async def update_{{ business_name_slug }}_crud(self, id: int, data: {{ class_name }}UpdateSchema) -> {{ class_name }}Model | None:
"""
更新
参数:
- id (int): 对象ID
- data ({{ class_name }}UpdateSchema): 更新模型
返回:
- {{ class_name }}Model | None: 模型实例或None
"""
return await self.update(id=id, data=data)
async def delete_{{ business_name_slug }}_crud(self, ids: list[int]) -> None:
"""
批量删除
参数:
- ids (list[int]): 对象ID列表
返回:
- None
"""
return await self.delete(ids=ids)
async def set_available_{{ business_name_slug }}_crud(self, ids: list[int], status: str) -> None:
"""
批量设置可用状态
参数:
- ids (list[int]): 对象ID列表
- status (str): 可用状态
返回:
- None
"""
return await self.set(ids=ids, status=status)
async def page_{{ business_name_slug }}_crud(self, offset: int, limit: int, order_by: list[dict] | None = None, search: dict | None = None, preload: list | None = None) -> dict:
"""
分页查询
参数:
- offset (int): 偏移量
- limit (int): 每页数量
- order_by (list[dict] | None): 排序参数,未提供时使用模型默认项
- search (dict | None): 查询参数,未提供时查询所有
- preload (list | None): 预加载关系,未提供时使用模型默认项
返回:
- Dict: 分页数据
"""
order_by_list = order_by or [{'{{ pk_column_name }}': 'asc'}]
search_dict = search or {}
return await self.page(
offset=offset,
limit=limit,
order_by=order_by_list,
search=search_dict,
out_schema={{ class_name }}OutSchema,
preload=preload
)
@@ -7,7 +7,6 @@ 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.core.logger import log
from app.utils.excel_util import ExcelUtil
from .crud import {{ class_name }}CRUD
@@ -18,166 +17,166 @@ from .schema import (
{{ 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_by_id_{{ business_name_slug }}_crud(id=id)
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_{{ business_name_slug }}_crud(search=search_dict, order_by=order_by)
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_{{ business_name_slug }}_crud(
result = await {{ class_name }}CRUD(auth).page(
offset=offset,
limit=page_size,
order_by=order_by_list,
search=search_dict
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 == '1' %}
{% 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_{{ business_name_slug }}_crud(data=data)
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_by_id_{{ business_name_slug }}_crud(id=id)
obj = await {{ class_name }}CRUD(auth).get(id=id)
if not obj:
raise CustomException(msg='更新失败,该数据不存在')
# 检查唯一性约束
{% for column in columns %}
{% if column.is_unique == '1' %}
{% 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_{{ business_name_slug }}_crud(id=id, data=data)
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_by_id_{{ business_name_slug }}_crud(id=id)
obj = await {{ class_name }}CRUD(auth).get(id=id)
if not obj:
raise CustomException(msg=f'删除失败,ID为{id}的数据不存在')
await {{ class_name }}CRUD(auth).delete_{{ business_name_slug }}_crud(ids=ids)
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_available_{{ business_name_slug }}_crud(ids=data.ids, status=data.status)
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文件内容
"""
@@ -190,7 +189,7 @@ class {{ class_name }}Service:
data = obj_list.copy()
for item in data:
# 处理状态
item["status"] = "启用" if item.get("status") == "0" else "停用"
item["status"] = "启用" if item.get("status") == 0 else "停用"
# 处理创建者
creator_info = item.get("created_id")
if isinstance(creator_info, dict):
@@ -204,12 +203,12 @@ class {{ class_name }}Service:
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 - 导入结果信息
"""
@@ -235,25 +234,25 @@ class {{ class_name }}Service:
# 重命名列名
df.rename(columns=header_dict, inplace=True)
# 验证必填字段
# 验证必填字段(非主键且不允许为空的列)
{% for column in columns %}
{% if column.required == '1' %}
{% 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 == field][0]
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:
@@ -264,10 +263,10 @@ class {{ class_name }}Service:
}
# 使用CreateSchema做校验后入库
create_schema = {{ class_name }}CreateSchema.model_validate(data)
# 检查唯一性约束
{% for column in columns %}
{% if column.is_unique == '1' %}
{% 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:
@@ -278,8 +277,8 @@ class {{ class_name }}Service:
continue
{% endif %}
{% endfor %}
await {{ class_name }}CRUD(auth).create_{{ business_name_slug }}_crud(data=create_schema)
await {{ class_name }}CRUD(auth).create(data=create_schema)
success_count += 1
except Exception as e:
error_msgs.append(f"第{count}行: {str(e)}")
@@ -289,16 +288,16 @@ class {{ class_name }}Service:
if error_msgs:
result += "\n错误信息:\n" + "\n".join(error_msgs)
return result
except Exception as e:
log.error(f"批量导入失败: {str(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文件的二进制数据
"""
@@ -309,16 +308,16 @@ class {{ class_name }}Service:
]
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,11 +1,10 @@
import request from "@utils/http";
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`,
@@ -14,15 +13,13 @@ const {{ class_name }}API = {
});
},
/** 获取详情 */
get{{ class_name }}Detail(id: number) {
get{{ class_name }}Detail(query: number) {
return request<ApiResponse<{{ class_name }}Table>>({
url: `${API_PATH}/detail/${id}`,
url: `${API_PATH}/detail/${query}`,
method: "get",
});
},
/** 新增 */
create{{ class_name }}(body: {{ class_name }}Form) {
return request<ApiResponse>({
url: `${API_PATH}/create`,
@@ -31,7 +28,6 @@ const {{ class_name }}API = {
});
},
/** 修改 */
update{{ class_name }}(id: number, body: {{ class_name }}Form) {
return request<ApiResponse>({
url: `${API_PATH}/update/${id}`,
@@ -40,44 +36,39 @@ const {{ class_name }}API = {
});
},
/** 删除(支持批量) */
delete{{ class_name }}(ids: number[]) {
delete{{ class_name }}(body: number[]) {
return request<ApiResponse>({
url: `${API_PATH}/delete`,
method: "delete",
data: ids,
data: body,
});
},
/** 批量启用/停用 */
batch{{ class_name }}(body: BatchType) {
return request<ApiResponse>({
url: `${API_PATH}/status/batch`,
url: `${API_PATH}/available/setting`,
method: "patch",
data: body,
});
},
/** 导出 Excel */
export{{ class_name }}(query: {{ class_name }}PageQuery) {
export{{ class_name }}(body: {{ class_name }}PageQuery) {
return request<Blob>({
url: `${API_PATH}/export`,
method: "post",
data: query,
data: body,
responseType: "blob",
});
},
/** 下载导入模板 */
downloadTemplate{{ class_name }}() {
return request<Blob>({
return request<ApiResponse>({
url: `${API_PATH}/download/template`,
method: "post",
responseType: "blob",
});
},
/** 导入 Excel */
import{{ class_name }}(body: FormData) {
return request<ApiResponse>({
url: `${API_PATH}/import`,
@@ -100,7 +91,7 @@ export default {{ class_name }}API;
export interface {{ class_name }}PageQuery extends PageQuery {
{% for column in columns %}
{# 主键列默认不参与查询 #}
{% if column.is_query and column.column_name != pk_column_name and column.query_type != "BETWEEN" and column.column_name not in ['created_time', 'updated_time'] %}
{% 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)
@@ -39,8 +39,8 @@
:perm-patch="['{{ permission_prefix }}:patch']"
:delete-loading="batchDeleting"
@add="openEditDialog('add')"
@import="openImportModal"
@export="openExportModal"
@import="openImport"
@export="openExport"
@delete="handleBatchDelete"
@more="runBatchStatus"
/>
@@ -65,73 +65,37 @@
width="920px"
dialog-class="crud-embed-dialog"
modal-class="crud-embed-dialog"
@close="handleCloseDialog"
:form-mode="dialogVisible.type"
:confirm-loading="submitLoading"
@cancel="handleCloseDialog"
@confirm="dialogVisible.type === 'detail' ? handleCloseDialog() : handleSubmit()"
>
<template v-if="dialogVisible.type === 'detail'">
<ElScrollbar max-height="70vh" :view-style="{ overflowX: 'hidden' }">
<ElDescriptions :column="4" border>
{% for column in columns %}
{% 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' %}
<ElDescriptionsItem label="状态" :span="2">
<ElTag :type="detailFormData.status === '0' ? 'success' : 'danger'">
{{ '{{' }} detailFormData.status === "0" ? "启用" : "停用" {{ '}}' }}
</ElTag>
</ElDescriptionsItem>
{% elif column.column_name == 'created_id' %}
<ElDescriptionsItem label="创建人" :span="2">
{{ '{{' }} detailFormData.created_by?.name {{ '}}' }}
</ElDescriptionsItem>
{% elif column.column_name == 'updated_id' %}
<ElDescriptionsItem label="更新人" :span="2">
{{ '{{' }} detailFormData.updated_by?.name {{ '}}' }}
</ElDescriptionsItem>
{% elif column.column_name == 'created_time' %}
<ElDescriptionsItem label="创建时间" :span="2">
{{ '{{' }} detailFormData.created_time {{ '}}' }}
</ElDescriptionsItem>
{% elif column.column_name == 'updated_time' %}
<ElDescriptionsItem label="更新时间" :span="2">
{{ '{{' }} detailFormData.updated_time {{ '}}' }}
</ElDescriptionsItem>
{% else %}
<ElDescriptionsItem label="{{ comment }}" :span="2">
{{ '{{' }} detailFormData.{{ column.column_name }} {{ '}}' }}
</ElDescriptionsItem>
{% endif %}
{% endfor %}
</ElDescriptions>
{% if table.sub %}
<ElDivider>关联{{ table.sub_table.function_name }}</ElDivider>
<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 %}
</ElTable>
{% endif %}
</ElScrollbar>
<FaDescriptions
:column="4"
:data="detailFormData"
:items="detailItems"
max-height="70vh"
/>
</template>
<template v-else>
<ElScrollbar max-height="70vh" :view-style="{ overflowX: 'hidden' }">
<FaForm
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"
/>
<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">
@@ -172,14 +136,14 @@
</FaDialog>
<FaImportDialog
v-model="importModalVisible"
v-model="importVisible"
:content-config="importContentConfig"
default-template-file-name="{{ module_name }}_import_template.xlsx"
@upload="handleCrudImportUpload"
/>
<FaExportDialog
v-model="exportModalVisible"
v-model="exportVisible"
:content-config="exportContentConfig"
:query-params="exportQueryParams"
:page-data="data"
@@ -191,34 +155,24 @@
<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 { renderTableOperationCell, type TableOperationAction } from "@/utils/table";
import { useTable } from "@/hooks/core/useTable";
import FaTableHeaderLeft from "@/components/tables/fa-table-header-left/index.vue";
import FaImportDialog from "@/components/modal/fa-import-dialog/index.vue";
import FaExportDialog from "@/components/modal/fa-export-dialog/index.vue";
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 FaSearchBarWithAudit from "@/components/forms/fa-search-bar/FaSearchBarWithAudit.vue";
import type { AuditSearchFormParams } from "@/components/forms/fa-search-bar/auditSearchFormItems";
import FaDialog from "@/components/modal/fa-dialog/index.vue";
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 { ElMessage, ElMessageBox, ElTag } from "element-plus";
import { ElTag, ElMessage } from "element-plus";
import { useDictStore } from "@/stores/modules/dict";
import { ResultEnum } from "@/enums/api/result.enum";
import FaForm from "@/components/forms/fa-form/index.vue";
import type { FormItem } from "@/components/forms/fa-form/index.vue";
{% set _img = namespace(need=false) %}
{% for column in columns %}
{% if (column.is_insert or column.is_edit) and column.html_type == "imageUpload" %}
{% set _img.need = true %}
{% endif %}
{% endfor %}
{% if _img.need %}
import SingleImageUpload from "@/components/Upload/SingleImageUpload.vue";
{% endif %}
defineOptions({
name: "{{ class_name }}",
@@ -226,6 +180,7 @@ defineOptions({
});
const { hasAuth } = useAuth();
const dictStore = useDictStore();
type {{ class_name }}SearchFormParams = {
{% for column in columns %}
@@ -374,7 +329,7 @@ const {
label: "状态",
width: 88,
formatter: (row: {{ class_name }}Table) => {
const ok = row.status === "0";
const ok = row.status === 0;
const cfg = ok
? { type: "success" as const, text: "启用" }
: { type: "info" as const, text: "停用" };
@@ -487,14 +442,33 @@ const exportContentConfig = computed(() => ({
},
}));
const dialogVisible = reactive({
title: "",
visible: false,
type: "create" as "create" | "update" | "detail",
});
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 %}
@@ -559,9 +533,15 @@ const dialogFormItems = computed<FormItem[]>(() => [
{% endfor %}
]);
const dataFormRef = ref();
const importModalVisible = ref(false);
const exportModalVisible = ref(false);
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 %}
@@ -622,7 +602,6 @@ function buildRowActions(row: {{ class_name }}Table): TableOperationAction[] {
key: "detail",
label: "详情",
artType: "view",
icon: "ri:file-list-3-line",
perm: "{{ permission_prefix }}:detail",
run: () => void openDetailDialog(row),
},
@@ -673,8 +652,10 @@ async function openEditDialog(type: "add" | "edit", row?: {{ class_name }}Table)
{% 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 %}
@@ -698,11 +679,12 @@ async function handleCloseDialog() {
}
async function handleSubmit() {
dataFormRef.value.validate(async (valid: boolean) => {
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();
@@ -714,6 +696,8 @@ async function handleSubmit() {
await resetForm();
} catch (error: unknown) {
console.error(error);
} finally {
submitLoading.value = false;
}
});
}
@@ -803,10 +787,6 @@ async function runBatchStatus(status: string) {
}
}
function openImportModal() {
importModalVisible.value = true;
}
async function handleCrudImportUpload(uploadFormData: FormData) {
try {
const res = await {{ class_name }}API.import{{ class_name }}(uploadFormData);
@@ -815,7 +795,7 @@ async function handleCrudImportUpload(uploadFormData: FormData) {
return;
}
ElMessage.success(res.data.msg || "导入成功");
importModalVisible.value = false;
importVisible.value = false;
await refreshData();
} catch (error) {
console.error("[Import]", error);
@@ -823,10 +803,6 @@ async function handleCrudImportUpload(uploadFormData: FormData) {
}
}
function openExportModal() {
exportModalVisible.value = true;
}
onMounted(async () => {
{% for column in columns %}
{% if column.is_query and column.dict_type and (column.html_type == "select" or column.html_type == "radio") %}
@@ -100,7 +100,7 @@ class Jinja2TemplateUtil:
@classmethod
def business_name_to_slug(cls, business_name: str | None) -> str:
"""
业务路径可含斜杠(如 ``demo/demo01``)用于目录与路由前缀;
业务路径可含斜杠(如 ``demo/subdir``)用于目录与路由前缀;
Python 函数/方法名仅使用最后一段并规范为合法 snake_case 片段。
参数: