refactor(module): 重构模块结构和代码生成模板

- 删除废弃的ticket和version模块相关代码
- 将resource模块从system迁移到monitor
- 移除前端资源管理相关配置
- 重构代码生成模板路径和配置
- 修复CRUD基类初始化参数问题
- 优化模板工具类路径处理
- 更新基础模型字段和配置
This commit is contained in:
zhangtao
2025-09-24 01:39:44 +08:00
parent 4a7533325a
commit d30d2faf6e
59 changed files with 663 additions and 2823 deletions
-4
View File
@@ -11,9 +11,6 @@ from .module_monitor import MonitorRouter
# 通用模块 # 通用模块
from .module_common import CommonRouter from .module_common import CommonRouter
# 资源管理模块
from .module_resource import ResourceRouter
# 示例模块 # 示例模块
from .module_example import ExampleRouter from .module_example import ExampleRouter
@@ -34,7 +31,6 @@ router = APIRouter()
router.include_router(SystemRouter) router.include_router(SystemRouter)
router.include_router(MonitorRouter) router.include_router(MonitorRouter)
router.include_router(CommonRouter) router.include_router(CommonRouter)
router.include_router(ResourceRouter)
router.include_router(ExampleRouter) router.include_router(ExampleRouter)
router.include_router(ApplicationRouter) router.include_router(ApplicationRouter)
router.include_router(AIRouter) router.include_router(AIRouter)
+1 -1
View File
@@ -14,7 +14,7 @@ class McpCRUD(CRUDBase[McpModel, McpCreateSchema, McpUpdateSchema]):
def __init__(self, auth: AuthSchema) -> None: def __init__(self, auth: AuthSchema) -> None:
"""初始化CRUD""" """初始化CRUD"""
self.auth = auth self.auth = auth
super().__init__(model=McpModel(), auth=auth) super().__init__(model=McpModel, auth=auth)
async def get_by_id_crud(self, id: int) -> Optional[McpModel]: async def get_by_id_crud(self, id: int) -> Optional[McpModel]:
"""详情""" """详情"""
@@ -14,7 +14,7 @@ class DemoCRUD(CRUDBase[DemoModel, DemoCreateSchema, DemoUpdateSchema]):
def __init__(self, auth: AuthSchema) -> None: def __init__(self, auth: AuthSchema) -> None:
"""初始化CRUD""" """初始化CRUD"""
self.auth = auth self.auth = auth
super().__init__(model=DemoModel, auth=auth) super().__init__(model=DemoModel(), auth=auth)
async def get_by_id_crud(self, id: int) -> Optional[DemoModel]: async def get_by_id_crud(self, id: int) -> Optional[DemoModel]:
"""详情""" """详情"""
@@ -1,10 +1,10 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from fastapi import APIRouter from fastapi import APIRouter
from .gencode.controller import genController from .gencode.controller import GenRouter
# 创建代码生成模块路由 # 创建代码生成模块路由
GeneratorRouter = APIRouter(prefix="/generator") GeneratorRouter = APIRouter(prefix="/generator")
# 包含代码生成路由 # 包含代码生成路由
GeneratorRouter.include_router(genController) GeneratorRouter.include_router(GenRouter)
@@ -1,107 +1,107 @@
# -*- coding:utf-8 -*- # -*- coding:utf-8 -*-
from datetime import datetime from datetime import datetime
from fastapi import APIRouter, Depends, Query, Request, Body from typing import List
from fastapi.responses import StreamingResponse from fastapi import APIRouter, Depends, Query, Body
from fastapi.responses import StreamingResponse, JSONResponse
from pydantic_validation_decorator import ValidateFields from pydantic_validation_decorator import ValidateFields
from sqlalchemy.ext.asyncio import AsyncSession
from app.common.enums import BusinessType
from app.common.response import SuccessResponse, ErrorResponse, StreamResponse from app.common.response import SuccessResponse, ErrorResponse, StreamResponse
from app.core.dependencies import AuthPermission from app.core.dependencies import AuthPermission
from app.core.router_class import OperationLogRoute from app.core.router_class import OperationLogRoute
from app.core.base_params import PaginationQueryParam from app.core.base_params import PaginationQueryParam
from app.common.request import PaginationService
from app.api.v1.module_system.auth.schema import AuthSchema from app.api.v1.module_system.auth.schema import AuthSchema
from app.api.v1.module_system.user.schema import UserOutSchema from app.api.v1.module_system.user.schema import UserOutSchema
from .param import GenTableQueryParam from .param import GenTableQueryParam
from .schema import DeleteGenTableSchema, EditGenTableSchema, GenTableSchema from .schema import GenTableDeleteSchema, GenTableUpdateSchema, GenTableOutSchema
from .service import GenTableColumnService, GenTableService from .service import GenTableColumnService, GenTableService
from app.utils.common_util import bytes2file_response from app.utils.common_util import bytes2file_response
from app.core.logger import logger from app.core.logger import logger
genController = APIRouter(route_class=OperationLogRoute, prefix='/tool/gen', tags=["代码生成模块"]) GenRouter = APIRouter(route_class=OperationLogRoute, prefix='/tool/gen', tags=["代码生成模块"])
@genController.get('/list', summary="查询代码生成业务表列表", description="查询代码生成业务表列表") @GenRouter.get('/list', summary="查询代码生成业务表列表", description="查询代码生成业务表列表")
async def get_gen_table_list( async def get_gen_table_list_controller(
page: PaginationQueryParam = Depends(), page: PaginationQueryParam = Depends(),
search: GenTableQueryParam = Depends(), search: GenTableQueryParam = Depends(),
auth: AuthSchema = Depends(AuthPermission(permissions=["tool:gen:list"])) auth: AuthSchema = Depends(AuthPermission(permissions=["tool:gen:list"]))
): ) -> JSONResponse:
# 获取分页数据 result_dict_list = await GenTableService.get_gen_table_list_services(auth=auth, query_object=search, is_page=False)
gen_page_query_result = await GenTableService.get_gen_table_list_services(auth, search, is_page=True) result_dict = await PaginationService.paginate(data_list=result_dict_list["items"], page_no=page.page_no, page_size=page.page_size)
logger.info('获取代码生成业务表列表成功') logger.info('获取代码生成业务表列表成功')
return SuccessResponse(data=gen_page_query_result, msg="获取代码生成业务表列表成功") return SuccessResponse(data=result_dict, msg="获取代码生成业务表列表成功")
@genController.get('/db/list', summary="查询数据库表列表", description="查询数据库表列表") @GenRouter.get('/db/list', summary="查询数据库表列表", description="查询数据库表列表")
async def get_gen_db_table_list( async def get_gen_db_table_list_controller(
page: PaginationQueryParam = Depends(), page: PaginationQueryParam = Depends(),
search: GenTableQueryParam = Depends(), search: GenTableQueryParam = Depends(),
auth: AuthSchema = Depends(AuthPermission(permissions=["tool:gen:list"])) auth: AuthSchema = Depends(AuthPermission(permissions=["tool:gen:list"]))
): ) -> JSONResponse:
# 获取分页数据 result_dict_list = await GenTableService.get_gen_db_table_list_services(auth=auth, query_object=search, is_page=False)
gen_page_query_result = await GenTableService.get_gen_db_table_list_services(auth, search, is_page=True) result_dict = await PaginationService.paginate(data_list=result_dict_list["items"], page_no=page.page_no, page_size=page.page_size)
logger.info('获取数据库表列表成功') logger.info('获取数据库表列表成功')
return SuccessResponse(data=gen_page_query_result, msg="获取数据库表列表成功") return SuccessResponse(data=result_dict, msg="获取数据库表列表成功")
@genController.post('/importTable', summary="导入表结构", description="导入表结构") @GenRouter.post('/importTable', summary="导入表结构", description="导入表结构")
@ValidateFields(validate_model='edit_gen_table') @ValidateFields(validate_model='edit_gen_table')
async def import_gen_table( async def import_gen_table_controller(
tables: str = Query(..., description="表名列表"), tables: List[str] = Body(..., description="表名列表", embed=True),
auth: AuthSchema = Depends(AuthPermission(permissions=["tool:gen:import"])), auth: AuthSchema = Depends(AuthPermission(permissions=["tool:gen:import"])),
current_user: UserOutSchema = Depends(lambda auth: auth.user) current_user: UserOutSchema = Depends(lambda auth: auth.user)
): ) -> JSONResponse:
table_names = tables.split(',') if tables else [] table_names = tables if tables else []
add_gen_table_list = await GenTableService.get_gen_db_table_list_by_name_services(auth, table_names) add_gen_table_list = await GenTableService.get_gen_db_table_list_by_name_services(auth, table_names)
result = await GenTableService.import_gen_table_services(auth, add_gen_table_list, current_user) result = await GenTableService.import_gen_table_services(auth, add_gen_table_list, current_user)
logger.info('导入表结构成功') logger.info('导入表结构成功')
return result return result
@genController.put('', summary="编辑业务表信息", description="编辑业务表信息") @GenRouter.put('', summary="编辑业务表信息", description="编辑业务表信息")
@ValidateFields(validate_model='edit_gen_table') @ValidateFields(validate_model='edit_gen_table')
async def edit_gen_table( async def edit_gen_table_controller(
data: EditGenTableSchema, data: GenTableUpdateSchema,
auth: AuthSchema = Depends(AuthPermission(permissions=["tool:gen:edit"])), auth: AuthSchema = Depends(AuthPermission(permissions=["tool:gen:edit"])),
current_user: UserOutSchema = Depends(lambda auth: auth.user) current_user: UserOutSchema = Depends(lambda auth: auth.user)
): ) -> JSONResponse:
data.update_by = current_user.username data.update_by = current_user.username
data.update_time = datetime.now() data.updated_at = datetime.now()
await GenTableService.validate_edit(data) await GenTableService.validate_edit(data)
edit_gen_result = await GenTableService.edit_gen_table_services(auth, data) edit_gen_result = await GenTableService.edit_gen_table_services(auth, data)
logger.info('编辑业务表信息成功') logger.info('编辑业务表信息成功')
return SuccessResponse(data=edit_gen_result, msg="编辑业务表信息成功") return SuccessResponse(data=edit_gen_result, msg="编辑业务表信息成功")
@genController.delete('/{table_ids}', summary="删除业务表信息", description="删除业务表信息") @GenRouter.delete('/{table_ids}', summary="删除业务表信息", description="删除业务表信息")
async def delete_gen_table( async def delete_gen_table_controller(
table_ids: str, table_ids: str,
auth: AuthSchema = Depends(AuthPermission(permissions=["tool:gen:remove"])) auth: AuthSchema = Depends(AuthPermission(permissions=["tool:gen:remove"]))
): ) -> JSONResponse:
delete_gen_table = DeleteGenTableSchema(table_ids=table_ids) delete_gen_table = GenTableDeleteSchema(table_ids=table_ids)
result = await GenTableService.delete_gen_table_services(auth, delete_gen_table) result = await GenTableService.delete_gen_table_services(auth, delete_gen_table)
logger.info('删除业务表信息成功') logger.info('删除业务表信息成功')
return result return result
@genController.post('/createTable', summary="创建表结构", description="创建表结构") @GenRouter.post('/createTable', summary="创建表结构", description="创建表结构")
async def create_table( async def create_table_controller(
sql: str = Query(..., description="SQL语句"), sql: str = Query(..., description="SQL语句"),
auth: AuthSchema = Depends(AuthPermission(permissions=["tool:gen:create"])), auth: AuthSchema = Depends(AuthPermission(permissions=["tool:gen:create"])),
current_user: UserOutSchema = Depends(lambda auth: auth.user) current_user: UserOutSchema = Depends(lambda auth: auth.user)
): ) -> JSONResponse:
result = await GenTableService.create_table_services(auth, sql, current_user) result = await GenTableService.create_table_services(auth, sql, current_user)
logger.info('创建表结构成功') logger.info('创建表结构成功')
return result return SuccessResponse(msg="创建表结构成功", data=result)
@genController.get('/batchGenCode', summary="批量生成代码", description="批量生成代码") @GenRouter.get('/batchGenCode', summary="批量生成代码", description="批量生成代码")
async def batch_gen_code( async def batch_gen_code_controller(
tables: str = Query(..., description="表名列表"), tables: str = Query(..., description="表名列表"),
auth: AuthSchema = Depends(AuthPermission(permissions=["tool:gen:code"])) auth: AuthSchema = Depends(AuthPermission(permissions=["tool:gen:code"]))
): ) -> StreamResponse:
table_names = tables.split(',') if tables else [] table_names = tables.split(',') if tables else []
batch_gen_code_result = await GenTableService.batch_gen_code_services(auth, table_names) batch_gen_code_result = await GenTableService.batch_gen_code_services(auth, table_names)
logger.info('批量生成代码成功') logger.info('批量生成代码成功')
@@ -112,11 +112,11 @@ async def batch_gen_code(
) )
@genController.get('/genCode/{table_name}', summary="生成代码到指定路径", description="生成代码到指定路径") @GenRouter.get('/genCode/{table_name}', summary="生成代码到指定路径", description="生成代码到指定路径")
async def gen_code_local( async def gen_code_local_controller(
table_name: str, table_name: str,
auth: AuthSchema = Depends(AuthPermission(permissions=["tool:gen:code"])) auth: AuthSchema = Depends(AuthPermission(permissions=["tool:gen:code"]))
): ) -> JSONResponse:
from app.config.setting import settings from app.config.setting import settings
if not settings.allow_overwrite: if not settings.allow_overwrite:
logger.error('【系统预设】不允许生成文件覆盖到本地') logger.error('【系统预设】不允许生成文件覆盖到本地')
@@ -126,11 +126,11 @@ async def gen_code_local(
return result return result
@genController.get('/{table_id}', summary="获取业务表详细信息", description="获取业务表详细信息") @GenRouter.get('/{table_id}', summary="获取业务表详细信息", description="获取业务表详细信息")
async def query_detail_gen_table( async def query_detail_gen_table_controller(
table_id: int, table_id: int,
auth: AuthSchema = Depends(AuthPermission(permissions=["tool:gen:query"])) auth: AuthSchema = Depends(AuthPermission(permissions=["tool:gen:query"]))
): ) -> JSONResponse:
gen_table = await GenTableService.get_gen_table_by_id_services(auth, table_id) gen_table = await GenTableService.get_gen_table_by_id_services(auth, table_id)
gen_tables = await GenTableService.get_gen_table_all_services(auth) gen_tables = await GenTableService.get_gen_table_all_services(auth)
gen_columns = await GenTableColumnService.get_gen_table_column_list_by_table_id_services(auth, table_id) gen_columns = await GenTableColumnService.get_gen_table_column_list_by_table_id_services(auth, table_id)
@@ -139,21 +139,21 @@ async def query_detail_gen_table(
return SuccessResponse(data=gen_table_detail_result, msg="获取业务表详细信息成功") return SuccessResponse(data=gen_table_detail_result, msg="获取业务表详细信息成功")
@genController.get('/preview/{table_id}', summary="预览代码", description="预览代码") @GenRouter.get('/preview/{table_id}', summary="预览代码", description="预览代码")
async def preview_code( async def preview_code_controller(
table_id: int, table_id: int,
auth: AuthSchema = Depends(AuthPermission(permissions=["tool:gen:preview"])) auth: AuthSchema = Depends(AuthPermission(permissions=["tool:gen:preview"]))
): ) -> JSONResponse:
preview_code_result = await GenTableService.preview_code_services(auth, table_id) preview_code_result = await GenTableService.preview_code_services(auth, table_id)
logger.info('预览代码成功') logger.info('预览代码成功')
return SuccessResponse(data=preview_code_result, msg="预览代码成功") return SuccessResponse(data=preview_code_result, msg="预览代码成功")
@genController.get('/synchDb/{table_name}', summary="同步数据库", description="同步数据库") @GenRouter.get('/synchDb/{table_name}', summary="同步数据库", description="同步数据库")
async def sync_db( async def sync_db_controller(
table_name: str, table_name: str,
auth: AuthSchema = Depends(AuthPermission(permissions=["tool:gen:edit"])) auth: AuthSchema = Depends(AuthPermission(permissions=["tool:gen:edit"]))
): ) -> JSONResponse:
result = await GenTableService.sync_db_services(auth, table_name) result = await GenTableService.sync_db_services(auth, table_name)
logger.info('同步数据库成功') logger.info('同步数据库成功')
return result return result
@@ -1,6 +1,7 @@
# -*- coding:utf-8 -*- # -*- coding:utf-8 -*-
from datetime import datetime, time from datetime import datetime, time
from sqlalchemy.engine.row import Row
from sqlalchemy import delete, func, select, text, update from sqlalchemy import delete, func, select, text, update
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload from sqlalchemy.orm import selectinload
@@ -9,25 +10,20 @@ from typing import List, Optional, Sequence, Any, Dict
from .model import GenTableModel, GenTableColumnModel from .model import GenTableModel, GenTableColumnModel
from app.config.setting import settings from app.config.setting import settings
from app.common.request import PaginationService from app.common.request import PaginationService
from .schema import ( from .schema import GenTableCreateSchema, GenTableUpdateSchema, GenTableOutSchema, GenTableDeleteSchema, GenTableColumnCreateSchema, GenTableColumnUpdateSchema, GenTableColumnOutSchema, GenTableColumnDeleteSchema
GenTableBaseSchema,
GenTableColumnBaseSchema,
GenTableColumnSchema,
GenTableSchema,
)
from .param import GenTableQueryParam from .param import GenTableQueryParam
from app.core.base_crud import CRUDBase from app.core.base_crud import CRUDBase
from app.api.v1.module_system.auth.schema import AuthSchema from app.api.v1.module_system.auth.schema import AuthSchema
class GenTableDao(CRUDBase[GenTableModel, GenTableBaseSchema, GenTableBaseSchema]): class GenTableCRUD(CRUDBase[GenTableModel, GenTableCreateSchema, GenTableUpdateSchema]):
""" """
代码生成业务表模块数据库操作层 代码生成业务表模块数据库操作层
""" """
def __init__(self, auth: AuthSchema) -> None: def __init__(self, auth: AuthSchema) -> None:
"""初始化CRUD""" """初始化CRUD"""
super().__init__(model=GenTableModel(), auth=auth) super().__init__(model=GenTableModel, auth=auth)
async def get_gen_table_by_id(self, db: AsyncSession, table_id: int) -> Optional[GenTableModel]: async def get_gen_table_by_id(self, db: AsyncSession, table_id: int) -> Optional[GenTableModel]:
""" """
@@ -104,23 +100,24 @@ class GenTableDao(CRUDBase[GenTableModel, GenTableBaseSchema, GenTableBaseSchema
# 构建查询条件 # 构建查询条件
conditions = [] conditions = []
# 访问name属性而不是table_name # 访问table_name属性
if query_object.name: if getattr(query_object, 'table_name', None) and query_object.table_name[1]:
conditions.append(func.lower(GenTableModel.table_name).like(f'%{str(query_object.name).lower()}%')) conditions.append(func.lower(GenTableModel.table_name).like(f'%{str(query_object.table_name[1]).lower()}%'))
# 访问table_comment属性 # 访问table_comment属性
if query_object.table_comment: if getattr(query_object, 'table_comment', None):
conditions.append(func.lower(GenTableModel.table_comment).like(f'%{str(query_object.table_comment).lower()}%')) conditions.append(func.lower(GenTableModel.table_comment).like(f'%{str(query_object.table_comment).lower()}%'))
# 访问created_at属性而不是start_time和end_time # 访问created_at属性而不是start_time和end_time
if hasattr(query_object, 'created_at') and query_object.created_at: if hasattr(query_object, 'created_at') and query_object.created_at:
if isinstance(query_object.created_at, tuple) and query_object.created_at[0] == "between": if isinstance(query_object.created_at, tuple) and query_object.created_at[0] == "between":
conditions.append(GenTableModel.create_time.between(*query_object.created_at[1])) conditions.append(GenTableModel.created_at.between(*query_object.created_at[1]))
query = ( query = (
select(GenTableModel) select(GenTableModel)
.options(selectinload(GenTableModel.columns)) .options(selectinload(GenTableModel.columns))
.where(*conditions) .where(*conditions)
.order_by(GenTableModel.created_at.desc())
.distinct() .distinct()
) )
@@ -154,47 +151,43 @@ class GenTableDao(CRUDBase[GenTableModel, GenTableBaseSchema, GenTableBaseSchema
:param is_page: 是否开启分页 :param is_page: 是否开启分页
:return: 数据库列表信息对象 :return: 数据库列表信息对象
""" """
if settings.DATABASE_TYPE == 'postgresql': query_sql = """
query_sql = """ SELECT table_name as table_name,
SELECT table_name as table_name, table_comment as table_comment,
table_comment as table_comment, create_time as create_time,
create_time as create_time, update_time as update_time
update_time as update_time from
from information_schema.tables
list_table where
where table_schema = (select database())
table_name not like 'apscheduler_%' and table_name not like 'apscheduler\_%'
and table_name not like 'gen_%' and table_name not like 'gen\_%'
and table_name not in (select table_name from gen_table) and table_name not in (select table_name from gen_table)
""" """
else: # 根据param.py中的定义,table_name是元组形式("like", value)
query_sql = """ if getattr(query_object, 'table_name', None) and query_object.table_name[1]:
SELECT table_name as table_name,
table_comment as table_comment,
create_time as create_time,
update_time as update_time
from
information_schema.tables
where
table_schema = (select database())
and table_name not like 'apscheduler\_%'
and table_name not like 'gen\_%'
and table_name not in (select table_name from gen_table)
"""
if query_object.name:
query_sql += """and lower(table_name) like lower(concat('%', :table_name, '%'))""" query_sql += """and lower(table_name) like lower(concat('%', :table_name, '%'))"""
if query_object.table_comment:
# 处理table_comment字段(如果有)
if getattr(query_object, 'table_comment', None):
query_sql += """and lower(table_comment) like lower(concat('%', :table_comment, '%'))""" query_sql += """and lower(table_comment) like lower(concat('%', :table_comment, '%'))"""
# 修复查询参数处理
query_params = { # 构建查询参数
k: v for k, v in query_object.model_dump(exclude_none=True, exclude={'page_no', 'page_size'}).items() query_params = {}
}
# 添加table_name查询参数
if getattr(query_object, 'table_name', None) and query_object.table_name[1]:
query_params['table_name'] = query_object.table_name[1]
# 添加table_comment查询参数
if getattr(query_object, 'table_comment', None):
query_params['table_comment'] = query_object.table_comment
query_sql += """order by create_time desc""" query_sql += """order by create_time desc"""
query = text(query_sql).bindparams(**query_params) query = text(query_sql).bindparams(**query_params)
# 执行查询 # 执行查询
result = await db.execute(select(query)) result = await db.execute(query)
all_data = list(result.fetchall()) all_data = list(result.fetchall())
# 使用PaginationService.paginate进行分页 # 使用PaginationService.paginate进行分页
@@ -222,49 +215,32 @@ class GenTableDao(CRUDBase[GenTableModel, GenTableBaseSchema, GenTableBaseSchema
:param table_names: 业务表名称组 :param table_names: 业务表名称组
:return: 数据库列表信息对象 :return: 数据库列表信息对象
""" """
if settings.DATABASE_TYPE == 'postgresql': query_sql = """
query_sql = """ select
select table_name as table_name,
table_name as table_name, table_comment as table_comment,
table_comment as table_comment, create_time as create_time,
create_time as create_time, update_time as update_time
update_time as update_time from
from information_schema.tables
list_table where
where table_schema = (select database())
table_name not like 'qrtz_%' and table_name in :table_names
and table_name not like 'gen_%' """
and table_name = any(:table_names)
"""
else:
query_sql = """
select
table_name as table_name,
table_comment as table_comment,
create_time as create_time,
update_time as update_time
from
information_schema.tables
where
table_name not like 'qrtz\_%'
and table_name not like 'gen\_%'
and table_schema = (select database())
and table_name in :table_names
"""
query = text(query_sql).bindparams(table_names=tuple(table_names)) query = text(query_sql).bindparams(table_names=tuple(table_names))
gen_db_table_list = (await db.execute(query)).fetchall() gen_db_table_list = (await db.execute(query)).fetchall()
return gen_db_table_list return gen_db_table_list
class GenTableColumnDao(CRUDBase[GenTableColumnModel, GenTableColumnBaseSchema, GenTableColumnBaseSchema]): class GenTableColumnCRUD(CRUDBase[GenTableColumnModel, GenTableColumnCreateSchema, GenTableColumnUpdateSchema]):
""" """
代码生成业务表字段模块数据库操作层 代码生成业务表字段模块数据库操作层
""" """
def __init__(self, auth: AuthSchema) -> None: def __init__(self, auth: AuthSchema) -> None:
"""初始化CRUD""" """初始化CRUD"""
super().__init__(model=GenTableColumnModel(), auth=auth) super().__init__(model=GenTableColumnModel, auth=auth)
async def get_gen_table_column_list_by_table_id(self, db: AsyncSession, table_id: int) -> Sequence[GenTableColumnModel]: async def get_gen_table_column_list_by_table_id(self, db: AsyncSession, table_id: int) -> Sequence[GenTableColumnModel]:
""" """
@@ -286,7 +262,7 @@ class GenTableColumnDao(CRUDBase[GenTableColumnModel, GenTableColumnBaseSchema,
return gen_table_column_list return gen_table_column_list
async def get_gen_db_table_columns_by_name(self, db: AsyncSession, table_name: str): async def get_gen_db_table_columns_by_name(self, db: AsyncSession, table_name: str) -> Sequence[Row[Any]]:
""" """
根据业务表名称获取业务表字段列表信息 根据业务表名称获取业务表字段列表信息
@@ -294,42 +270,32 @@ class GenTableColumnDao(CRUDBase[GenTableColumnModel, GenTableColumnBaseSchema,
:param table_name: 业务表名称 :param table_name: 业务表名称
:return: 业务表字段列表信息对象 :return: 业务表字段列表信息对象
""" """
if settings.DATABASE_TYPE == 'postgresql': query_sql = """
query_sql = """ select
select column_name as column_name,
column_name, is_required, is_pk, sort, column_comment, is_increment, column_type case
from when is_nullable = 'no' and column_key != 'PRI' then '1'
list_column else '0'
where end as is_required,
table_name = :table_name case
""" when column_key = 'PRI' then '1'
else: else '0'
query_sql = """ end as is_pk,
select ordinal_position as sort,
column_name as column_name, column_comment as column_comment,
case case
when is_nullable = 'no' and column_key != 'PRI' then '1' when extra = 'auto_increment' then '1'
else '0' else '0'
end as is_required, end as is_increment,
case column_type as column_type
when column_key = 'PRI' then '1' from
else '0' information_schema.columns
end as is_pk, where
ordinal_position as sort, table_schema = (select database())
column_comment as column_comment, and table_name = :table_name
case order by
when extra = 'auto_increment' then '1' ordinal_position
else '0' """
end as is_increment,
column_type as column_type
from
information_schema.columns
where
table_schema = (select database())
and table_name = :table_name
order by
ordinal_position
"""
query = text(query_sql).bindparams(table_name=table_name) query = text(query_sql).bindparams(table_name=table_name)
gen_db_table_columns = (await db.execute(query)).fetchall() gen_db_table_columns = (await db.execute(query)).fetchall()
@@ -1,6 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from datetime import datetime
from typing import Optional, List from typing import Optional, List
from sqlalchemy import String, Integer, ForeignKey from sqlalchemy import String, Integer, ForeignKey
from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.orm import Mapped, mapped_column, relationship
@@ -5,25 +5,21 @@ from typing import Optional
from fastapi import Query from fastapi import Query
from app.core.validator import DateTimeStr from app.core.validator import DateTimeStr
from app.core.base_params import PaginationQueryParam
from .schema import GenTableBaseSchema, GenTableColumnBaseSchema
class GenTableQueryParam(PaginationQueryParam, GenTableBaseSchema): class GenTableQueryParam:
"""数据库表查询参数""" """数据库表查询参数"""
def __init__( def __init__(
self, self,
name: Optional[str] = Query(None, description="名称"), table_name: Optional[str] = Query(None, description="表名称"),
status: Optional[bool] = Query(None, description="是否启用"), status: Optional[bool] = Query(None, description="是否启用"),
creator: Optional[int] = Query(None, description="创建人"), creator: Optional[int] = Query(None, description="创建人"),
start_time: Optional[DateTimeStr] = Query(None, description="开始时间", example="2023-01-01 00:00:00"), start_time: Optional[DateTimeStr] = Query(None, description="开始时间", example="2023-01-01 00:00:00"),
end_time: Optional[DateTimeStr] = Query(None, description="结束时间", example="2023-12-31 23:59:59"), end_time: Optional[DateTimeStr] = Query(None, description="结束时间", example="2023-12-31 23:59:59"),
) -> None: ) -> None:
super().__init__() # 存储查询条件,不直接赋值给父类属性
self.table_name = ("like", table_name)
# 模糊查询字段
self.name = ("like", name)
# 精确查询字段 # 精确查询字段
self.creator_id = creator self.creator_id = creator
@@ -36,21 +32,19 @@ class GenTableQueryParam(PaginationQueryParam, GenTableBaseSchema):
self.created_at = ("between", (start_datetime, end_datetime)) self.created_at = ("between", (start_datetime, end_datetime))
class GenTableColumnQueryParam(PaginationQueryParam, GenTableColumnBaseSchema): class GenTableColumnQueryParam:
"""数据库表字段查询参数""" """数据库表字段查询参数"""
def __init__( def __init__(
self, self,
name: Optional[str] = Query(None, description="名称"), column_name: Optional[str] = Query(None, description="列名称"),
status: Optional[bool] = Query(None, description="是否启用"), status: Optional[bool] = Query(None, description="是否启用"),
creator: Optional[int] = Query(None, description="创建人"), creator: Optional[int] = Query(None, description="创建人"),
start_time: Optional[DateTimeStr] = Query(None, description="开始时间", example="2023-01-01 00:00:00"), start_time: Optional[DateTimeStr] = Query(None, description="开始时间", example="2023-01-01 00:00:00"),
end_time: Optional[DateTimeStr] = Query(None, description="结束时间", example="2023-12-31 23:59:59"), end_time: Optional[DateTimeStr] = Query(None, description="结束时间", example="2023-12-31 23:59:59"),
) -> None: ) -> None:
super().__init__() # 存储查询条件,不直接赋值给父类属性
self.column_name = ("like", column_name)
# 模糊查询字段
self.name = ("like", name)
# 精确查询字段 # 精确查询字段
self.creator_id = creator self.creator_id = creator
@@ -1,23 +1,22 @@
# -*- coding:utf-8 -*- # -*- coding:utf-8 -*-
from datetime import datetime from typing import List, Literal, Optional
from typing import List, Literal, Optional, Union
from pydantic import BaseModel, ConfigDict, Field, model_validator from pydantic import BaseModel, ConfigDict, Field, model_validator
from pydantic.alias_generators import to_camel from pydantic.alias_generators import to_camel
from pydantic_validation_decorator import NotBlank from pydantic_validation_decorator import NotBlank
from app.utils.string_util import StringUtil from app.utils.string_util import StringUtil
from app.common.constant import GenConstant from app.common.constant import GenConstant
from app.core.base_schema import BaseSchema
class GenTableBaseSchema(BaseModel): class GenTableCreateSchema(BaseModel):
""" """
代码生成业务表对应pydantic模型 代码生成业务表对应pydantic模型
""" """
model_config = ConfigDict(alias_generator=to_camel, from_attributes=True) model_config = ConfigDict(from_attributes=True)
table_id: Optional[int] = Field(default=None, description='编号')
table_name: str = Field(default=..., description='表名称') table_name: str = Field(default=..., description='表名称')
table_comment: str = Field(default=..., description='表描述') table_comment: str = Field(default=..., description='表描述')
sub_table_name: Optional[str] = Field(default=None, description='关联子表的表名') sub_table_name: Optional[str] = Field(default=None, description='关联子表的表名')
@@ -34,12 +33,6 @@ class GenTableBaseSchema(BaseModel):
gen_path: Optional[str] = Field(default=None, description='生成路径(不填默认项目路径)') gen_path: Optional[str] = Field(default=None, description='生成路径(不填默认项目路径)')
options: Optional[str] = Field(default=None, description='其它生成选项') options: Optional[str] = Field(default=None, description='其它生成选项')
create_by: Optional[str] = Field(default=None, description='创建者')
create_time: Optional[datetime] = Field(default=None, description='创建时间')
update_by: Optional[str] = Field(default=None, description='更新者')
update_time: Optional[datetime] = Field(default=None, description='更新时间')
remark: Optional[str] = Field(default=None, description='备注')
@NotBlank(field_name='table_name', message='表名称不能为空') @NotBlank(field_name='table_name', message='表名称不能为空')
def get_table_name(self): def get_table_name(self):
return self.table_name return self.table_name
@@ -83,14 +76,14 @@ class GenTableBaseSchema(BaseModel):
self.get_function_author() self.get_function_author()
class GenTableSchema(GenTableBaseSchema): class GenTableUpdateSchema(GenTableCreateSchema):
""" """
代码生成业务表模型 代码生成业务表模型
""" """
pk_column: Optional['GenTableColumnSchema'] = Field(default=None, description='主键信息') pk_column: Optional['GenTableColumnUpdateSchema'] = Field(default=None, description='主键信息')
sub_table: Optional['GenTableSchema'] = Field(default=None, description='子表信息') sub_table: Optional['GenTableUpdateSchema'] = Field(default=None, description='子表信息')
columns: List['GenTableColumnSchema'] = Field(default=..., description='表列信息') columns: List['GenTableColumnUpdateSchema'] = Field(default=..., description='表列信息')
tree_code: Optional[str] = Field(default=None, description='树编码字段') tree_code: Optional[str] = Field(default=None, description='树编码字段')
tree_parent_code: Optional[str] = Field(default=None, description='树父编码字段') tree_parent_code: Optional[str] = Field(default=None, description='树父编码字段')
tree_name: Optional[str] = Field(default=None, description='树名称字段') tree_name: Optional[str] = Field(default=None, description='树名称字段')
@@ -101,35 +94,19 @@ class GenTableSchema(GenTableBaseSchema):
crud: Optional[bool] = Field(default=None, description='是否为单表') crud: Optional[bool] = Field(default=None, description='是否为单表')
@model_validator(mode='after') @model_validator(mode='after')
def check_some_is(self) -> 'GenTableSchema': def check_some_is(self) -> 'GenTableUpdateSchema':
self.sub = True if self.tpl_category and self.tpl_category == GenConstant.TPL_SUB else False self.sub = True if self.tpl_category and self.tpl_category == GenConstant.TPL_SUB else False
self.tree = True if self.tpl_category and self.tpl_category == GenConstant.TPL_TREE else False self.tree = True if self.tpl_category and self.tpl_category == GenConstant.TPL_TREE else False
self.crud = True if self.tpl_category and self.tpl_category == GenConstant.TPL_CRUD else False self.crud = True if self.tpl_category and self.tpl_category == GenConstant.TPL_CRUD else False
return self return self
class EditGenTableSchema(GenTableSchema): class GenTableOutSchema(GenTableUpdateSchema, BaseSchema):
""" """响应模型"""
修改代码生成业务表模型 model_config = ConfigDict(from_attributes=True)
"""
params: Optional['GenTableParamsSchema'] = Field(default=None, description='业务表参数')
class GenTableParamsSchema(BaseModel): class GenTableDeleteSchema(BaseModel):
"""
代码生成业务表参数模型
"""
model_config = ConfigDict(alias_generator=to_camel)
tree_code: Optional[str] = Field(default=None, description='树编码字段')
tree_parent_code: Optional[str] = Field(default=None, description='树父编码字段')
tree_name: Optional[str] = Field(default=None, description='树名称字段')
parent_menu_id: Optional[int] = Field(default=None, description='上级菜单ID字段')
class DeleteGenTableSchema(BaseModel):
""" """
删除代码生成业务表模型 删除代码生成业务表模型
""" """
@@ -139,15 +116,15 @@ class DeleteGenTableSchema(BaseModel):
table_ids: str = Field(description='需要删除的代码生成业务表ID') table_ids: str = Field(description='需要删除的代码生成业务表ID')
class GenTableColumnBaseSchema(BaseModel): class GenTableColumnCreateSchema(BaseModel):
""" """
代码生成业务表字段对应pydantic模型 代码生成业务表字段对应pydantic模型
""" """
model_config = ConfigDict(alias_generator=to_camel, from_attributes=True) model_config = ConfigDict(alias_generator=to_camel, from_attributes=True)
column_id: Optional[int] = Field(default=None, description='编号')
table_id: Optional[int] = Field(default=None, description='归属表编号') table_id: Optional[int] = Field(default=None, description='归属表编号')
column_name: str = Field(default=..., description='列名称') column_name: str = Field(default=..., description='列名称')
column_comment: Optional[str] = Field(default=None, description='列描述') column_comment: Optional[str] = Field(default=None, description='列描述')
column_type: str = Field(default=..., description='列类型') column_type: str = Field(default=..., description='列类型')
@@ -166,11 +143,6 @@ class GenTableColumnBaseSchema(BaseModel):
dict_type: str = Field(default=..., description='字典类型') dict_type: str = Field(default=..., description='字典类型')
sort: Optional[int] = Field(default=None, description='排序') sort: Optional[int] = Field(default=None, description='排序')
create_by: Optional[str] = Field(default=None, description='创建者')
create_time: Optional[datetime] = Field(default=None, description='创建时间')
update_by: Optional[str] = Field(default=None, description='更新者')
update_time: Optional[datetime] = Field(default=None, description='更新时间')
@NotBlank(field_name='python_field', message='Python属性不能为空') @NotBlank(field_name='python_field', message='Python属性不能为空')
def get_python_field(self): def get_python_field(self):
return self.python_field return self.python_field
@@ -179,7 +151,7 @@ class GenTableColumnBaseSchema(BaseModel):
self.get_python_field() self.get_python_field()
class GenTableColumnSchema(GenTableColumnBaseSchema): class GenTableColumnUpdateSchema(GenTableColumnCreateSchema):
""" """
代码生成业务表字段模型 代码生成业务表字段模型
""" """
@@ -197,7 +169,7 @@ class GenTableColumnSchema(GenTableColumnBaseSchema):
usable_column: Optional[bool] = Field(default=None, description='是否为基类字段白名单') usable_column: Optional[bool] = Field(default=None, description='是否为基类字段白名单')
@model_validator(mode='after') @model_validator(mode='after')
def check_some_is(self) -> 'GenTableColumnSchema': def check_some_is(self) -> 'GenTableColumnUpdateSchema':
self.cap_python_field = self.python_field[0].upper() + self.python_field[1:] if self.python_field else None self.cap_python_field = self.python_field[0].upper() + self.python_field[1:] if self.python_field else None
self.pk = True if self.is_pk and self.is_pk == '1' else False self.pk = True if self.is_pk and self.is_pk == '1' else False
self.increment = True if self.is_increment and self.is_increment == '1' else False self.increment = True if self.is_increment and self.is_increment == '1' else False
@@ -218,11 +190,16 @@ class GenTableColumnSchema(GenTableColumnBaseSchema):
return self return self
class DeleteGenTableColumnSchema(BaseModel): class GenTableColumnOutSchema(GenTableColumnUpdateSchema, BaseSchema):
"""响应模型"""
model_config = ConfigDict(from_attributes=True)
class GenTableColumnDeleteSchema(BaseModel):
""" """
删除代码生成业务表字段模型 删除代码生成业务表字段模型
""" """
model_config = ConfigDict(alias_generator=to_camel) model_config = ConfigDict(from_attributes=True)
column_ids: str = Field(description='需要删除的代码生成业务表字段ID') column_ids: str = Field(description='需要删除的代码生成业务表字段ID')
@@ -17,9 +17,9 @@ from app.common.constant import GenConstant
from app.common.response import SuccessResponse from app.common.response import SuccessResponse
from app.api.v1.module_system.user.schema import UserOutSchema from app.api.v1.module_system.user.schema import UserOutSchema
from app.api.v1.module_system.auth.schema import AuthSchema from app.api.v1.module_system.auth.schema import AuthSchema
from .schema import DeleteGenTableSchema, EditGenTableSchema, GenTableColumnSchema, GenTableSchema from .schema import GenTableCreateSchema, GenTableUpdateSchema, GenTableOutSchema, GenTableDeleteSchema, GenTableColumnCreateSchema, GenTableColumnUpdateSchema, GenTableColumnOutSchema, GenTableColumnDeleteSchema
from .param import GenTableQueryParam from .param import GenTableQueryParam
from .crud import GenTableColumnDao, GenTableDao from .crud import GenTableColumnCRUD, GenTableCRUD
from .model import GenTableModel, GenTableColumnModel from .model import GenTableModel, GenTableColumnModel
@@ -33,7 +33,7 @@ class GenTableService:
""" """
@classmethod @classmethod
async def get_gen_table_list_services( async def get_gen_table_list_service(
cls, auth: AuthSchema, query_object: GenTableQueryParam, is_page: bool = False cls, auth: AuthSchema, query_object: GenTableQueryParam, is_page: bool = False
): ):
""" """
@@ -44,17 +44,13 @@ class GenTableService:
:param is_page: 是否开启分页 :param is_page: 是否开启分页
:return: 代码生成业务列表信息对象 :return: 代码生成业务列表信息对象
""" """
# 确保db是AsyncSession类型 gen_table_dao = GenTableCRUD(auth=auth)
if not isinstance(auth.db, AsyncSession):
raise CustomException(msg='数据库会话类型不正确')
gen_table_dao = GenTableDao(auth=auth)
gen_table_list_result = await gen_table_dao.get_gen_table_list(auth.db, query_object, is_page) gen_table_list_result = await gen_table_dao.get_gen_table_list(auth.db, query_object, is_page)
return gen_table_list_result return gen_table_list_result
@classmethod @classmethod
async def get_gen_db_table_list_services( async def get_gen_db_table_list_service(
cls, auth: AuthSchema, query_object: GenTableQueryParam, is_page: bool = False cls, auth: AuthSchema, query_object: GenTableQueryParam, is_page: bool = False
): ):
""" """
@@ -65,17 +61,13 @@ class GenTableService:
:param is_page: 是否开启分页 :param is_page: 是否开启分页
:return: 数据库列表信息对象 :return: 数据库列表信息对象
""" """
# 确保db是AsyncSession类型 gen_table_dao = GenTableCRUD(auth=auth)
if not isinstance(auth.db, AsyncSession):
raise CustomException(msg='数据库会话类型不正确')
gen_table_dao = GenTableDao(auth=auth)
gen_db_table_list_result = await gen_table_dao.get_gen_db_table_list(auth.db, query_object, is_page) gen_db_table_list_result = await gen_table_dao.get_gen_db_table_list(auth.db, query_object, is_page)
return gen_db_table_list_result return gen_db_table_list_result
@classmethod @classmethod
async def get_gen_db_table_list_by_name_services(cls, auth: AuthSchema, table_names: List[str]) -> list[GenTableSchema]: async def get_gen_db_table_list_by_name_service(cls, auth: AuthSchema, table_names: List[str]) -> list[GenTableOutSchema]:
""" """
根据表名称组获取数据库列表信息service 根据表名称组获取数据库列表信息service
@@ -83,18 +75,14 @@ class GenTableService:
:param table_names: 表名称组 :param table_names: 表名称组
:return: 数据库列表信息对象 :return: 数据库列表信息对象
""" """
# 确保db是AsyncSession类型 gen_table_dao = GenTableCRUD(auth=auth)
if not isinstance(auth.db, AsyncSession):
raise CustomException(msg='数据库会话类型不正确')
gen_table_dao = GenTableDao(auth=auth)
gen_db_table_list_result = await gen_table_dao.get_gen_db_table_list_by_names(auth.db, table_names) gen_db_table_list_result = await gen_table_dao.get_gen_db_table_list_by_names(auth.db, table_names)
return [GenTableSchema(**gen_table) for gen_table in CamelCaseUtil.transform_result(gen_db_table_list_result)] return [GenTableOutSchema(**gen_table) for gen_table in CamelCaseUtil.transform_result(gen_db_table_list_result)]
@classmethod @classmethod
async def import_gen_table_services( async def import_gen_table_service(
cls, auth: AuthSchema, gen_table_list: List[GenTableSchema], current_user: UserOutSchema cls, auth: AuthSchema, gen_table_list: List[GenTableOutSchema], current_user: UserOutSchema
): ):
""" """
导入表结构service 导入表结构service
@@ -105,12 +93,8 @@ class GenTableService:
:return: 导入结果 :return: 导入结果
""" """
try: try:
# 确保db是AsyncSession类型 gen_table_dao = GenTableCRUD(auth=auth)
if not isinstance(auth.db, AsyncSession): gen_table_column_dao = GenTableColumnCRUD(auth=auth)
raise CustomException(msg='数据库会话类型不正确')
gen_table_dao = GenTableDao(auth=auth)
gen_table_column_dao = GenTableColumnDao(auth=auth)
for table in gen_table_list: for table in gen_table_list:
table_name = table.table_name table_name = table.table_name
@@ -120,24 +104,22 @@ class GenTableService:
table.table_id = add_gen_table.id table.table_id = add_gen_table.id
gen_table_columns = await gen_table_column_dao.get_gen_db_table_columns_by_name(auth.db, table_name or "") gen_table_columns = await gen_table_column_dao.get_gen_db_table_columns_by_name(auth.db, table_name or "")
for column in [ for column in [
GenTableColumnSchema(**gen_table_column) GenTableColumnOutSchema(**gen_table_column)
for gen_table_column in CamelCaseUtil.transform_result(gen_table_columns) for gen_table_column in CamelCaseUtil.transform_result(gen_table_columns)
]: ]:
GenUtils.init_column_field(column, table) GenUtils.init_column_field(column, table)
await gen_table_column_dao.create(data=column.model_dump()) await gen_table_column_dao.create(data=column.model_dump())
if isinstance(auth.db, AsyncSession): await auth.db.commit()
await auth.db.commit()
return SuccessResponse(msg='导入成功') return SuccessResponse(msg='导入成功')
except Exception as e: except Exception as e:
if isinstance(auth.db, AsyncSession): try:
try: await auth.db.rollback()
await auth.db.rollback() except:
except: pass # 忽略回滚错误
pass # 忽略回滚错误
raise CustomException(msg=f'导入失败, {str(e)}') raise CustomException(msg=f'导入失败, {str(e)}')
@classmethod @classmethod
async def edit_gen_table_services(cls, auth: AuthSchema, page_object: EditGenTableSchema) -> Dict[str, Any]: async def edit_gen_table_service(cls, auth: AuthSchema, page_object: GenTableUpdateSchema) -> Dict[str, Any]:
""" """
编辑业务表信息service 编辑业务表信息service
@@ -145,30 +127,26 @@ class GenTableService:
:param page_object: 编辑业务表对象 :param page_object: 编辑业务表对象
:return: 编辑业务表校验结果 :return: 编辑业务表校验结果
""" """
# 确保db是AsyncSession类型 gen_table_dao = GenTableCRUD(auth=auth)
if not isinstance(auth.db, AsyncSession): gen_table_column_dao = GenTableColumnCRUD(auth=auth)
raise CustomException(msg='数据库会话类型不正确')
gen_table_dao = GenTableDao(auth=auth)
gen_table_column_dao = GenTableColumnDao(auth=auth)
# 检查必要字段是否存在 # 检查必要字段是否存在
if page_object.table_id is None: if getattr(page_object, 'table_id', None) is None:
raise CustomException(msg='业务表ID不能为空') raise CustomException(msg='业务表ID不能为空')
edit_gen_table = page_object.model_dump(exclude_unset=True, by_alias=True) edit_gen_table = page_object.model_dump(exclude_unset=True, by_alias=True)
gen_table_info = await cls.get_gen_table_by_id_services(auth, page_object.table_id) gen_table_info = await cls.get_gen_table_by_id_service(auth, page_object.table_id)
if gen_table_info.table_id: if gen_table_info.table_id:
try: try:
# 处理params字段,确保不为None # 确保options字段存在且为有效JSON
params = edit_gen_table.get('params') if 'options' not in edit_gen_table or edit_gen_table['options'] is None:
if params is not None:
edit_gen_table['options'] = json.dumps(params)
else:
edit_gen_table['options'] = '{}' # 默认空对象 edit_gen_table['options'] = '{}' # 默认空对象
else:
# 移除params字段,因为options字段已经包含了序列化的params # 验证options是否为有效的JSON
edit_gen_table.pop('params', None) try:
json.loads(edit_gen_table['options'])
except json.JSONDecodeError:
edit_gen_table['options'] = '{}'
await gen_table_dao.update(id=page_object.table_id, data=edit_gen_table) await gen_table_dao.update(id=page_object.table_id, data=edit_gen_table)
if page_object.columns: if page_object.columns:
@@ -180,21 +158,19 @@ class GenTableService:
id=gen_table_column.column_id, id=gen_table_column.column_id,
data=gen_table_column.model_dump(by_alias=True) data=gen_table_column.model_dump(by_alias=True)
) )
if isinstance(auth.db, AsyncSession): await auth.db.commit()
await auth.db.commit()
return {"is_success": True, "message": "更新成功"} return {"is_success": True, "message": "更新成功"}
except Exception as e: except Exception as e:
if isinstance(auth.db, AsyncSession): try:
try: await auth.db.rollback()
await auth.db.rollback() except:
except: pass # 忽略回滚错误
pass # 忽略回滚错误
raise CustomException(msg=f'更新失败: {str(e)}') raise CustomException(msg=f'更新失败: {str(e)}')
else: else:
raise CustomException(msg='业务表不存在') raise CustomException(msg='业务表不存在')
@classmethod @classmethod
async def delete_gen_table_services(cls, auth: AuthSchema, page_object: DeleteGenTableSchema) -> SuccessResponse: async def delete_gen_table_service(cls, auth: AuthSchema, page_object: GenTableDeleteSchema) -> SuccessResponse:
""" """
删除业务表信息service 删除业务表信息service
@@ -202,12 +178,8 @@ class GenTableService:
:param page_object: 删除业务表对象 :param page_object: 删除业务表对象
:return: 删除业务表校验结果 :return: 删除业务表校验结果
""" """
# 确保db是AsyncSession类型 gen_table_dao = GenTableCRUD(auth=auth)
if not isinstance(auth.db, AsyncSession): gen_table_column_dao = GenTableColumnCRUD(auth=auth)
raise CustomException(msg='数据库会话类型不正确')
gen_table_dao = GenTableDao(auth=auth)
gen_table_column_dao = GenTableColumnDao(auth=auth)
if page_object.table_ids: if page_object.table_ids:
table_id_list = page_object.table_ids.split(',') table_id_list = page_object.table_ids.split(',')
@@ -220,21 +192,19 @@ class GenTableService:
if columns: if columns:
column_ids = [column.id for column in columns] column_ids = [column.id for column in columns]
await gen_table_column_dao.delete(ids=column_ids) await gen_table_column_dao.delete(ids=column_ids)
if isinstance(auth.db, AsyncSession): await auth.db.commit()
await auth.db.commit()
return SuccessResponse(msg='删除成功') return SuccessResponse(msg='删除成功')
except Exception as e: except Exception as e:
if isinstance(auth.db, AsyncSession): try:
try: await auth.db.rollback()
await auth.db.rollback() except:
except: pass # 忽略回滚错误
pass # 忽略回滚错误
raise CustomException(msg=f'删除失败: {str(e)}') raise CustomException(msg=f'删除失败: {str(e)}')
else: else:
raise CustomException(msg='传入业务表id为空') raise CustomException(msg='传入业务表id为空')
@classmethod @classmethod
async def get_gen_table_by_id_services(cls, auth: AuthSchema, table_id: int) -> GenTableSchema: async def get_gen_table_by_id_service(cls, auth: AuthSchema, table_id: int) -> GenTableOutSchema:
""" """
获取需要生成的业务表详细信息service 获取需要生成的业务表详细信息service
@@ -242,38 +212,32 @@ class GenTableService:
:param table_id: 需要生成的业务表id :param table_id: 需要生成的业务表id
:return: 需要生成的业务表id对应的信息 :return: 需要生成的业务表id对应的信息
""" """
# 确保db是AsyncSession类型 gen_table_dao = GenTableCRUD(auth=auth)
if not isinstance(auth.db, AsyncSession):
raise CustomException(msg='数据库会话类型不正确')
gen_table_dao = GenTableDao(auth=auth)
gen_table = await gen_table_dao.get_gen_table_by_id(auth.db, table_id) gen_table = await gen_table_dao.get_gen_table_by_id(auth.db, table_id)
if gen_table: if gen_table:
result = await cls.set_table_from_options(GenTableSchema(**CamelCaseUtil.transform_result(gen_table))) result = await cls.set_table_from_options(GenTableOutSchema(**CamelCaseUtil.transform_result(gen_table)))
return result return result
else: else:
raise CustomException(msg='业务表不存在') raise CustomException(msg='业务表不存在')
@classmethod @classmethod
async def get_gen_table_all_services(cls, auth: AuthSchema) -> list[GenTableSchema]: async def get_gen_table_all_service(cls, auth: AuthSchema) -> list[GenTableOutSchema]:
""" """
获取所有业务表信息service 获取所有业务表信息service
:param auth: 认证信息 :param auth: 认证信息
:return: 所有业务表信息 :return: 所有业务表信息列表
""" """
# 确保db是AsyncSession类型 gen_table_dao = GenTableCRUD(auth=auth)
if not isinstance(auth.db, AsyncSession): gen_tables = await gen_table_dao.get_gen_table_all(auth.db)
raise CustomException(msg='数据库会话类型不正确') result = []
for table in gen_tables:
gen_table_dao = GenTableDao(auth=auth) table_info = await cls.set_table_from_options(GenTableOutSchema(**CamelCaseUtil.transform_result(table)))
gen_table_all = await gen_table_dao.get_gen_table_all(auth.db) result.append(table_info)
result = [GenTableSchema(**gen_table) for gen_table in CamelCaseUtil.transform_result(gen_table_all)]
return result return result
@classmethod @classmethod
async def create_table_services(cls, auth: AuthSchema, sql: str, current_user: UserOutSchema) -> SuccessResponse: async def create_table_service(cls, auth: AuthSchema, sql: str, current_user: UserOutSchema) -> SuccessResponse:
""" """
创建表结构service 创建表结构service
@@ -282,28 +246,22 @@ class GenTableService:
:param current_user: 当前用户信息对象 :param current_user: 当前用户信息对象
:return: 创建表结构结果 :return: 创建表结构结果
""" """
# 确保db是AsyncSession类型 gen_table_dao = GenTableCRUD(auth=auth)
if not isinstance(auth.db, AsyncSession):
raise CustomException(msg='数据库会话类型不正确')
gen_table_dao = GenTableDao(auth=auth)
try: try:
# 执行SQL语句创建表 # 执行SQL语句创建表
await gen_table_dao.create_table_by_sql_dao(auth.db, [sql]) await gen_table_dao.create_table_by_sql_dao(auth.db, [sql])
if isinstance(auth.db, AsyncSession): await auth.db.commit()
await auth.db.commit()
return SuccessResponse(msg='创建表结构成功') return SuccessResponse(msg='创建表结构成功')
except Exception as e: except Exception as e:
if isinstance(auth.db, AsyncSession): try:
try: await auth.db.rollback()
await auth.db.rollback() except:
except: pass # 忽略回滚错误
pass # 忽略回滚错误
raise CustomException(msg=f'创建表结构失败: {str(e)}') raise CustomException(msg=f'创建表结构失败: {str(e)}')
@classmethod @classmethod
async def preview_code_services(cls, auth: AuthSchema, table_id: int) -> dict[Any, Any]: async def preview_code_service(cls, auth: AuthSchema, table_id: int) -> dict[Any, Any]:
""" """
预览代码service 预览代码service
@@ -311,7 +269,7 @@ class GenTableService:
:param table_id: 业务表id :param table_id: 业务表id
:return: 预览数据列表 :return: 预览数据列表
""" """
gen_table = await cls.get_gen_table_by_id_services(auth, table_id) gen_table = await cls.get_gen_table_by_id_service(auth, table_id)
await cls.set_sub_table(auth, gen_table) await cls.set_sub_table(auth, gen_table)
await cls._set_pk_column(gen_table) await cls._set_pk_column(gen_table)
env = TemplateInitializer.init_jinja2() env = TemplateInitializer.init_jinja2()
@@ -327,7 +285,7 @@ class GenTableService:
return preview_code_result return preview_code_result
@classmethod @classmethod
async def generate_code_services(cls, auth: AuthSchema, table_name: str) -> SuccessResponse: async def generate_code_service(cls, auth: AuthSchema, table_name: str) -> SuccessResponse:
""" """
生成代码至指定路径service 生成代码至指定路径service
@@ -351,7 +309,7 @@ class GenTableService:
return SuccessResponse(msg='生成代码成功') return SuccessResponse(msg='生成代码成功')
@classmethod @classmethod
async def batch_gen_code_services(cls, auth: AuthSchema, table_names: List[str]) -> bytes: async def batch_gen_code_service(cls, auth: AuthSchema, table_names: List[str]) -> bytes:
""" """
批量生成代码service 批量生成代码service
@@ -381,14 +339,11 @@ class GenTableService:
:param table_name: 业务表名称 :param table_name: 业务表名称
:return: 生成代码渲染模板相关信息 :return: 生成代码渲染模板相关信息
""" """
# 确保db是AsyncSession类型
if not isinstance(auth.db, AsyncSession):
raise CustomException(msg='数据库会话类型不正确')
gen_table_dao = GenTableDao(auth=auth) gen_table_dao = GenTableCRUD(auth=auth)
gen_table = await gen_table_dao.get_gen_table_by_name(auth.db, table_name) gen_table = await gen_table_dao.get_gen_table_by_name(auth.db, table_name)
if gen_table: if gen_table:
gen_table_schema = GenTableSchema(**CamelCaseUtil.transform_result(gen_table)) gen_table_schema = GenTableOutSchema(**CamelCaseUtil.transform_result(gen_table))
await cls.set_sub_table(auth, gen_table_schema) await cls.set_sub_table(auth, gen_table_schema)
await cls._set_pk_column(gen_table_schema) await cls._set_pk_column(gen_table_schema)
context = TemplateUtils.prepare_context(gen_table_schema) context = TemplateUtils.prepare_context(gen_table_schema)
@@ -403,7 +358,7 @@ class GenTableService:
raise CustomException(msg=f'业务表 {table_name} 不存在') raise CustomException(msg=f'业务表 {table_name} 不存在')
@classmethod @classmethod
def __get_gen_path(cls, gen_table: GenTableSchema, template: str) -> Optional[str]: def __get_gen_path(cls, gen_table: GenTableOutSchema, template: str) -> Optional[str]:
""" """
根据GenTableModel对象和模板名称生成路径 根据GenTableModel对象和模板名称生成路径
@@ -423,7 +378,7 @@ class GenTableService:
return None return None
@classmethod @classmethod
async def sync_db_services(cls, auth: AuthSchema, table_name: str) -> SuccessResponse: async def sync_db_service(cls, auth: AuthSchema, table_name: str) -> SuccessResponse:
""" """
同步数据库service 同步数据库service
@@ -431,21 +386,17 @@ class GenTableService:
:param table_name: 业务表名称 :param table_name: 业务表名称
:return: 同步数据库结果 :return: 同步数据库结果
""" """
# 确保db是AsyncSession类型 gen_table_dao = GenTableCRUD(auth=auth)
if not isinstance(auth.db, AsyncSession): gen_table_column_dao = GenTableColumnCRUD(auth=auth)
raise CustomException(msg='数据库会话类型不正确')
gen_table_dao = GenTableDao(auth=auth)
gen_table_column_dao = GenTableColumnDao(auth=auth)
gen_table = await gen_table_dao.get_gen_table_by_name(auth.db, table_name) gen_table = await gen_table_dao.get_gen_table_by_name(auth.db, table_name)
if gen_table: if gen_table:
table = GenTableSchema(**CamelCaseUtil.transform_result(gen_table)) table = GenTableOutSchema(**CamelCaseUtil.transform_result(gen_table))
table_columns = table.columns or [] # 确保不为None table_columns = table.columns or [] # 确保不为None
table_column_map = {column.column_name: column for column in table_columns} table_column_map = {column.column_name: column for column in table_columns}
query_db_table_columns = await gen_table_column_dao.get_gen_db_table_columns_by_name(auth.db, table_name) query_db_table_columns = await gen_table_column_dao.get_gen_db_table_columns_by_name(auth.db, table_name)
db_table_columns = [ db_table_columns = [
GenTableColumnSchema(**column) for column in CamelCaseUtil.transform_result(query_db_table_columns) GenTableColumnOutSchema(**column) for column in CamelCaseUtil.transform_result(query_db_table_columns)
] ]
if not db_table_columns: if not db_table_columns:
raise CustomException('同步数据失败,原表结构不存在') raise CustomException('同步数据失败,原表结构不存在')
@@ -476,21 +427,19 @@ class GenTableService:
for column in del_columns: for column in del_columns:
if column.column_id is not None: if column.column_id is not None:
await gen_table_column_dao.delete(ids=[column.column_id]) await gen_table_column_dao.delete(ids=[column.column_id])
if isinstance(auth.db, AsyncSession): await auth.db.commit()
await auth.db.commit()
return SuccessResponse(msg='同步成功') return SuccessResponse(msg='同步成功')
except Exception as e: except Exception as e:
if isinstance(auth.db, AsyncSession): try:
try: await auth.db.rollback()
await auth.db.rollback() except:
except: pass # 忽略回滚错误
pass # 忽略回滚错误
raise CustomException(msg=f'同步失败: {str(e)}') raise CustomException(msg=f'同步失败: {str(e)}')
else: else:
raise CustomException('业务表不存在') raise CustomException('业务表不存在')
@classmethod @classmethod
async def set_sub_table(cls, auth: AuthSchema, gen_table: GenTableSchema) -> None: async def set_sub_table(cls, auth: AuthSchema, gen_table: GenTableOutSchema) -> None:
""" """
设置主子表信息 设置主子表信息
@@ -498,18 +447,15 @@ class GenTableService:
:param gen_table: 业务表信息 :param gen_table: 业务表信息
:return: :return:
""" """
# 确保db是AsyncSession类型
if not isinstance(auth.db, AsyncSession):
raise CustomException(msg='数据库会话类型不正确')
if gen_table.sub_table_name: if gen_table.sub_table_name:
gen_table_dao = GenTableDao(auth=auth) gen_table_dao = GenTableCRUD(auth=auth)
sub_table = await gen_table_dao.get_gen_table_by_name(auth.db, gen_table.sub_table_name) sub_table = await gen_table_dao.get_gen_table_by_name(auth.db, gen_table.sub_table_name)
if sub_table: if sub_table:
gen_table.sub_table = GenTableSchema(**CamelCaseUtil.transform_result(sub_table)) gen_table.sub_table = GenTableOutSchema(**CamelCaseUtil.transform_result(sub_table))
@classmethod @classmethod
async def _set_pk_column(cls, gen_table: GenTableSchema) -> None: async def _set_pk_column(cls, gen_table: GenTableOutSchema) -> None:
""" """
设置主键列信息 设置主键列信息
@@ -533,7 +479,7 @@ class GenTableService:
gen_table.sub_table.pk_column = gen_table.sub_table.columns[0] gen_table.sub_table.pk_column = gen_table.sub_table.columns[0]
@classmethod @classmethod
async def set_table_from_options(cls, gen_table: GenTableSchema) -> GenTableSchema: async def set_table_from_options(cls, gen_table: GenTableOutSchema) -> GenTableOutSchema:
""" """
设置代码生成其他选项值 设置代码生成其他选项值
@@ -551,18 +497,18 @@ class GenTableService:
return gen_table return gen_table
@classmethod @classmethod
async def validate_edit(cls, edit_gen_table: EditGenTableSchema): async def validate_edit(cls, edit_gen_table: GenTableUpdateSchema):
""" """
编辑保存参数校验 编辑保存参数校验
:param edit_gen_table: 编辑业务表对象 :param edit_gen_table: 编辑业务表对象
""" """
if edit_gen_table.tpl_category == GenConstant.TPL_TREE: if edit_gen_table.tpl_category == GenConstant.TPL_TREE:
# 检查params是否为None # 从options字段获取参数,而不是params
if edit_gen_table.params is None: if not edit_gen_table.options:
raise CustomException(msg='树表参数不能为空') raise CustomException(msg='树表参数不能为空')
params_obj = edit_gen_table.params.model_dump(by_alias=True) params_obj = json.loads(edit_gen_table.options)
if GenConstant.TREE_CODE not in params_obj: if GenConstant.TREE_CODE not in params_obj:
raise CustomException(msg='树编码字段不能为空') raise CustomException(msg='树编码字段不能为空')
@@ -583,7 +529,7 @@ class GenTableColumnService:
""" """
@classmethod @classmethod
async def get_gen_table_column_list_by_table_id_services(cls, auth: AuthSchema, table_id: int): async def get_gen_table_column_list_by_table_id_service(cls, auth: AuthSchema, table_id: int) -> List[GenTableColumnOutSchema]:
""" """
获取业务表字段列表信息service 获取业务表字段列表信息service
@@ -591,14 +537,10 @@ class GenTableColumnService:
:param table_id: 业务表格id :param table_id: 业务表格id
:return: 业务表字段列表信息对象 :return: 业务表字段列表信息对象
""" """
# 确保db是AsyncSession类型 gen_table_column_dao = GenTableColumnCRUD(auth=auth)
if not isinstance(auth.db, AsyncSession):
raise CustomException(msg='数据库会话类型不正确')
gen_table_column_dao = GenTableColumnDao(auth=auth)
gen_table_column_list_result = await gen_table_column_dao.get_gen_table_column_list_by_table_id(auth.db, table_id) gen_table_column_list_result = await gen_table_column_dao.get_gen_table_column_list_by_table_id(auth.db, table_id)
return [ return [
GenTableColumnSchema(**gen_table_column) GenTableColumnOutSchema(**gen_table_column)
for gen_table_column in CamelCaseUtil.transform_result(gen_table_column_list_result) for gen_table_column in CamelCaseUtil.transform_result(gen_table_column_list_result)
] ]
@@ -6,6 +6,7 @@ from .cache.controller import CacheRouter
from .job.controller import JobRouter from .job.controller import JobRouter
from .online.controller import OnlineRouter from .online.controller import OnlineRouter
from .server.controller import ServerRouter from .server.controller import ServerRouter
from .resource.controller import ResourceRouter
MonitorRouter = APIRouter(prefix="/monitor") MonitorRouter = APIRouter(prefix="/monitor")
@@ -15,3 +16,4 @@ MonitorRouter.include_router(CacheRouter)
MonitorRouter.include_router(JobRouter) MonitorRouter.include_router(JobRouter)
MonitorRouter.include_router(OnlineRouter) MonitorRouter.include_router(OnlineRouter)
MonitorRouter.include_router(ServerRouter) MonitorRouter.include_router(ServerRouter)
MonitorRouter.include_router(ResourceRouter)
@@ -4,10 +4,8 @@ from fastapi import APIRouter, Body, Depends, Path, Query, Request, UploadFile,
from fastapi.responses import JSONResponse, StreamingResponse, FileResponse from fastapi.responses import JSONResponse, StreamingResponse, FileResponse
from typing import List, Optional from typing import List, Optional
from app.common.request import PaginationService
from app.common.response import StreamResponse, SuccessResponse from app.common.response import StreamResponse, SuccessResponse
from app.utils.common_util import bytes2file_response from app.utils.common_util import bytes2file_response
from app.core.base_params import PaginationQueryParam
from app.core.dependencies import AuthPermission from app.core.dependencies import AuthPermission
from app.core.router_class import OperationLogRoute from app.core.router_class import OperationLogRoute
from app.core.logger import logger from app.core.logger import logger
@@ -23,15 +21,15 @@ from .schema import (
from .service import ResourceService from .service import ResourceService
ResourceFileRouter = APIRouter(route_class=OperationLogRoute, prefix="/resource", tags=["资源管理"]) ResourceRouter = APIRouter(route_class=OperationLogRoute, prefix="/resource", tags=["资源管理"])
@ResourceFileRouter.get("/list", summary="获取目录列表", description="获取指定目录下的文件和子目录列表") @ResourceRouter.get("/list", summary="获取目录列表", description="获取指定目录下的文件和子目录列表")
async def get_directory_list_controller( async def get_directory_list_controller(
request: Request, request: Request,
path: Optional[str] = Query(None, description="目录路径"), path: Optional[str] = Query(None, description="目录路径"),
include_hidden: bool = Query(False, description="是否包含隐藏文件"), include_hidden: bool = Query(False, description="是否包含隐藏文件"),
auth: AuthSchema = Depends(AuthPermission(permissions=["resource:file:query"])) auth: AuthSchema = Depends(AuthPermission(permissions=["monitor:resource:query"]))
) -> JSONResponse: ) -> JSONResponse:
"""获取目录列表""" """获取目录列表"""
result_dict = await ResourceService.get_directory_list_service( result_dict = await ResourceService.get_directory_list_service(
@@ -44,11 +42,11 @@ async def get_directory_list_controller(
return SuccessResponse(data=result_dict, msg="获取目录列表成功") return SuccessResponse(data=result_dict, msg="获取目录列表成功")
@ResourceFileRouter.post("/search", summary="搜索资源", description="根据条件搜索资源") @ResourceRouter.post("/search", summary="搜索资源", description="根据条件搜索资源")
async def search_resources_controller( async def search_resources_controller(
request: Request, request: Request,
search: ResourceSearchSchema, search: ResourceSearchSchema,
auth: AuthSchema = Depends(AuthPermission(permissions=["resource:file:search"])) auth: AuthSchema = Depends(AuthPermission(permissions=["monitor:resource:search"]))
) -> JSONResponse: ) -> JSONResponse:
"""搜索资源""" """搜索资源"""
result_list = await ResourceService.search_resources_service( result_list = await ResourceService.search_resources_service(
@@ -60,12 +58,12 @@ async def search_resources_controller(
return SuccessResponse(data=result_list, msg=f"搜索成功,找到 {len(result_list)} 个结果") return SuccessResponse(data=result_list, msg=f"搜索成功,找到 {len(result_list)} 个结果")
@ResourceFileRouter.post("/upload", summary="上传文件", description="上传文件到指定目录") @ResourceRouter.post("/upload", summary="上传文件", description="上传文件到指定目录")
async def upload_file_controller( async def upload_file_controller(
file: UploadFile, file: UploadFile,
request: Request, request: Request,
target_path: Optional[str] = Form(None, description="目标目录路径"), target_path: Optional[str] = Form(None, description="目标目录路径"),
auth: AuthSchema = Depends(AuthPermission(permissions=["resource:file:upload"])) auth: AuthSchema = Depends(AuthPermission(permissions=["monitor:resource:upload"]))
) -> JSONResponse: ) -> JSONResponse:
"""上传文件""" """上传文件"""
result_dict = await ResourceService.upload_file_service( result_dict = await ResourceService.upload_file_service(
@@ -78,11 +76,11 @@ async def upload_file_controller(
return SuccessResponse(data=result_dict, msg="上传文件成功") return SuccessResponse(data=result_dict, msg="上传文件成功")
@ResourceFileRouter.get("/download", summary="下载文件", description="下载指定文件") @ResourceRouter.get("/download", summary="下载文件", description="下载指定文件")
async def download_file_controller( async def download_file_controller(
request: Request, request: Request,
path: str = Query(..., description="文件路径"), path: str = Query(..., description="文件路径"),
auth: AuthSchema = Depends(AuthPermission(permissions=["resource:file:download"])) auth: AuthSchema = Depends(AuthPermission(permissions=["monitor:resource:download"]))
) -> FileResponse: ) -> FileResponse:
"""下载文件""" """下载文件"""
file_path = await ResourceService.download_file_service( file_path = await ResourceService.download_file_service(
@@ -103,10 +101,10 @@ async def download_file_controller(
) )
@ResourceFileRouter.delete("/delete", summary="删除文件", description="删除指定文件或目录") @ResourceRouter.delete("/delete", summary="删除文件", description="删除指定文件或目录")
async def delete_files_controller( async def delete_files_controller(
paths: List[str] = Body(..., description="文件路径列表"), paths: List[str] = Body(..., description="文件路径列表"),
auth: AuthSchema = Depends(AuthPermission(permissions=["resource:file:delete"])) auth: AuthSchema = Depends(AuthPermission(permissions=["monitor:resource:delete"]))
) -> JSONResponse: ) -> JSONResponse:
"""删除文件""" """删除文件"""
await ResourceService.delete_file_service(auth=auth, paths=paths) await ResourceService.delete_file_service(auth=auth, paths=paths)
@@ -114,10 +112,10 @@ async def delete_files_controller(
return SuccessResponse(msg="删除文件成功") return SuccessResponse(msg="删除文件成功")
@ResourceFileRouter.post("/move", summary="移动文件", description="移动文件或目录") @ResourceRouter.post("/move", summary="移动文件", description="移动文件或目录")
async def move_file_controller( async def move_file_controller(
data: ResourceMoveSchema, data: ResourceMoveSchema,
auth: AuthSchema = Depends(AuthPermission(permissions=["resource:file:move"])) auth: AuthSchema = Depends(AuthPermission(permissions=["monitor:resource:move"]))
) -> JSONResponse: ) -> JSONResponse:
"""移动文件""" """移动文件"""
await ResourceService.move_file_service(auth=auth, data=data) await ResourceService.move_file_service(auth=auth, data=data)
@@ -125,10 +123,10 @@ async def move_file_controller(
return SuccessResponse(msg="移动文件成功") return SuccessResponse(msg="移动文件成功")
@ResourceFileRouter.post("/copy", summary="复制文件", description="复制文件或目录") @ResourceRouter.post("/copy", summary="复制文件", description="复制文件或目录")
async def copy_file_controller( async def copy_file_controller(
data: ResourceCopySchema, data: ResourceCopySchema,
auth: AuthSchema = Depends(AuthPermission(permissions=["resource:file:copy"])) auth: AuthSchema = Depends(AuthPermission(permissions=["monitor:resource:copy"]))
) -> JSONResponse: ) -> JSONResponse:
"""复制文件""" """复制文件"""
await ResourceService.copy_file_service(auth=auth, data=data) await ResourceService.copy_file_service(auth=auth, data=data)
@@ -136,10 +134,10 @@ async def copy_file_controller(
return SuccessResponse(msg="复制文件成功") return SuccessResponse(msg="复制文件成功")
@ResourceFileRouter.post("/rename", summary="重命名文件", description="重命名文件或目录") @ResourceRouter.post("/rename", summary="重命名文件", description="重命名文件或目录")
async def rename_file_controller( async def rename_file_controller(
data: ResourceRenameSchema, data: ResourceRenameSchema,
auth: AuthSchema = Depends(AuthPermission(permissions=["resource:file:rename"])) auth: AuthSchema = Depends(AuthPermission(permissions=["rmonitor:resource:rename"]))
) -> JSONResponse: ) -> JSONResponse:
"""重命名文件""" """重命名文件"""
await ResourceService.rename_file_service(auth=auth, data=data) await ResourceService.rename_file_service(auth=auth, data=data)
@@ -147,10 +145,10 @@ async def rename_file_controller(
return SuccessResponse(msg="重命名文件成功") return SuccessResponse(msg="重命名文件成功")
@ResourceFileRouter.post("/create-dir", summary="创建目录", description="在指定路径创建新目录") @ResourceRouter.post("/create-dir", summary="创建目录", description="在指定路径创建新目录")
async def create_directory_controller( async def create_directory_controller(
data: ResourceCreateDirSchema, data: ResourceCreateDirSchema,
auth: AuthSchema = Depends(AuthPermission(permissions=["resource:file:create_dir"])) auth: AuthSchema = Depends(AuthPermission(permissions=["monitor:resource:create_dir"]))
) -> JSONResponse: ) -> JSONResponse:
"""创建目录""" """创建目录"""
await ResourceService.create_directory_service(auth=auth, data=data) await ResourceService.create_directory_service(auth=auth, data=data)
@@ -158,10 +156,10 @@ async def create_directory_controller(
return SuccessResponse(msg="创建目录成功") return SuccessResponse(msg="创建目录成功")
@ResourceFileRouter.get("/stats", summary="获取资源统计", description="获取资源统计信息") @ResourceRouter.get("/stats", summary="获取资源统计", description="获取资源统计信息")
async def get_resource_stats_controller( async def get_resource_stats_controller(
request: Request, request: Request,
auth: AuthSchema = Depends(AuthPermission(permissions=["resource:stats:query"])) auth: AuthSchema = Depends(AuthPermission(permissions=["monitor:resource:query"]))
) -> JSONResponse: ) -> JSONResponse:
"""获取资源统计""" """获取资源统计"""
result_dict = await ResourceService.get_stats_service( result_dict = await ResourceService.get_stats_service(
@@ -172,11 +170,11 @@ async def get_resource_stats_controller(
return SuccessResponse(data=result_dict, msg="获取资源统计成功") return SuccessResponse(data=result_dict, msg="获取资源统计成功")
@ResourceFileRouter.post("/export", summary="导出资源列表", description="导出资源列表") @ResourceRouter.post("/export", summary="导出资源列表", description="导出资源列表")
async def export_resource_list_controller( async def export_resource_list_controller(
request: Request, request: Request,
search: ResourceSearchSchema, search: ResourceSearchSchema,
auth: AuthSchema = Depends(AuthPermission(permissions=["resource:file:export"])) auth: AuthSchema = Depends(AuthPermission(permissions=["monitor:resource:export"]))
) -> StreamingResponse: ) -> StreamingResponse:
"""导出资源列表""" """导出资源列表"""
# 获取搜索结果 # 获取搜索结果
@@ -1,11 +0,0 @@
# -*- coding: utf-8 -*-
from fastapi import APIRouter
from .resource.controller import ResourceFileRouter
ResourceRouter = APIRouter(prefix="/resource")
# 包含所有子路由
ResourceRouter.include_router(ResourceFileRouter)
@@ -11,13 +11,9 @@ from .dict.controller import DictRouter
from .params.controller import ParamsRouter from .params.controller import ParamsRouter
from .notice.controller import NoticeRouter from .notice.controller import NoticeRouter
from .log.controller import LogRouter from .log.controller import LogRouter
from .version.controller import VersionRouter
from .ticket.controller import TicketRouter
SystemRouter = APIRouter(prefix="/system") SystemRouter = APIRouter(prefix="/system")
# 包含所有子路由
SystemRouter.include_router(AuthRouter) SystemRouter.include_router(AuthRouter)
SystemRouter.include_router(UserRouter) SystemRouter.include_router(UserRouter)
SystemRouter.include_router(RoleRouter) SystemRouter.include_router(RoleRouter)
@@ -28,5 +24,3 @@ SystemRouter.include_router(DictRouter)
SystemRouter.include_router(ParamsRouter) SystemRouter.include_router(ParamsRouter)
SystemRouter.include_router(NoticeRouter) SystemRouter.include_router(NoticeRouter)
SystemRouter.include_router(LogRouter) SystemRouter.include_router(LogRouter)
SystemRouter.include_router(VersionRouter)
SystemRouter.include_router(TicketRouter)
@@ -161,8 +161,6 @@ class ParamsService:
@classmethod @classmethod
async def upload_service(cls, base_url: str, file: UploadFile) -> Dict: async def upload_service(cls, base_url: str, file: UploadFile) -> Dict:
"""上传文件""" """上传文件"""
if not file:
raise CustomException(msg="请选择要上传的文件")
filename, filepath, file_url = await UploadUtil.upload_file(file=file, base_url=base_url) filename, filepath, file_url = await UploadUtil.upload_file(file=file, base_url=base_url)
return UploadResponseSchema( return UploadResponseSchema(
@@ -1,71 +0,0 @@
# -*- coding: utf-8 -*-
from fastapi import APIRouter, Body, Depends, Path
from fastapi.responses import JSONResponse
from app.common.response import SuccessResponse
from app.common.request import PaginationService
from app.core.router_class import OperationLogRoute
from app.core.base_params import PaginationQueryParam
from app.core.dependencies import AuthPermission
from app.core.logger import logger
from app.api.v1.module_system.auth.schema import AuthSchema
from .param import TicketQueryParam
from .service import TicketService
from .schema import TicketCreateSchema, TicketUpdateSchema
TicketRouter = APIRouter(route_class=OperationLogRoute, prefix="/ticket", tags=["工单管理"])
@TicketRouter.get("/detail/{id}", summary="获取工单详情", description="获取工单详情")
async def get_ticket_detail_controller(
id: int = Path(..., description="工单ID"),
auth: AuthSchema = Depends(AuthPermission(permissions=["system:ticket:query"]))
) -> JSONResponse:
result_dict = await TicketService.get_ticket_detail_service(auth=auth, id=id)
logger.info(f"获取工单详情成功 {id}")
return SuccessResponse(data=result_dict, msg="获取工单详情成功")
@TicketRouter.get("/list", summary="查询工单列表", description="查询工单列表")
async def get_ticket_list_controller(
page: PaginationQueryParam = Depends(),
search: TicketQueryParam = Depends(),
auth: AuthSchema = Depends(AuthPermission(permissions=["system:ticket:query"]))
) -> JSONResponse:
result_dict_list = await TicketService.get_ticket_list_service(auth=auth, search=search, order_by=page.order_by)
result_dict = await PaginationService.paginate(data_list=result_dict_list, page_no=page.page_no, page_size=page.page_size)
logger.info("查询工单列表成功")
return SuccessResponse(data=result_dict, msg="查询工单列表成功")
@TicketRouter.post("/create", summary="创建工单", description="创建工单")
async def create_ticket_controller(
data: TicketCreateSchema,
auth: AuthSchema = Depends(AuthPermission(permissions=["system:ticket:create"]))
) -> JSONResponse:
result_dict = await TicketService.create_ticket_service(auth=auth, data=data)
logger.info(f"创建工单成功: {result_dict}")
return SuccessResponse(data=result_dict, msg="创建工单成功")
@TicketRouter.put("/update/{id}", summary="修改工单", description="修改工单")
async def update_ticket_controller(
data: TicketUpdateSchema,
id: int = Path(..., description="工单ID"),
auth: AuthSchema = Depends(AuthPermission(permissions=["system:ticket:update"]))
) -> JSONResponse:
result_dict = await TicketService.update_ticket_service(auth=auth, id=id, data=data)
logger.info(f"修改工单成功: {result_dict}")
return SuccessResponse(data=result_dict, msg="修改工单成功")
@TicketRouter.delete("/delete", summary="删除工单", description="删除工单")
async def delete_ticket_controller(
ids: list[int] = Body(..., description="ID列表"),
auth: AuthSchema = Depends(AuthPermission(permissions=["system:ticket:delete"]))
) -> JSONResponse:
await TicketService.delete_ticket_service(auth=auth, ids=ids)
logger.info(f"删除工单成功: {ids}")
return SuccessResponse(msg="删除工单成功")
@@ -1,56 +0,0 @@
from typing import Dict, List, Optional, Sequence
from app.core.base_crud import CRUDBase
from app.api.v1.module_system.auth.schema import AuthSchema
from .model import TicketModel
from .schema import TicketCreateSchema, TicketUpdateSchema
class TicketCRUD(CRUDBase[TicketModel, TicketCreateSchema, TicketUpdateSchema]):
"""工单 CRUD 操作"""
def __init__(self, auth: AuthSchema) -> None:
"""初始化工单CRUD"""
self.auth = auth
super().__init__(model=TicketModel, auth=auth)
async def get_by_id_crud(self, id: int) -> Optional[TicketModel]:
"""根据id获取工单信息
:param id: 工单ID
:return: 工单信息
"""
return await self.get(id=id)
async def get_list_crud(self, search: Optional[Dict] = None, order_by: Optional[List[Dict[str, str]]] = None) -> Sequence[TicketModel]:
"""获取工单列表
:param search: 搜索条件
:param order_by: 排序字段
:return: 工单列表
"""
return await self.list(search=search or {}, order_by=order_by or [])
async def create_crud(self, data: TicketCreateSchema) -> Optional[TicketModel]:
"""创建工单
:param data: 工单创建数据
:return: 创建的工单
"""
return await self.create(data=data)
async def update_crud(self, id: int, data: TicketUpdateSchema) -> Optional[TicketModel]:
"""更新工单
:param id: 工单ID
:param data: 工单更新数据
:return: 更新的工单
"""
return await self.update(id=id, data=data)
async def delete_crud(self, ids: List[int]) -> None:
"""批量删除工单
:param ids: 工单ID列表
"""
return await self.delete(ids=ids)
@@ -1,30 +0,0 @@
# -*- coding: utf-8 -*-
from typing import Optional
from sqlalchemy import String, Text, DateTime, ForeignKey
from sqlalchemy.orm import Mapped, mapped_column, relationship
from datetime import datetime
from app.core.base_model import CreatorMixin
class TicketModel(CreatorMixin):
"""
工单模型 - SQLAlchemy 2.0 语法
兼容 MySQL 和 PostgreSQL
"""
__tablename__ = 'system_ticket'
__table_args__ = ({'comment': '工单表'})
title: Mapped[str] = mapped_column(String(255), nullable=False, comment='工单标题')
status: Mapped[str] = mapped_column(String(50), default='pending', comment='工单状态(pending:待处理 progress:处理中 resolved:已解决 closed:已关闭)')
priority: Mapped[str] = mapped_column(String(50), default='medium', comment='优先级(low:低, medium:中, high:高, urgent:紧急)')
type: Mapped[Optional[str]] = mapped_column(String(50), comment='工单类型(bug:缺陷, feature:功能, task:任务)')
assignee_id: Mapped[Optional[int]] = mapped_column(ForeignKey('system_users.id'), comment='指派给用户ID')
reporter_id: Mapped[Optional[int]] = mapped_column(ForeignKey('system_users.id'), comment='报告人ID')
project: Mapped[Optional[str]] = mapped_column(String(100), comment='所属项目或模块')
version: Mapped[Optional[str]] = mapped_column(String(50), comment='版本号')
# 关系
assignee = relationship('UserModel', foreign_keys=[assignee_id])
reporter = relationship('UserModel', foreign_keys=[reporter_id])
@@ -1,36 +0,0 @@
# -*- coding: utf-8 -*-
from datetime import datetime
from typing import Optional
from fastapi import Query
from app.core.validator import DateTimeStr
class TicketQueryParam:
"""工单管理查询参数"""
def __init__(
self,
title: Optional[str] = Query(None, description="工单标题"),
status: Optional[bool] = Query(None, description="工单状态"),
priority: Optional[str] = Query(None, description="优先级"),
creator: Optional[int] = Query(None, description="创建人"),
start_time: Optional[DateTimeStr] = Query(None, description="开始时间", example="2023-01-01 00:00:00"),
end_time: Optional[DateTimeStr] = Query(None, description="结束时间", example="2023-12-31 23:59:59"),
) -> None:
super().__init__()
# 模糊查询字段
self.title = ("like", title)
# 精确查询字段
self.creator_id = creator
self.status = status
self.priority = priority
# 时间范围查询
if start_time and end_time:
start_datetime = datetime.strptime(str(start_time), '%Y-%m-%d %H:%M:%S')
end_datetime = datetime.strptime(str(end_time), '%Y-%m-%d %H:%M:%S')
self.created_at = ("between", (start_datetime, end_datetime))
@@ -1,31 +0,0 @@
# -*- coding: utf-8 -*-
from typing import Optional
from pydantic import BaseModel, ConfigDict, Field
from datetime import datetime
from app.core.base_schema import BaseSchema
class TicketCreateSchema(BaseModel):
"""创建工单"""
title: str = Field(..., max_length=255, description='工单标题')
description: Optional[str] = Field(default=None, description='工单描述')
status: Optional[str] = Field(default='pending', description='工单处理状态(pending:待处理 progress:处理中 resolved:已解决 closed:已关闭)')
priority: Optional[str] = Field(default='medium', description='优先级(low, medium, high, urgent)')
type: Optional[str] = Field(default='bug', description='工单类型(bug, feature, task)')
assignee_id: Optional[int] = Field(default=None, description='指派给用户ID')
reporter_id: int = Field(..., description='报告人ID')
project: Optional[str] = Field(default=None, max_length=100, description='所属项目')
version: Optional[str] = Field(default=None, max_length=50, description='版本号')
class TicketUpdateSchema(TicketCreateSchema):
"""更新工单"""
...
class TicketOutSchema(TicketCreateSchema, BaseSchema):
"""工单输出"""
model_config = ConfigDict(from_attributes=True)
...
@@ -1,60 +0,0 @@
# -*- coding: utf-8 -*-
from typing import Dict, List, Optional, Union
from app.api.v1.module_system.auth.schema import AuthSchema
from app.core.exceptions import CustomException
from .crud import TicketCRUD
from .schema import TicketCreateSchema, TicketUpdateSchema, TicketOutSchema
from .param import TicketQueryParam
class TicketService:
"""工单模块服务层"""
@classmethod
async def get_ticket_detail_service(cls, auth: AuthSchema, id: int) -> Dict:
"""获取工单详情"""
obj = await TicketCRUD(auth).get_by_id_crud(id=id)
return TicketOutSchema.model_validate(obj).model_dump()
@classmethod
async def get_ticket_list_service(cls, auth: AuthSchema, search: Optional[TicketQueryParam] = None, order_by: Optional[Union[str, List[Dict[str, str]]]] = None) -> List[Dict]:
"""获取工单列表"""
# 处理排序参数
processed_order_by = None
if order_by:
if isinstance(order_by, str):
processed_order_by = eval(order_by)
else:
processed_order_by = order_by
obj_list = await TicketCRUD(auth).get_list_crud(search=search.__dict__ if search else None, order_by=processed_order_by)
return [TicketOutSchema.model_validate(obj).model_dump() for obj in obj_list]
@classmethod
async def create_ticket_service(cls, auth: AuthSchema, data: TicketCreateSchema) -> Dict:
"""创建工单"""
obj = await TicketCRUD(auth).create_crud(data=data)
return TicketOutSchema.model_validate(obj).model_dump()
@classmethod
async def update_ticket_service(cls, auth: AuthSchema, id: int, data: TicketUpdateSchema) -> Dict:
"""更新工单"""
# 检查工单是否存在
obj = await TicketCRUD(auth).get_by_id_crud(id=id)
if not obj:
raise CustomException(msg='更新失败,该工单不存在')
obj = await TicketCRUD(auth).update_crud(id=id, data=data)
return TicketOutSchema.model_validate(obj).model_dump()
@classmethod
async def delete_ticket_service(cls, auth: AuthSchema, ids: List[int]) -> None:
"""删除工单"""
if len(ids) < 1:
raise CustomException(msg='删除失败,删除对象不能为空')
for id in ids:
obj = await TicketCRUD(auth).get_by_id_crud(id=id)
if not obj:
raise CustomException(msg='删除失败,该工单不存在')
await TicketCRUD(auth).delete_crud(ids=ids)
@@ -1,71 +0,0 @@
# -*- coding: utf-8 -*-
from fastapi import APIRouter, Body, Depends, Path
from fastapi.responses import JSONResponse
from app.common.response import SuccessResponse
from app.common.request import PaginationService
from app.core.router_class import OperationLogRoute
from app.core.base_params import PaginationQueryParam
from app.core.dependencies import AuthPermission
from app.core.logger import logger
from app.api.v1.module_system.auth.schema import AuthSchema
from .param import VersionQueryParam
from .service import VersionService
from .schema import VersionCreateSchema, VersionUpdateSchema
VersionRouter = APIRouter(route_class=OperationLogRoute, prefix="/version", tags=["版本管理"])
@VersionRouter.get("/detail/{id}", summary="获取版本详情", description="获取版本详情")
async def get_version_detail_controller(
id: int = Path(..., description="版本ID"),
auth: AuthSchema = Depends(AuthPermission(permissions=["system:version:query"]))
) -> JSONResponse:
result_dict = await VersionService.get_version_detail_service(auth=auth, id=id)
logger.info(f"获取版本详情成功 {id}")
return SuccessResponse(data=result_dict, msg="获取版本详情成功")
@VersionRouter.get("/list", summary="查询版本列表", description="查询版本列表")
async def get_version_list_controller(
page: PaginationQueryParam = Depends(),
search: VersionQueryParam = Depends(),
auth: AuthSchema = Depends(AuthPermission(permissions=["system:version:query"]))
) -> JSONResponse:
result_dict_list = await VersionService.get_version_list_service(auth=auth, search=search, order_by=page.order_by)
result_dict = await PaginationService.paginate(data_list=result_dict_list, page_no=page.page_no, page_size=page.page_size)
logger.info("查询版本列表成功")
return SuccessResponse(data=result_dict, msg="查询版本列表成功")
@VersionRouter.post("/create", summary="创建版本", description="创建版本")
async def create_version_controller(
data: VersionCreateSchema,
auth: AuthSchema = Depends(AuthPermission(permissions=["system:version:create"]))
) -> JSONResponse:
result_dict = await VersionService.create_version_service(auth=auth, data=data)
logger.info(f"创建版本成功: {result_dict}")
return SuccessResponse(data=result_dict, msg="创建版本成功")
@VersionRouter.put("/update/{id}", summary="修改版本", description="修改版本")
async def update_version_controller(
data: VersionUpdateSchema,
id: int = Path(..., description="版本ID"),
auth: AuthSchema = Depends(AuthPermission(permissions=["system:version:update"]))
) -> JSONResponse:
result_dict = await VersionService.update_version_service(auth=auth, id=id, data=data)
logger.info(f"修改版本成功: {result_dict}")
return SuccessResponse(data=result_dict, msg="修改版本成功")
@VersionRouter.delete("/delete", summary="删除版本", description="删除版本")
async def delete_version_controller(
ids: list[int] = Body(..., description="ID列表"),
auth: AuthSchema = Depends(AuthPermission(permissions=["system:version:delete"]))
) -> JSONResponse:
await VersionService.delete_version_service(auth=auth, ids=ids)
logger.info(f"删除版本成功: {ids}")
return SuccessResponse(msg="删除版本成功")
@@ -1,96 +0,0 @@
# -*- coding: utf-8 -*-
from typing import Dict, List, Optional, Sequence
from datetime import datetime
from app.core.base_crud import CRUDBase
from app.api.v1.module_system.auth.schema import AuthSchema
from .model import VersionModel
from .schema import VersionCreateSchema, VersionUpdateSchema
class VersionCRUD(CRUDBase[VersionModel, VersionCreateSchema, VersionUpdateSchema]):
"""版本模块数据层"""
def __init__(self, auth: AuthSchema) -> None:
"""初始化版本CRUD"""
self.auth = auth
super().__init__(model=VersionModel, auth=auth)
async def get_by_id_crud(self, id: int) -> Optional[VersionModel]:
"""根据id获取版本信息
:param id: 版本ID
:return: 版本信息
"""
return await self.get(id=id)
async def get_list_crud(self, search: Optional[Dict] = None, order_by: Optional[List[Dict[str, str]]] = None) -> Sequence[VersionModel]:
"""获取版本列表
:param search: 搜索条件
:param order_by: 排序字段
:return: 版本列表
"""
return await self.list(search=search or {}, order_by=order_by or [])
async def create_crud(self, data: VersionCreateSchema) -> Optional[VersionModel]:
"""创建版本
:param data: 版本创建数据
:return: 创建的版本
"""
# 处理 released_at 字段,确保字符串转换为 datetime 对象
data_dict = data.model_dump()
if data_dict.get('released_at') and isinstance(data_dict['released_at'], str):
try:
data_dict['released_at'] = datetime.strptime(data_dict['released_at'], '%Y-%m-%d %H:%M:%S')
except ValueError:
# 如果格式不正确,让数据库层处理错误
pass
return await self.create(data=data_dict)
async def update_crud(self, id: int, data: VersionUpdateSchema) -> Optional[VersionModel]:
"""更新版本
:param id: 版本ID
:param data: 版本更新数据
:return: 更新的版本
"""
# 处理 released_at 字段,确保字符串转换为 datetime 对象
data_dict = data.model_dump(exclude_unset=True, exclude={"id"})
if data_dict.get('released_at') and isinstance(data_dict['released_at'], str):
try:
data_dict['released_at'] = datetime.strptime(data_dict['released_at'], '%Y-%m-%d %H:%M:%S')
except ValueError:
# 如果格式不正确,让数据库层处理错误
pass
return await self.update(id=id, data=data_dict)
async def delete_crud(self, ids: List[int]) -> None:
"""批量删除版本
:param ids: 版本ID列表
"""
return await self.delete(ids=ids)
async def get_by_number_crud(self, version_number: str) -> Optional[VersionModel]:
"""根据版本号获取版本
:param version_number: 版本号
:return: 版本信息
"""
return await self.get(version_number=version_number)
async def get_by_status_crud(self, search: Optional[Dict] = None, order_by: Optional[List[Dict[str, str]]] = None) -> Sequence[VersionModel]:
"""根据状态获取版本列表
:param search: 搜索条件
:param order_by: 排序字段
:return: 版本列表
"""
if search is None:
search = {}
return await self.list(search=search, order_by=order_by or [])
@@ -1,24 +0,0 @@
# -*- coding: utf-8 -*-
from typing import Optional
from sqlalchemy import String, Text, DateTime
from sqlalchemy.orm import Mapped, mapped_column
from datetime import datetime
from app.core.base_model import CreatorMixin
class VersionModel(CreatorMixin):
"""
版本模型 - SQLAlchemy 2.0 语法
兼容 MySQL 和 PostgreSQL
"""
__tablename__ = 'system_version'
__table_args__ = ({'comment': '版本表'})
version_number: Mapped[str] = mapped_column(String(50), nullable=False, comment='版本号')
title: Mapped[str] = mapped_column(String(255), nullable=False, comment='版本标题')
release_notes: Mapped[Optional[str]] = mapped_column(Text, comment='发布说明')
status: Mapped[str] = mapped_column(String(50), default='draft', comment='版本状态(draft: 草稿, released: 已发布, archived: 已归档)')
project: Mapped[Optional[str]] = mapped_column(String(100), comment='所属项目')
released_at: Mapped[Optional[datetime]] = mapped_column(DateTime, comment='发布时间')
@@ -1,34 +0,0 @@
# -*- coding: utf-8 -*-
from datetime import datetime
from typing import Optional
from fastapi import Query
from app.core.validator import DateTimeStr
class VersionQueryParam:
"""版本管理查询参数"""
def __init__(
self,
title: Optional[str] = Query(None, description="版本标题"),
status: Optional[str] = Query(None, description="版本状态"),
creator: Optional[int] = Query(None, description="创建人"),
start_time: Optional[DateTimeStr] = Query(None, description="开始时间", example="2023-01-01 00:00:00"),
end_time: Optional[DateTimeStr] = Query(None, description="结束时间", example="2023-12-31 23:59:59"),
) -> None:
super().__init__()
# 模糊查询字段
self.title = ("like", title)
# 精确查询字段
self.creator_id = creator
self.status = status
# 时间范围查询
if start_time and end_time:
start_datetime = datetime.strptime(str(start_time), '%Y-%m-%d %H:%M:%S')
end_datetime = datetime.strptime(str(end_time), '%Y-%m-%d %H:%M:%S')
self.created_at = ("between", (start_datetime, end_datetime))
@@ -1,30 +0,0 @@
# -*- coding: utf-8 -*-
from typing import Optional
from pydantic import BaseModel, ConfigDict, Field
from datetime import datetime
from app.core.base_schema import BaseSchema
from app.core.validator import DateTimeStr
class VersionCreateSchema(BaseModel):
"""创建版本"""
version_number: str = Field(..., max_length=50, description='版本号')
title: str = Field(..., max_length=255, description='版本标题')
release_notes: Optional[str] = Field(default=None, description='发布说明')
description: Optional[str] = Field(default=None, description='工单描述')
status: Optional[str] = Field(default='draft', description='版本状态(draft: 草稿, released: 已发布, archived: 已归档)')
project: Optional[str] = Field(default=None, max_length=100, description='所属项目')
released_at: Optional[DateTimeStr] = Field(default=None, description='发布时间')
class VersionUpdateSchema(VersionCreateSchema):
"""更新版本"""
...
class VersionOutSchema(VersionCreateSchema, BaseSchema):
"""版本输出"""
model_config = ConfigDict(from_attributes=True)
...
@@ -1,87 +0,0 @@
# -*- coding: utf-8 -*-
from typing import Dict, List, Optional, Union
from app.api.v1.module_system.auth.schema import AuthSchema
from app.core.exceptions import CustomException
from .crud import VersionCRUD
from .schema import VersionCreateSchema, VersionUpdateSchema, VersionOutSchema
from .param import VersionQueryParam
class VersionService:
"""版本模块服务层"""
@classmethod
async def get_version_detail_service(cls, auth: AuthSchema, id: int) -> Dict:
"""获取版本详情"""
obj = await VersionCRUD(auth).get_by_id_crud(id=id)
return VersionOutSchema.model_validate(obj).model_dump()
@classmethod
async def get_version_list_service(cls, auth: AuthSchema, search: Optional[VersionQueryParam] = None, order_by: Optional[Union[str, List[Dict[str, str]]]] = None) -> List[Dict]:
"""获取版本列表"""
# 处理排序参数
processed_order_by = None
if order_by:
if isinstance(order_by, str):
processed_order_by = eval(order_by)
else:
processed_order_by = order_by
obj_list = await VersionCRUD(auth).get_list_crud(search=search.__dict__ if search else None, order_by=processed_order_by)
return [VersionOutSchema.model_validate(obj).model_dump() for obj in obj_list]
@classmethod
async def create_version_service(cls, auth: AuthSchema, data: VersionCreateSchema) -> Dict:
"""创建版本"""
# 检查版本号是否已存在
exist_obj = await VersionCRUD(auth).get_by_number_crud(version_number=data.version_number)
if exist_obj:
raise CustomException(msg='创建失败,版本号已存在')
obj = await VersionCRUD(auth).create_crud(data=data)
return VersionOutSchema.model_validate(obj).model_dump()
@classmethod
async def update_version_service(cls, auth: AuthSchema, id: int, data: VersionUpdateSchema) -> Dict:
"""更新版本"""
# 检查版本是否存在
obj = await VersionCRUD(auth).get_by_id_crud(id=id)
if not obj:
raise CustomException(msg='更新失败,该版本不存在')
# 检查版本号是否重复(如果提供了版本号)
if data.version_number:
exist_obj = await VersionCRUD(auth).get_by_number_crud(version_number=data.version_number)
if exist_obj and exist_obj.id != id:
raise CustomException(msg='更新失败,版本号已存在')
obj = await VersionCRUD(auth).update_crud(id=id, data=data)
return VersionOutSchema.model_validate(obj).model_dump()
@classmethod
async def delete_version_service(cls, auth: AuthSchema, ids: List[int]) -> None:
"""删除版本"""
if len(ids) < 1:
raise CustomException(msg='删除失败,删除对象不能为空')
for id in ids:
obj = await VersionCRUD(auth).get_by_id_crud(id=id)
if not obj:
raise CustomException(msg='删除失败,该版本不存在')
await VersionCRUD(auth).delete_crud(ids=ids)
@classmethod
async def get_version_by_status_service(cls, auth: AuthSchema, search: Optional[VersionQueryParam] = None, order_by: Optional[Union[str, List[Dict[str, str]]]] = None) -> List[Dict]:
"""根据状态获取版本列表"""
# 处理排序参数
processed_order_by = None
if order_by:
if isinstance(order_by, str):
processed_order_by = eval(order_by)
else:
processed_order_by = order_by
obj_list = await VersionCRUD(auth).get_by_status_crud(
search=search.__dict__ if search else None,
order_by=processed_order_by
)
return [VersionOutSchema.model_validate(obj).model_dump() for obj in obj_list]
+9 -10
View File
@@ -3,8 +3,7 @@
import os import os
from functools import lru_cache from functools import lru_cache
from pathlib import Path from pathlib import Path
from typing import Any, ClassVar, Dict, List, Optional, Union, Literal from typing import Any, ClassVar, Dict, List, Optional, Literal
from pydantic import MongoDsn, PostgresDsn, RedisDsn, MySQLDsn
from pydantic_settings import BaseSettings, SettingsConfigDict from pydantic_settings import BaseSettings, SettingsConfigDict
from pydantic_validation_decorator import Pattern from pydantic_validation_decorator import Pattern
from uvicorn.config import LifespanType from uvicorn.config import LifespanType
@@ -161,11 +160,6 @@ class Settings(BaseSettings):
IGNORE_OPERATION_FUNCTION: List[str] = ["get_captcha_for_login"] # 忽略记录的函数 IGNORE_OPERATION_FUNCTION: List[str] = ["get_captcha_for_login"] # 忽略记录的函数
LOG_RETENTION_DAYS: int = 30 # 日志保留天数,超过此天数的日志文件将被自动清理 LOG_RETENTION_DAYS: int = 30 # 日志保留天数,超过此天数的日志文件将被自动清理
# 日志文件说明:
# all.log - 包含所有级别日志(应用日志 + uvicorn日志 + uvicorn访问日志)
# error.log - 包含ERROR及以上级别日志(应用错误 + uvicorn错误)
# 轮转后文件名: info_日期.log, error_日期.log
# ================================================= # # ================================================= #
# ******************* Gzip压缩配置 ******************* # # ******************* Gzip压缩配置 ******************* #
# ================================================= # # ================================================= #
@@ -204,10 +198,16 @@ class Settings(BaseSettings):
STATIC_DIR: str = "static" # 目录名 STATIC_DIR: str = "static" # 目录名
STATIC_ROOT: Path = BASE_DIR.joinpath(STATIC_DIR) # 绝对路径 STATIC_ROOT: Path = BASE_DIR.joinpath(STATIC_DIR) # 绝对路径
# ================================================= #
# ***************** 模版文件配置 ***************** #
# ================================================= #
TEMPLATE: str = "templates"
TEMPLATE_DIR: Path = BASE_DIR.joinpath(TEMPLATE) # 模版目录
# ================================================= # # ================================================= #
# ***************** 动态文件配置 ***************** # # ***************** 动态文件配置 ***************** #
# ================================================= # # ================================================= #
UPLOAD_FILE_PATH: Path = 'static/upload' # 上传目录 UPLOAD_FILE_PATH: Path = BASE_DIR.joinpath('static/upload') # 上传目录
UPLOAD_MACHINE: str = 'A' # 上传机器标识 UPLOAD_MACHINE: str = 'A' # 上传机器标识
ALLOWED_EXTENSIONS: list[str] = [ # 允许的文件类型 ALLOWED_EXTENSIONS: list[str] = [ # 允许的文件类型
# 图片 # 图片
@@ -280,8 +280,7 @@ class Settings(BaseSettings):
@property @property
def EVENT_LIST(self) -> List[Optional[str]]: def EVENT_LIST(self) -> List[Optional[str]]:
"""获取事件列表"""
# 事件列表
EVENTS: List[Optional[str]] = [ EVENTS: List[Optional[str]] = [
"app.core.database.mongodb_connect" if self.MONGO_DB_ENABLE else None, "app.core.database.mongodb_connect" if self.MONGO_DB_ENABLE else None,
"app.core.database.redis_connect" if self.REDIS_ENABLE else None, "app.core.database.redis_connect" if self.REDIS_ENABLE else None,
+4
View File
@@ -27,8 +27,12 @@ class BaseSchema(BaseModel):
model_config = ConfigDict(from_attributes=True) model_config = ConfigDict(from_attributes=True)
id: int = Field(description="主键ID") id: int = Field(description="主键ID")
description: Optional[str] = Field(default=None, description="描述")
del_flag: str = Field(description="删除标志")
created_at: DateTimeStr = Field(description="创建时间") created_at: DateTimeStr = Field(description="创建时间")
create_by: Optional[str] = Field(default=None, description="创建者")
updated_at: DateTimeStr = Field(description="更新时间") updated_at: DateTimeStr = Field(description="更新时间")
update_by: Optional[str] = Field(default=None, description="更新者")
creator_id: Optional[int] = Field(default=None, description="创建人ID") creator_id: Optional[int] = Field(default=None, description="创建人ID")
creator: Optional[UserInfoSchema] = Field(default=None, description="创建人信息") creator: Optional[UserInfoSchema] = Field(default=None, description="创建人信息")
+211 -388
View File
@@ -1177,162 +1177,6 @@
"description": "初始化数据" "description": "初始化数据"
} }
] ]
},
{
"name": "版本管理",
"type": 2,
"icon": "el-icon-Guide",
"order": 10,
"permission": "system:version:query",
"route_name": "Version",
"route_path": "/system/version",
"component_path": "system/version/index",
"status": true,
"keep_alive": true,
"hidden": false,
"always_show": false,
"title": "版本管理",
"params": null,
"affix": false,
"redirect": null,
"description": "初始化数据",
"children": [
{
"name": "创建版本",
"type": 3,
"icon": null,
"order": 1,
"permission": "system:version:create",
"route_name": null,
"route_path": null,
"component_path": null,
"status": true,
"keep_alive": true,
"hidden": false,
"always_show": false,
"title": "创建版本",
"params": null,
"affix": false,
"redirect": null,
"description": "初始化数据"
},
{
"name": "修改版本",
"type": 3,
"icon": null,
"order": 2,
"permission": "system:version:update",
"route_name": null,
"route_path": null,
"component_path": null,
"status": true,
"keep_alive": true,
"hidden": false,
"always_show": false,
"title": "修改版本",
"params": null,
"affix": false,
"redirect": null,
"description": "初始化数据"
},
{
"name": "删除版本",
"type": 3,
"icon": null,
"order": 3,
"permission": "system:version:delete",
"route_name": null,
"route_path": null,
"component_path": null,
"status": true,
"keep_alive": true,
"hidden": false,
"always_show": false,
"title": "删除版本",
"params": null,
"affix": false,
"redirect": null,
"description": "初始化数据"
}
]
},
{
"name": "工单管理",
"type": 2,
"icon": "el-icon-Help",
"order": 11,
"permission": "system:ticket:query",
"route_name": "Ticket",
"route_path": "/system/ticket",
"component_path": "system/ticket/index",
"status": true,
"keep_alive": true,
"hidden": false,
"always_show": false,
"title": "工单管理",
"params": null,
"affix": false,
"redirect": null,
"description": "初始化数据",
"children": [
{
"name": "创建工单",
"type": 3,
"icon": null,
"order": 1,
"permission": "system:ticket:create",
"route_name": null,
"route_path": null,
"component_path": null,
"status": true,
"keep_alive": true,
"hidden": false,
"always_show": false,
"title": "创建工单",
"params": null,
"affix": false,
"redirect": null,
"description": "初始化数据"
},
{
"name": "修改工单",
"type": 3,
"icon": null,
"order": 2,
"permission": "system:ticket:update",
"route_name": null,
"route_path": null,
"component_path": null,
"status": true,
"keep_alive": true,
"hidden": false,
"always_show": false,
"title": "修改工单",
"params": null,
"affix": false,
"redirect": null,
"description": "初始化数据"
},
{
"name": "删除工单",
"type": 3,
"icon": null,
"order": 3,
"permission": "system:ticket:delete",
"route_name": null,
"route_path": null,
"component_path": null,
"status": true,
"keep_alive": true,
"hidden": false,
"always_show": false,
"title": "删除工单",
"params": null,
"affix": false,
"redirect": null,
"description": "初始化数据"
}
]
} }
] ]
}, },
@@ -1550,6 +1394,217 @@
"description": "初始化数据" "description": "初始化数据"
} }
] ]
},
{
"name": "文件管理",
"type": 2,
"icon": "el-icon-Files",
"order": 1,
"permission": "monitor:resource:query",
"route_name": "Resource",
"route_path": "/monitor/resource",
"component_path": "monitor/resource/index",
"status": true,
"keep_alive": true,
"hidden": false,
"always_show": false,
"title": "文件管理",
"params": null,
"affix": false,
"redirect": null,
"description": "初始化数据",
"children": [
{
"name": "文件上传",
"type": 3,
"icon": null,
"order": 1,
"permission": "monitor:resource:upload",
"route_name": null,
"route_path": null,
"component_path": null,
"status": true,
"keep_alive": true,
"hidden": false,
"always_show": false,
"title": "文件上传",
"params": null,
"affix": false,
"redirect": null,
"description": "初始化数据"
},
{
"name": "文件下载",
"type": 3,
"icon": null,
"order": 2,
"permission": "monitor:resource:download",
"route_name": null,
"route_path": null,
"component_path": null,
"status": true,
"keep_alive": true,
"hidden": false,
"always_show": false,
"title": "文件下载",
"params": null,
"affix": false,
"redirect": null,
"description": "初始化数据"
},
{
"name": "文件删除",
"type": 3,
"icon": null,
"order": 3,
"permission": "monitor:resource:delete",
"route_name": null,
"route_path": null,
"component_path": null,
"status": true,
"keep_alive": true,
"hidden": false,
"always_show": false,
"title": "文件删除",
"params": null,
"affix": false,
"redirect": null,
"description": "初始化数据"
},
{
"name": "文件移动",
"type": 3,
"icon": null,
"order": 4,
"permission": "monitor:resource:move",
"route_name": null,
"route_path": null,
"component_path": null,
"status": true,
"keep_alive": true,
"hidden": false,
"always_show": false,
"title": "文件移动",
"params": null,
"affix": false,
"redirect": null,
"description": "初始化数据"
},
{
"name": "文件复制",
"type": 3,
"icon": null,
"order": 5,
"permission": "rmonitor:resource:copy",
"route_name": null,
"route_path": null,
"component_path": null,
"status": true,
"keep_alive": true,
"hidden": false,
"always_show": false,
"title": "文件复制",
"params": null,
"affix": false,
"redirect": null,
"description": "初始化数据"
},
{
"name": "文件重命名",
"type": 3,
"icon": null,
"order": 6,
"permission": "monitor:resource:rename",
"route_name": null,
"route_path": null,
"component_path": null,
"status": true,
"keep_alive": true,
"hidden": false,
"always_show": false,
"title": "文件重命名",
"params": null,
"affix": false,
"redirect": null,
"description": "初始化数据"
},
{
"name": "创建目录",
"type": 3,
"icon": null,
"order": 7,
"permission": "monitor:resource:create_dir",
"route_name": null,
"route_path": null,
"component_path": null,
"status": true,
"keep_alive": true,
"hidden": false,
"always_show": false,
"title": "创建目录",
"params": null,
"affix": false,
"redirect": null,
"description": "初始化数据"
},
{
"name": "文件搜索",
"type": 3,
"icon": null,
"order": 8,
"permission": "monitor:resource:search",
"route_name": null,
"route_path": null,
"component_path": null,
"status": true,
"keep_alive": true,
"hidden": false,
"always_show": false,
"title": "文件搜索",
"params": null,
"affix": false,
"redirect": null,
"description": "初始化数据"
},
{
"name": "导出文件列表",
"type": 3,
"icon": null,
"order": 9,
"permission": "rmonitor:resource:export",
"route_name": null,
"route_path": null,
"component_path": null,
"status": true,
"keep_alive": true,
"hidden": false,
"always_show": false,
"title": "导出文件列表",
"params": null,
"affix": false,
"redirect": null,
"description": "初始化数据"
}
]
},
{
"name": "存储监控",
"type": 2,
"icon": "el-icon-PieChart",
"order": 2,
"permission": "monitor:stats:query",
"route_name": "Stats",
"route_path": "/monitor/stats",
"component_path": "monitor/stats/index",
"status": true,
"keep_alive": true,
"hidden": false,
"always_show": false,
"title": "存储监控",
"params": null,
"affix": false,
"redirect": null,
"description": "初始化数据"
} }
] ]
}, },
@@ -1905,238 +1960,6 @@
} }
] ]
}, },
{
"name": "存储管理",
"type": 1,
"icon": "el-icon-Coin",
"order": 7,
"permission": null,
"route_name": "Resource",
"route_path": "/resource",
"component_path": null,
"status": true,
"keep_alive": false,
"hidden": false,
"always_show": false,
"title": "存储管理",
"params": null,
"affix": false,
"redirect": "/resource/file",
"description": "初始化数据",
"children": [
{
"name": "文件管理",
"type": 2,
"icon": "el-icon-Files",
"order": 1,
"permission": "resource:file:query",
"route_name": "ResourceFile",
"route_path": "/resource/file",
"component_path": "resource/file/index",
"status": true,
"keep_alive": true,
"hidden": false,
"always_show": false,
"title": "文件管理",
"params": null,
"affix": false,
"redirect": null,
"description": "初始化数据",
"children": [
{
"name": "文件上传",
"type": 3,
"icon": null,
"order": 1,
"permission": "resource:file:upload",
"route_name": null,
"route_path": null,
"component_path": null,
"status": true,
"keep_alive": true,
"hidden": false,
"always_show": false,
"title": "文件上传",
"params": null,
"affix": false,
"redirect": null,
"description": "初始化数据"
},
{
"name": "文件下载",
"type": 3,
"icon": null,
"order": 2,
"permission": "resource:file:download",
"route_name": null,
"route_path": null,
"component_path": null,
"status": true,
"keep_alive": true,
"hidden": false,
"always_show": false,
"title": "文件下载",
"params": null,
"affix": false,
"redirect": null,
"description": "初始化数据"
},
{
"name": "文件删除",
"type": 3,
"icon": null,
"order": 3,
"permission": "resource:file:delete",
"route_name": null,
"route_path": null,
"component_path": null,
"status": true,
"keep_alive": true,
"hidden": false,
"always_show": false,
"title": "文件删除",
"params": null,
"affix": false,
"redirect": null,
"description": "初始化数据"
},
{
"name": "文件移动",
"type": 3,
"icon": null,
"order": 4,
"permission": "resource:file:move",
"route_name": null,
"route_path": null,
"component_path": null,
"status": true,
"keep_alive": true,
"hidden": false,
"always_show": false,
"title": "文件移动",
"params": null,
"affix": false,
"redirect": null,
"description": "初始化数据"
},
{
"name": "文件复制",
"type": 3,
"icon": null,
"order": 5,
"permission": "resource:file:copy",
"route_name": null,
"route_path": null,
"component_path": null,
"status": true,
"keep_alive": true,
"hidden": false,
"always_show": false,
"title": "文件复制",
"params": null,
"affix": false,
"redirect": null,
"description": "初始化数据"
},
{
"name": "文件重命名",
"type": 3,
"icon": null,
"order": 6,
"permission": "resource:file:rename",
"route_name": null,
"route_path": null,
"component_path": null,
"status": true,
"keep_alive": true,
"hidden": false,
"always_show": false,
"title": "文件重命名",
"params": null,
"affix": false,
"redirect": null,
"description": "初始化数据"
},
{
"name": "创建目录",
"type": 3,
"icon": null,
"order": 7,
"permission": "resource:file:create_dir",
"route_name": null,
"route_path": null,
"component_path": null,
"status": true,
"keep_alive": true,
"hidden": false,
"always_show": false,
"title": "创建目录",
"params": null,
"affix": false,
"redirect": null,
"description": "初始化数据"
},
{
"name": "文件搜索",
"type": 3,
"icon": null,
"order": 8,
"permission": "resource:file:search",
"route_name": null,
"route_path": null,
"component_path": null,
"status": true,
"keep_alive": true,
"hidden": false,
"always_show": false,
"title": "文件搜索",
"params": null,
"affix": false,
"redirect": null,
"description": "初始化数据"
},
{
"name": "导出文件列表",
"type": 3,
"icon": null,
"order": 9,
"permission": "resource:file:export",
"route_name": null,
"route_path": null,
"component_path": null,
"status": true,
"keep_alive": true,
"hidden": false,
"always_show": false,
"title": "导出文件列表",
"params": null,
"affix": false,
"redirect": null,
"description": "初始化数据"
}
]
},
{
"name": "存储监控",
"type": 2,
"icon": "el-icon-PieChart",
"order": 2,
"permission": "resource:stats:query",
"route_name": "Stats",
"route_path": "/resource/stats",
"component_path": "resource/stats/index",
"status": true,
"keep_alive": true,
"hidden": false,
"always_show": false,
"title": "存储监控",
"params": null,
"affix": false,
"redirect": null,
"description": "初始化数据"
}
]
},
{ {
"name": "AI大模型", "name": "AI大模型",
"type": 1, "type": 1,
@@ -434,41 +434,5 @@
{ {
"role_id": 1, "role_id": 1,
"menu_id": 109 "menu_id": 109
},
{
"role_id": 1,
"menu_id": 110
},
{
"role_id": 1,
"menu_id": 111
},
{
"role_id": 1,
"menu_id": 112
},
{
"role_id": 1,
"menu_id": 113
},
{
"role_id": 1,
"menu_id": 114
},
{
"role_id": 1,
"menu_id": 115
},
{
"role_id": 1,
"menu_id": 116
},
{
"role_id": 1,
"menu_id": 117
},
{
"role_id": 1,
"menu_id": 118
} }
] ]
+1 -1
View File
@@ -4,7 +4,7 @@ from typing import List
from app.common.constant import GenConstant from app.common.constant import GenConstant
from app.config.setting import settings from app.config.setting import settings
from app.api.v1.module_generator.gencode.schema import GenTableColumnSchema, GenTableSchema from app.api.v1.module_generator.gencode.schema import GenTableColumnOutSchema as GenTableColumnSchema, GenTableOutSchema as GenTableSchema
from .string_util import StringUtil from .string_util import StringUtil
+3 -3
View File
@@ -5,7 +5,7 @@ from jinja2 import Environment, FileSystemLoader
from typing import Dict, List, Set from typing import Dict, List, Set
from app.common.constant import GenConstant from app.common.constant import GenConstant
from app.config.setting import settings from app.config.setting import settings
from app.api.v1.module_generator.gencode.schema import GenTableSchema, GenTableColumnSchema from app.api.v1.module_generator.gencode.schema import GenTableOutSchema as GenTableSchema, GenTableColumnOutSchema as GenTableColumnSchema
from app.core.base_model import CamelCaseUtil, SnakeCaseUtil from app.core.base_model import CamelCaseUtil, SnakeCaseUtil
from app.core.exceptions import CustomException from app.core.exceptions import CustomException
from .string_util import StringUtil from .string_util import StringUtil
@@ -25,8 +25,8 @@ class TemplateInitializer:
""" """
try: try:
# 修复模板路径,使用正确的相对路径 # 修复模板路径,使用正确的相对路径
template_dir = os.path.join(os.path.dirname(__file__), '..', 'api', 'v1', 'module_generator', 'gencode', 'templates')
template_dir = os.path.abspath(template_dir) template_dir = settings.TEMPLATE_DIR
env = Environment( env = Environment(
loader=FileSystemLoader(template_dir), loader=FileSystemLoader(template_dir),
keep_trailing_newline=True, keep_trailing_newline=True,
-3
View File
@@ -19,8 +19,5 @@ VITE_APP_PORT=5180
# 超时时间 # 超时时间
VITE_TIMEOUT=10000 VITE_TIMEOUT=10000
# 资源管理根路径
VITE_RESOURCE_ROOT_PATH=/home/static
# WebSocket 端点(不配置则关闭) # WebSocket 端点(不配置则关闭)
VITE_APP_WS_ENDPOINT=ws://127.0.0.1:8001 VITE_APP_WS_ENDPOINT=ws://127.0.0.1:8001
-3
View File
@@ -16,8 +16,5 @@ VITE_APP_PORT=5180
# 超时时间 # 超时时间
VITE_TIMEOUT=10000 VITE_TIMEOUT=10000
# 资源管理根路径
VITE_RESOURCE_ROOT_PATH=/home/static
# WebSocket 端点(不配置则关闭) # WebSocket 端点(不配置则关闭)
VITE_APP_WS_ENDPOINT=wss://service.fastapiadmin.com VITE_APP_WS_ENDPOINT=wss://service.fastapiadmin.com
@@ -7,7 +7,7 @@ export const ResourceAPI = {
*/ */
getResourceList(query: ResourceListQuery) { getResourceList(query: ResourceListQuery) {
return request<ApiResponse<ResourceListResponse>>({ return request<ApiResponse<ResourceListResponse>>({
url: `/resource/resource/list`, url: `/monitor/resource/list`,
method: "get", method: "get",
params: query, params: query,
}); });
@@ -19,7 +19,7 @@ export const ResourceAPI = {
*/ */
searchResource(body: ResourceSearchQuery) { searchResource(body: ResourceSearchQuery) {
return request<ApiResponse<ResourceListResponse>>({ return request<ApiResponse<ResourceListResponse>>({
url: `/resource/resource/search`, url: `/monitor/resource/search`,
method: "post", method: "post",
data: body, data: body,
}); });
@@ -31,7 +31,7 @@ export const ResourceAPI = {
*/ */
uploadFile(formData: FormData) { uploadFile(formData: FormData) {
return request<ApiResponse<UploadFilePath>>({ return request<ApiResponse<UploadFilePath>>({
url: `/resource/resource/upload`, url: `/monitor/resource/upload`,
method: "post", method: "post",
data: formData, data: formData,
headers: { "Content-Type": "multipart/form-data" }, headers: { "Content-Type": "multipart/form-data" },
@@ -44,7 +44,7 @@ export const ResourceAPI = {
*/ */
downloadFile(path: string) { downloadFile(path: string) {
return request<Blob>({ return request<Blob>({
url: `/resource/resource/download`, url: `/monitor/resource/download`,
method: "get", method: "get",
params: { path }, params: { path },
responseType: "blob", responseType: "blob",
@@ -57,7 +57,7 @@ export const ResourceAPI = {
*/ */
deleteResource(body: string[]) { deleteResource(body: string[]) {
return request<ApiResponse>({ return request<ApiResponse>({
url: `/resource/resource/delete`, url: `/monitor/resource/delete`,
method: "delete", method: "delete",
data: body, data: body,
}); });
@@ -69,7 +69,7 @@ export const ResourceAPI = {
*/ */
moveResource(body: ResourceMoveQuery) { moveResource(body: ResourceMoveQuery) {
return request<ApiResponse>({ return request<ApiResponse>({
url: `/resource/resource/move`, url: `/monitor/resource/move`,
method: "post", method: "post",
data: body, data: body,
}); });
@@ -81,7 +81,7 @@ export const ResourceAPI = {
*/ */
copyResource(body: ResourceCopyQuery) { copyResource(body: ResourceCopyQuery) {
return request<ApiResponse>({ return request<ApiResponse>({
url: `/resource/resource/copy`, url: `/monitor/resource/copy`,
method: "post", method: "post",
data: body, data: body,
}); });
@@ -93,7 +93,7 @@ export const ResourceAPI = {
*/ */
renameResource(body: ResourceRenameQuery) { renameResource(body: ResourceRenameQuery) {
return request<ApiResponse>({ return request<ApiResponse>({
url: `/resource/resource/rename`, url: `/monitor/resource/rename`,
method: "post", method: "post",
data: body, data: body,
}); });
@@ -105,7 +105,7 @@ export const ResourceAPI = {
*/ */
createDirectory(body: ResourceCreateDirQuery) { createDirectory(body: ResourceCreateDirQuery) {
return request<ApiResponse>({ return request<ApiResponse>({
url: `/resource/resource/create-dir`, url: `/monitor/resource/create-dir`,
method: "post", method: "post",
data: body, data: body,
}); });
@@ -116,7 +116,7 @@ export const ResourceAPI = {
*/ */
getResourceStats() { getResourceStats() {
return request<ApiResponse<ResourceStats>>({ return request<ApiResponse<ResourceStats>>({
url: `/resource/resource/stats`, url: `/monitor/resource/stats`,
method: "get", method: "get",
}); });
}, },
@@ -127,7 +127,7 @@ export const ResourceAPI = {
*/ */
exportResource(body: ResourceSearchQuery) { exportResource(body: ResourceSearchQuery) {
return request<Blob>({ return request<Blob>({
url: `/resource/resource/export`, url: `/monitor/resource/export`,
method: "post", method: "post",
data: body, data: body,
responseType: "blob", responseType: "blob",
-83
View File
@@ -1,83 +0,0 @@
import request from "@/utils/request";
const TicketAPI = {
getTicketList(query: TicketPageQuery) {
return request<ApiResponse<PageResult<TicketTable[]>>>({
url: `/system/ticket/list`,
method: "get",
params: query,
});
},
getTicketDetail(query: number) {
return request<ApiResponse<TicketTable>>({
url: `/system/ticket/detail/${query}`,
method: "get",
});
},
createTicket(body: TicketForm) {
return request<ApiResponse>({
url: `/system/ticket/create`,
method: "post",
data: body,
});
},
updateTicket(id: number, body: TicketForm) {
return request<ApiResponse>({
url: `/system/ticket/update/${id}`,
method: "put",
data: body,
});
},
deleteTicket(body: number[]) {
return request<ApiResponse>({
url: `/system/ticket/delete`,
method: "delete",
data: body,
});
},
};
export default TicketAPI;
export interface TicketPageQuery extends PageQuery {
title?: string;
status?: string;
priority?: string;
start_time?: string;
end_time?: string;
}
export interface TicketTable {
index?: number;
id?: number;
title?: string;
priority?: string;
type?: string;
status?: string;
description?: string;
assignee?: creatorType;
reporter?: creatorType;
project?: string;
version?: string;
created_at?: string;
updated_at?: string;
creator?: creatorType;
}
export interface TicketForm {
id?: number;
title: string;
priority?: string;
type?: string;
status?: string;
description?: string;
assignee_id?: number;
reporter_id: number;
project?: string;
version?: string;
}
-78
View File
@@ -1,78 +0,0 @@
import request from "@/utils/request";
const VersionAPI = {
getVersionList(query: VersionPageQuery) {
return request<ApiResponse<PageResult<VersionTable[]>>>({
url: `/system/version/list`,
method: "get",
params: query,
});
},
getVersionDetail(query: number) {
return request<ApiResponse<VersionTable>>({
url: `/system/version/detail/${query}`,
method: "get",
});
},
createVersion(body: VersionForm) {
return request<ApiResponse>({
url: `/system/version/create`,
method: "post",
data: body,
});
},
updateVersion(id: number, body: VersionForm) {
return request<ApiResponse>({
url: `/system/version/update/${id}`,
method: "put",
data: body,
});
},
deleteVersion(body: number[]) {
return request<ApiResponse>({
url: `/system/version/delete`,
method: "delete",
data: body,
});
},
};
export default VersionAPI;
export interface VersionPageQuery extends PageQuery {
title?: string;
status?: string;
start_time?: string;
end_time?: string;
}
export interface VersionTable {
index?: number;
id?: number;
version_number?: string;
title?: string;
release_notes?: string;
project?: string;
status?: string;
released_at?: string;
description?: string;
created_at?: string;
updated_at?: string;
creator?: creatorType;
}
export interface VersionForm {
id?: number;
version_number: string;
title: string;
release_notes?: string;
project?: string;
status?: string;
released_at?: string;
description?: string;
}
-3
View File
@@ -1,7 +1,4 @@
export const ROLE_ROOT = "admin"; export const ROLE_ROOT = "admin";
// 资源管理根路径
export const RESOURCE_ROOT_PATH = import.meta.env.VITE_RESOURCE_ROOT_PATH || "/home/static";
// 🔗 导出所有存储键常量 // 🔗 导出所有存储键常量
export * from "./storage-keys"; export * from "./storage-keys";
-3
View File
@@ -20,9 +20,6 @@ interface ImportMetaEnv {
/** 超时时间 */ /** 超时时间 */
VITE_TIMEOUT: number; VITE_TIMEOUT: number;
/** 资源管理根路径 */
VITE_RESOURCE_ROOT_PATH: string;
/** ws 端点 */ /** ws 端点 */
VITE_APP_WS_ENDPOINT: string; VITE_APP_WS_ENDPOINT: string;
} }
@@ -96,8 +96,8 @@
<el-breadcrumb-item <el-breadcrumb-item
v-for="(item, index) in breadcrumbList" v-for="(item, index) in breadcrumbList"
:key="index" :key="index"
@click="handleBreadcrumbClick(item, index)"
:class="{ 'is-link': index < breadcrumbList.length - 1 }" :class="{ 'is-link': index < breadcrumbList.length - 1 }"
@click="handleBreadcrumbClick(item, index)"
> >
{{ item.name }} {{ item.name }}
</el-breadcrumb-item> </el-breadcrumb-item>
@@ -131,7 +131,7 @@
<Document v-else /> <Document v-else />
</el-icon> </el-icon>
<span <span
:class="{ 'file-name-clickable': !row.is_dir }" :class="{ 'file-name-clickable': true }"
@click="handleFileNameClick(row)" @click="handleFileNameClick(row)"
> >
{{ row.name }} {{ row.name }}
@@ -189,7 +189,7 @@
</el-icon> </el-icon>
</div> </div>
<div class="item-name">{{ item.name }}</div> <div class="item-name">{{ item.name }}</div>
<div class="item-size" v-if="!item.is_dir"> <div v-if="!item.is_dir" class="item-size">
{{ formatFileSize(item.size) }} {{ formatFileSize(item.size) }}
</div> </div>
</div> </div>
@@ -197,7 +197,7 @@
<!-- 分页区域 --> <!-- 分页区域 -->
<template #footer> <template #footer>
<pagination <Pagination
v-model:total="total" v-model:total="total"
v-model:page="pagination.page_no" v-model:page="pagination.page_no"
v-model:limit="pagination.page_size" v-model:limit="pagination.page_size"
@@ -218,10 +218,10 @@
:auto-upload="false" :auto-upload="false"
:multiple="true" :multiple="true"
:file-list="uploadFileList" :file-list="uploadFileList"
@change="handleUploadChange"
drag drag
@change="handleUploadChange"
> >
<el-icon class="el-icon--upload"><upload-filled /></el-icon> <el-icon class="el-icon--upload"><UploadFilled /></el-icon>
<div class="el-upload__text"> <div class="el-upload__text">
将文件拖到此处,或<em>点击上传</em> 将文件拖到此处,或<em>点击上传</em>
</div> </div>
@@ -233,7 +233,7 @@
</el-upload> </el-upload>
<template #footer> <template #footer>
<el-button @click="uploadDialogVisible = false">取消</el-button> <el-button @click="uploadDialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleUploadConfirm" :loading="uploading"> <el-button type="primary" :loading="uploading" @click="handleUploadConfirm">
确定上传 确定上传
</el-button> </el-button>
</template> </template>
@@ -287,11 +287,6 @@
import { ref, reactive, onMounted, computed } from 'vue' import { ref, reactive, onMounted, computed } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import { import {
Upload,
FolderAdd,
Refresh,
Search,
RefreshLeft,
List, List,
Grid, Grid,
Folder, Folder,
@@ -299,20 +294,20 @@ import {
UploadFilled, UploadFilled,
QuestionFilled QuestionFilled
} from '@element-plus/icons-vue' } from '@element-plus/icons-vue'
import Pagination from '@/components/Pagination/index.vue' import { ResourceAPI, type ResourceItem, type ResourceSearchQuery } from '@/api/monitor/resource'
import { ResourceAPI, type ResourceItem, type ResourceListQuery, type ResourceSearchQuery, type ResourceListResponse } from '@/api/resource/resource'
import { RESOURCE_ROOT_PATH } from '@/constants'
// 响应式数据 // 响应式数据
const loading = ref(false) const loading = ref(false)
const fileList = ref<ResourceItem[]>([]) const fileList = ref<ResourceItem[]>([])
const selectedItems = ref<ResourceItem[]>([]) const selectedItems = ref<ResourceItem[]>([])
const currentPath = ref(RESOURCE_ROOT_PATH) const breadcrumbList = ref([{ name: '资源根目录', path: '/' }])
const breadcrumbList = ref([{ name: '资源根目录', path: RESOURCE_ROOT_PATH }])
const showHiddenFiles = ref(false) const showHiddenFiles = ref(false)
const viewMode = ref<'list' | 'grid'>('list') const viewMode = ref<'list' | 'grid'>('list')
const total = ref(0) const total = ref(0)
// 路径相关数据
const currentPath = ref('/') // 添加当前路径的响应式变量
// 分页 // 分页
const pagination = reactive({ const pagination = reactive({
page_no: 1, page_no: 1,
@@ -347,25 +342,21 @@ const renameForm = reactive({
old_path: '' old_path: ''
}) })
// 工具函数:处理路径转换
const ensureRootPath = (path: string) => {
// 如果是HTTP URL,提取相对路径部分
if (path.startsWith('http')) {
const url = new URL(path)
return url.pathname.replace('/api/v1/static', '/home/static')
}
if (path.startsWith(RESOURCE_ROOT_PATH)) {
return path
}
return RESOURCE_ROOT_PATH + '/' + path.replace(/^\/+/, '')
}
// 计算属性 // 计算属性
const currentQuery = computed(() => ({ const currentQuery = computed(() => {
path: currentPath.value, // 构建查询参数
include_hidden: showHiddenFiles.value const query: any = {
})) include_hidden: showHiddenFiles.value
}
// 如果当前路径不是根路径,则添加路径参数
if (currentPath.value && currentPath.value !== '/') {
// 对于文件夹导航,直接传递文件夹名称
query.path = currentPath.value
}
return query
})
// 方法 // 方法
const loadFileList = async () => { const loadFileList = async () => {
@@ -397,54 +388,62 @@ const loadFileList = async () => {
} }
} }
const handleBreadcrumbClick = (item: any, index: number) => { const handleBreadcrumbClick = (item: any) => {
if (index < breadcrumbList.value.length - 1) { // 更新当前路径为点击的面包屑项路径
currentPath.value = ensureRootPath(item.path) currentPath.value = item.path
updateBreadcrumb() updateBreadcrumb()
loadFileList() loadFileList()
}
} }
const updateBreadcrumb = () => { const updateBreadcrumb = () => {
const rootPath = RESOURCE_ROOT_PATH // 对于根路径,直接显示根目录
let relativePath = currentPath.value if (currentPath.value === '/') {
breadcrumbList.value = [{ name: '资源根目录', path: '/' }]
// 处理HTTP URL格式的路径 return
if (currentPath.value.startsWith('http')) {
const url = new URL(currentPath.value)
relativePath = url.pathname.replace('/api/v1/static', '/home/static')
} }
relativePath = relativePath.replace(rootPath, '').replace(/^\/+/, '') // 对于嵌套路径,需要分解并构建面包屑
const pathParts = relativePath ? relativePath.split('/').filter(Boolean) : [] const parts = currentPath.value.split('/').filter(part => part !== '')
breadcrumbList.value = [ breadcrumbList.value = [
{ name: '资源根目录', path: rootPath }, { name: '资源根目录', path: '/' },
...pathParts.map((part, index) => ({ ...parts.map((part, index) => ({
name: part, name: part,
path: rootPath + '/' + pathParts.slice(0, index + 1).join('/') path: parts.slice(0, index + 1).join('/')
})) }))
] ]
} }
const handleFileNameClick = (row: ResourceItem) => { const handleFileNameClick = (row: ResourceItem) => {
if (row.is_dir) { if (row.is_dir) {
currentPath.value = ensureRootPath(row.path) // 如果当前在根路径,则直接使用文件夹名称
// 如果当前已在某个文件夹中,则拼接路径
if (currentPath.value === '/') {
currentPath.value = row.name
} else {
currentPath.value = currentPath.value + '/' + row.name
}
updateBreadcrumb() updateBreadcrumb()
loadFileList() loadFileList()
} else { } else {
// 文件预览 // 文件预览,使用后端返回的完整URL
handleFilePreview(row) handleFilePreview(row)
} }
} }
const handleItemClick = (item: ResourceItem) => { const handleItemClick = (item: ResourceItem) => {
if (item.is_dir) { if (item.is_dir) {
currentPath.value = ensureRootPath(item.path) // 如果当前在根路径,则直接使用文件夹名称
// 如果当前已在某个文件夹中,则拼接路径
if (currentPath.value === '/') {
currentPath.value = item.name
} else {
currentPath.value = currentPath.value + '/' + item.name
}
updateBreadcrumb() updateBreadcrumb()
loadFileList() loadFileList()
} else { } else {
// 文件预览 // 文件预览,使用后端返回的完整URL
handleFilePreview(item) handleFilePreview(item)
} }
} }
@@ -452,7 +451,13 @@ const handleItemClick = (item: ResourceItem) => {
// 文件预览 // 文件预览
const handleFilePreview = (file: ResourceItem) => { const handleFilePreview = (file: ResourceItem) => {
// 使用后端返回的完整URL路径进行预览 // 使用后端返回的完整URL路径进行预览
const previewUrl = file.path let previewUrl = file.path
// 如果是完整URL,直接使用
if (!previewUrl.startsWith('http')) {
// 对于相对路径,需要构建完整URL
previewUrl = `${window.location.origin}${previewUrl}`
}
// 根据文件类型决定预览方式 // 根据文件类型决定预览方式
const fileExtension = file.file_extension?.toLowerCase() || '' const fileExtension = file.file_extension?.toLowerCase() || ''
@@ -497,7 +502,19 @@ const handleUploadConfirm = async () => {
uploadFileList.value.forEach((file: any) => { uploadFileList.value.forEach((file: any) => {
formData.append('file', file.raw) formData.append('file', file.raw)
}) })
formData.append('target_path', ensureRootPath(currentPath.value))
// 处理目标路径
let targetPath = currentPath.value
if (targetPath.startsWith('http')) {
try {
const url = new URL(targetPath)
targetPath = url.pathname.replace('/api/v1/static', '') || '/'
} catch (error) {
console.warn('Failed to parse URL:', targetPath, error)
}
}
formData.append('target_path', targetPath)
await ResourceAPI.uploadFile(formData) await ResourceAPI.uploadFile(formData)
ElMessage.success('上传成功') ElMessage.success('上传成功')
@@ -528,8 +545,19 @@ const handleCreateDirConfirm = async () => {
} }
try { try {
// 处理父路径
let parentPath = currentPath.value
if (parentPath.startsWith('http')) {
try {
const url = new URL(parentPath)
parentPath = url.pathname.replace('/api/v1/static', '') || '/'
} catch (error) {
console.warn('Failed to parse URL:', parentPath, error)
}
}
await ResourceAPI.createDirectory({ await ResourceAPI.createDirectory({
parent_path: ensureRootPath(currentPath.value), parent_path: parentPath,
dir_name: createDirForm.dir_name.trim() dir_name: createDirForm.dir_name.trim()
}) })
ElMessage.success('创建成功') ElMessage.success('创建成功')
@@ -590,7 +618,17 @@ const handleShowHiddenChange = () => {
const handleDownload = async (item: ResourceItem) => { const handleDownload = async (item: ResourceItem) => {
try { try {
const filePath = ensureRootPath(item.path) // 处理完整URL路径
let filePath = item.path
if (filePath.startsWith('http')) {
try {
const url = new URL(filePath)
filePath = url.pathname.replace('/api/v1/static', '') || '/'
} catch (error) {
console.warn('Failed to parse URL:', filePath, error)
}
}
const response = await ResourceAPI.downloadFile(filePath) const response = await ResourceAPI.downloadFile(filePath)
const blob = response.data const blob = response.data
const url = window.URL.createObjectURL(blob) const url = window.URL.createObjectURL(blob)
@@ -608,7 +646,18 @@ const handleDownload = async (item: ResourceItem) => {
} }
const handleRename = (item: ResourceItem) => { const handleRename = (item: ResourceItem) => {
renameForm.old_path = ensureRootPath(item.path) // 处理完整URL路径
let oldPath = item.path
if (oldPath.startsWith('http')) {
try {
const url = new URL(oldPath)
oldPath = url.pathname.replace('/api/v1/static', '') || '/'
} catch (error) {
console.warn('Failed to parse URL:', oldPath, error)
}
}
renameForm.old_path = oldPath
renameForm.new_name = item.name renameForm.new_name = item.name
renameDialogVisible.value = true renameDialogVisible.value = true
} }
@@ -633,12 +682,6 @@ const handleRenameConfirm = async () => {
} }
} }
const handleMove = (item: ResourceItem) => {
// TODO: 实现移动功能
ElMessage.info('移动功能待实现')
}
const handleDelete = async (item: ResourceItem) => { const handleDelete = async (item: ResourceItem) => {
try { try {
await ElMessageBox.confirm( await ElMessageBox.confirm(
@@ -651,7 +694,17 @@ const handleDelete = async (item: ResourceItem) => {
} }
) )
const filePath = ensureRootPath(item.path) // 处理完整URL路径
let filePath = item.path
if (filePath.startsWith('http')) {
try {
const url = new URL(filePath)
filePath = url.pathname.replace('/api/v1/static', '') || '/'
} catch (error) {
console.warn('Failed to parse URL:', filePath, error)
}
}
await ResourceAPI.deleteResource([filePath]) await ResourceAPI.deleteResource([filePath])
ElMessage.success('删除成功') ElMessage.success('删除成功')
loadFileList() loadFileList()
@@ -663,16 +716,6 @@ const handleDelete = async (item: ResourceItem) => {
} }
} }
const handleSizeChange = (size: number) => {
pagination.page_size = size
loadFileList()
}
const handleCurrentChange = (page: number) => {
pagination.page_no = page
loadFileList()
}
const handlePagination = (params: { page: number; limit: number }) => { const handlePagination = (params: { page: number; limit: number }) => {
pagination.page_no = params.page pagination.page_no = params.page
pagination.page_size = params.limit pagination.page_size = params.limit
@@ -696,7 +739,20 @@ const handleBatchDelete = async () => {
} }
) )
const paths = selectedItems.value.map(item => ensureRootPath(item.path)) const paths = selectedItems.value.map(item => {
// 处理完整URL路径
let path = item.path
if (path.startsWith('http')) {
try {
const url = new URL(path)
path = url.pathname.replace('/api/v1/static', '') || '/'
} catch (error) {
console.warn('Failed to parse URL:', path, error)
}
}
return path
})
await ResourceAPI.deleteResource(paths) await ResourceAPI.deleteResource(paths)
ElMessage.success('删除成功') ElMessage.success('删除成功')
loadFileList() loadFileList()
@@ -723,10 +779,6 @@ const formatFileSize = (size?: number | null) => {
return `${fileSize.toFixed(1)} ${units[unitIndex]}` return `${fileSize.toFixed(1)} ${units[unitIndex]}`
} }
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleString()
}
// 生命周期 // 生命周期
onMounted(() => { onMounted(() => {
loadFileList() loadFileList()
@@ -120,7 +120,7 @@ import {
Monitor, Monitor,
QuestionFilled QuestionFilled
} from '@element-plus/icons-vue' } from '@element-plus/icons-vue'
import { ResourceAPI, type ResourceStats } from '@/api/resource/resource' import { ResourceAPI, type ResourceStats } from '@/api/monitor/resource'
// 响应式数据 // 响应式数据
const stats = ref<ResourceStats | null>(null) const stats = ref<ResourceStats | null>(null)
-578
View File
@@ -1,578 +0,0 @@
<!-- 工单管理 -->
<template>
<div class="app-container">
<!-- 搜索区域 -->
<div class="search-container">
<el-form ref="queryFormRef" :model="queryFormData" :inline="true" label-suffix=":">
<el-form-item prop="title" label="工单标题">
<el-input v-model="queryFormData.title" placeholder="请输入工单标题" clearable />
</el-form-item>
<el-form-item prop="priority" label="优先级">
<el-select v-model="queryFormData.priority" placeholder="请选择优先级" style="width: 167.5px" clearable>
<el-option value="low" label="低" />
<el-option value="medium" label="中" />
<el-option value="high" label="高" />
<el-option value="urgent" label="紧急" />
</el-select>
</el-form-item>
<el-form-item prop="status" label="状态">
<el-select v-model="queryFormData.status" placeholder="请选择状态" style="width: 167.5px" clearable>
<el-option value="pending" label="待处理" />
<el-option value="progress" label="处理中" />
<el-option value="resolved" label="已解决" />
<el-option value="closed" label="已关闭" />
</el-select>
</el-form-item>
<!-- 时间范围,收起状态下隐藏 -->
<el-form-item v-if="isExpand" prop="start_time" label="创建时间">
<DatePicker
v-model="dateRange"
@update:model-value="handleDateRangeChange"
/>
</el-form-item>
<!-- 查询、重置、展开/收起按钮 -->
<el-form-item class="search-buttons">
<el-button type="primary" icon="search" @click="handleQuery">
查询
</el-button>
<el-button icon="refresh" @click="handleResetQuery">
重置
</el-button>
<!-- 展开/收起 -->
<template v-if="isExpandable">
<el-link class="ml-3" type="primary" underline="never" @click="isExpand = !isExpand">
{{ isExpand ? "收起" : "展开" }}
<el-icon>
<template v-if="isExpand">
<ArrowUp />
</template>
<template v-else>
<ArrowDown />
</template>
</el-icon>
</el-link>
</template>
</el-form-item>
</el-form>
</div>
<!-- 内容区域 -->
<el-card shadow="hover" class="data-table">
<template #header>
<div class="card-header">
<span>
<el-tooltip content="工单管理维护系统。">
<QuestionFilled class="w-4 h-4 mx-1" />
</el-tooltip>
工单列表
</span>
</div>
</template>
<!-- 功能区域 -->
<div class="data-table__toolbar">
<div class="data-table__toolbar--actions">
<el-button type="success" icon="plus" @click="handleOpenDialog('create')">新增</el-button>
<el-button type="danger" icon="delete" :disabled="selectIds.length === 0" @click="handleDelete(selectIds)">批量删除</el-button>
</div>
<div class="data-table__toolbar--tools">
<el-tooltip content="刷新">
<el-button type="primary" icon="refresh" circle @click="handleRefresh" />
</el-tooltip>
<el-tooltip content="列表筛选">
<el-dropdown trigger="click">
<el-button type="default" icon="operation" circle />
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item v-for="column in tableColumns" :key="column.prop" :command="column">
<el-checkbox v-model="column.show">
{{ column.label }}
</el-checkbox>
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</el-tooltip>
</div>
</div>
<!-- 表格区域:工单列表 -->
<el-table ref="dataTableRef" v-loading="loading" :data="pageTableData" highlight-current-row class="data-table__content" height="450" border stripe @selection-change="handleSelectionChange">
<template #empty>
<el-empty :image-size="80" description="暂无数据" />
</template>
<el-table-column v-if="tableColumns.find(col => col.prop === 'selection')?.show" type="selection" min-width="55" align="center" />
<el-table-column v-if="tableColumns.find(col => col.prop === 'index')?.show" fixed label="序号" min-width="60" >
<template #default="scope">
{{ (queryFormData.page_no - 1) * queryFormData.page_size + scope.$index + 1 }}
</template>
</el-table-column>
<el-table-column v-if="tableColumns.find(col => col.prop === 'title')?.show" label="工单标题" prop="title" min-width="140" />
<el-table-column v-if="tableColumns.find(col => col.prop === 'status')?.show" label="状态" prop="status" min-width="100">
<template #default="scope">
<el-tag :type="getStatusTagType(scope.row.status)">
{{ getStatusLabel(scope.row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column v-if="tableColumns.find(col => col.prop === 'priority')?.show" label="优先级" prop="priority" min-width="80">
<template #default="scope">
<el-tag :type="getPriorityTagType(scope.row.priority)">
{{ getPriorityLabel(scope.row.priority) }}
</el-tag>
</template>
</el-table-column>
<el-table-column v-if="tableColumns.find(col => col.prop === 'type')?.show" label="类型" prop="type" min-width="80">
<template #default="scope">
{{ getTypeLabel(scope.row.type) }}
</template>
</el-table-column>
<el-table-column v-if="tableColumns.find(col => col.prop === 'project')?.show" label="项目" prop="project" min-width="100" />
<el-table-column v-if="tableColumns.find(col => col.prop === 'version')?.show" label="版本" prop="version" min-width="80" />
<el-table-column v-if="tableColumns.find(col => col.prop === 'assignee')?.show" label="指派给" prop="assignee" min-width="100">
<template #default="scope">
{{ scope.row.assignee?.name }}
</template>
</el-table-column>
<el-table-column v-if="tableColumns.find(col => col.prop === 'reporter')?.show" label="报告人" prop="reporter" min-width="100">
<template #default="scope">
{{ scope.row.reporter?.name }}
</template>
</el-table-column>
<el-table-column v-if="tableColumns.find(col => col.prop === 'created_at')?.show" label="创建时间" prop="created_at" min-width="180" sortable />
<el-table-column v-if="tableColumns.find(col => col.prop === 'updated_at')?.show" label="更新时间" prop="updated_at" min-width="180" sortable />
<el-table-column v-if="tableColumns.find(col => col.prop === 'creator')?.show" key="creator" label="创建人" min-width="100">
<template #default="scope">
{{ scope.row.creator?.name }}
</template>
</el-table-column>
<el-table-column v-if="tableColumns.find(col => col.prop === 'operation')?.show" fixed="right" label="操作" align="center" min-width="200">
<template #default="scope">
<el-button type="info" size="small" link icon="document" @click="handleOpenDialog('detail', scope.row.id)">详情</el-button>
<el-button type="primary" size="small" link icon="edit" @click="handleOpenDialog('update', scope.row.id)">编辑</el-button>
<el-button type="danger" size="small" link icon="delete" @click="handleDelete([scope.row.id])">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页区域 -->
<template #footer>
<pagination v-model:total="total" v-model:page="queryFormData.page_no" v-model:limit="queryFormData.page_size" @pagination="loadingData" />
</template>
</el-card>
<!-- 弹窗区域 -->
<el-dialog v-model="dialogVisible.visible" :title="dialogVisible.title" @close="handleCloseDialog">
<!-- 详情 -->
<template v-if="dialogVisible.type === 'detail'">
<el-descriptions :column="4" border>
<el-descriptions-item label="工单标题" :span="2">
{{ detailFormData.title }}
</el-descriptions-item>
<el-descriptions-item label="状态" :span="2">
<el-tag :type="getStatusTagType(detailFormData.status)">
{{ getStatusLabel(detailFormData.status) }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="优先级" :span="2">
<el-tag :type="getPriorityTagType(detailFormData.priority)">
{{ getPriorityLabel(detailFormData.priority) }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="类型" :span="2">
{{ getTypeLabel(detailFormData.type) }}
</el-descriptions-item>
<el-descriptions-item label="项目" :span="2">
{{ detailFormData.project }}
</el-descriptions-item>
<el-descriptions-item label="版本" :span="2">
{{ detailFormData.version }}
</el-descriptions-item>
<el-descriptions-item label="指派给" :span="2">
{{ detailFormData.assignee?.name }}
</el-descriptions-item>
<el-descriptions-item label="报告人" :span="2">
{{ detailFormData.reporter?.name }}
</el-descriptions-item>
<el-descriptions-item label="描述" :span="4">
{{ detailFormData.description }}
</el-descriptions-item>
<el-descriptions-item label="创建人" :span="2">
{{ detailFormData.creator?.name }}
</el-descriptions-item>
<el-descriptions-item label="创建时间" :span="2">
{{ detailFormData.created_at }}
</el-descriptions-item>
<el-descriptions-item label="更新时间" :span="2">
{{ detailFormData.updated_at }}
</el-descriptions-item>
</el-descriptions>
</template>
<!-- 新增、编辑表单 -->
<template v-else>
<el-form ref="dataFormRef" :model="formData" :rules="rules" label-suffix=":" label-width="auto" label-position="right">
<el-form-item label="工单标题" prop="title">
<el-input v-model="formData.title" placeholder="请输入工单标题" :maxlength="255" />
</el-form-item>
<el-form-item label="优先级" prop="priority">
<el-select v-model="formData.priority" placeholder="请选择优先级" clearable>
<el-option value="low" label="低" />
<el-option value="medium" label="中" />
<el-option value="high" label="高" />
<el-option value="urgent" label="紧急" />
</el-select>
</el-form-item>
<el-form-item label="类型" prop="type">
<el-select v-model="formData.type" placeholder="请选择类型" clearable>
<el-option value="bug" label="缺陷" />
<el-option value="feature" label="功能" />
<el-option value="task" label="任务" />
</el-select>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="formData.status" placeholder="请选择状态">
<el-option value="pending" label="待处理" />
<el-option value="progress" label="处理中" />
<el-option value="resolved" label="已解决" />
<el-option value="closed" label="已关闭" />
</el-select>
</el-form-item>
<el-form-item label="项目" prop="project">
<el-input v-model="formData.project" placeholder="请输入项目名称" :maxlength="100" />
</el-form-item>
<el-form-item label="版本" prop="version">
<el-input v-model="formData.version" placeholder="请输入版本号" :maxlength="50" />
</el-form-item>
<el-form-item label="指派给" prop="assignee_id">
<!-- 这里应该是一个用户选择器,暂时用输入框代替 -->
<el-input v-model.number="formData.assignee_id" placeholder="请输入指派用户ID" type="number" />
</el-form-item>
<el-form-item label="描述" prop="description">
<el-input v-model="formData.description" :rows="4" :maxlength="500" show-word-limit type="textarea" placeholder="请输入描述" />
</el-form-item>
</el-form>
</template>
<template #footer>
<div class="dialog-footer">
<!-- 详情弹窗不需要确定按钮的提交逻辑 -->
<el-button @click="handleCloseDialog">取消</el-button>
<el-button v-if="dialogVisible.type !== 'detail'" type="primary" @click="handleSubmit">确定</el-button>
<el-button v-else type="primary" @click="handleCloseDialog">确定</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
defineOptions({
name: "Ticket",
inheritAttrs: false,
});
import TicketAPI, { TicketTable, TicketForm, TicketPageQuery } from "@/api/system/ticket";
import { ElMessageBox } from "element-plus";
const queryFormRef = ref();
const dataFormRef = ref();
const total = ref(0);
const selectIds = ref<number[]>([]);
const loading = ref(false);
const isExpand = ref(false);
const isExpandable = ref(true);
// 分页表单
const pageTableData = ref<TicketTable[]>([]);
// 表格列配置
const tableColumns = ref([
{ prop: 'selection', label: '选择框', show: true },
{ prop: 'index', label: '序号', show: true },
{ prop: 'title', label: '工单标题', show: true },
{ prop: 'status', label: '状态', show: true },
{ prop: 'priority', label: '优先级', show: true },
{ prop: 'type', label: '类型', show: true },
{ prop: 'project', label: '项目', show: true },
{ prop: 'version', label: '版本', show: true },
{ prop: 'assignee', label: '指派给', show: true },
{ prop: 'reporter', label: '报告人', show: true },
{ prop: 'created_at', label: '创建时间', show: true },
{ prop: 'updated_at', label: '更新时间', show: true },
{ prop: 'creator', label: '创建人', show: true },
{ prop: 'operation', label: '操作', show: true }
])
// 详情表单
const detailFormData = ref<TicketTable>({});
// 分页查询参数
const queryFormData = reactive<TicketPageQuery>({
page_no: 1,
page_size: 10,
title: undefined,
priority: undefined,
status: undefined,
start_time: undefined,
end_time: undefined,
});
// 编辑表单
const formData = reactive<TicketForm>({
id: undefined,
title: '',
priority: 'medium',
type: 'bug',
status: 'pending',
description: undefined,
assignee_id: undefined,
reporter_id: 1, // 默认当前用户ID,实际应该从认证信息中获取
project: undefined,
version: undefined
})
// 弹窗状态
const dialogVisible = reactive({
title: "",
visible: false,
type: 'create' as 'create' | 'update' | 'detail',
});
// 表单验证规则
const rules = reactive({
title: [{ required: true, message: "请输入工单标题", trigger: "blur" }],
priority: [{ required: false, message: "请选择优先级", trigger: "blur" }],
type: [{ required: false, message: "请选择类型", trigger: "blur" }],
status: [{ required: true, message: "请选择状态", trigger: "blur" }],
project: [{ required: false, message: "请输入项目名称", trigger: "blur" }],
version: [{ required: false, message: "请输入版本号", trigger: "blur" }],
});
// 日期范围临时变量
const dateRange = ref<[Date, Date] | []>([]);
// 处理日期范围变化
function handleDateRangeChange(range: [Date, Date]) {
dateRange.value = range;
if (range && range.length === 2) {
queryFormData.start_time = range[0].toISOString();
queryFormData.end_time = range[1].toISOString();
} else {
queryFormData.start_time = undefined;
queryFormData.end_time = undefined;
}
}
// 获取状态标签类型
function getStatusTagType(status: string) {
switch (status) {
case 'pending': return 'info';
case 'progress': return 'warning';
case 'resolved': return 'success';
case 'closed': return 'danger';
default: return 'info';
}
}
// 获取状态标签文本
function getStatusLabel(status: string) {
switch (status) {
case 'pending': return '待处理';
case 'progress': return '处理中';
case 'resolved': return '已解决';
case 'closed': return '已关闭';
default: return status;
}
}
// 获取优先级标签类型
function getPriorityTagType(priority: string) {
switch (priority) {
case 'low': return '';
case 'medium': return 'warning';
case 'high': return 'danger';
case 'urgent': return 'danger';
default: return '';
}
}
// 获取优先级标签文本
function getPriorityLabel(priority: string) {
switch (priority) {
case 'low': return '低';
case 'medium': return '中';
case 'high': return '高';
case 'urgent': return '紧急';
default: return priority;
}
}
// 获取类型标签文本
function getTypeLabel(type: string) {
switch (type) {
case 'bug': return '缺陷';
case 'feature': return '功能';
case 'task': return '任务';
default: return type;
}
}
// 列表刷新
async function handleRefresh () {
await loadingData();
};
// 加载表格数据
async function loadingData() {
loading.value = true;
try {
const response = await TicketAPI.getTicketList(queryFormData);
pageTableData.value = response.data.data.items;
total.value = response.data.data.total;
}
catch (error: any) {
console.error(error);
}
finally {
loading.value = false;
}
}
// 查询(重置页码后获取数据)
async function handleQuery() {
queryFormData.page_no = 1;
loadingData();
}
// 重置查询
async function handleResetQuery() {
queryFormRef.value.resetFields();
queryFormData.page_no = 1;
dateRange.value = [];
queryFormData.start_time = undefined;
queryFormData.end_time = undefined;
loadingData();
}
// 定义初始表单数据常量
const initialFormData: TicketForm = {
id: undefined,
title: '',
priority: 'medium',
type: 'bug',
status: 'pending',
description: undefined,
assignee_id: undefined,
reporter_id: 1, // 默认当前用户ID,实际应该从认证信息中获取
project: undefined,
version: undefined
}
// 重置表单
async function resetForm() {
if (dataFormRef.value) {
dataFormRef.value.resetFields();
dataFormRef.value.clearValidate();
}
// 完全重置 formData 为初始状态
Object.assign(formData, initialFormData);
}
// 行复选框选中项变化
async function handleSelectionChange(selection: any) {
selectIds.value = selection.map((item: any) => item.id);
}
// 关闭弹窗
async function handleCloseDialog() {
dialogVisible.visible = false;
resetForm();
}
// 打开弹窗
async function handleOpenDialog(type: 'create' | 'update' | 'detail', id?: number) {
dialogVisible.type = type;
if (id) {
const response = await TicketAPI.getTicketDetail(id);
if (type === 'detail') {
dialogVisible.title = "工单详情";
Object.assign(detailFormData.value, response.data.data);
} else if (type === 'update') {
dialogVisible.title = "修改工单";
Object.assign(formData, response.data.data);
}
} else {
dialogVisible.title = "新增工单";
formData.id = undefined;
}
dialogVisible.visible = true;
}
// 提交表单(防抖)
async function handleSubmit() {
// 表单校验
dataFormRef.value.validate(async (valid: any) => {
if (valid) {
loading.value = true;
// 根据弹窗传入的参数(deatil\create\update)判断走什么逻辑
const id = formData.id;
if (id) {
try {
await TicketAPI.updateTicket(id, { id, ...formData })
dialogVisible.visible = false;
resetForm();
handleCloseDialog();
handleResetQuery();
} catch (error: any) {
console.error(error);
} finally {
loading.value = false;
}
} else {
try {
await TicketAPI.createTicket(formData)
dialogVisible.visible = false;
resetForm();
handleCloseDialog();
handleResetQuery();
} catch (error: any) {
console.error(error);
} finally {
loading.value = false;
}
}
}
});
}
// 删除、批量删除
async function handleDelete(ids: number[]) {
ElMessageBox.confirm("确认删除该项数据?", "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
}).then(async () => {
try {
loading.value = true;
await TicketAPI.deleteTicket(ids);
handleResetQuery();
} catch (error: any) {
console.error(error);
} finally {
loading.value = false;
}
}).catch(() => {
ElMessageBox.close();
});
}
onMounted(async () => {
// 加载表格数据
loadingData();
});
</script>
<style lang="scss" scoped></style>
-480
View File
@@ -1,480 +0,0 @@
<!-- 版本管理 -->
<template>
<div class="app-container">
<!-- 搜索区域 -->
<div class="search-container">
<el-form ref="queryFormRef" :model="queryFormData" :inline="true" label-suffix=":">
<el-form-item prop="title" label="版本标题">
<el-input v-model="queryFormData.title" placeholder="请输入版本标题" clearable />
</el-form-item>
<el-form-item prop="status" label="状态">
<el-select v-model="queryFormData.status" placeholder="请选择状态" style="width: 167.5px" clearable>
<el-option value="draft" label="草稿" />
<el-option value="released" label="已发布" />
<el-option value="archived" label="已归档" />
</el-select>
</el-form-item>
<!-- 时间范围,收起状态下隐藏 -->
<el-form-item v-if="isExpand" prop="start_time" label="创建时间">
<DatePicker v-model="dateRange" @update:model-value="handleDateRangeChange" />
</el-form-item>
<!-- 查询、重置、展开/收起按钮 -->
<el-form-item class="search-buttons">
<el-button type="primary" icon="search" @click="handleQuery">
查询
</el-button>
<el-button icon="refresh" @click="handleResetQuery">
重置
</el-button>
<!-- 展开/收起 -->
<template v-if="isExpandable">
<el-link class="ml-3" type="primary" underline="never" @click="isExpand = !isExpand">
{{ isExpand ? "收起" : "展开" }}
<el-icon>
<template v-if="isExpand">
<ArrowUp />
</template>
<template v-else>
<ArrowDown />
</template>
</el-icon>
</el-link>
</template>
</el-form-item>
</el-form>
</div>
<!-- 内容区域 -->
<el-card shadow="hover" class="data-table">
<template #header>
<div class="card-header">
<span>
<el-tooltip content="版本管理维护系统的版本信息。">
<QuestionFilled class="w-4 h-4 mx-1" />
</el-tooltip>
版本列表
</span>
</div>
</template>
<!-- 功能区域 -->
<div class="data-table__toolbar">
<div class="data-table__toolbar--actions">
<el-button type="success" icon="plus" @click="handleOpenDialog('create')">新增</el-button>
<el-button type="danger" icon="delete" :disabled="selectIds.length === 0" @click="handleDelete(selectIds)">批量删除</el-button>
</div>
<div class="data-table__toolbar--tools">
<el-tooltip content="刷新">
<el-button type="primary" icon="refresh" circle @click="handleRefresh" />
</el-tooltip>
<el-tooltip content="列表筛选">
<el-dropdown trigger="click">
<el-button type="default" icon="operation" circle />
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item v-for="column in tableColumns" :key="column.prop" :command="column">
<el-checkbox v-model="column.show">
{{ column.label }}
</el-checkbox>
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</el-tooltip>
</div>
</div>
<!-- 表格区域:版本列表 -->
<el-table ref="dataTableRef" v-loading="loading" :data="pageTableData" highlight-current-row class="data-table__content" height="450" border stripe @selection-change="handleSelectionChange">
<template #empty>
<el-empty :image-size="80" description="暂无数据" />
</template>
<el-table-column v-if="tableColumns.find(col => col.prop === 'selection')?.show" type="selection" min-width="55" align="center" />
<el-table-column v-if="tableColumns.find(col => col.prop === 'index')?.show" fixed label="序号" min-width="60">
<template #default="scope">
{{ (queryFormData.page_no - 1) * queryFormData.page_size + scope.$index + 1 }}
</template>
</el-table-column>
<el-table-column v-if="tableColumns.find(col => col.prop === 'version_number')?.show" label="版本号" prop="version_number" min-width="100" />
<el-table-column v-if="tableColumns.find(col => col.prop === 'title')?.show" label="版本标题" prop="title" min-width="140" />
<el-table-column v-if="tableColumns.find(col => col.prop === 'status')?.show" label="状态" prop="status" min-width="100">
<template #default="scope">
<el-tag :type="getStatusTagType(scope.row.status)">
{{ getStatusLabel(scope.row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column v-if="tableColumns.find(col => col.prop === 'project')?.show" label="所属项目" prop="project" min-width="120" />
<el-table-column v-if="tableColumns.find(col => col.prop === 'release_notes')?.show" label="发布说明" prop="release_notes" min-width="200" show-overflow-tooltip />
<el-table-column v-if="tableColumns.find(col => col.prop === 'released_at')?.show" label="发布时间" prop="released_at" min-width="180" sortable />
<el-table-column v-if="tableColumns.find(col => col.prop === 'created_at')?.show" label="创建时间" prop="created_at" min-width="180" sortable />
<el-table-column v-if="tableColumns.find(col => col.prop === 'updated_at')?.show" label="更新时间" prop="updated_at" min-width="180" sortable />
<el-table-column v-if="tableColumns.find(col => col.prop === 'creator')?.show" key="creator" label="创建人" min-width="100">
<template #default="scope">
{{ scope.row.creator?.name }}
</template>
</el-table-column>
<el-table-column v-if="tableColumns.find(col => col.prop === 'operation')?.show" fixed="right" label="操作" align="center" min-width="200">
<template #default="scope">
<el-button type="info" size="small" link icon="document" @click="handleOpenDialog('detail', scope.row.id)">详情</el-button>
<el-button type="primary" size="small" link icon="edit" @click="handleOpenDialog('update', scope.row.id)">编辑</el-button>
<el-button type="danger" size="small" link icon="delete" @click="handleDelete([scope.row.id])">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页区域 -->
<template #footer>
<pagination v-model:total="total" v-model:page="queryFormData.page_no" v-model:limit="queryFormData.page_size" @pagination="loadingData" />
</template>
</el-card>
<!-- 弹窗区域 -->
<el-dialog v-model="dialogVisible.visible" :title="dialogVisible.title" @close="handleCloseDialog">
<!-- 详情 -->
<template v-if="dialogVisible.type === 'detail'">
<el-descriptions :column="4" border>
<el-descriptions-item label="版本号" :span="2">
{{ detailFormData.version_number }}
</el-descriptions-item>
<el-descriptions-item label="版本标题" :span="2">
{{ detailFormData.title }}
</el-descriptions-item>
<el-descriptions-item label="状态" :span="2">
<el-tag :type="getStatusTagType(detailFormData.status)">
{{ getStatusLabel(detailFormData.status) }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="所属项目" :span="2">
{{ detailFormData.project }}
</el-descriptions-item>
<el-descriptions-item label="发布说明" :span="4">
{{ detailFormData.release_notes }}
</el-descriptions-item>
<el-descriptions-item label="发布时间" :span="2">
{{ detailFormData.released_at }}
</el-descriptions-item>
<el-descriptions-item label="描述" :span="2">
{{ detailFormData.description }}
</el-descriptions-item>
<el-descriptions-item label="创建人" :span="2">
{{ detailFormData.creator?.name }}
</el-descriptions-item>
<el-descriptions-item label="创建时间" :span="2">
{{ detailFormData.created_at }}
</el-descriptions-item>
<el-descriptions-item label="更新时间" :span="2">
{{ detailFormData.updated_at }}
</el-descriptions-item>
</el-descriptions>
</template>
<!-- 新增、编辑表单 -->
<template v-else>
<el-form ref="dataFormRef" :model="formData" :rules="rules" label-suffix=":" label-width="auto" label-position="right">
<el-form-item label="版本号" prop="version_number">
<el-input v-model="formData.version_number" placeholder="请输入版本号" :maxlength="50" />
</el-form-item>
<el-form-item label="版本标题" prop="title">
<el-input v-model="formData.title" placeholder="请输入版本标题" :maxlength="255" />
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="formData.status" placeholder="请选择状态">
<el-option value="draft" label="草稿" />
<el-option value="released" label="已发布" />
<el-option value="archived" label="已归档" />
</el-select>
</el-form-item>
<el-form-item label="所属项目" prop="project">
<el-input v-model="formData.project" placeholder="请输入所属项目" :maxlength="100" />
</el-form-item>
<el-form-item label="发布说明" prop="release_notes">
<el-input v-model="formData.release_notes" :rows="4" type="textarea" placeholder="请输入发布说明" />
</el-form-item>
<el-form-item label="发布时间" prop="released_at">
<el-date-picker v-model="formData.released_at" type="datetime" placeholder="请选择发布时间" format="YYYY-MM-DD HH:mm:ss" value-format="YYYY-MM-DD HH:mm:ss" />
</el-form-item>
<el-form-item label="描述" prop="description">
<el-input v-model="formData.description" :rows="4" :maxlength="500" show-word-limit type="textarea" placeholder="请输入描述" />
</el-form-item>
</el-form>
</template>
<template #footer>
<div class="dialog-footer">
<!-- 详情弹窗不需要确定按钮的提交逻辑 -->
<el-button @click="handleCloseDialog">取消</el-button>
<el-button v-if="dialogVisible.type !== 'detail'" type="primary" @click="handleSubmit">确定</el-button>
<el-button v-else type="primary" @click="handleCloseDialog">确定</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
defineOptions({
name: "Version",
inheritAttrs: false,
});
import VersionAPI, { VersionTable, VersionForm, VersionPageQuery } from "@/api/system/version";
import { ElMessageBox } from "element-plus";
const queryFormRef = ref();
const dataFormRef = ref();
const total = ref(0);
const selectIds = ref<number[]>([]);
const loading = ref(false);
const isExpand = ref(false);
const isExpandable = ref(true);
// 分页表单
const pageTableData = ref<VersionTable[]>([]);
// 表格列配置
const tableColumns = ref([
{ prop: 'selection', label: '选择框', show: true },
{ prop: 'index', label: '序号', show: true },
{ prop: 'version_number', label: '版本号', show: true },
{ prop: 'title', label: '版本标题', show: true },
{ prop: 'status', label: '状态', show: true },
{ prop: 'project', label: '所属项目', show: true },
{ prop: 'release_notes', label: '发布说明', show: true },
{ prop: 'released_at', label: '发布时间', show: true },
{ prop: 'created_at', label: '创建时间', show: true },
{ prop: 'updated_at', label: '更新时间', show: true },
{ prop: 'creator', label: '创建人', show: true },
{ prop: 'operation', label: '操作', show: true }
])
// 详情表单
const detailFormData = ref<VersionTable>({});
// 分页查询参数
const queryFormData = reactive<VersionPageQuery>({
page_no: 1,
page_size: 10,
title: undefined,
status: undefined,
start_time: undefined,
end_time: undefined,
});
// 编辑表单
const formData = reactive<VersionForm>({
id: undefined,
version_number: '',
title: '',
release_notes: undefined,
project: undefined,
status: 'draft',
released_at: undefined,
description: undefined,
})
// 弹窗状态
const dialogVisible = reactive({
title: "",
visible: false,
type: 'create' as 'create' | 'update' | 'detail',
});
// 表单验证规则
const rules = reactive({
version_number: [{ required: true, message: "请输入版本号", trigger: "blur" }],
title: [{ required: true, message: "请输入版本标题", trigger: "blur" }],
// status字段在后端有默认值'draft',因此不是必填项
});
// 日期范围临时变量
const dateRange = ref<[Date, Date] | []>([]);
// 处理日期范围变化
function handleDateRangeChange(range: [Date, Date]) {
dateRange.value = range;
if (range && range.length === 2) {
queryFormData.start_time = range[0]?.toISOString();
queryFormData.end_time = range[1]?.toISOString();
} else {
queryFormData.start_time = undefined;
queryFormData.end_time = undefined;
}
}
// 获取状态标签类型
function getStatusTagType(status: string | undefined) {
switch (status) {
case 'draft': return 'info';
case 'released': return 'success';
case 'archived': return 'warning';
default: return 'info';
}
}
// 获取状态标签文本
function getStatusLabel(status: string | undefined) {
switch (status) {
case 'draft': return '草稿';
case 'released': return '已发布';
case 'archived': return '已归档';
default: return status || '';
}
}
// 列表刷新
async function handleRefresh() {
await loadingData();
};
// 加载表格数据
async function loadingData() {
loading.value = true;
try {
const response = await VersionAPI.getVersionList(queryFormData);
pageTableData.value = response.data.data.items;
total.value = response.data.data.total;
}
catch (error: any) {
console.error(error);
}
finally {
loading.value = false;
}
}
// 查询(重置页码后获取数据)
async function handleQuery() {
queryFormData.page_no = 1;
loadingData();
}
// 重置查询
async function handleResetQuery() {
queryFormRef.value.resetFields();
queryFormData.page_no = 1;
dateRange.value = [];
queryFormData.start_time = undefined;
queryFormData.end_time = undefined;
loadingData();
}
// 定义初始表单数据常量
const initialFormData: VersionForm = {
id: undefined,
version_number: '',
title: '',
release_notes: undefined,
project: undefined,
status: 'draft',
released_at: undefined,
description: undefined,
}
// 重置表单
async function resetForm() {
if (dataFormRef.value) {
dataFormRef.value.resetFields();
dataFormRef.value.clearValidate();
}
// 完全重置 formData 为初始状态
Object.assign(formData, initialFormData);
}
// 行复选框选中项变化
async function handleSelectionChange(selection: any) {
selectIds.value = selection.map((item: any) => item.id);
}
// 关闭弹窗
async function handleCloseDialog() {
dialogVisible.visible = false;
resetForm();
}
// 打开弹窗
async function handleOpenDialog(type: 'create' | 'update' | 'detail', id?: number) {
dialogVisible.type = type;
if (id) {
const response = await VersionAPI.getVersionDetail(id);
if (type === 'detail') {
dialogVisible.title = "版本详情";
Object.assign(detailFormData.value, response.data.data);
} else if (type === 'update') {
dialogVisible.title = "修改版本";
Object.assign(formData, response.data.data);
}
} else {
dialogVisible.title = "新增版本";
formData.id = undefined;
}
dialogVisible.visible = true;
}
// 提交表单(防抖)
async function handleSubmit() {
// 表单校验
dataFormRef.value.validate(async (valid: any) => {
if (valid) {
loading.value = true;
// 根据弹窗传入的参数(detail\create\update)判断走什么逻辑
const id = formData.id;
if (id) {
try {
await VersionAPI.updateVersion(id, { id, ...formData })
dialogVisible.visible = false;
resetForm();
handleCloseDialog();
handleResetQuery();
} catch (error: any) {
console.error(error);
} finally {
loading.value = false;
}
} else {
try {
await VersionAPI.createVersion(formData)
dialogVisible.visible = false;
resetForm();
handleCloseDialog();
handleResetQuery();
} catch (error: any) {
console.error(error);
} finally {
loading.value = false;
}
}
}
});
}
// 删除、批量删除
async function handleDelete(ids: number[]) {
ElMessageBox.confirm("确认删除该项数据?", "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
}).then(async () => {
try {
loading.value = true;
await VersionAPI.deleteVersion(ids);
handleResetQuery();
} catch (error: any) {
console.error(error);
} finally {
loading.value = false;
}
}).catch(() => {
ElMessageBox.close();
});
}
onMounted(async () => {
// 加载表格数据
loadingData();
});
</script>
<style lang="scss" scoped></style>