mirror of
https://github.com/fastapiadmin/FastapiAdmin.git
synced 2026-09-28 06:57:54 +00:00
chore: 整理项目配置与代码优化
1. 统一前端环境变量命名,新增通用项目标题配置 2. 修复后端接口参数绑定方式,将Depends改为Query适配FastAPI解析 3. 优化表格列排序、搜索过滤功能与页面布局样式 4. 重构路由与状态管理代码,移除循环依赖 5. 更新依赖版本与脚本命令,修复构建警告 6. 调整多语言文案与快捷入口配置 7. 修复通知中心样式与接口返回格式
This commit is contained in:
+4
-5
@@ -59,12 +59,11 @@ docker/.env
|
|||||||
backend/env/.env.dev
|
backend/env/.env.dev
|
||||||
backend/env/.env.prod
|
backend/env/.env.prod
|
||||||
backend/env/.env.test
|
backend/env/.env.test
|
||||||
frontend/.env.dev
|
frontend/.env.development
|
||||||
frontend/.env.prod
|
frontend/.env.production
|
||||||
frontend/web/.env
|
frontend/web/.env
|
||||||
frontend/web/.env.dev
|
frontend/web/.env.development
|
||||||
frontend/web/.env.prod
|
frontend/web/.env.production
|
||||||
frontend/web/.env.test
|
|
||||||
|
|
||||||
frontend/node_modules
|
frontend/node_modules
|
||||||
frontend/dist
|
frontend/dist
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import asyncio
|
|||||||
import json
|
import json
|
||||||
from typing import Annotated, Any
|
from typing import Annotated, Any
|
||||||
|
|
||||||
from fastapi import APIRouter, Body, Depends, Path, Security, WebSocket, WebSocketDisconnect, status
|
from fastapi import APIRouter, Body, Depends, Path, Query, Security, WebSocket, WebSocketDisconnect, status
|
||||||
from fastapi.responses import JSONResponse
|
from fastapi.responses import JSONResponse
|
||||||
from redis.asyncio import Redis
|
from redis.asyncio import Redis
|
||||||
|
|
||||||
@@ -44,7 +44,7 @@ async def get_session_detail_controller(
|
|||||||
async def get_session_list_controller(
|
async def get_session_list_controller(
|
||||||
auth: Annotated[AuthSchema, Security(AuthPermission(["module_ai:chat:query"]))],
|
auth: Annotated[AuthSchema, Security(AuthPermission(["module_ai:chat:query"]))],
|
||||||
page: Annotated[PaginationQueryParam, Depends()],
|
page: Annotated[PaginationQueryParam, Depends()],
|
||||||
search: Annotated[ChatSessionQueryParam, Depends()],
|
search: Annotated[ChatSessionQueryParam, Query()],
|
||||||
) -> JSONResponse:
|
) -> JSONResponse:
|
||||||
service = ChatService(auth)
|
service = ChatService(auth)
|
||||||
result_dict = await service.page(
|
result_dict = await service.page(
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
|
|
||||||
from fastapi import APIRouter, Body, Depends, Path, Security, status
|
from fastapi import APIRouter, Body, Depends, Path, Query, Security, status
|
||||||
from fastapi.responses import JSONResponse
|
from fastapi.responses import JSONResponse
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
@@ -21,7 +21,7 @@ GenRouter = APIRouter(route_class=OperationLogRoute, prefix="/gencode", tags=["
|
|||||||
async def gen_table_list_controller(
|
async def gen_table_list_controller(
|
||||||
auth: Annotated[AuthSchema, Security(AuthPermission(["module_generator:gencode:query"]))],
|
auth: Annotated[AuthSchema, Security(AuthPermission(["module_generator:gencode:query"]))],
|
||||||
page: Annotated[PaginationQueryParam, Depends()],
|
page: Annotated[PaginationQueryParam, Depends()],
|
||||||
search: Annotated[GenTableQueryParam, Depends()],
|
search: Annotated[GenTableQueryParam, Query()],
|
||||||
db: Annotated[AsyncSession, Depends(db_getter)],
|
db: Annotated[AsyncSession, Depends(db_getter)],
|
||||||
) -> JSONResponse:
|
) -> JSONResponse:
|
||||||
order_by = [{"created_time": "desc"}]
|
order_by = [{"created_time": "desc"}]
|
||||||
@@ -40,7 +40,7 @@ async def gen_table_list_controller(
|
|||||||
async def get_gen_db_table_list_controller(
|
async def get_gen_db_table_list_controller(
|
||||||
auth: Annotated[AuthSchema, Security(AuthPermission(["module_generator:dblist:query"]))],
|
auth: Annotated[AuthSchema, Security(AuthPermission(["module_generator:dblist:query"]))],
|
||||||
page: Annotated[PaginationQueryParam, Depends()],
|
page: Annotated[PaginationQueryParam, Depends()],
|
||||||
search: Annotated[GenTableQueryParam, Depends()],
|
search: Annotated[GenTableQueryParam, Query()],
|
||||||
db: Annotated[AsyncSession, Depends(db_getter)],
|
db: Annotated[AsyncSession, Depends(db_getter)],
|
||||||
) -> JSONResponse:
|
) -> JSONResponse:
|
||||||
result_dict = await GenTableService(auth, db).get_gen_db_table_page(
|
result_dict = await GenTableService(auth, db).get_gen_db_table_page(
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
|
|
||||||
from fastapi import APIRouter, Body, Depends, Security
|
from fastapi import APIRouter, Body, Depends, Query, Security
|
||||||
from fastapi.responses import JSONResponse
|
from fastapi.responses import JSONResponse
|
||||||
from redis.asyncio.client import Redis
|
from redis.asyncio.client import Redis
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
@@ -21,7 +21,7 @@ OnlineRouter = APIRouter(route_class=OperationLogRoute, prefix="/online", tags=[
|
|||||||
async def get_online_list_controller(
|
async def get_online_list_controller(
|
||||||
redis: Annotated[Redis, Depends(redis_getter)],
|
redis: Annotated[Redis, Depends(redis_getter)],
|
||||||
page: Annotated[PaginationQueryParam, Depends()],
|
page: Annotated[PaginationQueryParam, Depends()],
|
||||||
search: Annotated[OnlineQueryParam, Depends()],
|
search: Annotated[OnlineQueryParam, Query()],
|
||||||
) -> JSONResponse:
|
) -> JSONResponse:
|
||||||
result_dict_list = await OnlineService.get_online_list(redis=redis, search=search)
|
result_dict_list = await OnlineService.get_online_list(redis=redis, search=search)
|
||||||
result_dict = await PaginationService.paginate(
|
result_dict = await PaginationService.paginate(
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ ResourceRouter = APIRouter(route_class=OperationLogRoute, prefix="/resource", ta
|
|||||||
async def get_directory_list_controller(
|
async def get_directory_list_controller(
|
||||||
request: Request,
|
request: Request,
|
||||||
page: Annotated[PaginationQueryParam, Depends()],
|
page: Annotated[PaginationQueryParam, Depends()],
|
||||||
search: Annotated[ResourceSearchQueryParam, Depends()],
|
search: Annotated[ResourceSearchQueryParam, Query()],
|
||||||
) -> JSONResponse:
|
) -> JSONResponse:
|
||||||
result_dict_list = await ResourceService.get_resources_list(search=search, base_url=str(request.base_url))
|
result_dict_list = await ResourceService.get_resources_list(search=search, base_url=str(request.base_url))
|
||||||
result_dict = await PaginationService.paginate(
|
result_dict = await PaginationService.paginate(
|
||||||
@@ -111,7 +111,7 @@ async def create_directory_controller(
|
|||||||
@ResourceRouter.post("/export", summary="导出资源列表", dependencies=[Security(AuthPermission(["module_monitor:resource:export"]))])
|
@ResourceRouter.post("/export", summary="导出资源列表", dependencies=[Security(AuthPermission(["module_monitor:resource:export"]))])
|
||||||
async def export_resource_list_controller(
|
async def export_resource_list_controller(
|
||||||
request: Request,
|
request: Request,
|
||||||
search: Annotated[ResourceSearchQueryParam, Depends()],
|
search: Annotated[ResourceSearchQueryParam, Query()],
|
||||||
) -> StreamingResponse:
|
) -> StreamingResponse:
|
||||||
result_dict_list = await ResourceService.get_resources_list(search=search, base_url=str(request.base_url))
|
result_dict_list = await ResourceService.get_resources_list(search=search, base_url=str(request.base_url))
|
||||||
export_result = await ResourceService.export_resource(data_list=result_dict_list)
|
export_result = await ResourceService.export_resource(data_list=result_dict_list)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
|
|
||||||
from fastapi import APIRouter, Body, Depends, Path, Security, status
|
from fastapi import APIRouter, Body, Depends, Path, Query, Security, status
|
||||||
from fastapi.responses import JSONResponse
|
from fastapi.responses import JSONResponse
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
@@ -19,7 +19,7 @@ DeptRouter = APIRouter(route_class=OperationLogRoute, prefix="/dept", tags=["部
|
|||||||
async def get_dept_tree_controller(
|
async def get_dept_tree_controller(
|
||||||
auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:dept:query"]))],
|
auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:dept:query"]))],
|
||||||
db: Annotated[AsyncSession, Depends(db_getter)],
|
db: Annotated[AsyncSession, Depends(db_getter)],
|
||||||
search: Annotated[DeptQueryParam, Depends()],
|
search: Annotated[DeptQueryParam, Query()],
|
||||||
) -> JSONResponse:
|
) -> JSONResponse:
|
||||||
order_by = [{"order": "asc"}]
|
order_by = [{"order": "asc"}]
|
||||||
result_dict_tree = await DeptService(auth, db).tree(search=search, order_by=order_by)
|
result_dict_tree = await DeptService(auth, db).tree(search=search, order_by=order_by)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
|
|
||||||
from fastapi import APIRouter, Body, Depends, Path, Security, status
|
from fastapi import APIRouter, Body, Depends, Path, Query, Security, status
|
||||||
from fastapi.responses import JSONResponse
|
from fastapi.responses import JSONResponse
|
||||||
from redis.asyncio.client import Redis
|
from redis.asyncio.client import Redis
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
@@ -39,7 +39,7 @@ async def get_type_detail_controller(
|
|||||||
async def get_type_list_controller(
|
async def get_type_list_controller(
|
||||||
auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:dict_type:query"]))],
|
auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:dict_type:query"]))],
|
||||||
page: Annotated[PaginationQueryParam, Depends()],
|
page: Annotated[PaginationQueryParam, Depends()],
|
||||||
search: Annotated[DictTypeQueryParam, Depends()],
|
search: Annotated[DictTypeQueryParam, Query()],
|
||||||
db: Annotated[AsyncSession, Depends(db_getter)],
|
db: Annotated[AsyncSession, Depends(db_getter)],
|
||||||
) -> JSONResponse:
|
) -> JSONResponse:
|
||||||
result_dict = await DictTypeService(auth, db).page(
|
result_dict = await DictTypeService(auth, db).page(
|
||||||
@@ -118,7 +118,7 @@ async def get_data_detail_controller(
|
|||||||
async def get_data_list_controller(
|
async def get_data_list_controller(
|
||||||
auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:dict_data:query"]))],
|
auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:dict_data:query"]))],
|
||||||
page: Annotated[PaginationQueryParam, Depends()],
|
page: Annotated[PaginationQueryParam, Depends()],
|
||||||
search: Annotated[DictDataQueryParam, Depends()],
|
search: Annotated[DictDataQueryParam, Query()],
|
||||||
db: Annotated[AsyncSession, Depends(db_getter)],
|
db: Annotated[AsyncSession, Depends(db_getter)],
|
||||||
) -> JSONResponse:
|
) -> JSONResponse:
|
||||||
order_by = [{"order": "asc"}]
|
order_by = [{"order": "asc"}]
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
|
|
||||||
from fastapi import APIRouter, Body, Depends, Path, Security
|
from fastapi import APIRouter, Body, Depends, Path, Query, Security
|
||||||
from fastapi.responses import JSONResponse, StreamingResponse
|
from fastapi.responses import JSONResponse, StreamingResponse
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
@@ -38,7 +38,7 @@ async def get_log_list_controller(
|
|||||||
auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:login_log:query"]))],
|
auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:login_log:query"]))],
|
||||||
db: Annotated[AsyncSession, Depends(db_getter)],
|
db: Annotated[AsyncSession, Depends(db_getter)],
|
||||||
page: Annotated[PaginationQueryParam, Depends()],
|
page: Annotated[PaginationQueryParam, Depends()],
|
||||||
search: Annotated[LoginLogQueryParam, Depends()],
|
search: Annotated[LoginLogQueryParam, Query()],
|
||||||
) -> JSONResponse:
|
) -> JSONResponse:
|
||||||
result_dict = await LoginLogService(auth, db).page(
|
result_dict = await LoginLogService(auth, db).page(
|
||||||
page_no=page.page_no,
|
page_no=page.page_no,
|
||||||
@@ -76,7 +76,7 @@ async def get_operation_log_list_controller(
|
|||||||
auth: Annotated[AuthSchema, Depends(get_current_user)],
|
auth: Annotated[AuthSchema, Depends(get_current_user)],
|
||||||
db: Annotated[AsyncSession, Depends(db_getter)],
|
db: Annotated[AsyncSession, Depends(db_getter)],
|
||||||
page: Annotated[PaginationQueryParam, Depends()],
|
page: Annotated[PaginationQueryParam, Depends()],
|
||||||
search: Annotated[OperationLogQueryParam, Depends()],
|
search: Annotated[OperationLogQueryParam, Query()],
|
||||||
) -> JSONResponse:
|
) -> JSONResponse:
|
||||||
result_dict = await OperationLogService(auth, db).page(
|
result_dict = await OperationLogService(auth, db).page(
|
||||||
page_no=page.page_no,
|
page_no=page.page_no,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
|
|
||||||
from fastapi import APIRouter, Body, Depends, Path, Security, status
|
from fastapi import APIRouter, Body, Depends, Path, Query, Security, status
|
||||||
from fastapi.responses import JSONResponse
|
from fastapi.responses import JSONResponse
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
@@ -19,7 +19,7 @@ MenuRouter = APIRouter(route_class=OperationLogRoute, prefix="/menu", tags=["菜
|
|||||||
async def get_menu_tree_controller(
|
async def get_menu_tree_controller(
|
||||||
auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:menu:query"]))],
|
auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:menu:query"]))],
|
||||||
db: Annotated[AsyncSession, Depends(db_getter)],
|
db: Annotated[AsyncSession, Depends(db_getter)],
|
||||||
search: Annotated[MenuQueryParam, Depends()],
|
search: Annotated[MenuQueryParam, Query()],
|
||||||
) -> JSONResponse:
|
) -> JSONResponse:
|
||||||
order_by = [{"order": "asc"}]
|
order_by = [{"order": "asc"}]
|
||||||
result_dict_tree = await MenuService(auth, db).tree(search=search, order_by=order_by)
|
result_dict_tree = await MenuService(auth, db).tree(search=search, order_by=order_by)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
|
|
||||||
from fastapi import APIRouter, Body, Depends, Path, Security, status
|
from fastapi import APIRouter, Body, Depends, Path, Query, Security, status
|
||||||
from fastapi.responses import JSONResponse
|
from fastapi.responses import JSONResponse
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
@@ -30,7 +30,7 @@ async def get_notice_list_controller(
|
|||||||
auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:notice:query"]))],
|
auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:notice:query"]))],
|
||||||
db: Annotated[AsyncSession, Depends(db_getter)],
|
db: Annotated[AsyncSession, Depends(db_getter)],
|
||||||
page: Annotated[PaginationQueryParam, Depends()],
|
page: Annotated[PaginationQueryParam, Depends()],
|
||||||
search: Annotated[NoticeQueryParam, Depends()],
|
search: Annotated[NoticeQueryParam, Query()],
|
||||||
) -> JSONResponse:
|
) -> JSONResponse:
|
||||||
result_dict = await NoticeService(auth, db).page(
|
result_dict = await NoticeService(auth, db).page(
|
||||||
page_no=page.page_no,
|
page_no=page.page_no,
|
||||||
@@ -88,4 +88,4 @@ async def get_notice_list_available_controller(
|
|||||||
db: Annotated[AsyncSession, Depends(db_getter)],
|
db: Annotated[AsyncSession, Depends(db_getter)],
|
||||||
) -> JSONResponse:
|
) -> JSONResponse:
|
||||||
result_dict = await NoticeService(auth, db).available_page()
|
result_dict = await NoticeService(auth, db).available_page()
|
||||||
return SuccessResponse(data=result_dict, msg="查询已启用公告列表成功")
|
return SuccessResponse(data=result_dict.items, msg="查询已启用公告列表成功")
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
|
|
||||||
from fastapi import APIRouter, Body, Depends, Path, Security, status
|
from fastapi import APIRouter, Body, Depends, Path, Query, Security, status
|
||||||
from fastapi.responses import JSONResponse, StreamingResponse
|
from fastapi.responses import JSONResponse, StreamingResponse
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
@@ -21,7 +21,7 @@ async def get_obj_list_controller(
|
|||||||
auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:position:query"]))],
|
auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:position:query"]))],
|
||||||
db: Annotated[AsyncSession, Depends(db_getter)],
|
db: Annotated[AsyncSession, Depends(db_getter)],
|
||||||
page: Annotated[PaginationQueryParam, Depends()],
|
page: Annotated[PaginationQueryParam, Depends()],
|
||||||
search: Annotated[PositionQueryParam, Depends()],
|
search: Annotated[PositionQueryParam, Query()],
|
||||||
) -> JSONResponse:
|
) -> JSONResponse:
|
||||||
result_dict = await PositionService(auth, db).page(
|
result_dict = await PositionService(auth, db).page(
|
||||||
page_no=page.page_no,
|
page_no=page.page_no,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
|
|
||||||
from fastapi import APIRouter, Body, Depends, Path, Security, status
|
from fastapi import APIRouter, Body, Depends, Path, Query, Security, status
|
||||||
from fastapi.responses import JSONResponse, StreamingResponse
|
from fastapi.responses import JSONResponse, StreamingResponse
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
@@ -21,7 +21,7 @@ async def get_role_list_controller(
|
|||||||
auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:role:query"]))],
|
auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:role:query"]))],
|
||||||
db: Annotated[AsyncSession, Depends(db_getter)],
|
db: Annotated[AsyncSession, Depends(db_getter)],
|
||||||
page: Annotated[PaginationQueryParam, Depends()],
|
page: Annotated[PaginationQueryParam, Depends()],
|
||||||
search: Annotated[RoleQueryParam, Depends()],
|
search: Annotated[RoleQueryParam, Query()],
|
||||||
) -> JSONResponse:
|
) -> JSONResponse:
|
||||||
result_dict = await RoleService(auth, db).page(
|
result_dict = await RoleService(auth, db).page(
|
||||||
page_no=page.page_no,
|
page_no=page.page_no,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
|
|
||||||
from fastapi import APIRouter, Body, Depends, Path, Security, status
|
from fastapi import APIRouter, Body, Depends, Path, Query, Security, status
|
||||||
from fastapi.responses import JSONResponse, StreamingResponse
|
from fastapi.responses import JSONResponse, StreamingResponse
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
@@ -21,7 +21,7 @@ async def ticket_list_controller(
|
|||||||
auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:ticket:query"]))],
|
auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:ticket:query"]))],
|
||||||
db: Annotated[AsyncSession, Depends(db_getter)],
|
db: Annotated[AsyncSession, Depends(db_getter)],
|
||||||
page: Annotated[PaginationQueryParam, Depends()],
|
page: Annotated[PaginationQueryParam, Depends()],
|
||||||
search: Annotated[TicketQueryParam, Depends()],
|
search: Annotated[TicketQueryParam, Query()],
|
||||||
) -> JSONResponse:
|
) -> JSONResponse:
|
||||||
result = await TicketService(auth, db).page(
|
result = await TicketService(auth, db).page(
|
||||||
page_no=page.page_no,
|
page_no=page.page_no,
|
||||||
|
|||||||
@@ -3,15 +3,11 @@ from typing import Annotated
|
|||||||
|
|
||||||
from fastapi import APIRouter, Body, Depends, File, Path, Query, Security, UploadFile, status
|
from fastapi import APIRouter, Body, Depends, File, Path, Query, Security, UploadFile, status
|
||||||
from fastapi.responses import JSONResponse, StreamingResponse
|
from fastapi.responses import JSONResponse, StreamingResponse
|
||||||
from redis.asyncio.client import Redis
|
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from app.api.v1.module_system.auth.service import CaptchaService
|
|
||||||
from app.common.response import ResponseSchema, StreamResponse, SuccessResponse
|
from app.common.response import ResponseSchema, StreamResponse, SuccessResponse
|
||||||
from app.config.setting import settings
|
|
||||||
from app.core.base_schema import AuthSchema, BatchSetAvailable, PageResultSchema, PaginationQueryParam
|
from app.core.base_schema import AuthSchema, BatchSetAvailable, PageResultSchema, PaginationQueryParam
|
||||||
from app.core.dependencies import AuthPermission, db_getter, get_current_user, redis_getter
|
from app.core.dependencies import AuthPermission, db_getter, get_current_user
|
||||||
from app.core.exceptions import CustomException
|
|
||||||
from app.core.logger import logger
|
from app.core.logger import logger
|
||||||
from app.core.router_class import OperationLogRoute
|
from app.core.router_class import OperationLogRoute
|
||||||
from app.utils.common_util import bytes2file_response
|
from app.utils.common_util import bytes2file_response
|
||||||
@@ -78,18 +74,8 @@ async def reset_password_controller(
|
|||||||
@UserRouter.post("/password/forget", summary="忘记密码", response_model=ResponseSchema[UserOutSchema])
|
@UserRouter.post("/password/forget", summary="忘记密码", response_model=ResponseSchema[UserOutSchema])
|
||||||
async def forget_password_controller(
|
async def forget_password_controller(
|
||||||
db: Annotated[AsyncSession, Depends(db_getter)],
|
db: Annotated[AsyncSession, Depends(db_getter)],
|
||||||
redis: Annotated[Redis, Depends(redis_getter)],
|
|
||||||
data: Annotated[UserForgetPasswordSchema, Body(description="忘记密码参数")],
|
data: Annotated[UserForgetPasswordSchema, Body(description="忘记密码参数")],
|
||||||
) -> JSONResponse:
|
) -> JSONResponse:
|
||||||
# 安全加固:忘记密码必须先校验图形验证码(防暴力枚举用户名/手机号接管账户)
|
|
||||||
if settings.CAPTCHA_ENABLE:
|
|
||||||
if not data.captcha_key or not data.captcha:
|
|
||||||
raise CustomException(msg="验证码不能为空")
|
|
||||||
await CaptchaService.check_captcha(
|
|
||||||
redis=redis,
|
|
||||||
key=data.captcha_key,
|
|
||||||
)
|
|
||||||
|
|
||||||
auth = AuthSchema()
|
auth = AuthSchema()
|
||||||
user_forget_password_result = await UserService(auth, db).forget_password(data=data)
|
user_forget_password_result = await UserService(auth, db).forget_password(data=data)
|
||||||
logger.info(f"{data.username} 重置密码成功")
|
logger.info(f"{data.username} 重置密码成功")
|
||||||
@@ -99,18 +85,8 @@ async def forget_password_controller(
|
|||||||
@UserRouter.post("/register", summary="用户注册", response_model=ResponseSchema[UserOutSchema])
|
@UserRouter.post("/register", summary="用户注册", response_model=ResponseSchema[UserOutSchema])
|
||||||
async def register_controller(
|
async def register_controller(
|
||||||
db: Annotated[AsyncSession, Depends(db_getter)],
|
db: Annotated[AsyncSession, Depends(db_getter)],
|
||||||
redis: Annotated[Redis, Depends(redis_getter)],
|
|
||||||
data: Annotated[UserRegisterSchema, Body(description="用户注册参数")],
|
data: Annotated[UserRegisterSchema, Body(description="用户注册参数")],
|
||||||
) -> JSONResponse:
|
) -> JSONResponse:
|
||||||
# 安全加固:注册必须先校验图形验证码(防暴力注册)
|
|
||||||
if settings.CAPTCHA_ENABLE:
|
|
||||||
if not data.captcha_key or not data.captcha:
|
|
||||||
raise CustomException(msg="验证码不能为空")
|
|
||||||
await CaptchaService.check_captcha(
|
|
||||||
redis=redis,
|
|
||||||
key=data.captcha_key,
|
|
||||||
)
|
|
||||||
|
|
||||||
auth = AuthSchema()
|
auth = AuthSchema()
|
||||||
register_result = await UserService(auth, db).register(data=data)
|
register_result = await UserService(auth, db).register(data=data)
|
||||||
logger.info(f"新用户注册成功: {data.username}")
|
logger.info(f"新用户注册成功: {data.username}")
|
||||||
@@ -122,7 +98,7 @@ async def get_user_list_controller(
|
|||||||
auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:user:query"]))],
|
auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:user:query"]))],
|
||||||
db: Annotated[AsyncSession, Depends(db_getter)],
|
db: Annotated[AsyncSession, Depends(db_getter)],
|
||||||
page: Annotated[PaginationQueryParam, Depends()],
|
page: Annotated[PaginationQueryParam, Depends()],
|
||||||
search: Annotated[UserQueryParam, Depends()],
|
search: Annotated[UserQueryParam, Query()],
|
||||||
) -> JSONResponse:
|
) -> JSONResponse:
|
||||||
result_dict = await UserService(auth, db).page(
|
result_dict = await UserService(auth, db).page(
|
||||||
page_no=page.page_no,
|
page_no=page.page_no,
|
||||||
|
|||||||
@@ -71,9 +71,6 @@ class UserForgetPasswordSchema(BaseModel):
|
|||||||
|
|
||||||
username: str = Field(..., min_length=3, max_length=32, description="用户名")
|
username: str = Field(..., min_length=3, max_length=32, description="用户名")
|
||||||
new_password: str = Field(..., min_length=6, max_length=128, description="新密码")
|
new_password: str = Field(..., min_length=6, max_length=128, description="新密码")
|
||||||
mobile: str | None = Field(default=None, max_length=11, description="手机号")
|
|
||||||
captcha_key: str | None = Field(default=None, description="图形验证码 key(必填,防暴力枚举)")
|
|
||||||
captcha: str | None = Field(default=None, description="图形验证码")
|
|
||||||
|
|
||||||
@field_validator("username")
|
@field_validator("username")
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -97,12 +94,6 @@ class UserForgetPasswordSchema(BaseModel):
|
|||||||
raise ValueError("密码长度不能超过 128 位")
|
raise ValueError("密码长度不能超过 128 位")
|
||||||
return value
|
return value
|
||||||
|
|
||||||
@field_validator("mobile")
|
|
||||||
@classmethod
|
|
||||||
def validate_mobile(cls, value: str | None):
|
|
||||||
"""校验手机号格式"""
|
|
||||||
return mobile_validator(value)
|
|
||||||
|
|
||||||
|
|
||||||
class UserChangePasswordSchema(BaseModel):
|
class UserChangePasswordSchema(BaseModel):
|
||||||
"""修改密码"""
|
"""修改密码"""
|
||||||
@@ -189,8 +180,6 @@ class UserRegisterSchema(BaseModel):
|
|||||||
password: str = Field(..., min_length=6, max_length=128, description="密码")
|
password: str = Field(..., min_length=6, max_length=128, description="密码")
|
||||||
email: EmailStr | None = Field(default=None, description="邮箱")
|
email: EmailStr | None = Field(default=None, description="邮箱")
|
||||||
name: str | None = Field(default=None, max_length=32, description="名称")
|
name: str | None = Field(default=None, max_length=32, description="名称")
|
||||||
captcha_key: str | None = Field(default=None, description="图形验证码 key(必填,防暴力枚举)")
|
|
||||||
captcha: str | None = Field(default=None, description="图形验证码")
|
|
||||||
|
|
||||||
@field_validator("username")
|
@field_validator("username")
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|||||||
@@ -29,8 +29,8 @@ from .schema import (
|
|||||||
UserUpdateSchema,
|
UserUpdateSchema,
|
||||||
)
|
)
|
||||||
|
|
||||||
# 用户管理列表/详情预加载:只需 dept(用于填充 dept_name)
|
# 用户管理列表/详情预加载
|
||||||
_USER_PRELOAD = ["dept"]
|
_USER_PRELOAD = ["dept", "roles", "positions"]
|
||||||
|
|
||||||
# 当前用户信息预加载:需完整嵌套关联
|
# 当前用户信息预加载:需完整嵌套关联
|
||||||
_USER_CURRENT_PRELOAD = ["dept", "positions", "roles.menus", "roles.depts"]
|
_USER_CURRENT_PRELOAD = ["dept", "positions", "roles.menus", "roles.depts"]
|
||||||
@@ -48,6 +48,8 @@ class UserService:
|
|||||||
result = UserOutSchema.model_validate(user)
|
result = UserOutSchema.model_validate(user)
|
||||||
if user.dept:
|
if user.dept:
|
||||||
result.dept_name = user.dept.name
|
result.dept_name = user.dept.name
|
||||||
|
result.role_ids = [r.id for r in user.roles]
|
||||||
|
result.position_ids = [p.id for p in user.positions]
|
||||||
return result
|
return result
|
||||||
|
|
||||||
async def get_list(
|
async def get_list(
|
||||||
@@ -60,6 +62,8 @@ class UserService:
|
|||||||
for user, item in zip(user_list, result, strict=True):
|
for user, item in zip(user_list, result, strict=True):
|
||||||
if user.dept:
|
if user.dept:
|
||||||
item.dept_name = user.dept.name
|
item.dept_name = user.dept.name
|
||||||
|
item.role_ids = [r.id for r in user.roles]
|
||||||
|
item.position_ids = [p.id for p in user.positions]
|
||||||
return result
|
return result
|
||||||
|
|
||||||
async def page(
|
async def page(
|
||||||
@@ -70,14 +74,23 @@ class UserService:
|
|||||||
order_by: list[dict[str, str]] | None = None,
|
order_by: list[dict[str, str]] | None = None,
|
||||||
) -> PageResultSchema[UserOutSchema]:
|
) -> PageResultSchema[UserOutSchema]:
|
||||||
offset = (page_no - 1) * page_size
|
offset = (page_no - 1) * page_size
|
||||||
return await UserCRUD(self.auth, self.db).page(
|
page_result = await UserCRUD(self.auth, self.db).page(
|
||||||
offset=offset,
|
offset=offset,
|
||||||
limit=page_size,
|
limit=page_size,
|
||||||
order_by=order_by or [{"id": "asc"}],
|
order_by=order_by or [{"id": "asc"}],
|
||||||
search=search_to_dict(search),
|
search=search_to_dict(search),
|
||||||
out_schema=UserOutSchema,
|
|
||||||
preload=_USER_PRELOAD,
|
preload=_USER_PRELOAD,
|
||||||
)
|
)
|
||||||
|
items: list[UserOutSchema] = []
|
||||||
|
for user in page_result.items:
|
||||||
|
item = UserOutSchema.model_validate(user)
|
||||||
|
if user.dept:
|
||||||
|
item.dept_name = user.dept.name
|
||||||
|
item.role_ids = [r.id for r in user.roles]
|
||||||
|
item.position_ids = [p.id for p in user.positions]
|
||||||
|
items.append(item)
|
||||||
|
page_result.items = items
|
||||||
|
return page_result # type: ignore[return-value]
|
||||||
|
|
||||||
async def create(self, data: UserCreateSchema) -> UserOutSchema:
|
async def create(self, data: UserCreateSchema) -> UserOutSchema:
|
||||||
if data.is_superuser:
|
if data.is_superuser:
|
||||||
@@ -263,8 +276,6 @@ class UserService:
|
|||||||
raise CustomException(msg="用户已停用")
|
raise CustomException(msg="用户已停用")
|
||||||
if user.is_superuser:
|
if user.is_superuser:
|
||||||
raise CustomException(msg="超级管理员密码不能重置")
|
raise CustomException(msg="超级管理员密码不能重置")
|
||||||
if data.mobile and user.mobile != data.mobile:
|
|
||||||
raise CustomException(msg="手机号不匹配")
|
|
||||||
|
|
||||||
new_password_hash = PwdUtil.hash_password(password=data.new_password)
|
new_password_hash = PwdUtil.hash_password(password=data.new_password)
|
||||||
await UserCRUD(self.auth, self.db).change_password(id=user.id, password_hash=new_password_hash)
|
await UserCRUD(self.auth, self.db).change_password(id=user.id, password_hash=new_password_hash)
|
||||||
@@ -280,12 +291,14 @@ class UserService:
|
|||||||
username=data.username,
|
username=data.username,
|
||||||
password=PwdUtil.hash_password(password=data.password),
|
password=PwdUtil.hash_password(password=data.password),
|
||||||
name=data.name or data.username,
|
name=data.name or data.username,
|
||||||
email=data.email,
|
|
||||||
status=0,
|
status=0,
|
||||||
)
|
)
|
||||||
new_user = await UserCRUD(self.auth, self.db).create_obj_crud(data=create_data)
|
create_data_dict = create_data.model_dump(exclude_none=True, exclude={"role_ids", "position_ids"})
|
||||||
|
new_user = await UserCRUD(self.auth, self.db).create(data=create_data_dict)
|
||||||
if not new_user:
|
if not new_user:
|
||||||
raise CustomException(msg="注册失败")
|
raise CustomException(msg="注册失败")
|
||||||
|
# 注册时 auth 无用户,created_id/updated_id 未设置,补充为自身ID
|
||||||
|
await UserCRUD(self.auth, self.db).set([new_user.id], created_id=new_user.id, updated_id=new_user.id)
|
||||||
logger.info(f"新用户注册成功: {data.username}")
|
logger.info(f"新用户注册成功: {data.username}")
|
||||||
return await self.detail(id=new_user.id)
|
return await self.detail(id=new_user.id)
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
|
|
||||||
from fastapi import APIRouter, Body, Depends, Path, Security, status
|
from fastapi import APIRouter, Body, Depends, Path, Query, Security, status
|
||||||
from fastapi.responses import JSONResponse
|
from fastapi.responses import JSONResponse
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
@@ -26,7 +26,7 @@ async def get_version_list_controller(
|
|||||||
auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:version:query"]))],
|
auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:version:query"]))],
|
||||||
db: Annotated[AsyncSession, Depends(db_getter)],
|
db: Annotated[AsyncSession, Depends(db_getter)],
|
||||||
page: Annotated[PaginationQueryParam, Depends()],
|
page: Annotated[PaginationQueryParam, Depends()],
|
||||||
search: Annotated[VersionQueryParam, Depends()],
|
search: Annotated[VersionQueryParam, Query()],
|
||||||
) -> JSONResponse:
|
) -> JSONResponse:
|
||||||
service = VersionService(auth, db)
|
service = VersionService(auth, db)
|
||||||
result = await service.page(page_no=page.page_no, page_size=page.page_size, search=search)
|
result = await service.page(page_no=page.page_no, page_size=page.page_size, search=search)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
|
|
||||||
from fastapi import APIRouter, Body, Depends, Path, Security
|
from fastapi import APIRouter, Body, Depends, Path, Query, Security
|
||||||
from fastapi.responses import JSONResponse
|
from fastapi.responses import JSONResponse
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
@@ -109,7 +109,7 @@ async def remove_job_controller(
|
|||||||
async def get_job_log_list_controller(
|
async def get_job_log_list_controller(
|
||||||
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:cronjob:job:query"]))],
|
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:cronjob:job:query"]))],
|
||||||
page: Annotated[PaginationQueryParam, Depends()],
|
page: Annotated[PaginationQueryParam, Depends()],
|
||||||
search: Annotated[JobQueryParam, Depends()],
|
search: Annotated[JobQueryParam, Query()],
|
||||||
db: Annotated[AsyncSession, Depends(db_getter)],
|
db: Annotated[AsyncSession, Depends(db_getter)],
|
||||||
) -> JSONResponse:
|
) -> JSONResponse:
|
||||||
result_dict = await JobService(auth, db).get_job_log_page(
|
result_dict = await JobService(auth, db).get_job_log_page(
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
|
|
||||||
from fastapi import APIRouter, Body, Depends, Path, Security, status
|
from fastapi import APIRouter, Body, Depends, Path, Query, Security, status
|
||||||
from fastapi.responses import JSONResponse
|
from fastapi.responses import JSONResponse
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
@@ -40,7 +40,7 @@ async def get_obj_detail_controller(
|
|||||||
async def get_obj_list_controller(
|
async def get_obj_list_controller(
|
||||||
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:cronjob:node:query"]))],
|
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:cronjob:node:query"]))],
|
||||||
page: Annotated[PaginationQueryParam, Depends()],
|
page: Annotated[PaginationQueryParam, Depends()],
|
||||||
search: Annotated[NodeQueryParam, Depends()],
|
search: Annotated[NodeQueryParam, Query()],
|
||||||
db: Annotated[AsyncSession, Depends(db_getter)],
|
db: Annotated[AsyncSession, Depends(db_getter)],
|
||||||
) -> JSONResponse:
|
) -> JSONResponse:
|
||||||
service = NodeService(auth, db)
|
service = NodeService(auth, db)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
|
|
||||||
from fastapi import APIRouter, Body, Depends, Path, Security, status
|
from fastapi import APIRouter, Body, Depends, Path, Query, Security, status
|
||||||
from fastapi.responses import JSONResponse
|
from fastapi.responses import JSONResponse
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
@@ -30,7 +30,7 @@ async def get_workflow_list_controller(
|
|||||||
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:workflow:flow:query"]))],
|
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:workflow:flow:query"]))],
|
||||||
db: Annotated[AsyncSession, Depends(db_getter)],
|
db: Annotated[AsyncSession, Depends(db_getter)],
|
||||||
page: Annotated[PaginationQueryParam, Depends()],
|
page: Annotated[PaginationQueryParam, Depends()],
|
||||||
search: Annotated[WorkflowQueryParam, Depends()],
|
search: Annotated[WorkflowQueryParam, Query()],
|
||||||
) -> JSONResponse:
|
) -> JSONResponse:
|
||||||
result_dict = await WorkflowService(auth, db).get_workflow_page(
|
result_dict = await WorkflowService(auth, db).get_workflow_page(
|
||||||
page_no=page.page_no,
|
page_no=page.page_no,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
|
|
||||||
from fastapi import APIRouter, Body, Depends, Path, Security, status
|
from fastapi import APIRouter, Body, Depends, Path, Query, Security, status
|
||||||
from fastapi.responses import JSONResponse
|
from fastapi.responses import JSONResponse
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
@@ -41,7 +41,7 @@ async def get_workflow_node_type_list_controller(
|
|||||||
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:workflow:nodes:query"]))],
|
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:workflow:nodes:query"]))],
|
||||||
db: Annotated[AsyncSession, Depends(db_getter)],
|
db: Annotated[AsyncSession, Depends(db_getter)],
|
||||||
page: Annotated[PaginationQueryParam, Depends()],
|
page: Annotated[PaginationQueryParam, Depends()],
|
||||||
search: Annotated[WorkflowNodeTypeQueryParam, Depends()],
|
search: Annotated[WorkflowNodeTypeQueryParam, Query()],
|
||||||
) -> JSONResponse:
|
) -> JSONResponse:
|
||||||
service = WorkflowNodeTypeService(auth, db)
|
service = WorkflowNodeTypeService(auth, db)
|
||||||
result_dict = await service.get_page(
|
result_dict = await service.get_page(
|
||||||
|
|||||||
@@ -2,13 +2,12 @@
|
|||||||
import urllib.parse
|
import urllib.parse
|
||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
|
|
||||||
from fastapi import APIRouter, Body, Depends, File, Path, Query, Security, UploadFile
|
from fastapi import APIRouter, Body, Depends, File, Path, Query, UploadFile, status
|
||||||
from fastapi.responses import JSONResponse, StreamingResponse
|
from fastapi.responses import JSONResponse, StreamingResponse
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from app.core.base_schema import AuthSchema
|
|
||||||
from app.common.response import ResponseSchema, StreamResponse, SuccessResponse
|
from app.common.response import ResponseSchema, StreamResponse, SuccessResponse
|
||||||
from app.core.base_schema import BatchSetAvailable, PageResultSchema, PaginationQueryParam
|
from app.core.base_schema import AuthSchema, BatchSetAvailable, PageResultSchema, PaginationQueryParam
|
||||||
from app.core.dependencies import AuthPermission, db_getter
|
from app.core.dependencies import AuthPermission, db_getter
|
||||||
from app.core.router_class import OperationLogRoute
|
from app.core.router_class import OperationLogRoute
|
||||||
from app.utils.common_util import bytes2file_response
|
from app.utils.common_util import bytes2file_response
|
||||||
@@ -19,57 +18,26 @@ from .service import {{ class_name }}Service
|
|||||||
{{ class_name }}Router = APIRouter(route_class=OperationLogRoute, prefix="/{{ module_name }}", tags=["{{ function_name }}模块"])
|
{{ class_name }}Router = APIRouter(route_class=OperationLogRoute, prefix="/{{ module_name }}", tags=["{{ function_name }}模块"])
|
||||||
|
|
||||||
|
|
||||||
@{{ class_name }}Router.get(
|
@{{ class_name }}Router.get("/detail/{id}", summary="获取{{ function_name }}详情", response_model=ResponseSchema[{{ class_name }}OutSchema])
|
||||||
"/detail/{id}",
|
|
||||||
summary="获取{{ function_name }}详情",
|
|
||||||
response_model=ResponseSchema[{{ class_name }}OutSchema],
|
|
||||||
)
|
|
||||||
async def get_obj_detail_controller(
|
async def get_obj_detail_controller(
|
||||||
|
auth: Annotated[AuthSchema, Depends(AuthPermission(["{{ permission_prefix }}:detail"]))],
|
||||||
id: Annotated[int, Path(description="{{ function_name }}ID")],
|
id: Annotated[int, Path(description="{{ function_name }}ID")],
|
||||||
auth: Annotated[AuthSchema, Security(AuthPermission(["{{ permission_prefix }}:detail"]))],
|
|
||||||
db: Annotated[AsyncSession, Depends(db_getter)],
|
db: Annotated[AsyncSession, Depends(db_getter)],
|
||||||
) -> JSONResponse:
|
) -> JSONResponse:
|
||||||
"""
|
service = {{ class_name }}Service(auth, db)
|
||||||
获取{{ function_name }}详情
|
result_dict = await service.detail(id=id)
|
||||||
|
|
||||||
参数:
|
|
||||||
- id (int): {{ function_name }}ID
|
|
||||||
- auth (AuthSchema): 认证信息模型
|
|
||||||
- db (AsyncSession): 数据库会话
|
|
||||||
|
|
||||||
返回:
|
|
||||||
- JSONResponse: 包含{{ function_name }}详情的JSON响应
|
|
||||||
"""
|
|
||||||
result_dict = await {{ class_name }}Service.detail_service(id=id, auth=auth, db=db)
|
|
||||||
return SuccessResponse(data=result_dict, msg="获取{{ function_name }}详情成功")
|
return SuccessResponse(data=result_dict, msg="获取{{ function_name }}详情成功")
|
||||||
|
|
||||||
|
|
||||||
@{{ class_name }}Router.get(
|
@{{ class_name }}Router.get("/list", summary="分页查询{{ function_name }}", response_model=ResponseSchema[PageResultSchema[{{ class_name }}OutSchema]])
|
||||||
"/list",
|
|
||||||
summary="分页查询{{ function_name }}",
|
|
||||||
response_model=ResponseSchema[PageResultSchema[{{ class_name }}OutSchema]],
|
|
||||||
)
|
|
||||||
async def get_obj_list_controller(
|
async def get_obj_list_controller(
|
||||||
|
auth: Annotated[AuthSchema, Depends(AuthPermission(["{{ permission_prefix }}:query"]))],
|
||||||
page: Annotated[PaginationQueryParam, Depends()],
|
page: Annotated[PaginationQueryParam, Depends()],
|
||||||
search: Annotated[{{ class_name }}QueryParam, Query()],
|
search: Annotated[{{ class_name }}QueryParam, Query()],
|
||||||
auth: Annotated[AuthSchema, Security(AuthPermission(["{{ permission_prefix }}:query"]))],
|
|
||||||
db: Annotated[AsyncSession, Depends(db_getter)],
|
db: Annotated[AsyncSession, Depends(db_getter)],
|
||||||
) -> JSONResponse:
|
) -> JSONResponse:
|
||||||
"""
|
service = {{ class_name }}Service(auth, db)
|
||||||
查询{{ function_name }}列表
|
result_dict = await service.page(
|
||||||
|
|
||||||
参数:
|
|
||||||
- page (PaginationQueryParam): 分页查询参数
|
|
||||||
- search ({{ class_name }}QueryParam): 查询参数
|
|
||||||
- auth (AuthSchema): 认证信息模型
|
|
||||||
- db (AsyncSession): 数据库会话
|
|
||||||
|
|
||||||
返回:
|
|
||||||
- JSONResponse: 包含{{ function_name }}列表分页信息的JSON响应
|
|
||||||
"""
|
|
||||||
result_dict = await {{ class_name }}Service.page_service(
|
|
||||||
auth=auth,
|
|
||||||
db=db,
|
|
||||||
page_no=page.page_no,
|
page_no=page.page_no,
|
||||||
page_size=page.page_size,
|
page_size=page.page_size,
|
||||||
search=search,
|
search=search,
|
||||||
@@ -78,130 +46,60 @@ async def get_obj_list_controller(
|
|||||||
return SuccessResponse(data=result_dict, msg="查询{{ function_name }}列表成功")
|
return SuccessResponse(data=result_dict, msg="查询{{ function_name }}列表成功")
|
||||||
|
|
||||||
|
|
||||||
@{{ class_name }}Router.post(
|
@{{ class_name }}Router.post("/create", status_code=status.HTTP_201_CREATED, summary="创建{{ function_name }}", response_model=ResponseSchema[{{ class_name }}OutSchema])
|
||||||
"/create",
|
|
||||||
summary="创建{{ function_name }}",
|
|
||||||
response_model=ResponseSchema[{{ class_name }}OutSchema],
|
|
||||||
)
|
|
||||||
async def create_obj_controller(
|
async def create_obj_controller(
|
||||||
|
auth: Annotated[AuthSchema, Depends(AuthPermission(["{{ permission_prefix }}:create"]))],
|
||||||
data: Annotated[{{ class_name }}CreateSchema, Body(description="创建参数")],
|
data: Annotated[{{ class_name }}CreateSchema, Body(description="创建参数")],
|
||||||
auth: Annotated[AuthSchema, Security(AuthPermission(["{{ permission_prefix }}:create"]))],
|
|
||||||
db: Annotated[AsyncSession, Depends(db_getter)],
|
db: Annotated[AsyncSession, Depends(db_getter)],
|
||||||
) -> JSONResponse:
|
) -> JSONResponse:
|
||||||
"""
|
service = {{ class_name }}Service(auth, db)
|
||||||
创建{{ function_name }}
|
result_dict = await service.create(data=data)
|
||||||
|
|
||||||
参数:
|
|
||||||
- data ({{ class_name }}CreateSchema): {{ function_name }}创建模型
|
|
||||||
- auth (AuthSchema): 认证信息模型
|
|
||||||
- db (AsyncSession): 数据库会话
|
|
||||||
|
|
||||||
返回:
|
|
||||||
- JSONResponse: 包含创建{{ function_name }}详情的JSON响应
|
|
||||||
"""
|
|
||||||
result_dict = await {{ class_name }}Service.create_service(auth=auth, db=db, data=data)
|
|
||||||
return SuccessResponse(data=result_dict, msg="创建{{ function_name }}成功")
|
return SuccessResponse(data=result_dict, msg="创建{{ function_name }}成功")
|
||||||
|
|
||||||
|
|
||||||
@{{ class_name }}Router.put(
|
@{{ class_name }}Router.put("/update/{id}", summary="修改{{ function_name }}", response_model=ResponseSchema[{{ class_name }}OutSchema])
|
||||||
"/update/{id}",
|
|
||||||
summary="修改{{ function_name }}",
|
|
||||||
response_model=ResponseSchema[{{ class_name }}OutSchema],
|
|
||||||
)
|
|
||||||
async def update_obj_controller(
|
async def update_obj_controller(
|
||||||
data: Annotated[{{ class_name }}UpdateSchema, Body(description="更新参数")],
|
auth: Annotated[AuthSchema, Depends(AuthPermission(["{{ permission_prefix }}:update"]))],
|
||||||
id: Annotated[int, Path(description="{{ function_name }}ID")],
|
id: Annotated[int, Path(description="{{ function_name }}ID")],
|
||||||
auth: Annotated[AuthSchema, Security(AuthPermission(["{{ permission_prefix }}:update"]))],
|
data: Annotated[{{ class_name }}UpdateSchema, Body(description="修改参数")],
|
||||||
db: Annotated[AsyncSession, Depends(db_getter)],
|
db: Annotated[AsyncSession, Depends(db_getter)],
|
||||||
) -> JSONResponse:
|
) -> JSONResponse:
|
||||||
"""
|
service = {{ class_name }}Service(auth, db)
|
||||||
修改{{ function_name }}
|
result_dict = await service.update(id=id, data=data)
|
||||||
|
|
||||||
参数:
|
|
||||||
- data ({{ class_name }}UpdateSchema): {{ function_name }}更新模型
|
|
||||||
- id (int): {{ function_name }}ID
|
|
||||||
- auth (AuthSchema): 认证信息模型
|
|
||||||
- db (AsyncSession): 数据库会话
|
|
||||||
|
|
||||||
返回:
|
|
||||||
- JSONResponse: 包含修改{{ function_name }}详情的JSON响应
|
|
||||||
"""
|
|
||||||
result_dict = await {{ class_name }}Service.update_service(auth=auth, db=db, id=id, data=data)
|
|
||||||
return SuccessResponse(data=result_dict, msg="修改{{ function_name }}成功")
|
return SuccessResponse(data=result_dict, msg="修改{{ function_name }}成功")
|
||||||
|
|
||||||
|
|
||||||
@{{ class_name }}Router.delete(
|
@{{ class_name }}Router.delete("/delete", summary="删除{{ function_name }}", response_model=ResponseSchema[None])
|
||||||
"/delete",
|
|
||||||
summary="删除{{ function_name }}",
|
|
||||||
response_model=ResponseSchema[None],
|
|
||||||
)
|
|
||||||
async def delete_obj_controller(
|
async def delete_obj_controller(
|
||||||
|
auth: Annotated[AuthSchema, Depends(AuthPermission(["{{ permission_prefix }}:delete"]))],
|
||||||
ids: Annotated[list[int], Body(description="ID列表")],
|
ids: Annotated[list[int], Body(description="ID列表")],
|
||||||
auth: Annotated[AuthSchema, Security(AuthPermission(["{{ permission_prefix }}:delete"]))],
|
|
||||||
db: Annotated[AsyncSession, Depends(db_getter)],
|
db: Annotated[AsyncSession, Depends(db_getter)],
|
||||||
) -> JSONResponse:
|
) -> JSONResponse:
|
||||||
"""
|
service = {{ class_name }}Service(auth, db)
|
||||||
删除{{ function_name }}
|
await service.delete(ids=ids)
|
||||||
|
|
||||||
参数:
|
|
||||||
- ids (list[int]): {{ function_name }}ID列表
|
|
||||||
- auth (AuthSchema): 认证信息模型
|
|
||||||
- db (AsyncSession): 数据库会话
|
|
||||||
|
|
||||||
返回:
|
|
||||||
- JSONResponse: 包含删除{{ function_name }}详情的JSON响应
|
|
||||||
"""
|
|
||||||
await {{ class_name }}Service.delete_service(auth=auth, db=db, ids=ids)
|
|
||||||
return SuccessResponse(msg="删除{{ function_name }}成功")
|
return SuccessResponse(msg="删除{{ function_name }}成功")
|
||||||
|
|
||||||
|
|
||||||
@{{ class_name }}Router.patch(
|
@{{ class_name }}Router.patch("/status/batch", summary="批量修改{{ function_name }}状态", response_model=ResponseSchema[None])
|
||||||
"/status/batch",
|
|
||||||
summary="批量修改{{ function_name }}状态",
|
|
||||||
response_model=ResponseSchema[None],
|
|
||||||
)
|
|
||||||
async def batch_set_available_obj_controller(
|
async def batch_set_available_obj_controller(
|
||||||
|
auth: Annotated[AuthSchema, Depends(AuthPermission(["{{ permission_prefix }}:patch"]))],
|
||||||
data: Annotated[BatchSetAvailable, Body(description="状态设置")],
|
data: Annotated[BatchSetAvailable, Body(description="状态设置")],
|
||||||
auth: Annotated[AuthSchema, Security(AuthPermission(["{{ permission_prefix }}:patch"]))],
|
|
||||||
db: Annotated[AsyncSession, Depends(db_getter)],
|
db: Annotated[AsyncSession, Depends(db_getter)],
|
||||||
) -> JSONResponse:
|
) -> JSONResponse:
|
||||||
"""
|
service = {{ class_name }}Service(auth, db)
|
||||||
批量修改{{ function_name }}状态
|
await service.set_available(data=data)
|
||||||
|
|
||||||
参数:
|
|
||||||
- data (BatchSetAvailable): 批量修改{{ function_name }}状态模型
|
|
||||||
- auth (AuthSchema): 认证信息模型
|
|
||||||
- db (AsyncSession): 数据库会话
|
|
||||||
|
|
||||||
返回:
|
|
||||||
- JSONResponse: 包含批量修改{{ function_name }}状态详情的JSON响应
|
|
||||||
"""
|
|
||||||
await {{ class_name }}Service.set_available_service(auth=auth, db=db, data=data)
|
|
||||||
return SuccessResponse(msg="批量修改{{ function_name }}状态成功")
|
return SuccessResponse(msg="批量修改{{ function_name }}状态成功")
|
||||||
|
|
||||||
|
|
||||||
@{{ class_name }}Router.post(
|
@{{ class_name }}Router.post("/export", summary="导出{{ function_name }}")
|
||||||
"/export",
|
|
||||||
summary="导出{{ function_name }}",
|
|
||||||
)
|
|
||||||
async def export_obj_list_controller(
|
async def export_obj_list_controller(
|
||||||
search: Annotated[{{ class_name }}QueryParam, Query(description="查询参数")],
|
auth: Annotated[AuthSchema, Depends(AuthPermission(["{{ permission_prefix }}:export"]))],
|
||||||
auth: Annotated[AuthSchema, Security(AuthPermission(["{{ permission_prefix }}:export"]))],
|
search: Annotated[{{ class_name }}QueryParam, Query()],
|
||||||
db: Annotated[AsyncSession, Depends(db_getter)],
|
db: Annotated[AsyncSession, Depends(db_getter)],
|
||||||
) -> StreamingResponse:
|
) -> StreamingResponse:
|
||||||
"""
|
service = {{ class_name }}Service(auth, db)
|
||||||
导出{{ function_name }}
|
result_dict_list = await service.get_list(search=search)
|
||||||
|
export_result = {{ class_name }}Service.batch_export(obj_list=[item.model_dump() for item in result_dict_list])
|
||||||
参数:
|
|
||||||
- search ({{ class_name }}QueryParam): 查询参数
|
|
||||||
- auth (AuthSchema): 认证信息模型
|
|
||||||
- db (AsyncSession): 数据库会话
|
|
||||||
|
|
||||||
返回:
|
|
||||||
- StreamingResponse: 包含{{ function_name }}列表的Excel文件流响应
|
|
||||||
"""
|
|
||||||
result_dict_list = await {{ class_name }}Service.list_service(search=search, auth=auth, db=db)
|
|
||||||
export_result = {{ class_name }}Service.batch_export_service(obj_list=result_dict_list)
|
|
||||||
|
|
||||||
return StreamResponse(
|
return StreamResponse(
|
||||||
data=bytes2file_response(export_result),
|
data=bytes2file_response(export_result),
|
||||||
@@ -210,46 +108,20 @@ async def export_obj_list_controller(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@{{ class_name }}Router.post(
|
@{{ class_name }}Router.post("/import", summary="导入{{ function_name }}", response_model=ResponseSchema[str])
|
||||||
"/import",
|
|
||||||
summary="导入{{ function_name }}",
|
|
||||||
response_model=ResponseSchema[str],
|
|
||||||
)
|
|
||||||
async def import_obj_list_controller(
|
async def import_obj_list_controller(
|
||||||
file: Annotated[UploadFile, File(description="导入的Excel文件")],
|
file: Annotated[UploadFile, File(description="导入文件")],
|
||||||
auth: Annotated[AuthSchema, Security(AuthPermission(["{{ permission_prefix }}:import"]))],
|
auth: Annotated[AuthSchema, Depends(AuthPermission(["{{ permission_prefix }}:import"]))],
|
||||||
db: Annotated[AsyncSession, Depends(db_getter)],
|
db: Annotated[AsyncSession, Depends(db_getter)],
|
||||||
) -> JSONResponse:
|
) -> JSONResponse:
|
||||||
"""
|
service = {{ class_name }}Service(auth, db)
|
||||||
导入{{ function_name }}
|
batch_import_result = await service.batch_import(file=file, update_support=True)
|
||||||
|
|
||||||
参数:
|
|
||||||
- file (UploadFile): 导入的Excel文件
|
|
||||||
- auth (AuthSchema): 认证信息模型
|
|
||||||
- db (AsyncSession): 数据库会话
|
|
||||||
|
|
||||||
返回:
|
|
||||||
- JSONResponse: 包含导入{{ function_name }}详情的JSON响应
|
|
||||||
"""
|
|
||||||
batch_import_result = await {{ class_name }}Service.batch_import_service(
|
|
||||||
file=file, auth=auth, db=db, update_support=True
|
|
||||||
)
|
|
||||||
return SuccessResponse(data=batch_import_result, msg="导入{{ function_name }}成功")
|
return SuccessResponse(data=batch_import_result, msg="导入{{ function_name }}成功")
|
||||||
|
|
||||||
|
|
||||||
@{{ class_name }}Router.post(
|
@{{ class_name }}Router.post("/download/template", summary="获取{{ function_name }}导入模板", dependencies=[Depends(AuthPermission(["{{ permission_prefix }}:download"]))])
|
||||||
"/download/template",
|
|
||||||
summary="获取{{ function_name }}导入模板",
|
|
||||||
dependencies=[Security(AuthPermission(["{{ permission_prefix }}:download"]))],
|
|
||||||
)
|
|
||||||
async def export_obj_template_controller() -> StreamingResponse:
|
async def export_obj_template_controller() -> StreamingResponse:
|
||||||
"""
|
import_template_result = {{ class_name }}Service.import_template_download()
|
||||||
获取{{ function_name }}导入模板
|
|
||||||
|
|
||||||
返回:
|
|
||||||
- StreamingResponse: 包含{{ function_name }}导入模板的Excel文件流响应
|
|
||||||
"""
|
|
||||||
import_template_result = {{ class_name }}Service.import_template_download_service()
|
|
||||||
|
|
||||||
return StreamResponse(
|
return StreamResponse(
|
||||||
data=bytes2file_response(import_template_result),
|
data=bytes2file_response(import_template_result),
|
||||||
|
|||||||
@@ -5,13 +5,13 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
from sqlalchemy.orm import Mapped, mapped_column
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
from app.core.base_model import ModelMixin, TenantMixin, UserMixin
|
from app.core.base_model import ModelMixin, UserMixin
|
||||||
{% if table.sub and not is_sub_entity %}
|
{% if table.sub and not is_sub_entity %}
|
||||||
from ..{{ sub_module_name }}.model import {{ sub_model_class_name }}
|
from ..{{ sub_module_name }}.model import {{ sub_model_class_name }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
class {{ class_name }}Model(ModelMixin, TenantMixin, UserMixin):
|
class {{ class_name }}Model(ModelMixin, UserMixin):
|
||||||
"""
|
"""
|
||||||
{{ function_name }}表
|
{{ function_name }}表
|
||||||
"""
|
"""
|
||||||
@@ -21,7 +21,11 @@ class {{ class_name }}Model(ModelMixin, TenantMixin, UserMixin):
|
|||||||
{% for column in columns %}
|
{% for column in columns %}
|
||||||
{% if column.column_name not in ['id', 'uuid', 'tenant_id', 'created_time', 'updated_time', 'created_id', 'updated_id', 'is_deleted', 'deleted_time', 'deleted_id'] %}
|
{% if column.column_name not in ['id', 'uuid', 'tenant_id', 'created_time', 'updated_time', 'created_id', 'updated_id', 'is_deleted', 'deleted_time', 'deleted_id'] %}
|
||||||
{% set sqlalchemy_type = column|get_sqlalchemy_type %}
|
{% set sqlalchemy_type = column|get_sqlalchemy_type %}
|
||||||
{{ column.column_name }}: Mapped[{{ column.python_type }} | None] = mapped_column({{ sqlalchemy_type }}, {% if column.is_pk %}primary_key=True, {% endif %}{% if column.is_increment %}autoincrement=True, {% endif %}{% if (not column.is_nullable) or column.is_pk %}nullable=False{% else %}nullable=True{% endif %}, comment='{{ column.column_comment }}')
|
{% if not column.is_nullable %}
|
||||||
|
{{ column.column_name }}: Mapped[{{ column.python_type }}] = mapped_column({{ sqlalchemy_type }}, {% if column.is_pk %}primary_key=True, {% endif %}{% if column.is_increment %}autoincrement=True, {% endif %}nullable=False, comment='{{ column.column_comment }}')
|
||||||
|
{% else %}
|
||||||
|
{{ column.column_name }}: Mapped[{{ column.python_type }} | None] = mapped_column({{ sqlalchemy_type }}, {% if column.is_pk %}primary_key=True, {% endif %}{% if column.is_increment %}autoincrement=True, {% endif %}nullable=True, comment='{{ column.column_comment }}')
|
||||||
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
@@ -35,7 +39,11 @@ class {{ class_name }}Model(ModelMixin, TenantMixin, UserMixin):
|
|||||||
{% if column.column_name == sub_table_fk_name %}
|
{% if column.column_name == sub_table_fk_name %}
|
||||||
{{ column.column_name }}: Mapped[{{ column.python_type }} | None] = mapped_column({{ sqlalchemy_type }}, ForeignKey('{{ parent_table_name }}.{{ parent_pk_column_name }}', ondelete='CASCADE'), {% if column.is_pk %}primary_key=True, {% endif %}{% if column.is_increment %}autoincrement=True, {% endif %}{% if (not column.is_nullable) or column.is_pk %}nullable=False{% else %}nullable=True{% endif %}, comment='{{ column.column_comment }}')
|
{{ column.column_name }}: Mapped[{{ column.python_type }} | None] = mapped_column({{ sqlalchemy_type }}, ForeignKey('{{ parent_table_name }}.{{ parent_pk_column_name }}', ondelete='CASCADE'), {% if column.is_pk %}primary_key=True, {% endif %}{% if column.is_increment %}autoincrement=True, {% endif %}{% if (not column.is_nullable) or column.is_pk %}nullable=False{% else %}nullable=True{% endif %}, comment='{{ column.column_comment }}')
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ column.column_name }}: Mapped[{{ column.python_type }} | None] = mapped_column({{ sqlalchemy_type }}, {% if column.is_pk %}primary_key=True, {% endif %}{% if column.is_increment %}autoincrement=True, {% endif %}{% if (not column.is_nullable) or column.is_pk %}nullable=False{% else %}nullable=True{% endif %}, comment='{{ column.column_comment }}')
|
{% if not column.is_nullable %}
|
||||||
|
{{ column.column_name }}: Mapped[{{ column.python_type }}] = mapped_column({{ sqlalchemy_type }}, {% if column.is_pk %}primary_key=True, {% endif %}{% if column.is_increment %}autoincrement=True, {% endif %}nullable=False, comment='{{ column.column_comment }}')
|
||||||
|
{% else %}
|
||||||
|
{{ column.column_name }}: Mapped[{{ column.python_type }} | None] = mapped_column({{ sqlalchemy_type }}, {% if column.is_pk %}primary_key=True, {% endif %}{% if column.is_increment %}autoincrement=True, {% endif %}nullable=True, comment='{{ column.column_comment }}')
|
||||||
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|||||||
@@ -9,6 +9,21 @@ from pydantic import BaseModel, ConfigDict, Field
|
|||||||
{{ import_stmt }}
|
{{ import_stmt }}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
from app.core.base_schema import BaseSchema, UserBySchema, BaseQueryParam, UserByQueryParam
|
from app.core.base_schema import BaseSchema, UserBySchema, BaseQueryParam, UserByQueryParam
|
||||||
|
{% set has_date_validator = false %}
|
||||||
|
{% set has_time_validator = false %}
|
||||||
|
{% set has_datetime_validator = false %}
|
||||||
|
{% for column in columns %}
|
||||||
|
{% if column.python_type == 'date' %}
|
||||||
|
{% set has_date_validator = true %}
|
||||||
|
{% elif column.python_type == 'time' %}
|
||||||
|
{% set has_time_validator = true %}
|
||||||
|
{% elif column.python_type == 'datetime' %}
|
||||||
|
{% set has_datetime_validator = true %}
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
{% if has_date_validator or has_time_validator or has_datetime_validator %}
|
||||||
|
from app.core.validator import {% if has_date_validator %}DateStr{% endif %}{% if has_time_validator %}{% if has_date_validator %}, {% endif %}TimeStr{% endif %}{% if has_datetime_validator %}{% if has_date_validator or has_time_validator %}, {% endif %}DateTimeStr{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
class {{ class_name }}CreateSchema(BaseModel):
|
class {{ class_name }}CreateSchema(BaseModel):
|
||||||
"""
|
"""
|
||||||
@@ -22,6 +37,12 @@ class {{ class_name }}CreateSchema(BaseModel):
|
|||||||
{{ column.column_name }}: str | None = Field(default=None, description='{{ column.column_comment }}')
|
{{ column.column_name }}: str | None = Field(default=None, description='{{ column.column_comment }}')
|
||||||
{% elif column.python_type == "bool" or column.html_type == "switch" %}
|
{% elif column.python_type == "bool" or column.html_type == "switch" %}
|
||||||
{{ column.column_name }}: bool = Field(default=True, description='{{ column.column_comment }}')
|
{{ column.column_name }}: bool = Field(default=True, description='{{ column.column_comment }}')
|
||||||
|
{% elif column.python_type == "date" %}
|
||||||
|
{{ column.column_name }}: DateStr | None = Field(default=None, description='{{ column.column_comment }}')
|
||||||
|
{% elif column.python_type == "time" %}
|
||||||
|
{{ column.column_name }}: TimeStr | None = Field(default=None, description='{{ column.column_comment }}')
|
||||||
|
{% elif column.python_type == "datetime" %}
|
||||||
|
{{ column.column_name }}: DateTimeStr | None = Field(default=None, description='{{ column.column_comment }}')
|
||||||
{% elif column.is_nullable %}
|
{% elif column.is_nullable %}
|
||||||
{{ column.column_name }}: {{ column.python_type }} | None = Field(default=None, description='{{ column.column_comment }}')
|
{{ column.column_name }}: {{ column.python_type }} | None = Field(default=None, description='{{ column.column_comment }}')
|
||||||
{% else %}
|
{% else %}
|
||||||
@@ -31,11 +52,25 @@ class {{ class_name }}CreateSchema(BaseModel):
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
|
|
||||||
class {{ class_name }}UpdateSchema({{ class_name }}CreateSchema):
|
class {{ class_name }}UpdateSchema(BaseModel):
|
||||||
"""
|
"""
|
||||||
{{ function_name }}更新模型
|
{{ function_name }}更新模型
|
||||||
"""
|
"""
|
||||||
...
|
{% for column in columns %}
|
||||||
|
{% if column.column_name not in ['uuid', 'tenant_id', 'created_time', 'updated_time', 'created_id', 'updated_id', 'is_deleted', 'deleted_time', 'deleted_id'] and column.column_name != pk_column_name %}
|
||||||
|
{% if column.column_name == 'status' %}
|
||||||
|
{{ column.column_name }}: {{ column.python_type }} | None = Field(default=None{% if column.python_type == "int" %}, ge=0, le=1{% endif %}, description='{{ column.column_comment }}')
|
||||||
|
{% elif column.python_type == "date" %}
|
||||||
|
{{ column.column_name }}: DateStr | None = Field(default=None, description='{{ column.column_comment }}')
|
||||||
|
{% elif column.python_type == "time" %}
|
||||||
|
{{ column.column_name }}: TimeStr | None = Field(default=None, description='{{ column.column_comment }}')
|
||||||
|
{% elif column.python_type == "datetime" %}
|
||||||
|
{{ column.column_name }}: DateTimeStr | None = Field(default=None, description='{{ column.column_comment }}')
|
||||||
|
{% else %}
|
||||||
|
{{ column.column_name }}: {{ column.python_type }} | None = Field(default=None, description='{{ column.column_comment }}')
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
|
||||||
class {{ class_name }}OutSchema({{ class_name }}CreateSchema, BaseSchema, UserBySchema):
|
class {{ class_name }}OutSchema({{ class_name }}CreateSchema, BaseSchema, UserBySchema):
|
||||||
@@ -50,6 +85,6 @@ class {{ class_name }}QueryParam(BaseQueryParam, UserByQueryParam):
|
|||||||
|
|
||||||
{% for column in columns %}
|
{% for column in columns %}
|
||||||
{% if column.is_query and column.column_name not in ['created_time', 'updated_time', 'created_id', 'updated_id', 'tenant_id', 'is_deleted', 'deleted_time', 'deleted_id'] %}
|
{% if column.is_query and column.column_name not in ['created_time', 'updated_time', 'created_id', 'updated_id', 'tenant_id', 'is_deleted', 'deleted_time', 'deleted_id'] %}
|
||||||
{{ column.column_name }}: {{ column.python_type }} | None = Field(None, description="{{ column.column_comment }}")
|
{{ column.column_name }}: {{ column.python_type }} | None = Field(None, description="{{ column.column_comment }}"{% if column.query_type == 'LIKE' %}, json_schema_extra={"q": "like"}{% else %}, json_schema_extra={"q": "eq"}{% endif %})
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|||||||
@@ -1,194 +1,104 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
import io
|
from typing import Any
|
||||||
import pandas as pd
|
|
||||||
from fastapi import UploadFile
|
from fastapi import UploadFile
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from app.core.base_schema import AuthSchema
|
from app.core.base_schema import AuthSchema, BatchSetAvailable, PageResultSchema
|
||||||
from app.core.base_schema import BatchSetAvailable
|
|
||||||
from app.core.exceptions import CustomException
|
from app.core.exceptions import CustomException
|
||||||
from app.core.logger import logger
|
from app.core.logger import logger
|
||||||
|
from app.utils.common_util import search_to_dict
|
||||||
from app.utils.excel_util import ExcelUtil
|
from app.utils.excel_util import ExcelUtil
|
||||||
|
|
||||||
from .crud import {{ class_name }}CRUD
|
from .crud import {{ class_name }}CRUD
|
||||||
from .schema import (
|
from .schema import (
|
||||||
{{ class_name }}CreateSchema,
|
{{ class_name }}CreateSchema,
|
||||||
{{ class_name }}UpdateSchema,
|
|
||||||
{{ class_name }}OutSchema,
|
{{ class_name }}OutSchema,
|
||||||
{{ class_name }}QueryParam
|
{{ class_name }}QueryParam,
|
||||||
|
{{ class_name }}UpdateSchema,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class {{ class_name }}Service:
|
class {{ class_name }}Service:
|
||||||
"""
|
"""{{ function_name }}模块服务层"""
|
||||||
{{ function_name }}服务层
|
|
||||||
"""
|
|
||||||
|
|
||||||
@classmethod
|
def __init__(self, auth: AuthSchema, db: AsyncSession) -> None:
|
||||||
async def detail_service(cls, auth: AuthSchema, db: AsyncSession, id: int) -> dict:
|
self.auth = auth
|
||||||
"""
|
self.db = db
|
||||||
详情
|
|
||||||
|
|
||||||
参数:
|
async def detail(self, id: int) -> {{ class_name }}OutSchema:
|
||||||
- auth: AuthSchema - 认证信息
|
obj = await {{ class_name }}CRUD(self.auth, self.db).get(id=id)
|
||||||
- db: AsyncSession - 数据库会话
|
|
||||||
- id: int - 数据ID
|
|
||||||
|
|
||||||
返回:
|
|
||||||
- dict - 数据详情
|
|
||||||
"""
|
|
||||||
obj = await {{ class_name }}CRUD(auth, db).get(id=id)
|
|
||||||
if not obj:
|
if not obj:
|
||||||
raise CustomException(msg="该数据不存在")
|
raise CustomException(msg="该数据不存在")
|
||||||
return {{ class_name }}OutSchema.model_validate(obj).model_dump()
|
return {{ class_name }}OutSchema.model_validate(obj)
|
||||||
|
|
||||||
@classmethod
|
async def get_list(
|
||||||
async def list_service(cls, auth: AuthSchema, db: AsyncSession, search: {{ class_name }}QueryParam | None = None, order_by: list[dict] | None = None) -> list[dict]:
|
self,
|
||||||
"""
|
search: {{ class_name }}QueryParam | None = None,
|
||||||
列表查询
|
order_by: list[dict[str, str]] | None = None,
|
||||||
|
) -> list[{{ class_name }}OutSchema]:
|
||||||
|
obj_list = await {{ class_name }}CRUD(self.auth, self.db).get_list(search=search_to_dict(search), order_by=order_by)
|
||||||
|
return [{{ class_name }}OutSchema.model_validate(obj) for obj in obj_list]
|
||||||
|
|
||||||
参数:
|
async def page(
|
||||||
- auth: AuthSchema - 认证信息
|
self,
|
||||||
- db: AsyncSession - 数据库会话
|
page_no: int,
|
||||||
- search: {{ class_name }}QueryParam | None - 查询参数
|
page_size: int,
|
||||||
- order_by: list[dict] | None - 排序参数
|
search: {{ class_name }}QueryParam | None = None,
|
||||||
|
order_by: list[dict[str, str]] | None = None,
|
||||||
返回:
|
) -> PageResultSchema[{{ class_name }}OutSchema]:
|
||||||
- list[dict] - 数据列表
|
|
||||||
"""
|
|
||||||
search_dict = search.__dict__ if search else None
|
|
||||||
obj_list = await {{ class_name }}CRUD(auth, db).list(search=search_dict, order_by=order_by)
|
|
||||||
return [{{ class_name }}OutSchema.model_validate(obj).model_dump() for obj in obj_list]
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
async def page_service(cls, auth: AuthSchema, db: AsyncSession, page_no: int, page_size: int, search: {{ class_name }}QueryParam | None = None, order_by: list[dict] | None = None) -> dict:
|
|
||||||
"""
|
|
||||||
分页查询(数据库分页)
|
|
||||||
|
|
||||||
参数:
|
|
||||||
- auth: AuthSchema - 认证信息
|
|
||||||
- db: AsyncSession - 数据库会话
|
|
||||||
- page_no: int - 页码
|
|
||||||
- page_size: int - 每页数量
|
|
||||||
- search: {{ class_name }}QueryParam | None - 查询参数
|
|
||||||
- order_by: list[dict] | None - 排序参数
|
|
||||||
|
|
||||||
返回:
|
|
||||||
- dict - 分页查询结果
|
|
||||||
"""
|
|
||||||
search_dict = search.__dict__ if search else {}
|
|
||||||
order_by_list = order_by or [{'{{ pk_column_name }}': 'asc'}]
|
|
||||||
offset = (page_no - 1) * page_size
|
offset = (page_no - 1) * page_size
|
||||||
result = await {{ class_name }}CRUD(auth, db).page(
|
return await {{ class_name }}CRUD(self.auth, self.db).page(
|
||||||
offset=offset,
|
offset=offset,
|
||||||
limit=page_size,
|
limit=page_size,
|
||||||
order_by=order_by_list,
|
order_by=order_by or [{"{{ pk_column_name }}": "asc"}],
|
||||||
search=search_dict,
|
search=search_to_dict(search, {}),
|
||||||
out_schema={{ class_name }}OutSchema
|
out_schema={{ class_name }}OutSchema,
|
||||||
)
|
)
|
||||||
return result
|
|
||||||
|
|
||||||
@classmethod
|
async def create(self, data: {{ class_name }}CreateSchema) -> {{ class_name }}OutSchema:
|
||||||
async def create_service(cls, auth: AuthSchema, db: AsyncSession, data: {{ class_name }}CreateSchema) -> dict:
|
|
||||||
"""
|
|
||||||
创建
|
|
||||||
|
|
||||||
参数:
|
|
||||||
- auth: AuthSchema - 认证信息
|
|
||||||
- db: AsyncSession - 数据库会话
|
|
||||||
- data: {{ class_name }}CreateSchema - 创建数据
|
|
||||||
|
|
||||||
返回:
|
|
||||||
- dict - 创建结果
|
|
||||||
"""
|
|
||||||
{% for column in columns %}
|
{% for column in columns %}
|
||||||
{% if column.is_unique and column.column_name != 'uuid' %}
|
{% if column.is_unique and column.column_name != 'uuid' %}
|
||||||
obj = await {{ class_name }}CRUD(auth, db).get({{ column.column_name }}=data.{{ column.column_name }})
|
obj = await {{ class_name }}CRUD(self.auth, self.db).get({{ column.column_name }}=data.{{ column.column_name }})
|
||||||
if obj:
|
if obj:
|
||||||
raise CustomException(msg='创建失败,{{ column.column_comment }}已存在')
|
raise CustomException(msg="创建失败,{{ column.column_comment }}已存在")
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
obj = await {{ class_name }}CRUD(auth, db).create(data=data)
|
obj = await {{ class_name }}CRUD(self.auth, self.db).create(data=data)
|
||||||
return {{ class_name }}OutSchema.model_validate(obj).model_dump()
|
return {{ class_name }}OutSchema.model_validate(obj)
|
||||||
|
|
||||||
@classmethod
|
async def update(self, id: int, data: {{ class_name }}UpdateSchema) -> {{ class_name }}OutSchema:
|
||||||
async def update_service(cls, auth: AuthSchema, db: AsyncSession, id: int, data: {{ class_name }}UpdateSchema) -> dict:
|
obj = await {{ class_name }}CRUD(self.auth, self.db).get(id=id)
|
||||||
"""
|
|
||||||
更新
|
|
||||||
|
|
||||||
参数:
|
|
||||||
- auth: AuthSchema - 认证信息
|
|
||||||
- db: AsyncSession - 数据库会话
|
|
||||||
- id: int - 数据ID
|
|
||||||
- data: {{ class_name }}UpdateSchema - 更新数据
|
|
||||||
|
|
||||||
返回:
|
|
||||||
- dict - 更新结果
|
|
||||||
"""
|
|
||||||
# 检查数据是否存在
|
|
||||||
obj = await {{ class_name }}CRUD(auth, db).get(id=id)
|
|
||||||
if not obj:
|
if not obj:
|
||||||
raise CustomException(msg='更新失败,该数据不存在')
|
raise CustomException(msg="更新失败,该数据不存在")
|
||||||
|
|
||||||
# 检查唯一性约束
|
|
||||||
{% for column in columns %}
|
{% for column in columns %}
|
||||||
{% if column.is_unique and column.column_name != 'uuid' %}
|
{% if column.is_unique and column.column_name != 'uuid' %}
|
||||||
exist_obj = await {{ class_name }}CRUD(auth, db).get({{ column.column_name }}=data.{{ column.column_name }})
|
exist_obj = await {{ class_name }}CRUD(self.auth, self.db).get({{ column.column_name }}=data.{{ column.column_name }})
|
||||||
if exist_obj and getattr(exist_obj, '{{ pk_column_name }}') != id:
|
if exist_obj and exist_obj.{{ pk_column_name }} != id:
|
||||||
raise CustomException(msg='更新失败,{{ column.column_comment }}重复')
|
raise CustomException(msg="更新失败,{{ column.column_comment }}重复")
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
obj = await {{ class_name }}CRUD(auth, db).update(id=id, data=data)
|
obj = await {{ class_name }}CRUD(self.auth, self.db).update(id=id, data=data)
|
||||||
return {{ class_name }}OutSchema.model_validate(obj).model_dump()
|
return {{ class_name }}OutSchema.model_validate(obj)
|
||||||
|
|
||||||
@classmethod
|
async def delete(self, ids: list[int]) -> None:
|
||||||
async def delete_service(cls, auth: AuthSchema, db: AsyncSession, ids: list[int]) -> None:
|
if not ids:
|
||||||
"""
|
raise CustomException(msg="删除失败,删除对象不能为空")
|
||||||
删除
|
objs = await {{ class_name }}CRUD(self.auth, self.db).get_list(search={"{{ pk_column_name }}": ("in", ids)})
|
||||||
|
obj_map = {o.{{ pk_column_name }}: o for o in objs}
|
||||||
|
for id_ in ids:
|
||||||
|
if id_ not in obj_map:
|
||||||
|
raise CustomException(msg="删除失败,该数据不存在")
|
||||||
|
await {{ class_name }}CRUD(self.auth, self.db).delete(ids=ids)
|
||||||
|
|
||||||
参数:
|
async def set_available(self, data: BatchSetAvailable) -> None:
|
||||||
- auth: AuthSchema - 认证信息
|
await {{ class_name }}CRUD(self.auth, self.db).set(ids=data.ids, status=data.status)
|
||||||
- db: AsyncSession - 数据库会话
|
|
||||||
- ids: list[int] - 数据ID列表
|
|
||||||
|
|
||||||
返回:
|
|
||||||
- None
|
|
||||||
"""
|
|
||||||
if len(ids) < 1:
|
|
||||||
raise CustomException(msg='删除失败,删除对象不能为空')
|
|
||||||
for id in ids:
|
|
||||||
obj = await {{ class_name }}CRUD(auth, db).get(id=id)
|
|
||||||
if not obj:
|
|
||||||
raise CustomException(msg=f'删除失败,ID为{id}的数据不存在')
|
|
||||||
await {{ class_name }}CRUD(auth, db).delete(ids=ids)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
async def set_available_service(cls, auth: AuthSchema, db: AsyncSession, data: BatchSetAvailable) -> None:
|
|
||||||
"""
|
|
||||||
批量设置状态
|
|
||||||
|
|
||||||
参数:
|
|
||||||
- auth: AuthSchema - 认证信息
|
|
||||||
- db: AsyncSession - 数据库会话
|
|
||||||
- data: BatchSetAvailable - 批量设置状态数据
|
|
||||||
|
|
||||||
返回:
|
|
||||||
- None
|
|
||||||
"""
|
|
||||||
await {{ class_name }}CRUD(auth, db).set(ids=data.ids, status=data.status)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def batch_export_service(obj_list: list[dict]) -> bytes:
|
def batch_export(obj_list: list[dict[str, Any]]) -> bytes:
|
||||||
"""
|
|
||||||
批量导出
|
|
||||||
|
|
||||||
参数:
|
|
||||||
- obj_list: list[dict] - 数据列表
|
|
||||||
|
|
||||||
返回:
|
|
||||||
- bytes - 导出的Excel文件内容
|
|
||||||
"""
|
|
||||||
mapping_dict = {
|
mapping_dict = {
|
||||||
{% for column in columns %}
|
{% for column in columns %}
|
||||||
{% if column.column_name not in ['uuid', 'tenant_id', 'updated_id', 'is_deleted', 'deleted_time', 'deleted_id'] %}
|
{% if column.column_name not in ['uuid', 'tenant_id', 'updated_id', 'is_deleted', 'deleted_time', 'deleted_id'] %}
|
||||||
@@ -196,12 +106,10 @@ class {{ class_name }}Service:
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
}
|
}
|
||||||
# 复制数据并转换状态
|
|
||||||
data = obj_list.copy()
|
data = obj_list.copy()
|
||||||
for item in data:
|
for item in data:
|
||||||
# 处理状态
|
|
||||||
item["status"] = "启用" if item.get("status") == 0 else "停用"
|
item["status"] = "启用" if item.get("status") == 0 else "停用"
|
||||||
# 处理创建者
|
|
||||||
creator_info = item.get("created_id")
|
creator_info = item.get("created_id")
|
||||||
if isinstance(creator_info, dict):
|
if isinstance(creator_info, dict):
|
||||||
item["created_id"] = creator_info.get("name", "未知")
|
item["created_id"] = creator_info.get("name", "未知")
|
||||||
@@ -210,20 +118,7 @@ class {{ class_name }}Service:
|
|||||||
|
|
||||||
return ExcelUtil.export_list2excel(list_data=data, mapping_dict=mapping_dict)
|
return ExcelUtil.export_list2excel(list_data=data, mapping_dict=mapping_dict)
|
||||||
|
|
||||||
@classmethod
|
async def batch_import(self, file: UploadFile, update_support: bool = False) -> str:
|
||||||
async def batch_import_service(cls, auth: AuthSchema, db: AsyncSession, file: UploadFile, update_support: bool = False) -> str:
|
|
||||||
"""
|
|
||||||
批量导入
|
|
||||||
|
|
||||||
参数:
|
|
||||||
- auth: AuthSchema - 认证信息
|
|
||||||
- db: AsyncSession - 数据库会话
|
|
||||||
- file: UploadFile - 上传的Excel文件
|
|
||||||
- update_support: bool - 是否支持更新存在数据
|
|
||||||
|
|
||||||
返回:
|
|
||||||
- str - 导入结果信息
|
|
||||||
"""
|
|
||||||
header_dict = {
|
header_dict = {
|
||||||
{% for column in columns %}
|
{% for column in columns %}
|
||||||
{% if column.column_name not in ['id', 'uuid', 'tenant_id', 'created_time', 'updated_time', 'created_id', 'updated_id', 'is_deleted', 'deleted_time', 'deleted_id'] and column.column_name != pk_column_name %}
|
{% if column.column_name not in ['id', 'uuid', 'tenant_id', 'created_time', 'updated_time', 'created_id', 'updated_id', 'is_deleted', 'deleted_time', 'deleted_id'] and column.column_name != pk_column_name %}
|
||||||
@@ -233,71 +128,63 @@ class {{ class_name }}Service:
|
|||||||
}
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 读取Excel文件
|
|
||||||
contents = await file.read()
|
contents = await file.read()
|
||||||
df = pd.read_excel(io.BytesIO(contents))
|
rows = ExcelUtil.read_excel_to_dicts(contents)
|
||||||
await file.close()
|
await file.close()
|
||||||
|
|
||||||
if df.empty:
|
if not rows:
|
||||||
raise CustomException(msg="导入文件为空")
|
raise CustomException(msg="导入文件为空")
|
||||||
|
|
||||||
# 检查表头是否完整
|
missing_headers = [h for h in header_dict if h not in rows[0]]
|
||||||
missing_headers = [header for header in header_dict.keys() if header not in df.columns]
|
|
||||||
if missing_headers:
|
if missing_headers:
|
||||||
raise CustomException(msg=f"导入文件缺少必要的列: {', '.join(missing_headers)}")
|
raise CustomException(msg=f"导入文件缺少必要的列: {', '.join(missing_headers)}")
|
||||||
|
|
||||||
# 重命名列名
|
# 将中文字段名映射为英文字段
|
||||||
df.rename(columns=header_dict, inplace=True)
|
mapped_rows = []
|
||||||
|
for row in rows:
|
||||||
|
mapped_rows.append({en: row.get(ch) for ch, en in header_dict.items()})
|
||||||
|
|
||||||
# 验证必填字段(非主键且不允许为空的列)
|
required_fields = [
|
||||||
{% for column in columns %}
|
{% for column in columns %}
|
||||||
{% if column.is_nullable is false and column.is_pk is false and column.column_name not in ['id', 'uuid', 'tenant_id', 'created_time', 'updated_time', 'created_id', 'updated_id', 'is_deleted', 'deleted_time', 'deleted_id'] %}
|
{% if column.is_nullable is false and column.is_pk is false and column.column_name not in ['id', 'uuid', 'tenant_id', 'created_time', 'updated_time', 'created_id', 'updated_id', 'is_deleted', 'deleted_time', 'deleted_id'] %}
|
||||||
|
"{{ column.column_name }}",
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
]
|
||||||
errors = []
|
errors = []
|
||||||
missing_rows = df[df['{{ column.column_name }}'].isnull()].index.tolist()
|
for field in required_fields:
|
||||||
if missing_rows:
|
missing_indices = [i + 1 for i, r in enumerate(mapped_rows) if r.get(field) is None]
|
||||||
field_name = [k for k,v in header_dict.items() if v == '{{ column.column_name }}'][0]
|
if missing_indices:
|
||||||
rows_str = "、".join([str(i+1) for i in missing_rows])
|
field_name = next((k for k, v in header_dict.items() if v == field), field)
|
||||||
errors.append(f"{field_name}不能为空,第{rows_str}行")
|
rows_str = "、".join(str(i) for i in missing_indices)
|
||||||
|
errors.append(f"{field_name}不能为空,第{rows_str}行")
|
||||||
if errors:
|
if errors:
|
||||||
raise CustomException(msg=f"导入失败,以下行缺少必要字段:\n{'; '.join(errors)}")
|
raise CustomException(msg=f"导入失败,以下行缺少必要字段:\n{'; '.join(errors)}")
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
error_msgs = []
|
error_msgs = []
|
||||||
success_count = 0
|
success_count = 0
|
||||||
count = 0
|
|
||||||
|
|
||||||
for _index, row in df.iterrows():
|
for i, row in enumerate(mapped_rows, start=1):
|
||||||
count += 1
|
|
||||||
try:
|
try:
|
||||||
data = {
|
create_schema = {{ class_name }}CreateSchema.model_validate(row)
|
||||||
{% for column in columns %}
|
|
||||||
{% if column.column_name not in ['id', 'uuid', 'tenant_id', 'created_time', 'updated_time', 'created_id', 'updated_id', 'is_deleted', 'deleted_time', 'deleted_id'] and column.column_name != pk_column_name %}
|
|
||||||
"{{ column.column_name }}": row['{{ column.column_name }}'],
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
}
|
|
||||||
# 使用CreateSchema做校验后入库
|
|
||||||
create_schema = {{ class_name }}CreateSchema.model_validate(data)
|
|
||||||
|
|
||||||
# 检查唯一性约束
|
|
||||||
{% for column in columns %}
|
{% for column in columns %}
|
||||||
{% if column.is_unique and column.column_name not in ['uuid', 'tenant_id', 'created_time', 'updated_time', 'created_id', 'updated_id', 'is_deleted', 'deleted_time', 'deleted_id'] %}
|
{% if column.is_unique and column.column_name not in ['uuid', 'tenant_id', 'created_time', 'updated_time', 'created_id', 'updated_id', 'is_deleted', 'deleted_time', 'deleted_id'] %}
|
||||||
exists_obj = await {{ class_name }}CRUD(auth, db).get({{ column.column_name }}=create_schema.{{ column.column_name }})
|
exists_obj = await {{ class_name }}CRUD(self.auth, self.db).get({{ column.column_name }}=create_schema.{{ column.column_name }})
|
||||||
if exists_obj:
|
if exists_obj:
|
||||||
if update_support:
|
if update_support:
|
||||||
await {{ class_name }}CRUD(auth, db).update(id=getattr(exists_obj, '{{ pk_column_name }}'), data=create_schema)
|
await {{ class_name }}CRUD(self.auth, self.db).update(id=getattr(exists_obj, '{{ pk_column_name }}'), data=create_schema)
|
||||||
success_count += 1
|
success_count += 1
|
||||||
else:
|
else:
|
||||||
error_msgs.append(f"第{count}行: {{ column.column_comment }} {create_schema.{{ column.column_name }}} 已存在")
|
error_msgs.append(f"第{i}行: {{ column.column_comment }} {create_schema.{{ column.column_name }}} 已存在")
|
||||||
continue
|
continue
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
await {{ class_name }}CRUD(auth, db).create(data=create_schema)
|
await {{ class_name }}CRUD(self.auth, self.db).create(data=create_schema)
|
||||||
success_count += 1
|
success_count += 1
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error_msgs.append(f"第{count}行: {str(e)}")
|
error_msgs.append(f"第{i}行: {e!s}")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
result = f"成功导入 {success_count} 条数据"
|
result = f"成功导入 {success_count} 条数据"
|
||||||
@@ -306,17 +193,11 @@ class {{ class_name }}Service:
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"批量导入失败: {str(e)}")
|
logger.error(f"批量导入失败: {e!s}")
|
||||||
raise CustomException(msg=f"导入失败: {str(e)}")
|
raise CustomException(msg=f"导入失败: {e!s}")
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def import_template_download_service() -> bytes:
|
def import_template_download() -> bytes:
|
||||||
"""
|
|
||||||
下载导入模板
|
|
||||||
|
|
||||||
返回:
|
|
||||||
- bytes - Excel文件的二进制数据
|
|
||||||
"""
|
|
||||||
header_list = [
|
header_list = [
|
||||||
{% for column in columns %}
|
{% for column in columns %}
|
||||||
{% if column.column_name not in ['id', 'uuid', 'tenant_id', 'created_time', 'updated_time', 'created_id', 'updated_id', 'is_deleted', 'deleted_time', 'deleted_id'] and column.column_name != pk_column_name %}
|
{% if column.column_name not in ['id', 'uuid', 'tenant_id', 'created_time', 'updated_time', 'created_id', 'updated_id', 'is_deleted', 'deleted_time', 'deleted_id'] and column.column_name != pk_column_name %}
|
||||||
@@ -337,5 +218,5 @@ class {{ class_name }}Service:
|
|||||||
return ExcelUtil.get_excel_template(
|
return ExcelUtil.get_excel_template(
|
||||||
header_list=header_list,
|
header_list=header_list,
|
||||||
selector_header_list=selector_header_list,
|
selector_header_list=selector_header_list,
|
||||||
option_list=option_list
|
option_list=option_list,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ const {{ class_name }}API = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
downloadTemplate{{ class_name }}() {
|
downloadTemplate{{ class_name }}() {
|
||||||
return request<ApiResponse>({
|
return request<Blob>({
|
||||||
url: `${API_PATH}/download/template`,
|
url: `${API_PATH}/download/template`,
|
||||||
method: "post",
|
method: "post",
|
||||||
responseType: "blob",
|
responseType: "blob",
|
||||||
|
|||||||
@@ -70,7 +70,8 @@
|
|||||||
:form-mode="dialogVisible.type"
|
:form-mode="dialogVisible.type"
|
||||||
:confirm-loading="submitLoading"
|
:confirm-loading="submitLoading"
|
||||||
@cancel="handleCloseDialog"
|
@cancel="handleCloseDialog"
|
||||||
@confirm="dialogVisible.type === 'detail' ? handleCloseDialog() : handleSubmit()"
|
@close="handleCloseDialog"
|
||||||
|
@confirm="handleSubmit()"
|
||||||
>
|
>
|
||||||
<template v-if="dialogVisible.type === 'detail'">
|
<template v-if="dialogVisible.type === 'detail'">
|
||||||
<FaDescriptions
|
<FaDescriptions
|
||||||
@@ -92,7 +93,7 @@
|
|||||||
label-suffix=":"
|
label-suffix=":"
|
||||||
:label-width="100"
|
:label-width="100"
|
||||||
label-position="right"
|
label-position="right"
|
||||||
:span="24"
|
:span="12"
|
||||||
:gutter="16"
|
:gutter="16"
|
||||||
:show-reset="false"
|
:show-reset="false"
|
||||||
:show-submit="false"
|
:show-submit="false"
|
||||||
@@ -124,16 +125,6 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #footer>
|
|
||||||
<div class="dialog-footer" style="padding-right: var(--el-dialog-padding-primary)">
|
|
||||||
<ElButton @click="handleCloseDialog">取消</ElButton>
|
|
||||||
<ElButton v-if="dialogVisible.type !== 'detail'" type="primary" @click="handleSubmit">
|
|
||||||
确定
|
|
||||||
</ElButton>
|
|
||||||
<ElButton v-else type="primary" @click="handleCloseDialog">确定</ElButton>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</FaDialog>
|
</FaDialog>
|
||||||
|
|
||||||
<FaImportDialog
|
<FaImportDialog
|
||||||
@@ -154,26 +145,23 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useAuth } from "@/hooks/core/useAuth";
|
import type { TableOperationAction } from "@/utils/table";
|
||||||
import { renderTableOperationCell, type TableOperationAction } from "@/utils/table";
|
import { renderTableOperationCell, stripPaginationParams, toCrudCols } from "@utils";
|
||||||
import { useTable } from "@/hooks/core/useTable";
|
import { useCrudForm } from "@/hooks/core/useCrudForm";
|
||||||
import { useImportExport } from "@/hooks/core/useImportExport";
|
|
||||||
import { useCrudDialog } from "@/hooks/core/useCrudDialog";
|
|
||||||
import { useTableSelection } from "@/hooks/core/useTableSelection";
|
|
||||||
import { confirmDelete, confirmBatchDelete, confirmAction } from "@/hooks/core/useConfirm";
|
import { confirmDelete, confirmBatchDelete, confirmAction } from "@/hooks/core/useConfirm";
|
||||||
import { stripPaginationParams } from "@/utils/query";
|
import { ResultEnum } from "@/enums/api/result.enum";
|
||||||
import type { IContentConfig, IObject } from "@/components/modal/types";
|
import type { IContentConfig, IObject } from "@/components/modal/types";
|
||||||
import type { AuditSearchFormParams } from "@/components/forms/fa-search-bar/auditSearchFormItems";
|
import type { AuditSearchFormParams } from "@/components/forms/fa-search-bar/auditSearchFormItems";
|
||||||
import type { FormItem } from "@/components/forms/fa-form/index.vue";
|
import type { FormItem } from "@/components/forms/fa-form/index.vue";
|
||||||
import type { ColumnOption } from "@/types/component";
|
import type { ColumnOption } from "@/types/component";
|
||||||
|
import FaDescriptions from "@/components/others/fa-descriptions/index.vue";
|
||||||
|
import FaForm from "@/components/forms/fa-form/index.vue";
|
||||||
|
import FaTableHeader from "@/components/tables/fa-table-header/index.vue";
|
||||||
import {{ class_name }}API, {
|
import {{ class_name }}API, {
|
||||||
type {{ class_name }}Form,
|
type {{ class_name }}Form,
|
||||||
type {{ class_name }}PageQuery,
|
type {{ class_name }}PageQuery,
|
||||||
type {{ class_name }}Table,
|
type {{ class_name }}Table,
|
||||||
} from "@/api/{{ package_name }}/{{ module_name }}";
|
} from "@/api/{{ package_name }}/{{ module_name }}";
|
||||||
import { ResultEnum } from "@/enums/api/result.enum";
|
|
||||||
import { ElMessage } from "element-plus";
|
|
||||||
|
|
||||||
{% if has_dict_column %}
|
{% if has_dict_column %}
|
||||||
import { useDictStore } from "@/stores/modules/dict";
|
import { useDictStore } from "@/stores/modules/dict";
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -186,7 +174,6 @@ defineOptions({
|
|||||||
inheritAttrs: false,
|
inheritAttrs: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { hasAuth } = useAuth();
|
|
||||||
{% if has_dict_column %}
|
{% if has_dict_column %}
|
||||||
const dictStore = useDictStore();
|
const dictStore = useDictStore();
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -387,9 +374,9 @@ const {
|
|||||||
formatter: (row: {{ class_name }}Table) => row.updated_by?.name ?? "—",
|
formatter: (row: {{ class_name }}Table) => row.updated_by?.name ?? "—",
|
||||||
},
|
},
|
||||||
{% elif column.column_name == "created_time" %}
|
{% elif column.column_name == "created_time" %}
|
||||||
{ prop: "created_time", label: "创建时间", width: 168, showOverflowTooltip: true },
|
{ prop: "created_time", label: "创建时间", width: 168, sortable: true, showOverflowTooltip: true },
|
||||||
{% elif column.column_name == "updated_time" %}
|
{% elif column.column_name == "updated_time" %}
|
||||||
{ prop: "updated_time", label: "更新时间", width: 168, showOverflowTooltip: true },
|
{ prop: "updated_time", label: "更新时间", width: 168, sortable: true, showOverflowTooltip: true },
|
||||||
{% elif column.python_type == "bool" or column.html_type == "checkbox" %}
|
{% elif column.python_type == "bool" or column.html_type == "checkbox" %}
|
||||||
{
|
{
|
||||||
prop: "{{ column.column_name }}",
|
prop: "{{ column.column_name }}",
|
||||||
@@ -435,17 +422,7 @@ const {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const crudCols = computed(() =>
|
const crudCols = toCrudCols(columns);
|
||||||
columns.value.map((c: ColumnOption<{{ class_name }}Table>) => {
|
|
||||||
const t = (c as { type?: string }).type;
|
|
||||||
return {
|
|
||||||
prop: c.prop,
|
|
||||||
label: c.label,
|
|
||||||
type: t === "selection" ? ("selection" as const) : ("default" as const),
|
|
||||||
show: true,
|
|
||||||
};
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const exportQueryParams = computed(() => {
|
const exportQueryParams = computed(() => {
|
||||||
return stripPaginationParams(searchParams as Record<string, unknown>);
|
return stripPaginationParams(searchParams as Record<string, unknown>);
|
||||||
@@ -569,14 +546,30 @@ const dialogFormItems: FormItem[] = [
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
];
|
];
|
||||||
|
|
||||||
const dataFormRef = ref<{
|
const dataFormRef = ref<InstanceType<typeof FaForm> | null>(null);
|
||||||
resetFields: () => void;
|
|
||||||
clearValidate: () => void;
|
|
||||||
validate: (cb: (valid: boolean) => void) => void;
|
|
||||||
} | null>(null);
|
|
||||||
const submitLoading = ref(false);
|
|
||||||
const formRenderKey = ref(0);
|
const formRenderKey = ref(0);
|
||||||
|
|
||||||
|
const crud = useCrudForm<{{ class_name }}Form>({
|
||||||
|
formData,
|
||||||
|
initialFormData: createInitialFormData(),
|
||||||
|
dialogVisible,
|
||||||
|
dataFormRef,
|
||||||
|
formRenderKey,
|
||||||
|
detailApi: (id: number) => {{ class_name }}API.get{{ class_name }}Detail(id),
|
||||||
|
createApi: (data: {{ class_name }}Form) => {{ class_name }}API.create{{ class_name }}(data),
|
||||||
|
updateApi: (id: number, data: {{ class_name }}Form) => {{ class_name }}API.update{{ class_name }}(id, data),
|
||||||
|
titles: { create: "新增", update: "修改", detail: "详情" },
|
||||||
|
detailFormData,
|
||||||
|
onCreateSuccess: async () => {
|
||||||
|
await refreshCreate();
|
||||||
|
},
|
||||||
|
onUpdateSuccess: async () => {
|
||||||
|
await refreshUpdate();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { submitLoading } = crud;
|
||||||
|
|
||||||
const { importVisible, exportVisible, openImport, openExport } = useImportExport();
|
const { importVisible, exportVisible, openImport, openExport } = useImportExport();
|
||||||
|
|
||||||
const handleSearch = async (params: {{ class_name }}SearchFormParams) => {
|
const handleSearch = async (params: {{ class_name }}SearchFormParams) => {
|
||||||
@@ -598,7 +591,7 @@ const handleSearch = async (params: {{ class_name }}SearchFormParams) => {
|
|||||||
? params.updated_time
|
? params.updated_time
|
||||||
: undefined,
|
: undefined,
|
||||||
} as Record<string, unknown>);
|
} as Record<string, unknown>);
|
||||||
getData();
|
await getData();
|
||||||
};
|
};
|
||||||
|
|
||||||
const onResetSearch = async () => {
|
const onResetSearch = async () => {
|
||||||
@@ -623,7 +616,7 @@ function buildRowActions(row: {{ class_name }}Table): TableOperationAction[] {
|
|||||||
label: "详情",
|
label: "详情",
|
||||||
artType: "view",
|
artType: "view",
|
||||||
perm: "{{ permission_prefix }}:detail",
|
perm: "{{ permission_prefix }}:detail",
|
||||||
run: () => void openDetailDialog(row),
|
run: () => void crud.handleOpenDialog("detail", row[PK] as number),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "edit",
|
key: "edit",
|
||||||
@@ -631,7 +624,7 @@ function buildRowActions(row: {{ class_name }}Table): TableOperationAction[] {
|
|||||||
artType: "edit",
|
artType: "edit",
|
||||||
icon: "ri:edit-2-line",
|
icon: "ri:edit-2-line",
|
||||||
perm: "{{ permission_prefix }}:update",
|
perm: "{{ permission_prefix }}:update",
|
||||||
run: () => void openEditDialog("edit", row),
|
run: () => void crud.handleOpenDialog("update", row[PK] as number),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "delete",
|
key: "delete",
|
||||||
@@ -642,7 +635,7 @@ function buildRowActions(row: {{ class_name }}Table): TableOperationAction[] {
|
|||||||
run: () => deleteRow(row),
|
run: () => deleteRow(row),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
return all.filter((a) => a.perm != null && hasAuth(a.perm));
|
return all;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatOperationCell(row: {{ class_name }}Table) {
|
function formatOperationCell(row: {{ class_name }}Table) {
|
||||||
@@ -651,95 +644,22 @@ function formatOperationCell(row: {{ class_name }}Table) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function openDetailDialog(row: {{ class_name }}Table) {
|
|
||||||
if (!row[PK]) return;
|
|
||||||
const response = await {{ class_name }}API.get{{ class_name }}Detail(row[PK] as number);
|
|
||||||
dialogVisible.type = "detail";
|
|
||||||
dialogVisible.title = "详情";
|
|
||||||
detailFormData.value = response.data.data ?? { ...row };
|
|
||||||
{% if table.sub %}
|
|
||||||
subTableData.value = (response.data.data as any)?.{{ sub_rel_list_name }} ?? [];
|
|
||||||
{% endif %}
|
|
||||||
dialogVisible.visible = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleAdd() {
|
async function handleAdd() {
|
||||||
createLoading.value = true;
|
createLoading.value = true;
|
||||||
try {
|
try {
|
||||||
await openEditDialog("add");
|
await crud.handleOpenDialog("create");
|
||||||
} finally {
|
} finally {
|
||||||
createLoading.value = false;
|
createLoading.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function openEditDialog(type: "add" | "edit", row?: {{ class_name }}Table) {
|
|
||||||
dialogVisible.type = type === "add" ? "create" : "update";
|
|
||||||
if (type === "add") {
|
|
||||||
dialogVisible.title = "新增";
|
|
||||||
Object.assign(formData.value, createInitialFormData());
|
|
||||||
formData.value[PK] = undefined;
|
|
||||||
{% if table.sub %}
|
|
||||||
subTableData.value = [];
|
|
||||||
{% endif %}
|
|
||||||
formRenderKey.value += 1;
|
|
||||||
} else if (row?.[PK]) {
|
|
||||||
dialogVisible.title = "修改";
|
|
||||||
formRenderKey.value += 1;
|
|
||||||
const response = await {{ class_name }}API.get{{ class_name }}Detail(row[PK] as number);
|
|
||||||
Object.assign(formData.value, response.data.data);
|
|
||||||
{% if table.sub %}
|
|
||||||
subTableData.value = (response.data.data as any)?.{{ sub_rel_list_name }} ?? [];
|
|
||||||
{% endif %}
|
|
||||||
}
|
|
||||||
dialogVisible.visible = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function resetForm() {
|
|
||||||
if (dataFormRef.value) {
|
|
||||||
dataFormRef.value.resetFields();
|
|
||||||
dataFormRef.value.clearValidate();
|
|
||||||
}
|
|
||||||
Object.assign(formData.value, createInitialFormData());
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleCloseDialog() {
|
async function handleCloseDialog() {
|
||||||
dialogVisible.visible = false;
|
await crud.handleCloseDialog();
|
||||||
await resetForm();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleSubmit() {
|
async function handleSubmit() {
|
||||||
dataFormRef.value?.validate(async (valid: boolean) => {
|
await crud.handleSubmit();
|
||||||
if (!valid) return;
|
|
||||||
const submitData = { ...formData.value };
|
|
||||||
const id = formData.value[PK] as number | undefined;
|
|
||||||
try {
|
|
||||||
if (id) {
|
|
||||||
await {{ class_name }}API.update{{ class_name }}(id, { [PK]: id, ...submitData });
|
|
||||||
await refreshUpdate();
|
|
||||||
} else {
|
|
||||||
await {{ class_name }}API.create{{ class_name }}(submitData);
|
|
||||||
await refreshCreate();
|
|
||||||
}
|
|
||||||
dialogVisible.visible = false;
|
|
||||||
await resetForm();
|
|
||||||
} catch (error: unknown) {
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteRow = async (row: {{ class_name }}Table) => {
|
|
||||||
if (!row[PK]) return;
|
|
||||||
try {
|
|
||||||
await confirmDelete(`确定删除该{{ function_name }}吗?此操作不可恢复!`);
|
|
||||||
await {{ class_name }}API.delete{{ class_name }}([row[PK] as number]);
|
|
||||||
faTableRef.value?.elTableRef?.clearSelection();
|
|
||||||
await refreshRemove();
|
|
||||||
} catch {
|
|
||||||
// 用户取消
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
{% if table.sub %}
|
{% if table.sub %}
|
||||||
const subTableData = ref<any[]>([]);
|
const subTableData = ref<any[]>([]);
|
||||||
|
|
||||||
@@ -758,6 +678,18 @@ function removeSubRow(index: number) {
|
|||||||
}
|
}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
const deleteRow = async (row: {{ class_name }}Table) => {
|
||||||
|
if (!row[PK]) return;
|
||||||
|
try {
|
||||||
|
await confirmDelete(`确定删除该{{ function_name }}吗?此操作不可恢复!`);
|
||||||
|
await {{ class_name }}API.delete{{ class_name }}([row[PK] as number]);
|
||||||
|
faTableRef.value?.elTableRef?.clearSelection();
|
||||||
|
await refreshRemove();
|
||||||
|
} catch {
|
||||||
|
// 用户取消
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
async function handleBatchDelete() {
|
async function handleBatchDelete() {
|
||||||
const ids = selectedIds.value;
|
const ids = selectedIds.value;
|
||||||
if (ids.length === 0) return;
|
if (ids.length === 0) return;
|
||||||
@@ -774,7 +706,7 @@ async function handleBatchDelete() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function runBatchStatus(status: number) {
|
async function runBatchStatus(value: "enable" | "disable") {
|
||||||
const ids = selectedIds.value;
|
const ids = selectedIds.value;
|
||||||
if (ids.length === 0) {
|
if (ids.length === 0) {
|
||||||
ElMessage.warning("请先在列表中勾选数据");
|
ElMessage.warning("请先在列表中勾选数据");
|
||||||
@@ -782,9 +714,10 @@ async function runBatchStatus(status: number) {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await confirmAction(
|
await confirmAction(
|
||||||
`确认对选中的 ${ids.length} 条数据${status === 0 ? "启用" : "停用"}?`,
|
`确认对选中的 ${ids.length} 条数据${value === "enable" ? "启用" : "停用"}?`,
|
||||||
"批量设置"
|
"批量设置"
|
||||||
);
|
);
|
||||||
|
const status = value === "enable" ? 0 : 1;
|
||||||
await {{ class_name }}API.batch{{ class_name }}({ ids, status });
|
await {{ class_name }}API.batch{{ class_name }}({ ids, status });
|
||||||
// 成功 / 失败提示由 axios 拦截器统一处理
|
// 成功 / 失败提示由 axios 拦截器统一处理
|
||||||
faTableRef.value?.elTableRef?.clearSelection();
|
faTableRef.value?.elTableRef?.clearSelection();
|
||||||
@@ -803,8 +736,8 @@ async function handleCrudImportUpload(formData: FormData) {
|
|||||||
await refreshData();
|
await refreshData();
|
||||||
}
|
}
|
||||||
// 非 SUCCESS 分支提示由 axios 拦截器统一处理
|
// 非 SUCCESS 分支提示由 axios 拦截器统一处理
|
||||||
} catch (error) {
|
} catch (error: unknown) {
|
||||||
console.error("[Import]", error);
|
if (import.meta.env.DEV) console.error("[Import]", error);
|
||||||
/* 接口错误已由拦截器提示 */
|
/* 接口错误已由拦截器提示 */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
# 【通用】环境变量 - 所有环境共享
|
# 【通用】环境变量 - 所有环境共享
|
||||||
|
|
||||||
|
# 项目名称
|
||||||
|
VITE_APP_TITLE = FastapiAdmin
|
||||||
|
|
||||||
# 应用版本号
|
# 应用版本号
|
||||||
VITE_VERSION = 3.0.0
|
VITE_VERSION = 3.0.0
|
||||||
|
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
# 【测试】环境变量 - 覆盖通用配置(案例)
|
|
||||||
|
|
||||||
# 环境标识
|
|
||||||
VITE_APP_ENV = test
|
|
||||||
|
|
||||||
# 项目名称
|
|
||||||
VITE_APP_TITLE = FastapiAdmin
|
|
||||||
|
|
||||||
# 浏览器侧同源前缀(与 Vite 代理配合时用 /,请求走 localhost:端口 + 代理)
|
|
||||||
VITE_API_URL = /
|
|
||||||
|
|
||||||
# 代理目标:本机后端
|
|
||||||
VITE_API_BASE_URL = http://127.0.0.1:8001
|
|
||||||
|
|
||||||
# 是否删除控制台输出
|
|
||||||
VITE_DROP_CONSOLE = false
|
|
||||||
|
|
||||||
# WebSocket 端点(AI对话功能需要配置)
|
|
||||||
VITE_APP_WS_ENDPOINT = ws://localhost:8001
|
|
||||||
@@ -6,16 +6,14 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"i": "pnpm install",
|
"i": "pnpm install",
|
||||||
"dev": "vite --mode dev",
|
"dev": "vite --mode development",
|
||||||
"dev:force": "vite --force --mode dev",
|
"dev:force": "vite --force --mode development",
|
||||||
"prod": "vite --mode prod",
|
"prod": "vite --mode production",
|
||||||
"build": "vue-tsc --noEmit && vite build",
|
"build": "vue-tsc --noEmit && vite build",
|
||||||
"build:prod": "pnpm vite build --mode prod",
|
"build:prod": "pnpm vite build --mode production",
|
||||||
"build:dev": "pnpm vite build --mode dev",
|
"build:dev": "pnpm vite build --mode development",
|
||||||
"build:test": "pnpm vite build --mode test",
|
"serve:prod": "pnpm vite preview --mode production",
|
||||||
"serve:prod": "pnpm vite preview --mode prod",
|
"serve:dev": "pnpm vite preview --mode development",
|
||||||
"serve:dev": "pnpm vite preview --mode dev",
|
|
||||||
"serve:test": "pnpm vite preview --mode test",
|
|
||||||
"clean": "pnpx rimraf node_modules",
|
"clean": "pnpx rimraf node_modules",
|
||||||
"ts:check": "pnpm vue-tsc --noEmit --skipLibCheck",
|
"ts:check": "pnpm vue-tsc --noEmit --skipLibCheck",
|
||||||
"npm:check": "pnpx npm-check-updates -u",
|
"npm:check": "pnpx npm-check-updates -u",
|
||||||
@@ -78,7 +76,7 @@
|
|||||||
"@vue-flow/controls": "^1.1.3",
|
"@vue-flow/controls": "^1.1.3",
|
||||||
"@vue-flow/core": "^1.48.1",
|
"@vue-flow/core": "^1.48.1",
|
||||||
"@vue-flow/minimap": "^1.5.4",
|
"@vue-flow/minimap": "^1.5.4",
|
||||||
"@vueuse/core": "^13.9.0",
|
"@vueuse/core": "^14.3.0",
|
||||||
"@wangeditor-next/editor": "^5.7.0",
|
"@wangeditor-next/editor": "^5.7.0",
|
||||||
"@wangeditor-next/editor-for-vue": "^5.1.14",
|
"@wangeditor-next/editor-for-vue": "^5.1.14",
|
||||||
"axios": "^1.16.1",
|
"axios": "^1.16.1",
|
||||||
|
|||||||
Generated
+5
-31
@@ -49,8 +49,8 @@ importers:
|
|||||||
specifier: ^1.5.4
|
specifier: ^1.5.4
|
||||||
version: 1.5.4(@vue-flow/core@1.48.2(vue@3.5.34(typescript@6.0.3)))(vue@3.5.34(typescript@6.0.3))
|
version: 1.5.4(@vue-flow/core@1.48.2(vue@3.5.34(typescript@6.0.3)))(vue@3.5.34(typescript@6.0.3))
|
||||||
'@vueuse/core':
|
'@vueuse/core':
|
||||||
specifier: ^13.9.0
|
specifier: ^14.3.0
|
||||||
version: 13.9.0(vue@3.5.34(typescript@6.0.3))
|
version: 14.3.0(vue@3.5.34(typescript@6.0.3))
|
||||||
'@wangeditor-next/editor':
|
'@wangeditor-next/editor':
|
||||||
specifier: ^5.7.0
|
specifier: ^5.7.0
|
||||||
version: 5.7.0
|
version: 5.7.0
|
||||||
@@ -300,7 +300,7 @@ importers:
|
|||||||
version: 8.59.3(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3)
|
version: 8.59.3(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3)
|
||||||
unplugin-auto-import:
|
unplugin-auto-import:
|
||||||
specifier: ^20.3.0
|
specifier: ^20.3.0
|
||||||
version: 20.3.0(@vueuse/core@13.9.0(vue@3.5.34(typescript@6.0.3)))
|
version: 20.3.0(@vueuse/core@14.3.0(vue@3.5.34(typescript@6.0.3)))
|
||||||
unplugin-element-plus:
|
unplugin-element-plus:
|
||||||
specifier: ^0.10.0
|
specifier: ^0.10.0
|
||||||
version: 0.10.0
|
version: 0.10.0
|
||||||
@@ -1821,11 +1821,6 @@ packages:
|
|||||||
'@vueuse/core@10.11.1':
|
'@vueuse/core@10.11.1':
|
||||||
resolution: {integrity: sha512-guoy26JQktXPcz+0n3GukWIy/JDNKti9v6VEMu6kV2sYBsWuGiTU8OWdg+ADfUbHg3/3DlqySDe7JmdHrktiww==}
|
resolution: {integrity: sha512-guoy26JQktXPcz+0n3GukWIy/JDNKti9v6VEMu6kV2sYBsWuGiTU8OWdg+ADfUbHg3/3DlqySDe7JmdHrktiww==}
|
||||||
|
|
||||||
'@vueuse/core@13.9.0':
|
|
||||||
resolution: {integrity: sha512-ts3regBQyURfCE2BcytLqzm8+MmLlo5Ln/KLoxDVcsZ2gzIwVNnQpQOL/UKV8alUqjSZOlpFZcRNsLRqj+OzyA==}
|
|
||||||
peerDependencies:
|
|
||||||
vue: ^3.5.0
|
|
||||||
|
|
||||||
'@vueuse/core@14.3.0':
|
'@vueuse/core@14.3.0':
|
||||||
resolution: {integrity: sha512-aHfz47g0ZhMtTVHmIzMVpJy8ePhhOy68GY5bv110+5DVtZ+W7BsOx+m61UNQqfrWyPztIHIanWa3E2tib3NFIw==}
|
resolution: {integrity: sha512-aHfz47g0ZhMtTVHmIzMVpJy8ePhhOy68GY5bv110+5DVtZ+W7BsOx+m61UNQqfrWyPztIHIanWa3E2tib3NFIw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -1834,20 +1829,12 @@ packages:
|
|||||||
'@vueuse/metadata@10.11.1':
|
'@vueuse/metadata@10.11.1':
|
||||||
resolution: {integrity: sha512-IGa5FXd003Ug1qAZmyE8wF3sJ81xGLSqTqtQ6jaVfkeZ4i5kS2mwQF61yhVqojRnenVew5PldLyRgvdl4YYuSw==}
|
resolution: {integrity: sha512-IGa5FXd003Ug1qAZmyE8wF3sJ81xGLSqTqtQ6jaVfkeZ4i5kS2mwQF61yhVqojRnenVew5PldLyRgvdl4YYuSw==}
|
||||||
|
|
||||||
'@vueuse/metadata@13.9.0':
|
|
||||||
resolution: {integrity: sha512-1AFRvuiGphfF7yWixZa0KwjYH8ulyjDCC0aFgrGRz8+P4kvDFSdXLVfTk5xAN9wEuD1J6z4/myMoYbnHoX07zg==}
|
|
||||||
|
|
||||||
'@vueuse/metadata@14.3.0':
|
'@vueuse/metadata@14.3.0':
|
||||||
resolution: {integrity: sha512-BwxmbAzwAVF50+MW57GXOUEV61nFBGnlBvrTqj49PqWJu3uw7hdu72ztXeZ33RdZtDY6kO+bfCAE1PCn88Tktw==}
|
resolution: {integrity: sha512-BwxmbAzwAVF50+MW57GXOUEV61nFBGnlBvrTqj49PqWJu3uw7hdu72ztXeZ33RdZtDY6kO+bfCAE1PCn88Tktw==}
|
||||||
|
|
||||||
'@vueuse/shared@10.11.1':
|
'@vueuse/shared@10.11.1':
|
||||||
resolution: {integrity: sha512-LHpC8711VFZlDaYUXEBbFBCQ7GS3dVU9mjOhhMhXP6txTV4EhYQg/KGnQuvt/sPAtoUKq7VVUnL6mVtFoL42sA==}
|
resolution: {integrity: sha512-LHpC8711VFZlDaYUXEBbFBCQ7GS3dVU9mjOhhMhXP6txTV4EhYQg/KGnQuvt/sPAtoUKq7VVUnL6mVtFoL42sA==}
|
||||||
|
|
||||||
'@vueuse/shared@13.9.0':
|
|
||||||
resolution: {integrity: sha512-e89uuTLMh0U5cZ9iDpEI2senqPGfbPRTHM/0AaQkcxnpqjkZqDYP8rpfm7edOz8s+pOCOROEy1PIveSW8+fL5g==}
|
|
||||||
peerDependencies:
|
|
||||||
vue: ^3.5.0
|
|
||||||
|
|
||||||
'@vueuse/shared@14.3.0':
|
'@vueuse/shared@14.3.0':
|
||||||
resolution: {integrity: sha512-bZpge9eSXwa4ToSiqJ7j6KRwhAsneMFoSz3LMWKQDkqimm3D/tbFlrklrs/IOqC8tEcYmXQZJ6N0UrjhBirVCg==}
|
resolution: {integrity: sha512-bZpge9eSXwa4ToSiqJ7j6KRwhAsneMFoSz3LMWKQDkqimm3D/tbFlrklrs/IOqC8tEcYmXQZJ6N0UrjhBirVCg==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -6302,13 +6289,6 @@ snapshots:
|
|||||||
- '@vue/composition-api'
|
- '@vue/composition-api'
|
||||||
- vue
|
- vue
|
||||||
|
|
||||||
'@vueuse/core@13.9.0(vue@3.5.34(typescript@6.0.3))':
|
|
||||||
dependencies:
|
|
||||||
'@types/web-bluetooth': 0.0.21
|
|
||||||
'@vueuse/metadata': 13.9.0
|
|
||||||
'@vueuse/shared': 13.9.0(vue@3.5.34(typescript@6.0.3))
|
|
||||||
vue: 3.5.34(typescript@6.0.3)
|
|
||||||
|
|
||||||
'@vueuse/core@14.3.0(vue@3.5.34(typescript@6.0.3))':
|
'@vueuse/core@14.3.0(vue@3.5.34(typescript@6.0.3))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/web-bluetooth': 0.0.21
|
'@types/web-bluetooth': 0.0.21
|
||||||
@@ -6318,8 +6298,6 @@ snapshots:
|
|||||||
|
|
||||||
'@vueuse/metadata@10.11.1': {}
|
'@vueuse/metadata@10.11.1': {}
|
||||||
|
|
||||||
'@vueuse/metadata@13.9.0': {}
|
|
||||||
|
|
||||||
'@vueuse/metadata@14.3.0': {}
|
'@vueuse/metadata@14.3.0': {}
|
||||||
|
|
||||||
'@vueuse/shared@10.11.1(vue@3.5.34(typescript@6.0.3))':
|
'@vueuse/shared@10.11.1(vue@3.5.34(typescript@6.0.3))':
|
||||||
@@ -6329,10 +6307,6 @@ snapshots:
|
|||||||
- '@vue/composition-api'
|
- '@vue/composition-api'
|
||||||
- vue
|
- vue
|
||||||
|
|
||||||
'@vueuse/shared@13.9.0(vue@3.5.34(typescript@6.0.3))':
|
|
||||||
dependencies:
|
|
||||||
vue: 3.5.34(typescript@6.0.3)
|
|
||||||
|
|
||||||
'@vueuse/shared@14.3.0(vue@3.5.34(typescript@6.0.3))':
|
'@vueuse/shared@14.3.0(vue@3.5.34(typescript@6.0.3))':
|
||||||
dependencies:
|
dependencies:
|
||||||
vue: 3.5.34(typescript@6.0.3)
|
vue: 3.5.34(typescript@6.0.3)
|
||||||
@@ -8973,7 +8947,7 @@ snapshots:
|
|||||||
|
|
||||||
universalify@2.0.1: {}
|
universalify@2.0.1: {}
|
||||||
|
|
||||||
unplugin-auto-import@20.3.0(@vueuse/core@13.9.0(vue@3.5.34(typescript@6.0.3))):
|
unplugin-auto-import@20.3.0(@vueuse/core@14.3.0(vue@3.5.34(typescript@6.0.3))):
|
||||||
dependencies:
|
dependencies:
|
||||||
local-pkg: 1.1.2
|
local-pkg: 1.1.2
|
||||||
magic-string: 0.30.21
|
magic-string: 0.30.21
|
||||||
@@ -8982,7 +8956,7 @@ snapshots:
|
|||||||
unplugin: 2.3.11
|
unplugin: 2.3.11
|
||||||
unplugin-utils: 0.3.1
|
unplugin-utils: 0.3.1
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@vueuse/core': 13.9.0(vue@3.5.34(typescript@6.0.3))
|
'@vueuse/core': 14.3.0(vue@3.5.34(typescript@6.0.3))
|
||||||
|
|
||||||
unplugin-element-plus@0.10.0:
|
unplugin-element-plus@0.10.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|||||||
@@ -142,20 +142,14 @@ export default UserAPI;
|
|||||||
export interface ForgetPasswordForm {
|
export interface ForgetPasswordForm {
|
||||||
username: string;
|
username: string;
|
||||||
new_password: string;
|
new_password: string;
|
||||||
mobile?: string;
|
|
||||||
confirmPassword: string;
|
confirmPassword: string;
|
||||||
captcha_key?: string;
|
|
||||||
captcha?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RegisterForm {
|
export interface RegisterForm {
|
||||||
username: string;
|
username: string;
|
||||||
password: string;
|
password: string;
|
||||||
confirmPassword: string;
|
confirmPassword: string;
|
||||||
email?: string;
|
|
||||||
name?: string;
|
name?: string;
|
||||||
captcha_key?: string;
|
|
||||||
captcha?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UserPageQuery extends PageQuery, UserByQueryParams {
|
export interface UserPageQuery extends PageQuery, UserByQueryParams {
|
||||||
|
|||||||
@@ -29,36 +29,36 @@
|
|||||||
<!-- 卡片网格 -->
|
<!-- 卡片网格 -->
|
||||||
<div v-else-if="!isEmpty" :style="{ padding: `0 ${gutter / 2}px` }">
|
<div v-else-if="!isEmpty" :style="{ padding: `0 ${gutter / 2}px` }">
|
||||||
<ElRow :gutter="gutter">
|
<ElRow :gutter="gutter">
|
||||||
<ElCol
|
<ElCol
|
||||||
v-for="(item, index) in items"
|
v-for="(item, index) in items"
|
||||||
:key="item[keyField] ?? index"
|
:key="item[keyField] ?? index"
|
||||||
:xs="xs"
|
:xs="xs"
|
||||||
:sm="sm"
|
:sm="sm"
|
||||||
:md="md"
|
:md="md"
|
||||||
:lg="lg"
|
:lg="lg"
|
||||||
:xl="xl"
|
:xl="xl"
|
||||||
class="mb-4"
|
class="mb-4"
|
||||||
>
|
>
|
||||||
<ElCard
|
<ElCard
|
||||||
class="fa-card"
|
class="fa-card"
|
||||||
:class="cardClass"
|
:class="cardClass"
|
||||||
shadow="hover"
|
shadow="hover"
|
||||||
:header-class="headerClass"
|
:header-class="headerClass"
|
||||||
:body-class="bodyClass"
|
:body-class="bodyClass"
|
||||||
:footer-class="footerClass"
|
:footer-class="footerClass"
|
||||||
@click="(e: MouseEvent) => emit('itemClick', item, e)"
|
@click="(e: MouseEvent) => emit('itemClick', item, e)"
|
||||||
>
|
>
|
||||||
<template v-if="$slots.header" #header>
|
<template v-if="$slots.header" #header>
|
||||||
<slot name="header" :item="item" :index="index" />
|
<slot name="header" :item="item" :index="index" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<slot :item="item" :index="index" />
|
<slot :item="item" :index="index" />
|
||||||
|
|
||||||
<template v-if="$slots.footer" #footer>
|
<template v-if="$slots.footer" #footer>
|
||||||
<slot name="footer" :item="item" :index="index" />
|
<slot name="footer" :item="item" :index="index" />
|
||||||
</template>
|
</template>
|
||||||
</ElCard>
|
</ElCard>
|
||||||
</ElCol>
|
</ElCol>
|
||||||
</ElRow>
|
</ElRow>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -86,7 +86,15 @@
|
|||||||
|
|
||||||
<script setup lang="ts" generic="T extends Record<string, any>">
|
<script setup lang="ts" generic="T extends Record<string, any>">
|
||||||
import { computed } from "vue";
|
import { computed } from "vue";
|
||||||
import { ElCard, ElRow, ElCol, ElEmpty, ElSkeleton, ElSkeletonItem, ElScrollbar } from "element-plus";
|
import {
|
||||||
|
ElCard,
|
||||||
|
ElRow,
|
||||||
|
ElCol,
|
||||||
|
ElEmpty,
|
||||||
|
ElSkeleton,
|
||||||
|
ElSkeletonItem,
|
||||||
|
ElScrollbar,
|
||||||
|
} from "element-plus";
|
||||||
import FaPagination from "@/components/others/fa-pagination/index.vue";
|
import FaPagination from "@/components/others/fa-pagination/index.vue";
|
||||||
|
|
||||||
defineOptions({ name: "FaCardGrid" });
|
defineOptions({ name: "FaCardGrid" });
|
||||||
|
|||||||
@@ -35,7 +35,7 @@
|
|||||||
:model-value="modelValue?.created_id == null ? undefined : modelValue.created_id"
|
:model-value="modelValue?.created_id == null ? undefined : modelValue.created_id"
|
||||||
@update:model-value="
|
@update:model-value="
|
||||||
(v: number | undefined) => {
|
(v: number | undefined) => {
|
||||||
modelValue.value['created_id'] = v;
|
modelValue['created_id'] = v;
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
@confirm-click="emitImmediateSearch"
|
@confirm-click="emitImmediateSearch"
|
||||||
@@ -50,7 +50,7 @@
|
|||||||
:model-value="modelValue?.updated_id == null ? undefined : modelValue.updated_id"
|
:model-value="modelValue?.updated_id == null ? undefined : modelValue.updated_id"
|
||||||
@update:model-value="
|
@update:model-value="
|
||||||
(v: number | undefined) => {
|
(v: number | undefined) => {
|
||||||
modelValue.value['updated_id'] = v;
|
modelValue['updated_id'] = v;
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
@confirm-click="emitImmediateSearch"
|
@confirm-click="emitImmediateSearch"
|
||||||
|
|||||||
@@ -124,6 +124,7 @@ import { resolveIconForFaSvgIcon } from "@utils";
|
|||||||
import { nextTick, onBeforeUnmount, onMounted, watch, ref, computed } from "vue";
|
import { nextTick, onBeforeUnmount, onMounted, watch, ref, computed } from "vue";
|
||||||
import { useRouter } from "vue-router";
|
import { useRouter } from "vue-router";
|
||||||
import { ElMessage } from "element-plus";
|
import { ElMessage } from "element-plus";
|
||||||
|
import { MagicStick, Position, Tools } from "@element-plus/icons-vue";
|
||||||
import { useSettingsStore } from "@stores";
|
import { useSettingsStore } from "@stores";
|
||||||
import { AiChatAPI, ChatSession, ChatSessionDetail } from "@/api/module_ai/chat";
|
import { AiChatAPI, ChatSession, ChatSessionDetail } from "@/api/module_ai/chat";
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,10 @@
|
|||||||
<ElRadio value="specify">
|
<ElRadio value="specify">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<span class="mr-2">指定</span>
|
<span class="mr-2">指定</span>
|
||||||
<ElCheckboxGroup v-model="fields.second.specified" :disabled="fields.second.mode !== 'specify'">
|
<ElCheckboxGroup
|
||||||
|
v-model="fields.second.specified"
|
||||||
|
:disabled="fields.second.mode !== 'specify'"
|
||||||
|
>
|
||||||
<ElCheckbox v-for="v in secondOptions" :key="v" :label="v" :value="v" />
|
<ElCheckbox v-for="v in secondOptions" :key="v" :label="v" :value="v" />
|
||||||
</ElCheckboxGroup>
|
</ElCheckboxGroup>
|
||||||
</div>
|
</div>
|
||||||
@@ -57,7 +60,10 @@
|
|||||||
<ElRadio value="specify">
|
<ElRadio value="specify">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<span class="mr-2">指定</span>
|
<span class="mr-2">指定</span>
|
||||||
<ElCheckboxGroup v-model="fields.minute.specified" :disabled="fields.minute.mode !== 'specify'">
|
<ElCheckboxGroup
|
||||||
|
v-model="fields.minute.specified"
|
||||||
|
:disabled="fields.minute.mode !== 'specify'"
|
||||||
|
>
|
||||||
<ElCheckbox v-for="v in minuteOptions" :key="v" :label="v" :value="v" />
|
<ElCheckbox v-for="v in minuteOptions" :key="v" :label="v" :value="v" />
|
||||||
</ElCheckboxGroup>
|
</ElCheckboxGroup>
|
||||||
</div>
|
</div>
|
||||||
@@ -89,7 +95,10 @@
|
|||||||
<ElRadio value="specify">
|
<ElRadio value="specify">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<span class="mr-2">指定</span>
|
<span class="mr-2">指定</span>
|
||||||
<ElCheckboxGroup v-model="fields.hour.specified" :disabled="fields.hour.mode !== 'specify'">
|
<ElCheckboxGroup
|
||||||
|
v-model="fields.hour.specified"
|
||||||
|
:disabled="fields.hour.mode !== 'specify'"
|
||||||
|
>
|
||||||
<ElCheckbox v-for="v in hourOptions" :key="v" :label="v" :value="v" />
|
<ElCheckbox v-for="v in hourOptions" :key="v" :label="v" :value="v" />
|
||||||
</ElCheckboxGroup>
|
</ElCheckboxGroup>
|
||||||
</div>
|
</div>
|
||||||
@@ -130,7 +139,10 @@
|
|||||||
<ElRadio value="specify">
|
<ElRadio value="specify">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<span class="mr-2">指定</span>
|
<span class="mr-2">指定</span>
|
||||||
<ElCheckboxGroup v-model="fields.day.specified" :disabled="fields.day.mode !== 'specify'">
|
<ElCheckboxGroup
|
||||||
|
v-model="fields.day.specified"
|
||||||
|
:disabled="fields.day.mode !== 'specify'"
|
||||||
|
>
|
||||||
<ElCheckbox v-for="v in dayOptions" :key="v" :label="v" :value="v" />
|
<ElCheckbox v-for="v in dayOptions" :key="v" :label="v" :value="v" />
|
||||||
</ElCheckboxGroup>
|
</ElCheckboxGroup>
|
||||||
</div>
|
</div>
|
||||||
@@ -163,7 +175,10 @@
|
|||||||
<ElRadio value="specify">
|
<ElRadio value="specify">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<span class="mr-2">指定</span>
|
<span class="mr-2">指定</span>
|
||||||
<ElCheckboxGroup v-model="fields.month.specified" :disabled="fields.month.mode !== 'specify'">
|
<ElCheckboxGroup
|
||||||
|
v-model="fields.month.specified"
|
||||||
|
:disabled="fields.month.mode !== 'specify'"
|
||||||
|
>
|
||||||
<ElCheckbox v-for="v in monthOptions" :key="v" :label="v" :value="v" />
|
<ElCheckbox v-for="v in monthOptions" :key="v" :label="v" :value="v" />
|
||||||
</ElCheckboxGroup>
|
</ElCheckboxGroup>
|
||||||
</div>
|
</div>
|
||||||
@@ -200,7 +215,10 @@
|
|||||||
<ElRadio value="specify">
|
<ElRadio value="specify">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<span class="mr-2">指定</span>
|
<span class="mr-2">指定</span>
|
||||||
<ElCheckboxGroup v-model="fields.week.specified" :disabled="fields.week.mode !== 'specify'">
|
<ElCheckboxGroup
|
||||||
|
v-model="fields.week.specified"
|
||||||
|
:disabled="fields.week.mode !== 'specify'"
|
||||||
|
>
|
||||||
<ElCheckbox v-for="v in weekOptions" :key="v" :label="v" :value="v" />
|
<ElCheckbox v-for="v in weekOptions" :key="v" :label="v" :value="v" />
|
||||||
</ElCheckboxGroup>
|
</ElCheckboxGroup>
|
||||||
</div>
|
</div>
|
||||||
@@ -220,266 +238,293 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<span class="text-xs text-(--el-text-color-secondary)">下次执行:</span>
|
<span class="text-xs text-(--el-text-color-secondary)">下次执行:</span>
|
||||||
<span class="text-sm">{{ nextRun || '—' }}</span>
|
<span class="text-sm">{{ nextRun || "—" }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, nextTick, ref, watch } from 'vue'
|
import { computed, nextTick, ref, watch } from "vue";
|
||||||
import cronParser from 'cron-parser'
|
import cronParser from "cron-parser";
|
||||||
import dayjs from 'dayjs'
|
import dayjs from "dayjs";
|
||||||
|
|
||||||
interface FieldState {
|
interface FieldState {
|
||||||
mode: string
|
mode: string;
|
||||||
rangeStart: string
|
rangeStart: string;
|
||||||
rangeEnd: string
|
rangeEnd: string;
|
||||||
stepFrom: string
|
stepFrom: string;
|
||||||
stepInterval: string
|
stepInterval: string;
|
||||||
specified: string[]
|
specified: string[];
|
||||||
extra: string
|
extra: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Fields {
|
interface Fields {
|
||||||
second: FieldState
|
second: FieldState;
|
||||||
minute: FieldState
|
minute: FieldState;
|
||||||
hour: FieldState
|
hour: FieldState;
|
||||||
day: FieldState
|
day: FieldState;
|
||||||
month: FieldState
|
month: FieldState;
|
||||||
week: FieldState
|
week: FieldState;
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultField = (init: Partial<FieldState> = {}): FieldState => ({
|
const defaultField = (init: Partial<FieldState> = {}): FieldState => ({
|
||||||
mode: 'every',
|
mode: "every",
|
||||||
rangeStart: '0',
|
rangeStart: "0",
|
||||||
rangeEnd: '0',
|
rangeEnd: "0",
|
||||||
stepFrom: '0',
|
stepFrom: "0",
|
||||||
stepInterval: '1',
|
stepInterval: "1",
|
||||||
specified: [],
|
specified: [],
|
||||||
extra: '',
|
extra: "",
|
||||||
...init,
|
...init,
|
||||||
})
|
});
|
||||||
|
|
||||||
const fields = ref<Fields>({
|
const fields = ref<Fields>({
|
||||||
second: defaultField({ rangeStart: '0', rangeEnd: '59', stepFrom: '0' }),
|
second: defaultField({ rangeStart: "0", rangeEnd: "59", stepFrom: "0" }),
|
||||||
minute: defaultField({ rangeStart: '0', rangeEnd: '59', stepFrom: '0' }),
|
minute: defaultField({ rangeStart: "0", rangeEnd: "59", stepFrom: "0" }),
|
||||||
hour: defaultField({ rangeStart: '0', rangeEnd: '23', stepFrom: '0' }),
|
hour: defaultField({ rangeStart: "0", rangeEnd: "23", stepFrom: "0" }),
|
||||||
day: defaultField({ mode: 'every', rangeStart: '1', rangeEnd: '31', stepFrom: '1' }),
|
day: defaultField({ mode: "every", rangeStart: "1", rangeEnd: "31", stepFrom: "1" }),
|
||||||
month: defaultField({ mode: 'every', rangeStart: '1', rangeEnd: '12', stepFrom: '1' }),
|
month: defaultField({ mode: "every", rangeStart: "1", rangeEnd: "12", stepFrom: "1" }),
|
||||||
week: defaultField({ mode: 'any', rangeStart: '1', rangeEnd: '7', stepFrom: '1' }),
|
week: defaultField({ mode: "any", rangeStart: "1", rangeEnd: "7", stepFrom: "1" }),
|
||||||
})
|
});
|
||||||
|
|
||||||
const activeTab = ref('秒')
|
const activeTab = ref("秒");
|
||||||
|
|
||||||
const props = defineProps<{ modelValue?: string }>()
|
const props = defineProps<{ modelValue?: string }>();
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
'update:modelValue': [value: string]
|
"update:modelValue": [value: string];
|
||||||
}>()
|
}>();
|
||||||
|
|
||||||
const pad = (n: number) => n.toString().padStart(2, '0')
|
const pad = (n: number) => n.toString().padStart(2, "0");
|
||||||
const rangeOptions = (count: number, start = 0) => Array.from({ length: count }, (_, i) => pad(start + i))
|
const rangeOptions = (count: number, start = 0) =>
|
||||||
const secondOptions = rangeOptions(60)
|
Array.from({ length: count }, (_, i) => pad(start + i));
|
||||||
const minuteOptions = rangeOptions(60)
|
const secondOptions = rangeOptions(60);
|
||||||
const hourOptions = rangeOptions(24)
|
const minuteOptions = rangeOptions(60);
|
||||||
const dayOptions = rangeOptions(31, 1)
|
const hourOptions = rangeOptions(24);
|
||||||
const monthOptions = rangeOptions(12, 1)
|
const dayOptions = rangeOptions(31, 1);
|
||||||
const weekOptions = rangeOptions(7, 1)
|
const monthOptions = rangeOptions(12, 1);
|
||||||
|
const weekOptions = rangeOptions(7, 1);
|
||||||
|
|
||||||
function toSegment(field: FieldState): string {
|
function toSegment(field: FieldState): string {
|
||||||
switch (field.mode) {
|
switch (field.mode) {
|
||||||
case 'every':
|
case "every":
|
||||||
return '*'
|
return "*";
|
||||||
case 'any':
|
case "any":
|
||||||
return '?'
|
return "?";
|
||||||
case 'range':
|
case "range":
|
||||||
return `${field.rangeStart}-${field.rangeEnd}`
|
return `${field.rangeStart}-${field.rangeEnd}`;
|
||||||
case 'step':
|
case "step":
|
||||||
return `${field.stepFrom}/${field.stepInterval}`
|
return `${field.stepFrom}/${field.stepInterval}`;
|
||||||
case 'specify':
|
case "specify":
|
||||||
return field.specified.map((v) => parseInt(v)).sort((a, b) => a - b).join(',') || '*'
|
return (
|
||||||
case 'last':
|
field.specified
|
||||||
return 'L'
|
.map((v) => parseInt(v))
|
||||||
case 'lastWeekday':
|
.sort((a, b) => a - b)
|
||||||
return `${field.extra}W`
|
.join(",") || "*"
|
||||||
case 'lastWeek':
|
);
|
||||||
return `${field.extra}L`
|
case "last":
|
||||||
case 'nthWeek':
|
return "L";
|
||||||
return `${field.rangeStart}#${field.rangeEnd}`
|
case "lastWeekday":
|
||||||
|
return `${field.extra}W`;
|
||||||
|
case "lastWeek":
|
||||||
|
return `${field.extra}L`;
|
||||||
|
case "nthWeek":
|
||||||
|
return `${field.rangeStart}#${field.rangeEnd}`;
|
||||||
default:
|
default:
|
||||||
return '*'
|
return "*";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const cron = computed(() => {
|
const cron = computed(() => {
|
||||||
const f = fields.value
|
const f = fields.value;
|
||||||
return `${toSegment(f.second)} ${toSegment(f.minute)} ${toSegment(f.hour)} ${toSegment(f.day)} ${toSegment(f.month)} ${toSegment(f.week)}`
|
return `${toSegment(f.second)} ${toSegment(f.minute)} ${toSegment(f.hour)} ${toSegment(f.day)} ${toSegment(f.month)} ${toSegment(f.week)}`;
|
||||||
})
|
});
|
||||||
|
|
||||||
const cronDesc = computed(() => {
|
const cronDesc = computed(() => {
|
||||||
const { second, minute, hour, day, month, week } = fields.value
|
const { second, minute, hour, day, month, week } = fields.value;
|
||||||
const W = ['', '周日', '周一', '周二', '周三', '周四', '周五', '周六']
|
const W = ["", "周日", "周一", "周二", "周三", "周四", "周五", "周六"];
|
||||||
|
|
||||||
// 层级: 年 -> 月 -> 日/周 -> 时 -> 分 -> 秒
|
// 层级: 年 -> 月 -> 日/周 -> 时 -> 分 -> 秒
|
||||||
const parts: string[] = []
|
const parts: string[] = [];
|
||||||
parts.push('每年')
|
parts.push("每年");
|
||||||
|
|
||||||
// 月
|
// 月
|
||||||
if (month.mode === 'specify' && month.specified.length) parts.push(`${month.specified.join('、')}月`)
|
if (month.mode === "specify" && month.specified.length)
|
||||||
else parts.push('每月')
|
parts.push(`${month.specified.join("、")}月`);
|
||||||
|
else parts.push("每月");
|
||||||
|
|
||||||
// 日/周
|
// 日/周
|
||||||
if (day.mode === 'specify') parts.push(`${day.specified.join('、')}号`)
|
if (day.mode === "specify") parts.push(`${day.specified.join("、")}号`);
|
||||||
else if (day.mode === 'last') parts.push('最后一天')
|
else if (day.mode === "last") parts.push("最后一天");
|
||||||
else if (day.mode === 'lastWeekday') parts.push(`${day.extra}号最近工作日`)
|
else if (day.mode === "lastWeekday") parts.push(`${day.extra}号最近工作日`);
|
||||||
else if (week.mode === 'specify') parts.push(week.specified.map((v) => W[+v] || v).join('、'))
|
else if (week.mode === "specify") parts.push(week.specified.map((v) => W[+v] || v).join("、"));
|
||||||
else if (week.mode === 'range') parts.push(`${W[+week.rangeStart] || ''}至${W[+week.rangeEnd] || ''}`)
|
else if (week.mode === "range")
|
||||||
else if (week.mode === 'lastWeek') parts.push(`最后一个${W[+week.extra] || ''}`)
|
parts.push(`${W[+week.rangeStart] || ""}至${W[+week.rangeEnd] || ""}`);
|
||||||
else if (week.mode === 'every') parts.push(day.mode === 'any' ? '每周' : '每天')
|
else if (week.mode === "lastWeek") parts.push(`最后一个${W[+week.extra] || ""}`);
|
||||||
else parts.push('每天')
|
else if (week.mode === "every") parts.push(day.mode === "any" ? "每周" : "每天");
|
||||||
|
else parts.push("每天");
|
||||||
|
|
||||||
// 时
|
// 时
|
||||||
if (hour.mode === 'every') parts.push('每时')
|
if (hour.mode === "every") parts.push("每时");
|
||||||
else if (hour.mode === 'step') parts.push(`每${hour.stepInterval}时`)
|
else if (hour.mode === "step") parts.push(`每${hour.stepInterval}时`);
|
||||||
else if (hour.mode === 'specify' && hour.specified.length === 1) parts.push(`${hour.specified[0]}时`)
|
else if (hour.mode === "specify" && hour.specified.length === 1)
|
||||||
|
parts.push(`${hour.specified[0]}时`);
|
||||||
|
|
||||||
// 分
|
// 分
|
||||||
if (minute.mode === 'every') parts.push('每分')
|
if (minute.mode === "every") parts.push("每分");
|
||||||
else if (minute.mode === 'step') parts.push(`每${minute.stepInterval}分`)
|
else if (minute.mode === "step") parts.push(`每${minute.stepInterval}分`);
|
||||||
else if (minute.mode === 'specify' && minute.specified.length === 1) parts.push(`${minute.specified[0]}分`)
|
else if (minute.mode === "specify" && minute.specified.length === 1)
|
||||||
|
parts.push(`${minute.specified[0]}分`);
|
||||||
|
|
||||||
// 秒
|
// 秒
|
||||||
if (second.mode === 'every') parts.push('每秒')
|
if (second.mode === "every") parts.push("每秒");
|
||||||
else if (second.mode === 'step') parts.push(`每${second.stepInterval}秒`)
|
else if (second.mode === "step") parts.push(`每${second.stepInterval}秒`);
|
||||||
else if (second.mode === 'specify' && second.specified.length === 1) parts.push(`${second.specified[0]}秒`)
|
else if (second.mode === "specify" && second.specified.length === 1)
|
||||||
|
parts.push(`${second.specified[0]}秒`);
|
||||||
|
|
||||||
return parts.join(' ')
|
return parts.join(" ");
|
||||||
})
|
});
|
||||||
|
|
||||||
let skipParse = false
|
let skipParse = false;
|
||||||
watch(cron, (val) => {
|
watch(cron, (val) => {
|
||||||
skipParse = true
|
skipParse = true;
|
||||||
emit('update:modelValue', val)
|
emit("update:modelValue", val);
|
||||||
nextTick(() => { skipParse = false })
|
nextTick(() => {
|
||||||
})
|
skipParse = false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// ---- 解析 cron 表达式 → 回填 fields ----
|
// ---- 解析 cron 表达式 → 回填 fields ----
|
||||||
function parseSegment(seg: string, field: FieldState): void {
|
function parseSegment(seg: string, field: FieldState): void {
|
||||||
if (seg === '*') {
|
if (seg === "*") {
|
||||||
field.mode = 'every'
|
field.mode = "every";
|
||||||
} else if (seg === '?') {
|
} else if (seg === "?") {
|
||||||
field.mode = 'any'
|
field.mode = "any";
|
||||||
} else if (seg === 'L') {
|
} else if (seg === "L") {
|
||||||
field.mode = 'last'
|
field.mode = "last";
|
||||||
} else if (seg.includes('W')) {
|
} else if (seg.includes("W")) {
|
||||||
field.mode = 'lastWeekday'
|
field.mode = "lastWeekday";
|
||||||
field.extra = seg.replace('W', '')
|
field.extra = seg.replace("W", "");
|
||||||
} else if (seg.includes('L')) {
|
} else if (seg.includes("L")) {
|
||||||
field.mode = 'lastWeek'
|
field.mode = "lastWeek";
|
||||||
field.extra = seg.replace('L', '')
|
field.extra = seg.replace("L", "");
|
||||||
} else if (seg.includes('#')) {
|
} else if (seg.includes("#")) {
|
||||||
field.mode = 'nthWeek'
|
field.mode = "nthWeek";
|
||||||
const [a, b] = seg.split('#')
|
const [a, b] = seg.split("#");
|
||||||
field.rangeStart = a!
|
field.rangeStart = a!;
|
||||||
field.rangeEnd = b!
|
field.rangeEnd = b!;
|
||||||
} else if (seg.includes('-')) {
|
} else if (seg.includes("-")) {
|
||||||
field.mode = 'range'
|
field.mode = "range";
|
||||||
const [a, b] = seg.split('-')
|
const [a, b] = seg.split("-");
|
||||||
field.rangeStart = a!
|
field.rangeStart = a!;
|
||||||
field.rangeEnd = b!
|
field.rangeEnd = b!;
|
||||||
} else if (seg.includes('/')) {
|
} else if (seg.includes("/")) {
|
||||||
field.mode = 'step'
|
field.mode = "step";
|
||||||
const [a, b] = seg.split('/')
|
const [a, b] = seg.split("/");
|
||||||
field.stepFrom = a!
|
field.stepFrom = a!;
|
||||||
field.stepInterval = b!
|
field.stepInterval = b!;
|
||||||
} else if (seg.includes(',')) {
|
} else if (seg.includes(",")) {
|
||||||
field.mode = 'specify'
|
field.mode = "specify";
|
||||||
field.specified = seg.split(',').map((v) => pad(parseInt(v)))
|
field.specified = seg.split(",").map((v) => pad(parseInt(v)));
|
||||||
} else if (/^\d+$/.test(seg)) {
|
} else if (/^\d+$/.test(seg)) {
|
||||||
field.mode = 'specify'
|
field.mode = "specify";
|
||||||
field.specified = [pad(parseInt(seg))]
|
field.specified = [pad(parseInt(seg))];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyCron(expr: string) {
|
function applyCron(expr: string) {
|
||||||
const parts = expr.trim().split(/\s+/)
|
const parts = expr.trim().split(/\s+/);
|
||||||
if (parts.length < 6) return
|
if (parts.length < 6) return;
|
||||||
const keys: (keyof Fields)[] = ['second', 'minute', 'hour', 'day', 'month', 'week']
|
const keys: (keyof Fields)[] = ["second", "minute", "hour", "day", "month", "week"];
|
||||||
keys.forEach((key, i) => {
|
keys.forEach((key, i) => {
|
||||||
parseSegment(parts[i]!, fields.value[key])
|
parseSegment(parts[i]!, fields.value[key]);
|
||||||
})
|
});
|
||||||
// 自动定位到第一个非默认的 tab
|
// 自动定位到第一个非默认的 tab
|
||||||
const idx = parts.findIndex((p) => p !== '*' && p !== '?')
|
const idx = parts.findIndex((p) => p !== "*" && p !== "?");
|
||||||
const tabMap = ['秒', '分钟', '小时', '日', '月', '周']
|
const tabMap = ["秒", "分钟", "小时", "日", "月", "周"];
|
||||||
activeTab.value = idx >= 0 ? tabMap[idx]! : '秒'
|
activeTab.value = idx >= 0 ? tabMap[idx]! : "秒";
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(() => props.modelValue, (val) => {
|
watch(
|
||||||
if (skipParse) return
|
() => props.modelValue,
|
||||||
if (val) applyCron(val)
|
(val) => {
|
||||||
}, { immediate: true })
|
if (skipParse) return;
|
||||||
|
if (val) applyCron(val);
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
// ---- 下一次执行时间 ----
|
// ---- 下一次执行时间 ----
|
||||||
const nextRun = ref('')
|
const nextRun = ref("");
|
||||||
|
|
||||||
function calcNextRun(expr: string) {
|
function calcNextRun(expr: string) {
|
||||||
if (!expr?.trim()) { nextRun.value = ''; return }
|
if (!expr?.trim()) {
|
||||||
|
nextRun.value = "";
|
||||||
|
return;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const parser = (cronParser as any).default ?? cronParser
|
const parser = (cronParser as any).default ?? cronParser;
|
||||||
const interval = parser.parse(expr, {
|
const interval = parser.parse(expr, {
|
||||||
currentDate: new Date(),
|
currentDate: new Date(),
|
||||||
})
|
});
|
||||||
nextRun.value = dayjs(interval.next().toDate()).format('YYYY-MM-DD HH:mm:ss')
|
nextRun.value = dayjs(interval.next().toDate()).format("YYYY-MM-DD HH:mm:ss");
|
||||||
} catch {
|
} catch {
|
||||||
nextRun.value = '无效表达式'
|
nextRun.value = "无效表达式";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(cron, (val) => calcNextRun(val), { immediate: true })
|
watch(cron, (val) => calcNextRun(val), { immediate: true });
|
||||||
|
|
||||||
const clear = () => {
|
const clear = () => {
|
||||||
fields.value = {
|
fields.value = {
|
||||||
second: defaultField({ rangeStart: '0', rangeEnd: '59', stepFrom: '0' }),
|
second: defaultField({ rangeStart: "0", rangeEnd: "59", stepFrom: "0" }),
|
||||||
minute: defaultField({ rangeStart: '0', rangeEnd: '59', stepFrom: '0' }),
|
minute: defaultField({ rangeStart: "0", rangeEnd: "59", stepFrom: "0" }),
|
||||||
hour: defaultField({ rangeStart: '0', rangeEnd: '23', stepFrom: '0' }),
|
hour: defaultField({ rangeStart: "0", rangeEnd: "23", stepFrom: "0" }),
|
||||||
day: defaultField({ mode: 'every', rangeStart: '1', rangeEnd: '31', stepFrom: '1' }),
|
day: defaultField({ mode: "every", rangeStart: "1", rangeEnd: "31", stepFrom: "1" }),
|
||||||
month: defaultField({ mode: 'every', rangeStart: '1', rangeEnd: '12', stepFrom: '1' }),
|
month: defaultField({ mode: "every", rangeStart: "1", rangeEnd: "12", stepFrom: "1" }),
|
||||||
week: defaultField({ mode: 'any', rangeStart: '1', rangeEnd: '7', stepFrom: '1' }),
|
week: defaultField({ mode: "any", rangeStart: "1", rangeEnd: "7", stepFrom: "1" }),
|
||||||
}
|
};
|
||||||
activeTab.value = '秒'
|
activeTab.value = "秒";
|
||||||
}
|
};
|
||||||
|
|
||||||
defineExpose({ clear })
|
defineExpose({ clear });
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.cron-selector {
|
.cron-selector {
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
|
|
||||||
:deep(.el-tabs__content) { padding: 8px; }
|
:deep(.el-tabs__content) {
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
:deep(.el-radio-group) {
|
:deep(.el-radio-group) {
|
||||||
.el-radio {
|
.el-radio {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: auto;
|
height: auto;
|
||||||
margin-bottom: 6px;
|
|
||||||
min-height: 28px;
|
min-height: 28px;
|
||||||
|
margin-bottom: 6px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.el-checkbox-group) {
|
:deep(.el-checkbox-group) {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
.el-checkbox { margin-right: 6px; height: auto; margin-bottom: 6px; }
|
|
||||||
|
.el-checkbox {
|
||||||
|
height: auto;
|
||||||
|
margin-right: 6px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.cron-preview {
|
.cron-preview {
|
||||||
margin-top: 12px;
|
|
||||||
padding: 10px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
|
padding: 10px;
|
||||||
|
margin-top: 12px;
|
||||||
background-color: var(--el-fill-color);
|
background-color: var(--el-fill-color);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,22 +11,22 @@ const fastEnterConfig: FastEnterConfig = {
|
|||||||
// 应用列表
|
// 应用列表
|
||||||
applications: [
|
applications: [
|
||||||
{
|
{
|
||||||
name: "功能引导",
|
name: "用户管理",
|
||||||
description: "产品操作指南",
|
description: "系统用户管理与维护",
|
||||||
icon: "ri:compass-3-line",
|
icon: "ri:user-settings-line",
|
||||||
iconColor: "#009688",
|
|
||||||
enabled: true,
|
|
||||||
order: 1,
|
|
||||||
routeName: "FastlinkTutorial",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "文章列表",
|
|
||||||
description: "文章管理与查看",
|
|
||||||
icon: "ri:article-line",
|
|
||||||
iconColor: "#377dff",
|
iconColor: "#377dff",
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
order: 1,
|
||||||
|
routeName: "User",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "角色管理",
|
||||||
|
description: "角色权限配置与分配",
|
||||||
|
icon: "ri:shield-user-line",
|
||||||
|
iconColor: "#FF6B35",
|
||||||
|
enabled: true,
|
||||||
order: 2,
|
order: 2,
|
||||||
routeName: "FastlinkArticleList",
|
routeName: "Role",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "定价",
|
name: "定价",
|
||||||
@@ -49,7 +49,7 @@ const fastEnterConfig: FastEnterConfig = {
|
|||||||
{
|
{
|
||||||
name: "官方文档",
|
name: "官方文档",
|
||||||
description: "使用指南与开发文档",
|
description: "使用指南与开发文档",
|
||||||
icon: "ri:bill-line",
|
icon: "ri:book-open-line",
|
||||||
iconColor: "#ffb100",
|
iconColor: "#ffb100",
|
||||||
enabled: true,
|
enabled: true,
|
||||||
order: 5,
|
order: 5,
|
||||||
@@ -110,17 +110,16 @@ const fastEnterConfig: FastEnterConfig = {
|
|||||||
isDialog: true,
|
isDialog: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "个人中心",
|
name: "操作日志",
|
||||||
enabled: true,
|
enabled: true,
|
||||||
order: 5,
|
order: 5,
|
||||||
routeName: "FastlinkProfile",
|
routeName: "Log",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "留言管理",
|
name: "个人中心",
|
||||||
enabled: true,
|
enabled: true,
|
||||||
order: 6,
|
order: 6,
|
||||||
routeName: "FastlinkArticleList",
|
routeName: "FastlinkProfile",
|
||||||
routeQuery: { commentWall: "1" },
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -34,10 +34,10 @@ export function useSiteConfig() {
|
|||||||
if (favicon?.config_value) updateFavicon(favicon.config_value);
|
if (favicon?.config_value) updateFavicon(favicon.config_value);
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 初始化:强制拉取配置并同步标题/favicon */
|
/** 初始化:优先使用缓存配置同步标题/favicon,无缓存时从接口获取 */
|
||||||
const initSiteConfig = async () => {
|
const initSiteConfig = async () => {
|
||||||
try {
|
try {
|
||||||
await configStore.getConfig(true);
|
await configStore.getConfig();
|
||||||
applyConfig();
|
applyConfig();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[SiteConfig] 获取配置失败:", error);
|
console.error("[SiteConfig] 获取配置失败:", error);
|
||||||
|
|||||||
@@ -278,6 +278,7 @@ const { isFullscreen, toggle: toggleFullscreen } = useFullscreen();
|
|||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
initLanguage();
|
initLanguage();
|
||||||
document.addEventListener("click", bodyCloseNotice);
|
document.addEventListener("click", bodyCloseNotice);
|
||||||
|
noticeStore.getNotice();
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
|
|||||||
@@ -21,8 +21,11 @@
|
|||||||
class="box-border flex-c px-3.5 py-3.5 c-p last:border-b-0 hover:bg-g-200/60"
|
class="box-border flex-c px-3.5 py-3.5 c-p last:border-b-0 hover:bg-g-200/60"
|
||||||
@click="handleMarkAsRead(index)"
|
@click="handleMarkAsRead(index)"
|
||||||
>
|
>
|
||||||
<div class="size-9 leading-9 text-center rounded-lg flex-cc bg-theme/12 text-theme">
|
<div
|
||||||
<FaSvgIcon class="text-lg bg-transparent!" icon="ri:notification-3-line" />
|
class="size-9 leading-9 text-center rounded-lg flex-cc"
|
||||||
|
:class="item.type === 2 ? 'bg-warning/12 text-warning' : 'bg-theme/12 text-theme'"
|
||||||
|
>
|
||||||
|
<FaSvgIcon class="text-lg bg-transparent!" :icon="getNoticeIcon(item.type)" />
|
||||||
</div>
|
</div>
|
||||||
<div class="w-[calc(100%-45px)] ml-3.5">
|
<div class="w-[calc(100%-45px)] ml-3.5">
|
||||||
<h4 class="text-sm font-normal leading-5.5 text-g-900">{{ item.title }}</h4>
|
<h4 class="text-sm font-normal leading-5.5 text-g-900">{{ item.title }}</h4>
|
||||||
@@ -58,10 +61,13 @@ import NoticeAPI from "@/api/module_system/notice";
|
|||||||
|
|
||||||
defineOptions({ name: "FaNotification" });
|
defineOptions({ name: "FaNotification" });
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
interface NoticeItem {
|
interface NoticeItem {
|
||||||
title: string;
|
title: string;
|
||||||
time: string;
|
time: string;
|
||||||
read: boolean;
|
read: boolean;
|
||||||
|
type: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -81,6 +87,9 @@ const visible = ref(false);
|
|||||||
const noticeList = ref<NoticeItem[]>([]);
|
const noticeList = ref<NoticeItem[]>([]);
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
|
|
||||||
|
const getNoticeIcon = (type: number) =>
|
||||||
|
type === 2 ? "ri:megaphone-line" : "ri:notification-3-line";
|
||||||
|
|
||||||
const fetchNotices = async () => {
|
const fetchNotices = async () => {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
@@ -90,6 +99,7 @@ const fetchNotices = async () => {
|
|||||||
title: n.notice_title ?? "",
|
title: n.notice_title ?? "",
|
||||||
time: n.created_time ?? "",
|
time: n.created_time ?? "",
|
||||||
read: false,
|
read: false,
|
||||||
|
type: Number(n.notice_type) || 1,
|
||||||
}));
|
}));
|
||||||
} catch {
|
} catch {
|
||||||
noticeList.value = [];
|
noticeList.value = [];
|
||||||
@@ -104,8 +114,7 @@ watch(visible, (v) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const handleViewAll = () => {
|
const handleViewAll = () => {
|
||||||
const router = useRouter();
|
router.push("/system/notice");
|
||||||
router.push("/module_system/notice");
|
|
||||||
emit("update:value", false);
|
emit("update:value", false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -369,7 +369,7 @@
|
|||||||
"user": "User"
|
"user": "User"
|
||||||
},
|
},
|
||||||
"placeholder": {
|
"placeholder": {
|
||||||
"username": "Please enter username",
|
"username": "Please enter account",
|
||||||
"password": "Please enter password",
|
"password": "Please enter password",
|
||||||
"email": "Please enter email",
|
"email": "Please enter email",
|
||||||
"slider": "Please slide to verify"
|
"slider": "Please slide to verify"
|
||||||
@@ -473,7 +473,7 @@
|
|||||||
"username": "Please enter account",
|
"username": "Please enter account",
|
||||||
"password": "Please enter password",
|
"password": "Please enter password",
|
||||||
"confirmPassword": "Please enter password again",
|
"confirmPassword": "Please enter password again",
|
||||||
"email": "Please enter email"
|
"name": "Please enter nickname"
|
||||||
},
|
},
|
||||||
"rule": {
|
"rule": {
|
||||||
"confirmPasswordRequired": "Please enter your password again",
|
"confirmPasswordRequired": "Please enter your password again",
|
||||||
|
|||||||
@@ -369,7 +369,7 @@
|
|||||||
"user": "普通用户"
|
"user": "普通用户"
|
||||||
},
|
},
|
||||||
"placeholder": {
|
"placeholder": {
|
||||||
"username": "请输入用户名",
|
"username": "请输入账号",
|
||||||
"password": "请输入密码",
|
"password": "请输入密码",
|
||||||
"email": "请输入邮箱",
|
"email": "请输入邮箱",
|
||||||
"slider": "请拖动滑块完成验证"
|
"slider": "请拖动滑块完成验证"
|
||||||
@@ -473,7 +473,7 @@
|
|||||||
"username": "请输入账号",
|
"username": "请输入账号",
|
||||||
"password": "请输入密码",
|
"password": "请输入密码",
|
||||||
"confirmPassword": "请再次输入密码",
|
"confirmPassword": "请再次输入密码",
|
||||||
"email": "请输入邮箱"
|
"name": "请输入昵称"
|
||||||
},
|
},
|
||||||
"rule": {
|
"rule": {
|
||||||
"confirmPasswordRequired": "请再次输入密码",
|
"confirmPasswordRequired": "请再次输入密码",
|
||||||
|
|||||||
@@ -294,12 +294,6 @@ function getDashboardMenuTreeForMerge(): AppRouteRecord {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const mergeShellHomeMenu: AppRouteRecord = {
|
|
||||||
path: "/home",
|
|
||||||
name: "Home",
|
|
||||||
meta: { ...HOME_MENU_META, shellRoute: true },
|
|
||||||
};
|
|
||||||
|
|
||||||
function normalizeMenuPath(path?: string): string {
|
function normalizeMenuPath(path?: string): string {
|
||||||
if (!path || !path.trim()) return "";
|
if (!path || !path.trim()) return "";
|
||||||
const p = path.trim();
|
const p = path.trim();
|
||||||
@@ -347,6 +341,12 @@ export function mergeShellRoutesIntoMenu(menuList: AppRouteRecord[]): AppRouteRe
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const mergeShellHomeMenu: AppRouteRecord = {
|
||||||
|
path: "/home",
|
||||||
|
name: "Home",
|
||||||
|
meta: { ...HOME_MENU_META, shellRoute: true },
|
||||||
|
};
|
||||||
|
|
||||||
tryPush(mergeShellHomeMenu);
|
tryPush(mergeShellHomeMenu);
|
||||||
if (!paths.has("/dashboard")) {
|
if (!paths.has("/dashboard")) {
|
||||||
tryPush(dashboardRoutesToShellMenu(getDashboardMenuTreeForMerge()));
|
tryPush(dashboardRoutesToShellMenu(getDashboardMenuTreeForMerge()));
|
||||||
|
|||||||
@@ -13,8 +13,10 @@ import { useUserStore, useMenuStore, useWorktabStore } from "@/store";
|
|||||||
import { IframeRouteManager, ROUTE_PATH_LOGIN_ALT } from "./routes";
|
import { IframeRouteManager, ROUTE_PATH_LOGIN_ALT } from "./routes";
|
||||||
import { router, HOME_PAGE_PATH } from "./index";
|
import { router, HOME_PAGE_PATH } from "./index";
|
||||||
import { setPageTitle, setWorktab } from "@utils/navigation";
|
import { setPageTitle, setWorktab } from "@utils/navigation";
|
||||||
|
import { MenuProcessor } from "./MenuProcessor";
|
||||||
import { NProgress } from "@utils/ui";
|
import { NProgress } from "@utils/ui";
|
||||||
import { Auth } from "@utils/auth";
|
import { Auth } from "@utils/auth";
|
||||||
|
import { refreshState } from "./refresh";
|
||||||
|
|
||||||
/** 全局 loading 状态(用于路由切换时显示加载遮罩) */
|
/** 全局 loading 状态(用于路由切换时显示加载遮罩) */
|
||||||
const globalLoading = ref(false);
|
const globalLoading = ref(false);
|
||||||
@@ -38,36 +40,12 @@ function isAnonymousPublicPath(path: string): boolean {
|
|||||||
function isLoginRoute(to: RouteLocationNormalized): boolean {
|
function isLoginRoute(to: RouteLocationNormalized): boolean {
|
||||||
return to.path === "/login" || to.path === ROUTE_PATH_LOGIN_ALT;
|
return to.path === "/login" || to.path === ROUTE_PATH_LOGIN_ALT;
|
||||||
}
|
}
|
||||||
// ──────── 守卫状态 ────────
|
|
||||||
|
|
||||||
let pendingLoading = false;
|
|
||||||
let routeInitFailed = false;
|
|
||||||
|
|
||||||
/** 获取是否正在加载动态路由 */
|
|
||||||
export function getPendingLoading(): boolean {
|
|
||||||
return pendingLoading;
|
|
||||||
}
|
|
||||||
/** 重置加载状态 */
|
|
||||||
export function resetPendingLoading(): void {
|
|
||||||
pendingLoading = false;
|
|
||||||
}
|
|
||||||
/** 获取路由初始化是否失败 */
|
|
||||||
export function getRouteInitFailed(): boolean {
|
|
||||||
return routeInitFailed;
|
|
||||||
}
|
|
||||||
/** 重置路由初始化失败状态 */
|
|
||||||
export function resetRouteInitState(): void {
|
|
||||||
routeInitFailed = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ──────── 前置守卫 ────────
|
// ──────── 前置守卫 ────────
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 前置守卫:导航前检查登录状态、动态路由加载、权限校验
|
* 前置守卫:导航前检查登录状态、动态路由加载、权限校验
|
||||||
*/
|
*/
|
||||||
export function setupBeforeEachGuard(router: Router): void {
|
export function setupBeforeEachGuard(router: Router): void {
|
||||||
let dynamicRoutesRegistered = false;
|
|
||||||
|
|
||||||
router.beforeEach(async (to) => {
|
router.beforeEach(async (to) => {
|
||||||
// 初始化守卫状态
|
// 初始化守卫状态
|
||||||
if (globalLoading.value) globalLoading.value = false;
|
if (globalLoading.value) globalLoading.value = false;
|
||||||
@@ -86,19 +64,19 @@ export function setupBeforeEachGuard(router: Router): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 路由初始化失败 → 跳转 500
|
// 路由初始化失败 → 跳转 500
|
||||||
if (routeInitFailed && !isAnonymousPublicPath(to.path)) {
|
if (refreshState.routeInitFailed && !isAnonymousPublicPath(to.path)) {
|
||||||
return "/500";
|
return "/500";
|
||||||
}
|
}
|
||||||
|
|
||||||
// 已登录、动态路由未注册 → 加载
|
// 已登录、动态路由未注册 → 加载
|
||||||
if (!dynamicRoutesRegistered && !isAnonymousPublicPath(to.path)) {
|
if (!refreshState.dynamicRoutesRegistered && !isAnonymousPublicPath(to.path)) {
|
||||||
// 正在加载中 → 跳转首页,避免进入后路由未注册导致 404
|
// 正在加载中 → 跳转首页,避免进入后路由未注册导致 404
|
||||||
if (pendingLoading) {
|
if (refreshState.pendingLoading) {
|
||||||
return { path: HOME_PAGE_PATH, replace: true };
|
return { path: HOME_PAGE_PATH, replace: true };
|
||||||
}
|
}
|
||||||
const redirect = await handleDynamicRoutes(to);
|
const redirect = await handleDynamicRoutes(to);
|
||||||
if (redirect) return redirect;
|
if (redirect) return redirect;
|
||||||
dynamicRoutesRegistered = true;
|
refreshState.dynamicRoutesRegistered = true;
|
||||||
|
|
||||||
// 注册动态路由后,若原导航被 catch-all 404 捕获(F5 刷新场景),重定向触发重新解析
|
// 注册动态路由后,若原导航被 catch-all 404 捕获(F5 刷新场景),重定向触发重新解析
|
||||||
if (to.matched.some((r) => r.name === "CatchAll404")) {
|
if (to.matched.some((r) => r.name === "CatchAll404")) {
|
||||||
@@ -152,8 +130,8 @@ async function handleLoginStatus(to: RouteLocationNormalized): Promise<boolean>
|
|||||||
async function handleDynamicRoutes(
|
async function handleDynamicRoutes(
|
||||||
to: RouteLocationNormalized
|
to: RouteLocationNormalized
|
||||||
): Promise<undefined | string | { path: string; replace: boolean }> {
|
): Promise<undefined | string | { path: string; replace: boolean }> {
|
||||||
if (pendingLoading) return;
|
if (refreshState.pendingLoading) return;
|
||||||
pendingLoading = true;
|
refreshState.pendingLoading = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 异常恢复:菜单空了但路由还在,做反注册
|
// 异常恢复:菜单空了但路由还在,做反注册
|
||||||
@@ -166,7 +144,6 @@ async function handleDynamicRoutes(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 获取菜单
|
// 获取菜单
|
||||||
const { MenuProcessor } = await import("./MenuProcessor");
|
|
||||||
const menuProcessor = new MenuProcessor();
|
const menuProcessor = new MenuProcessor();
|
||||||
const menuList = await menuProcessor.getMenuList();
|
const menuList = await menuProcessor.getMenuList();
|
||||||
|
|
||||||
@@ -206,10 +183,10 @@ async function handleDynamicRoutes(
|
|||||||
return undefined;
|
return undefined;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[路由守卫] 路由初始化失败:", error);
|
console.error("[路由守卫] 路由初始化失败:", error);
|
||||||
routeInitFailed = true;
|
refreshState.routeInitFailed = true;
|
||||||
return "/500";
|
return "/500";
|
||||||
} finally {
|
} finally {
|
||||||
pendingLoading = false;
|
refreshState.pendingLoading = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -319,42 +296,6 @@ export class RoutePermissionValidator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ──────── 动态路由卸载 ────────
|
|
||||||
|
|
||||||
/** 卸载动态路由 + 清理 iframe 路由(退出登录时调用) */
|
|
||||||
export async function resetDynamicRoutesSync(): Promise<void> {
|
|
||||||
const menuStore = useMenuStore();
|
|
||||||
const removeRouteFns = menuStore.removeRouteFns;
|
|
||||||
removeRouteFns.forEach((fn: () => void) => fn());
|
|
||||||
menuStore.menuList.length = 0;
|
|
||||||
menuStore.removeRouteFns.length = 0;
|
|
||||||
IframeRouteManager.getInstance().clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 重新拉菜单 + 重新注册(管理员手动刷新菜单时调用) */
|
|
||||||
export async function refreshMenuAndRoutes(): Promise<void> {
|
|
||||||
await resetDynamicRoutesSync();
|
|
||||||
const { MenuProcessor } = await import("./MenuProcessor");
|
|
||||||
const { RouteRegistry } = await import("./route-loader");
|
|
||||||
const menuProcessor = new MenuProcessor();
|
|
||||||
const menuList = await menuProcessor.getMenuList();
|
|
||||||
// 更新侧栏菜单并注册动态路由
|
|
||||||
useMenuStore().setMenuList(menuList);
|
|
||||||
const routeRegistry = new RouteRegistry(router);
|
|
||||||
routeRegistry.register(menuList);
|
|
||||||
useMenuStore().addRemoveRouteFns(routeRegistry.getRemoveRouteFns());
|
|
||||||
// 菜单变更后清理无效的持久化标签
|
|
||||||
useWorktabStore().validateWorktabs(router);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 延迟重置(token 过期降级时使用 3000ms 等待过渡动画) */
|
|
||||||
export async function resetRouterState(delay: number = 0): Promise<void> {
|
|
||||||
if (delay > 0) {
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
||||||
}
|
|
||||||
await resetDynamicRoutesSync();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ──────── 后置守卫 ────────
|
// ──────── 后置守卫 ────────
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { App } from "vue";
|
import type { App } from "vue";
|
||||||
import { createRouter, createWebHashHistory } from "vue-router";
|
import { createRouter, createWebHashHistory } from "vue-router";
|
||||||
import { HOME_ROUTE_NAME, ROOT_LAYOUT_ROUTE_NAME, staticRoutes } from "./routes";
|
import { HOME_ROUTE_NAME, ROOT_LAYOUT_ROUTE_NAME, staticRoutes } from "./routes";
|
||||||
import { setupAfterEachGuard } from "./guards";
|
import { setupAfterEachGuard, setupBeforeEachGuard } from "./guards";
|
||||||
import "@utils/ui";
|
import "@utils/ui";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -21,7 +21,6 @@ export const router = createRouter({
|
|||||||
|
|
||||||
/** 注册守卫 + 挂载 router 到 Vue app(在 main.ts 调用) */
|
/** 注册守卫 + 挂载 router 到 Vue app(在 main.ts 调用) */
|
||||||
export async function initRouter(app: App<Element>): Promise<void> {
|
export async function initRouter(app: App<Element>): Promise<void> {
|
||||||
const { setupBeforeEachGuard } = await import("./guards");
|
|
||||||
setupBeforeEachGuard(router);
|
setupBeforeEachGuard(router);
|
||||||
setupAfterEachGuard(router);
|
setupAfterEachGuard(router);
|
||||||
app.use(router);
|
app.use(router);
|
||||||
@@ -32,8 +31,6 @@ export const HOME_PAGE_PATH = "/home";
|
|||||||
|
|
||||||
export { HOME_ROUTE_NAME, ROOT_LAYOUT_ROUTE_NAME };
|
export { HOME_ROUTE_NAME, ROOT_LAYOUT_ROUTE_NAME };
|
||||||
|
|
||||||
/** 动态路由注册相关类(从 `@/router` 按需导入) */
|
|
||||||
export { RouteRegistry, ComponentLoader, RouteTransformer } from "./route-loader";
|
|
||||||
/** iframe 路由管理器 */
|
/** iframe 路由管理器 */
|
||||||
export { IframeRouteManager } from "./routes";
|
export { IframeRouteManager } from "./routes";
|
||||||
/** 菜单处理(获取、过滤、壳层补全) */
|
/** 菜单处理(获取、过滤、壳层补全) */
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
/**
|
||||||
|
* 路由刷新/重置工具函数
|
||||||
|
*
|
||||||
|
* guards.ts 和 store/index.ts 共享的路由操作模块。
|
||||||
|
* router 实例由调用方作为参数传入,存储通过静态导入获取。
|
||||||
|
*
|
||||||
|
* @module router/refresh
|
||||||
|
*/
|
||||||
|
import type { Router } from "vue-router";
|
||||||
|
import { useMenuStore, useWorktabStore } from "@/store";
|
||||||
|
import { MenuProcessor } from "./MenuProcessor";
|
||||||
|
import { IframeRouteManager } from "./routes";
|
||||||
|
|
||||||
|
// ──────── 守卫状态 ────────
|
||||||
|
export const refreshState = {
|
||||||
|
pendingLoading: false,
|
||||||
|
routeInitFailed: false,
|
||||||
|
dynamicRoutesRegistered: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
export function resetRouteInitState(): void {
|
||||||
|
refreshState.routeInitFailed = false;
|
||||||
|
refreshState.dynamicRoutesRegistered = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ──────── 路由操作 ────────
|
||||||
|
|
||||||
|
/** 异步清理已注册的动态路由和菜单 */
|
||||||
|
export async function resetDynamicRoutesSync(): Promise<void> {
|
||||||
|
const menuStore = useMenuStore();
|
||||||
|
const removeRouteFns = menuStore.removeRouteFns;
|
||||||
|
removeRouteFns.forEach((fn: () => void) => fn());
|
||||||
|
menuStore.menuList.length = 0;
|
||||||
|
menuStore.removeRouteFns.length = 0;
|
||||||
|
IframeRouteManager.getInstance().clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 重新拉菜单 + 重新注册(管理员手动刷新菜单时调用) */
|
||||||
|
export async function refreshMenuAndRoutes(router: Router): Promise<void> {
|
||||||
|
await resetDynamicRoutesSync();
|
||||||
|
const { RouteRegistry } = await import("./route-loader");
|
||||||
|
const menuProcessor = new MenuProcessor();
|
||||||
|
const menuList = await menuProcessor.getMenuList();
|
||||||
|
// 更新侧栏菜单并注册动态路由
|
||||||
|
useMenuStore().setMenuList(menuList);
|
||||||
|
const routeRegistry = new RouteRegistry(router);
|
||||||
|
routeRegistry.register(menuList);
|
||||||
|
useMenuStore().addRemoveRouteFns(routeRegistry.getRemoveRouteFns());
|
||||||
|
// 菜单变更后清理无效的持久化标签
|
||||||
|
useWorktabStore().validateWorktabs(router);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 延迟重置(token 过期降级时使用 3000ms 等待过渡动画) */
|
||||||
|
export async function resetRouterState(delay: number = 0): Promise<void> {
|
||||||
|
if (delay > 0) {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
||||||
|
}
|
||||||
|
await resetDynamicRoutesSync();
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@ import type { AppRouteRecord } from "@/types/router";
|
|||||||
import { h } from "vue";
|
import { h } from "vue";
|
||||||
import {
|
import {
|
||||||
IframeRouteManager,
|
IframeRouteManager,
|
||||||
|
IframeView,
|
||||||
NestedRouterParent,
|
NestedRouterParent,
|
||||||
ROOT_LAYOUT_ROUTE_NAME,
|
ROOT_LAYOUT_ROUTE_NAME,
|
||||||
ROUTE_COMPONENT_LAYOUT,
|
ROUTE_COMPONENT_LAYOUT,
|
||||||
@@ -21,8 +22,8 @@ import {
|
|||||||
|
|
||||||
// ──────── ComponentLoader ────────
|
// ──────── ComponentLoader ────────
|
||||||
|
|
||||||
/** 页面组件映射表(eager 加载:src/views 下所有 .vue 文件) */
|
/** 页面组件映射表(eager 加载:src/views 及 layouts 下所有 .vue 文件) */
|
||||||
const pageComponents = import.meta.glob("/src/views/**/*.vue", { eager: true });
|
const pageComponents = import.meta.glob("/src/{views,layouts}/**/*.vue", { eager: true });
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 组件加载器
|
* 组件加载器
|
||||||
@@ -54,17 +55,12 @@ export class ComponentLoader {
|
|||||||
|
|
||||||
/** Layout 框架组件 */
|
/** Layout 框架组件 */
|
||||||
loadLayout(): any {
|
loadLayout(): any {
|
||||||
const mod = pageComponents["/src/layouts/index.vue"];
|
return (pageComponents["/src/layouts/index.vue"] as any)?.default;
|
||||||
return mod ? (mod as any).default : null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** IframeView 组件 */
|
/** IframeView 组件 */
|
||||||
loadIframe(): any {
|
loadIframe(): any {
|
||||||
return () =>
|
return IframeView;
|
||||||
import("./routes").then((m) => {
|
|
||||||
const IframeView = (m as any).default || (m as any).IframeView;
|
|
||||||
return IframeView;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** NestedRouterParent 占位组件 */
|
/** NestedRouterParent 占位组件 */
|
||||||
@@ -203,7 +199,7 @@ export class RouteTransformer {
|
|||||||
return {
|
return {
|
||||||
path: `/${firstSegment}`,
|
path: `/${firstSegment}`,
|
||||||
name: route.name || firstSegment,
|
name: route.name || firstSegment,
|
||||||
component: this.loader.loadLayout() || (() => import("@/layouts/index.vue")),
|
component: this.loader.loadLayout(),
|
||||||
meta: { title: route.meta?.title, icon: route.meta?.icon },
|
meta: { title: route.meta?.title, icon: route.meta?.icon },
|
||||||
redirect: route.path.startsWith("/") ? route.path : `/${route.path}`,
|
redirect: route.path.startsWith("/") ? route.path : `/${route.path}`,
|
||||||
children: [
|
children: [
|
||||||
@@ -231,7 +227,7 @@ export class RouteTransformer {
|
|||||||
return {
|
return {
|
||||||
path: `/${firstSegment}`,
|
path: `/${firstSegment}`,
|
||||||
name: route.name || firstSegment,
|
name: route.name || firstSegment,
|
||||||
component: this.loader.loadLayout() || (() => import("@/layouts/index.vue")),
|
component: this.loader.loadLayout(),
|
||||||
meta: { title: route.meta?.title, icon: route.meta?.icon },
|
meta: { title: route.meta?.title, icon: route.meta?.icon },
|
||||||
redirect: fullMenuPath,
|
redirect: fullMenuPath,
|
||||||
children: [
|
children: [
|
||||||
|
|||||||
@@ -13,6 +13,22 @@ import type { AppRouteRecord, RouteMeta } from "@/types/router";
|
|||||||
import { defineComponent, h, onMounted, ref } from "vue";
|
import { defineComponent, h, onMounted, ref } from "vue";
|
||||||
import { RouterView, useRoute } from "vue-router";
|
import { RouterView, useRoute } from "vue-router";
|
||||||
import { $t } from "@/locales";
|
import { $t } from "@/locales";
|
||||||
|
import LayoutComponent from "@/layouts/index.vue";
|
||||||
|
import DashboardWorkplace from "@views/dashboard/workplace/index.vue";
|
||||||
|
import DashboardAnalysis from "@views/dashboard/analysis/index.vue";
|
||||||
|
import DashboardScreen from "@views/dashboard/screen/index.vue";
|
||||||
|
import RedirectView from "@views/redirect/index.vue";
|
||||||
|
import LoginView from "@views/module_system/auth/login/index.vue";
|
||||||
|
import Exception401 from "@views/exception/401/index.vue";
|
||||||
|
import Exception403 from "@views/exception/403/index.vue";
|
||||||
|
import Exception404 from "@views/exception/404/index.vue";
|
||||||
|
import Exception500 from "@views/exception/500/index.vue";
|
||||||
|
import DashboardHome from "@views/dashboard/home/index.vue";
|
||||||
|
import FastlinkProfile from "@views/fastlink/current/profile.vue";
|
||||||
|
import FastlinkChangelog from "@views/fastlink/changelog/index.vue";
|
||||||
|
import FastlinkPricing from "@views/fastlink/pricing/index.vue";
|
||||||
|
import FastlinkTutorial from "@views/fastlink/tutorial/index.vue";
|
||||||
|
import FastlinkFachat from "@views/fastlink/fachat/index.vue";
|
||||||
|
|
||||||
// ──────── IframeRouteManager ────────
|
// ──────── IframeRouteManager ────────
|
||||||
|
|
||||||
@@ -101,13 +117,13 @@ export const dashboardLayoutChildren: AppRouteRecordRaw[] = [
|
|||||||
{
|
{
|
||||||
path: "workplace",
|
path: "workplace",
|
||||||
name: "DashboardWorkplace",
|
name: "DashboardWorkplace",
|
||||||
component: () => import("@views/dashboard/workplace/index.vue"),
|
component: DashboardWorkplace,
|
||||||
meta: { title: "menus.dashboard.workplace", icon: "ri:bar-chart-box-line", keepAlive: true },
|
meta: { title: "menus.dashboard.workplace", icon: "ri:bar-chart-box-line", keepAlive: true },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "analysis",
|
path: "analysis",
|
||||||
name: "DashboardAnalysis",
|
name: "DashboardAnalysis",
|
||||||
component: () => import("@views/dashboard/analysis/index.vue"),
|
component: DashboardAnalysis,
|
||||||
meta: {
|
meta: {
|
||||||
title: "menus.dashboard.analysis",
|
title: "menus.dashboard.analysis",
|
||||||
icon: "ri:align-item-bottom-line",
|
icon: "ri:align-item-bottom-line",
|
||||||
@@ -117,7 +133,7 @@ export const dashboardLayoutChildren: AppRouteRecordRaw[] = [
|
|||||||
{
|
{
|
||||||
path: "screen",
|
path: "screen",
|
||||||
name: "DashboardScreen",
|
name: "DashboardScreen",
|
||||||
component: () => import("@views/dashboard/screen/index.vue"),
|
component: DashboardScreen,
|
||||||
meta: { title: "数据大屏", icon: "ri:tv-line", keepAlive: false, hidden: false },
|
meta: { title: "数据大屏", icon: "ri:tv-line", keepAlive: false, hidden: false },
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -147,13 +163,10 @@ export const ROUTE_COMPONENT_NESTED_PARENT = "/nested/router-view-parent";
|
|||||||
/** 登录页的备用 path(守卫判断用) */
|
/** 登录页的备用 path(守卫判断用) */
|
||||||
export const ROUTE_PATH_LOGIN_ALT = "/auth/login";
|
export const ROUTE_PATH_LOGIN_ALT = "/auth/login";
|
||||||
|
|
||||||
/** 主框架 Layout 懒加载 */
|
|
||||||
export const Layout = () => import("@/layouts/index.vue");
|
|
||||||
|
|
||||||
// ──────── IframeView 组件 ────────
|
// ──────── IframeView 组件 ────────
|
||||||
|
|
||||||
/** iframe 子路由的 Vue 组件 —— 从 IframeRouteManager 获取链接,加载时显示 loading */
|
/** iframe 子路由的 Vue 组件 —— 从 IframeRouteManager 获取链接,加载时显示 loading */
|
||||||
const IframeView = defineComponent({
|
export const IframeView = defineComponent({
|
||||||
name: "IframeView",
|
name: "IframeView",
|
||||||
setup() {
|
setup() {
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
@@ -199,11 +212,11 @@ export const staticRoutes: AppRouteRecordRaw[] = [
|
|||||||
{
|
{
|
||||||
path: "/redirect",
|
path: "/redirect",
|
||||||
meta: { hidden: true },
|
meta: { hidden: true },
|
||||||
component: Layout,
|
component: LayoutComponent,
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: "/redirect/:path(.*)",
|
path: "/redirect/:path(.*)",
|
||||||
component: () => import("@views/redirect/index.vue"),
|
component: RedirectView,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -212,44 +225,44 @@ export const staticRoutes: AppRouteRecordRaw[] = [
|
|||||||
path: "/login",
|
path: "/login",
|
||||||
name: "Login",
|
name: "Login",
|
||||||
meta: { hidden: true, isHideTab: true, title: "menus.login.title" },
|
meta: { hidden: true, isHideTab: true, title: "menus.login.title" },
|
||||||
component: () => import("@views/module_system/auth/login/index.vue"),
|
component: LoginView,
|
||||||
},
|
},
|
||||||
// 异常页
|
// 异常页
|
||||||
{
|
{
|
||||||
path: "/401",
|
path: "/401",
|
||||||
name: "401",
|
name: "401",
|
||||||
meta: { hidden: true, title: "401" },
|
meta: { hidden: true, title: "401" },
|
||||||
component: () => import("@views/exception/401/index.vue"),
|
component: Exception401,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/403",
|
path: "/403",
|
||||||
name: "403",
|
name: "403",
|
||||||
component: () => import("@views/exception/403/index.vue"),
|
component: Exception403,
|
||||||
meta: { hidden: true, title: "403" },
|
meta: { hidden: true, title: "403" },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/404",
|
path: "/404",
|
||||||
name: "404",
|
name: "404",
|
||||||
meta: { hidden: true, title: "404" },
|
meta: { hidden: true, title: "404" },
|
||||||
component: () => import("@views/exception/404/index.vue"),
|
component: Exception404,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/500",
|
path: "/500",
|
||||||
name: "500",
|
name: "500",
|
||||||
meta: { hidden: true, title: "500" },
|
meta: { hidden: true, title: "500" },
|
||||||
component: () => import("@views/exception/500/index.vue"),
|
component: Exception500,
|
||||||
},
|
},
|
||||||
// 根 Layout:存放壳层路由(home/dashboard/fastlink)
|
// 根 Layout:存放壳层路由(home/dashboard/fastlink)
|
||||||
{
|
{
|
||||||
path: "/",
|
path: "/",
|
||||||
name: ROOT_LAYOUT_ROUTE_NAME,
|
name: ROOT_LAYOUT_ROUTE_NAME,
|
||||||
redirect: "/home",
|
redirect: "/home",
|
||||||
component: Layout,
|
component: LayoutComponent,
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: "home",
|
path: "home",
|
||||||
name: HOME_ROUTE_NAME,
|
name: HOME_ROUTE_NAME,
|
||||||
component: () => import("@views/dashboard/home/index.vue"),
|
component: DashboardHome,
|
||||||
meta: HOME_MENU_META,
|
meta: HOME_MENU_META,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -271,7 +284,7 @@ export const staticRoutes: AppRouteRecordRaw[] = [
|
|||||||
path: "profile",
|
path: "profile",
|
||||||
name: "FastlinkProfile",
|
name: "FastlinkProfile",
|
||||||
meta: { title: $t("menus.system.userCenter"), icon: "ri:user-line", hidden: true },
|
meta: { title: $t("menus.system.userCenter"), icon: "ri:user-line", hidden: true },
|
||||||
component: () => import("@views/fastlink/current/profile.vue"),
|
component: FastlinkProfile,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "changelog",
|
path: "changelog",
|
||||||
@@ -283,7 +296,7 @@ export const staticRoutes: AppRouteRecordRaw[] = [
|
|||||||
keepAlive: true,
|
keepAlive: true,
|
||||||
isHideTab: true,
|
isHideTab: true,
|
||||||
},
|
},
|
||||||
component: () => import("@views/fastlink/changelog/index.vue"),
|
component: FastlinkChangelog,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "pricing",
|
path: "pricing",
|
||||||
@@ -295,7 +308,7 @@ export const staticRoutes: AppRouteRecordRaw[] = [
|
|||||||
keepAlive: true,
|
keepAlive: true,
|
||||||
isHideTab: true,
|
isHideTab: true,
|
||||||
},
|
},
|
||||||
component: () => import("@views/fastlink/pricing/index.vue"),
|
component: FastlinkPricing,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "tutorial",
|
path: "tutorial",
|
||||||
@@ -307,7 +320,7 @@ export const staticRoutes: AppRouteRecordRaw[] = [
|
|||||||
keepAlive: true,
|
keepAlive: true,
|
||||||
isHideTab: true,
|
isHideTab: true,
|
||||||
},
|
},
|
||||||
component: () => import("@views/fastlink/tutorial/index.vue"),
|
component: FastlinkTutorial,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "fachat",
|
path: "fachat",
|
||||||
@@ -319,7 +332,7 @@ export const staticRoutes: AppRouteRecordRaw[] = [
|
|||||||
keepAlive: true,
|
keepAlive: true,
|
||||||
isHideTab: true,
|
isHideTab: true,
|
||||||
},
|
},
|
||||||
component: () => import("@views/fastlink/fachat/index.vue"),
|
component: FastlinkFachat,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -328,7 +341,7 @@ export const staticRoutes: AppRouteRecordRaw[] = [
|
|||||||
// iframe 外部链接
|
// iframe 外部链接
|
||||||
{
|
{
|
||||||
path: "/outside",
|
path: "/outside",
|
||||||
component: () => import("@/layouts/index.vue"),
|
component: LayoutComponent,
|
||||||
name: "Outside",
|
name: "Outside",
|
||||||
meta: { title: "menus.outside.title" },
|
meta: { title: "menus.outside.title" },
|
||||||
children: [
|
children: [
|
||||||
@@ -344,7 +357,7 @@ export const staticRoutes: AppRouteRecordRaw[] = [
|
|||||||
{
|
{
|
||||||
path: "/:pathMatch(.*)*",
|
path: "/:pathMatch(.*)*",
|
||||||
name: "CatchAll404",
|
name: "CatchAll404",
|
||||||
component: () => import("@views/exception/404/index.vue"),
|
component: Exception404,
|
||||||
meta: { hidden: true, title: "404" },
|
meta: { hidden: true, title: "404" },
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import type { App } from "vue";
|
import type { App } from "vue";
|
||||||
import { createPinia } from "pinia";
|
import { createPinia } from "pinia";
|
||||||
import piniaPluginPersistedstate from "pinia-plugin-persistedstate";
|
import piniaPluginPersistedstate from "pinia-plugin-persistedstate";
|
||||||
|
import { router } from "@/router";
|
||||||
|
import { refreshMenuAndRoutes } from "@/router/refresh";
|
||||||
import { useUserStore } from "./modules/user.store";
|
import { useUserStore } from "./modules/user.store";
|
||||||
import { useDictStore } from "./modules/dict.store";
|
import { useDictStore } from "./modules/dict.store";
|
||||||
import { useNoticeStore } from "./modules/notice.store";
|
import { useNoticeStore } from "./modules/notice.store";
|
||||||
@@ -62,7 +64,7 @@ export async function refreshAppCaches(opts: RefreshCacheOptions = {}) {
|
|||||||
tasks.push(configStore.getConfig(true));
|
tasks.push(configStore.getConfig(true));
|
||||||
}
|
}
|
||||||
if (refreshNotice) {
|
if (refreshNotice) {
|
||||||
tasks.push(noticeStore.getNotice());
|
tasks.push(noticeStore.getNotice(true));
|
||||||
}
|
}
|
||||||
if (dictTypes && dictTypes.length > 0) {
|
if (dictTypes && dictTypes.length > 0) {
|
||||||
if (clearDictBefore) dictStore.clearDictData();
|
if (clearDictBefore) dictStore.clearDictData();
|
||||||
@@ -72,8 +74,7 @@ export async function refreshAppCaches(opts: RefreshCacheOptions = {}) {
|
|||||||
await Promise.allSettled(tasks);
|
await Promise.allSettled(tasks);
|
||||||
|
|
||||||
if (refreshRoutes) {
|
if (refreshRoutes) {
|
||||||
const { refreshMenuAndRoutes } = await import("@/router/guards");
|
await refreshMenuAndRoutes(router);
|
||||||
await refreshMenuAndRoutes();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (clearTags) {
|
if (clearTags) {
|
||||||
|
|||||||
@@ -75,14 +75,7 @@ export const useConfigStore = defineStore(
|
|||||||
console.warn("[configStore] getInitConfig: 响应 data 非数组", response?.data);
|
console.warn("[configStore] getInitConfig: 响应 data 非数组", response?.data);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
list.forEach((item: ConfigTable) => {
|
applyConfigList(list);
|
||||||
if (item.config_value !== undefined && item.config_key) {
|
|
||||||
configData.value[item.config_key] = item;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
isConfigLoaded.value = true;
|
|
||||||
_lastFetchedAt = Date.now();
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn("[configStore] 获取配置失败:", error);
|
console.warn("[configStore] 获取配置失败:", error);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -90,6 +83,16 @@ export const useConfigStore = defineStore(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function applyConfigList(list: ConfigTable[]) {
|
||||||
|
list.forEach((item: ConfigTable) => {
|
||||||
|
if (item.config_value !== undefined && item.config_key) {
|
||||||
|
configData.value[item.config_key] = item;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
isConfigLoaded.value = true;
|
||||||
|
_lastFetchedAt = Date.now();
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
configData,
|
configData,
|
||||||
isConfigLoaded,
|
isConfigLoaded,
|
||||||
|
|||||||
@@ -46,11 +46,16 @@ export const useNoticeStore = defineStore(
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取通知列表
|
* 获取通知列表
|
||||||
|
* @param force 是否强制刷新
|
||||||
*/
|
*/
|
||||||
async function getNotice() {
|
async function getNotice(force = false) {
|
||||||
|
if (!force && isNoticeLoaded.value) return;
|
||||||
const response = await NoticeAPI.listNoticeAvailable();
|
const response = await NoticeAPI.listNoticeAvailable();
|
||||||
const items = Array.isArray(response.data?.data) ? response.data.data : [];
|
const items = Array.isArray(response.data?.data) ? response.data.data : [];
|
||||||
// 过滤掉已读的通知
|
applyNoticeItems(items);
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyNoticeItems(items: NoticeTable[]) {
|
||||||
const readSet = new Set(readIds.value);
|
const readSet = new Set(readIds.value);
|
||||||
const filtered = items.filter(
|
const filtered = items.filter(
|
||||||
(item: NoticeTable) => item.id !== undefined && !readSet.has(item.id as number)
|
(item: NoticeTable) => item.id !== undefined && !readSet.has(item.id as number)
|
||||||
|
|||||||
@@ -15,13 +15,7 @@ import { ElNotification } from "element-plus";
|
|||||||
import { store, useDictStore } from "@stores";
|
import { store, useDictStore } from "@stores";
|
||||||
import type { UserInfo } from "@/api/module_system/user";
|
import type { UserInfo } from "@/api/module_system/user";
|
||||||
import { ResultEnum } from "@/enums/api/result.enum";
|
import { ResultEnum } from "@/enums/api/result.enum";
|
||||||
|
import { resetRouteInitState, resetRouterState } from "@/router/refresh";
|
||||||
/** 延迟加载 guards 工具函数,避免 user.store 与 guards 的循环依赖 */
|
|
||||||
let _routerUtilsPromise: Promise<typeof import("@/router/guards")> | null = null;
|
|
||||||
const getRouterUtils = () => {
|
|
||||||
if (!_routerUtilsPromise) _routerUtilsPromise = import("@/router/guards");
|
|
||||||
return _routerUtilsPromise;
|
|
||||||
};
|
|
||||||
|
|
||||||
/** {@link useUserStore} 的 `logout` 可选参数 */
|
/** {@link useUserStore} 的 `logout` 可选参数 */
|
||||||
export interface LogoutOptions {
|
export interface LogoutOptions {
|
||||||
@@ -258,7 +252,7 @@ export const useUserStore = defineStore(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 清除上次会话里「动态路由初始化失败」标记,避免重新登录后侧栏/菜单不注册
|
// 清除上次会话里「动态路由初始化失败」标记,避免重新登录后侧栏/菜单不注册
|
||||||
(await getRouterUtils()).resetRouteInitState();
|
resetRouteInitState();
|
||||||
Auth.setTokens(accessToken, refreshToken, rememberMe.value);
|
Auth.setTokens(accessToken, refreshToken, rememberMe.value);
|
||||||
setToken(accessToken, refreshToken);
|
setToken(accessToken, refreshToken);
|
||||||
|
|
||||||
@@ -299,7 +293,7 @@ export const useUserStore = defineStore(
|
|||||||
resetAllState();
|
resetAllState();
|
||||||
sessionStorage.removeItem("iframeRoutes");
|
sessionStorage.removeItem("iframeRoutes");
|
||||||
useMenuStore().setHomePath("");
|
useMenuStore().setHomePath("");
|
||||||
(await getRouterUtils()).resetRouterState(500);
|
resetRouterState(500);
|
||||||
|
|
||||||
if (shouldNavigate) {
|
if (shouldNavigate) {
|
||||||
const currentRoute = router.currentRoute.value;
|
const currentRoute = router.currentRoute.value;
|
||||||
|
|||||||
@@ -1,14 +1,9 @@
|
|||||||
import type {
|
import type { Directive } from "vue";
|
||||||
AuthDirective,
|
import type { HighlightDirective, RippleDirective } from "@/directives";
|
||||||
RolesDirective,
|
|
||||||
RippleDirective,
|
|
||||||
HighlightDirective,
|
|
||||||
} from "@/directives";
|
|
||||||
|
|
||||||
declare module "vue" {
|
declare module "vue" {
|
||||||
export interface GlobalDirectives {
|
export interface GlobalDirectives {
|
||||||
vAuth: AuthDirective;
|
vHasPerm: Directive;
|
||||||
vRoles: RolesDirective;
|
|
||||||
vRipple: RippleDirective;
|
vRipple: RippleDirective;
|
||||||
vHighlight: HighlightDirective;
|
vHighlight: HighlightDirective;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -187,6 +187,12 @@ import { getDashboardMock } from "@/mock/dashboard";
|
|||||||
|
|
||||||
import bannerIcon4 from "@imgs/3d/icon4.webp";
|
import bannerIcon4 from "@imgs/3d/icon4.webp";
|
||||||
import cover2 from "@imgs/cover/img2.webp";
|
import cover2 from "@imgs/cover/img2.webp";
|
||||||
|
import Banner from "./modules/banner.vue";
|
||||||
|
import NewUser from "./modules/new-user.vue";
|
||||||
|
import TodoList from "./modules/todo-list.vue";
|
||||||
|
import CardList from "./modules/card-list.vue";
|
||||||
|
import AboutProject from "./modules/about-project.vue";
|
||||||
|
import QuickLinks from "./modules/quick-links.vue";
|
||||||
|
|
||||||
const mock = getDashboardMock();
|
const mock = getDashboardMock();
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
@@ -220,13 +226,6 @@ const FaImageCard = defineAsyncComponent(
|
|||||||
const FaTimelineListCard = defineAsyncComponent(
|
const FaTimelineListCard = defineAsyncComponent(
|
||||||
() => import("@/components/cards/fa-timeline-list-card/index.vue")
|
() => import("@/components/cards/fa-timeline-list-card/index.vue")
|
||||||
);
|
);
|
||||||
const Banner = defineAsyncComponent(() => import("./modules/banner.vue"));
|
|
||||||
const NewUser = defineAsyncComponent(() => import("./modules/new-user.vue"));
|
|
||||||
const TodoList = defineAsyncComponent(() => import("./modules/todo-list.vue"));
|
|
||||||
const CardList = defineAsyncComponent(() => import("./modules/card-list.vue"));
|
|
||||||
const AboutProject = defineAsyncComponent(() => import("./modules/about-project.vue"));
|
|
||||||
const QuickLinks = defineAsyncComponent(() => import("./modules/quick-links.vue"));
|
|
||||||
|
|
||||||
function handleBannerDemoConfirm() {
|
function handleBannerDemoConfirm() {
|
||||||
// TODO: 接入真实操作
|
// TODO: 接入真实操作
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -324,12 +324,14 @@ const {
|
|||||||
prop: "created_time",
|
prop: "created_time",
|
||||||
label: "创建时间",
|
label: "创建时间",
|
||||||
width: 168,
|
width: 168,
|
||||||
|
sortable: true,
|
||||||
showOverflowTooltip: true,
|
showOverflowTooltip: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
prop: "updated_time",
|
prop: "updated_time",
|
||||||
label: "更新时间",
|
label: "更新时间",
|
||||||
width: 168,
|
width: 168,
|
||||||
|
sortable: true,
|
||||||
showOverflowTooltip: true,
|
showOverflowTooltip: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -92,7 +92,7 @@
|
|||||||
label-suffix=":"
|
label-suffix=":"
|
||||||
:label-width="100"
|
:label-width="100"
|
||||||
label-position="right"
|
label-position="right"
|
||||||
:span="24"
|
:span="12"
|
||||||
:gutter="16"
|
:gutter="16"
|
||||||
:show-reset="false"
|
:show-reset="false"
|
||||||
:show-submit="false"
|
:show-submit="false"
|
||||||
@@ -299,8 +299,20 @@ const {
|
|||||||
{ prop: "datetime_val", label: "日期时间", minWidth: 168, showOverflowTooltip: true },
|
{ prop: "datetime_val", label: "日期时间", minWidth: 168, showOverflowTooltip: true },
|
||||||
{ prop: "text_val", label: "长文本", minWidth: 120, showOverflowTooltip: true },
|
{ prop: "text_val", label: "长文本", minWidth: 120, showOverflowTooltip: true },
|
||||||
{ prop: "description", label: "描述", minWidth: 120, showOverflowTooltip: true },
|
{ prop: "description", label: "描述", minWidth: 120, showOverflowTooltip: true },
|
||||||
{ prop: "created_time", label: "创建时间", width: 168, showOverflowTooltip: true },
|
{
|
||||||
{ prop: "updated_time", label: "更新时间", width: 168, showOverflowTooltip: true },
|
prop: "created_time",
|
||||||
|
label: "创建时间",
|
||||||
|
width: 168,
|
||||||
|
sortable: true,
|
||||||
|
showOverflowTooltip: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: "updated_time",
|
||||||
|
label: "更新时间",
|
||||||
|
width: 168,
|
||||||
|
sortable: true,
|
||||||
|
showOverflowTooltip: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
prop: "created_by",
|
prop: "created_by",
|
||||||
label: "创建人",
|
label: "创建人",
|
||||||
|
|||||||
+1
-5
@@ -61,11 +61,7 @@
|
|||||||
label="数据库名称"
|
label="数据库名称"
|
||||||
:show-overflow-tooltip="true"
|
:show-overflow-tooltip="true"
|
||||||
></ElTableColumn>
|
></ElTableColumn>
|
||||||
<ElTableColumn
|
<ElTableColumn prop="table_name" label="表名称" :show-overflow-tooltip="true"></ElTableColumn>
|
||||||
prop="table_name"
|
|
||||||
label="表名称"
|
|
||||||
:show-overflow-tooltip="true"
|
|
||||||
></ElTableColumn>
|
|
||||||
<ElTableColumn
|
<ElTableColumn
|
||||||
prop="table_comment"
|
prop="table_comment"
|
||||||
label="表描述"
|
label="表描述"
|
||||||
|
|||||||
@@ -705,6 +705,10 @@ async function handleDelete(row?: GenTableSchema): Promise<void> {
|
|||||||
type GencodeSearchForm = {
|
type GencodeSearchForm = {
|
||||||
table_name?: string;
|
table_name?: string;
|
||||||
table_comment?: string;
|
table_comment?: string;
|
||||||
|
created_id?: number;
|
||||||
|
updated_id?: number;
|
||||||
|
created_time?: string[];
|
||||||
|
updated_time?: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
function buildGencodeRowActions(row: GenTableSchema): TableOperationAction[] {
|
function buildGencodeRowActions(row: GenTableSchema): TableOperationAction[] {
|
||||||
@@ -748,6 +752,10 @@ function formatGencodeOperationCell(row: GenTableSchema) {
|
|||||||
const searchForm = ref<GencodeSearchForm>({
|
const searchForm = ref<GencodeSearchForm>({
|
||||||
table_name: undefined,
|
table_name: undefined,
|
||||||
table_comment: undefined,
|
table_comment: undefined,
|
||||||
|
created_id: undefined,
|
||||||
|
updated_id: undefined,
|
||||||
|
created_time: undefined,
|
||||||
|
updated_time: undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
const showSearchBar = ref(true);
|
const showSearchBar = ref(true);
|
||||||
@@ -819,12 +827,14 @@ const useTableResult = useTable({
|
|||||||
prop: "created_time",
|
prop: "created_time",
|
||||||
label: "创建时间",
|
label: "创建时间",
|
||||||
width: 168,
|
width: 168,
|
||||||
|
sortable: true,
|
||||||
showOverflowTooltip: true,
|
showOverflowTooltip: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
prop: "updated_time",
|
prop: "updated_time",
|
||||||
label: "更新时间",
|
label: "更新时间",
|
||||||
width: 168,
|
width: 168,
|
||||||
|
sortable: true,
|
||||||
showOverflowTooltip: true,
|
showOverflowTooltip: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -865,6 +875,16 @@ async function handleSearchBarSearch(params: GencodeSearchForm) {
|
|||||||
replaceSearchParams({
|
replaceSearchParams({
|
||||||
table_name: params.table_name,
|
table_name: params.table_name,
|
||||||
table_comment: params.table_comment,
|
table_comment: params.table_comment,
|
||||||
|
created_id: params.created_id,
|
||||||
|
updated_id: params.updated_id,
|
||||||
|
created_time:
|
||||||
|
Array.isArray(params.created_time) && params.created_time.length === 2
|
||||||
|
? params.created_time
|
||||||
|
: undefined,
|
||||||
|
updated_time:
|
||||||
|
Array.isArray(params.updated_time) && params.updated_time.length === 2
|
||||||
|
? params.updated_time
|
||||||
|
: undefined,
|
||||||
});
|
});
|
||||||
getData();
|
getData();
|
||||||
}
|
}
|
||||||
@@ -873,6 +893,10 @@ async function onResetSearch() {
|
|||||||
searchForm.value = {
|
searchForm.value = {
|
||||||
table_name: undefined,
|
table_name: undefined,
|
||||||
table_comment: undefined,
|
table_comment: undefined,
|
||||||
|
created_id: undefined,
|
||||||
|
updated_id: undefined,
|
||||||
|
created_time: undefined,
|
||||||
|
updated_time: undefined,
|
||||||
};
|
};
|
||||||
await resetSearchParams();
|
await resetSearchParams();
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-1
@@ -372,7 +372,10 @@ async function handleCacheValue(cacheKey: string) {
|
|||||||
const data = response.data.data;
|
const data = response.data.data;
|
||||||
cacheForm.value = {
|
cacheForm.value = {
|
||||||
...data,
|
...data,
|
||||||
cache_value: typeof data.cache_value === "string" ? data.cache_value : JSON.stringify(data.cache_value, null, 2),
|
cache_value:
|
||||||
|
typeof data.cache_value === "string"
|
||||||
|
? data.cache_value
|
||||||
|
: JSON.stringify(data.cache_value, null, 2),
|
||||||
};
|
};
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
if (import.meta.env.DEV) console.error("获取缓存内容失败:", error);
|
if (import.meta.env.DEV) console.error("获取缓存内容失败:", error);
|
||||||
|
|||||||
+15
-16
@@ -21,6 +21,18 @@
|
|||||||
</template>
|
</template>
|
||||||
</ElInput>
|
</ElInput>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
|
<ElFormItem prop="name">
|
||||||
|
<ElInput
|
||||||
|
class="custom-height"
|
||||||
|
v-model.trim="registerForm.name"
|
||||||
|
clearable
|
||||||
|
:placeholder="$t('register.placeholder.name')"
|
||||||
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<ElIcon><User /></ElIcon>
|
||||||
|
</template>
|
||||||
|
</ElInput>
|
||||||
|
</ElFormItem>
|
||||||
<ElTooltip :visible="isCapsLock" :content="$t('login.capsLock')" placement="right">
|
<ElTooltip :visible="isCapsLock" :content="$t('login.capsLock')" placement="right">
|
||||||
<ElFormItem prop="password">
|
<ElFormItem prop="password">
|
||||||
<ElInput
|
<ElInput
|
||||||
@@ -57,18 +69,6 @@
|
|||||||
</ElInput>
|
</ElInput>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
</ElTooltip>
|
</ElTooltip>
|
||||||
<ElFormItem v-if="showEmail" prop="email">
|
|
||||||
<ElInput
|
|
||||||
class="custom-height"
|
|
||||||
v-model.trim="registerForm.email"
|
|
||||||
clearable
|
|
||||||
:placeholder="$t('login.placeholder.email')"
|
|
||||||
>
|
|
||||||
<template #prefix>
|
|
||||||
<ElIcon><Message /></ElIcon>
|
|
||||||
</template>
|
|
||||||
</ElInput>
|
|
||||||
</ElFormItem>
|
|
||||||
<ElFormItem>
|
<ElFormItem>
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
<ElCheckbox v-model="registerAgreementReadModel">
|
<ElCheckbox v-model="registerAgreementReadModel">
|
||||||
@@ -108,7 +108,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Lock, Message, User } from "@element-plus/icons-vue";
|
import { Lock, User } from "@element-plus/icons-vue";
|
||||||
import type { RegisterForm } from "@/api/module_system/user";
|
import type { RegisterForm } from "@/api/module_system/user";
|
||||||
import type { FormRules } from "element-plus";
|
import type { FormRules } from "element-plus";
|
||||||
|
|
||||||
@@ -117,14 +117,13 @@ const registerForm = defineModel<RegisterForm>("registerForm", { required: true
|
|||||||
defineOptions({ name: "FaLoginRegisterPanel" });
|
defineOptions({ name: "FaLoginRegisterPanel" });
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
registerRules: FormRules<RegisterForm & { email: string }>;
|
registerRules: FormRules<RegisterForm>;
|
||||||
formKey: number | string;
|
formKey: number | string;
|
||||||
registerLoading: boolean;
|
registerLoading: boolean;
|
||||||
userAgreementHref: string;
|
userAgreementHref: string;
|
||||||
showEmail?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
withDefaults(defineProps<Props>(), { showEmail: false });
|
withDefaults(defineProps<Props>(), {});
|
||||||
|
|
||||||
const registerAgreementReadModel = defineModel<boolean>("registerAgreementRead", {
|
const registerAgreementReadModel = defineModel<boolean>("registerAgreementRead", {
|
||||||
required: true,
|
required: true,
|
||||||
|
|||||||
@@ -90,7 +90,6 @@
|
|||||||
:register-rules="registerRules"
|
:register-rules="registerRules"
|
||||||
:form-key="formKey"
|
:form-key="formKey"
|
||||||
:register-loading="registerLoading"
|
:register-loading="registerLoading"
|
||||||
:show-email="true"
|
|
||||||
:user-agreement-href="userAgreementHref"
|
:user-agreement-href="userAgreementHref"
|
||||||
@submit="submitRegister"
|
@submit="submitRegister"
|
||||||
@to-login="setAuthPanel('login')"
|
@to-login="setAuthPanel('login')"
|
||||||
@@ -373,11 +372,11 @@ const codeLoading = ref(false);
|
|||||||
|
|
||||||
const registerAgreementRead = ref(false);
|
const registerAgreementRead = ref(false);
|
||||||
|
|
||||||
const registerForm = reactive<RegisterForm & { email: string }>({
|
const registerForm = reactive<RegisterForm>({
|
||||||
username: "",
|
username: "",
|
||||||
|
name: "",
|
||||||
password: "",
|
password: "",
|
||||||
confirmPassword: "",
|
confirmPassword: "",
|
||||||
email: "",
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const forgetForm = reactive<ForgetPasswordForm>({
|
const forgetForm = reactive<ForgetPasswordForm>({
|
||||||
@@ -409,8 +408,9 @@ const validateRegisterConfirm = (_rule: unknown, value: string, callback: (e?: E
|
|||||||
callback();
|
callback();
|
||||||
};
|
};
|
||||||
|
|
||||||
const registerRules = computed<FormRules<RegisterForm & { email: string }>>(() => ({
|
const registerRules = computed<FormRules<RegisterForm>>(() => ({
|
||||||
username: [{ required: true, message: t("login.message.username.required"), trigger: "blur" }],
|
username: [{ required: true, message: t("login.message.username.required"), trigger: "blur" }],
|
||||||
|
name: [{ required: true, message: "请输入昵称", trigger: "blur" }],
|
||||||
password: [
|
password: [
|
||||||
{ required: true, validator: validateRegisterPassword, trigger: "blur" },
|
{ required: true, validator: validateRegisterPassword, trigger: "blur" },
|
||||||
{ min: 6, message: t("login.message.password.min"), trigger: "blur" },
|
{ min: 6, message: t("login.message.password.min"), trigger: "blur" },
|
||||||
@@ -420,14 +420,6 @@ const registerRules = computed<FormRules<RegisterForm & { email: string }>>(() =
|
|||||||
{ min: 6, message: t("login.message.password.min"), trigger: "blur" },
|
{ min: 6, message: t("login.message.password.min"), trigger: "blur" },
|
||||||
{ validator: validateRegisterConfirm, trigger: "blur" },
|
{ validator: validateRegisterConfirm, trigger: "blur" },
|
||||||
],
|
],
|
||||||
email: [
|
|
||||||
{ required: true, message: t("login.email.required"), trigger: "blur" },
|
|
||||||
{
|
|
||||||
type: "email",
|
|
||||||
message: t("login.email.invalid"),
|
|
||||||
trigger: "blur",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const validateForgetConfirm = (_rule: unknown, value: string, callback: (e?: Error) => void) => {
|
const validateForgetConfirm = (_rule: unknown, value: string, callback: (e?: Error) => void) => {
|
||||||
@@ -662,7 +654,7 @@ async function submitRegister() {
|
|||||||
registerForm.username = "";
|
registerForm.username = "";
|
||||||
registerForm.password = "";
|
registerForm.password = "";
|
||||||
registerForm.confirmPassword = "";
|
registerForm.confirmPassword = "";
|
||||||
registerForm.email = "";
|
registerForm.name = "";
|
||||||
registerAgreementRead.value = false;
|
registerAgreementRead.value = false;
|
||||||
setAuthPanel("login");
|
setAuthPanel("login");
|
||||||
await handleSubmit();
|
await handleSubmit();
|
||||||
|
|||||||
@@ -415,8 +415,20 @@ const { columnChecks, columns } = useTableColumns<DeptTable>(
|
|||||||
},
|
},
|
||||||
{ prop: "order", label: "排序", width: 88, showOverflowTooltip: true },
|
{ prop: "order", label: "排序", width: 88, showOverflowTooltip: true },
|
||||||
{ prop: "description", label: "描述", minWidth: 100, showOverflowTooltip: true },
|
{ prop: "description", label: "描述", minWidth: 100, showOverflowTooltip: true },
|
||||||
{ prop: "created_time", label: "创建时间", width: 168, showOverflowTooltip: true },
|
{
|
||||||
{ prop: "updated_time", label: "更新时间", width: 168, showOverflowTooltip: true },
|
prop: "created_time",
|
||||||
|
label: "创建时间",
|
||||||
|
width: 168,
|
||||||
|
sortable: true,
|
||||||
|
showOverflowTooltip: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: "updated_time",
|
||||||
|
label: "更新时间",
|
||||||
|
width: 168,
|
||||||
|
sortable: true,
|
||||||
|
showOverflowTooltip: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
prop: "operation",
|
prop: "operation",
|
||||||
label: "操作",
|
label: "操作",
|
||||||
|
|||||||
@@ -225,12 +225,14 @@ type OpSearchForm = {
|
|||||||
request_path?: string;
|
request_path?: string;
|
||||||
request_ip?: string;
|
request_ip?: string;
|
||||||
created_time?: string[];
|
created_time?: string[];
|
||||||
|
updated_time?: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
const opSearchForm = ref<OpSearchForm>({
|
const opSearchForm = ref<OpSearchForm>({
|
||||||
request_path: undefined,
|
request_path: undefined,
|
||||||
request_ip: undefined,
|
request_ip: undefined,
|
||||||
created_time: undefined,
|
created_time: undefined,
|
||||||
|
updated_time: undefined,
|
||||||
});
|
});
|
||||||
const opShowSearchBar = ref(true);
|
const opShowSearchBar = ref(true);
|
||||||
const opSearchBarRef = ref<InstanceType<typeof FaSearchBar> | null>(null);
|
const opSearchBarRef = ref<InstanceType<typeof FaSearchBar> | null>(null);
|
||||||
@@ -261,6 +263,8 @@ function buildOpReplaceParams(p: OpSearchForm): Record<string, unknown> {
|
|||||||
request_ip: p.request_ip,
|
request_ip: p.request_ip,
|
||||||
created_time:
|
created_time:
|
||||||
Array.isArray(p.created_time) && p.created_time.length === 2 ? p.created_time : undefined,
|
Array.isArray(p.created_time) && p.created_time.length === 2 ? p.created_time : undefined,
|
||||||
|
updated_time:
|
||||||
|
Array.isArray(p.updated_time) && p.updated_time.length === 2 ? p.updated_time : undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -323,7 +327,13 @@ const {
|
|||||||
},
|
},
|
||||||
{ prop: "process_time", label: "处理时间", minWidth: 120 },
|
{ prop: "process_time", label: "处理时间", minWidth: 120 },
|
||||||
{ prop: "description", label: "描述", minWidth: 120, showOverflowTooltip: true },
|
{ prop: "description", label: "描述", minWidth: 120, showOverflowTooltip: true },
|
||||||
{ prop: "created_time", label: "创建时间", width: 168, showOverflowTooltip: true },
|
{
|
||||||
|
prop: "created_time",
|
||||||
|
label: "创建时间",
|
||||||
|
width: 168,
|
||||||
|
sortable: true,
|
||||||
|
showOverflowTooltip: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
prop: "operation",
|
prop: "operation",
|
||||||
label: "操作",
|
label: "操作",
|
||||||
@@ -385,6 +395,7 @@ function onOpResetSearch() {
|
|||||||
request_path: undefined,
|
request_path: undefined,
|
||||||
request_ip: undefined,
|
request_ip: undefined,
|
||||||
created_time: undefined,
|
created_time: undefined,
|
||||||
|
updated_time: undefined,
|
||||||
};
|
};
|
||||||
void opResetSearchParams();
|
void opResetSearchParams();
|
||||||
}
|
}
|
||||||
@@ -464,12 +475,18 @@ async function handleOpBatchDelete() {
|
|||||||
|
|
||||||
// ==================== 登录日志 ====================
|
// ==================== 登录日志 ====================
|
||||||
|
|
||||||
type LoginSearchForm = { username?: string; status?: number; created_time?: string[] };
|
type LoginSearchForm = {
|
||||||
|
username?: string;
|
||||||
|
status?: number;
|
||||||
|
created_time?: string[];
|
||||||
|
updated_time?: string[];
|
||||||
|
};
|
||||||
|
|
||||||
const loginSearchForm = ref<LoginSearchForm>({
|
const loginSearchForm = ref<LoginSearchForm>({
|
||||||
username: undefined,
|
username: undefined,
|
||||||
status: undefined,
|
status: undefined,
|
||||||
created_time: undefined,
|
created_time: undefined,
|
||||||
|
updated_time: undefined,
|
||||||
});
|
});
|
||||||
const loginShowSearchBar = ref(true);
|
const loginShowSearchBar = ref(true);
|
||||||
const loginSearchBarRef = ref<InstanceType<typeof FaSearchBar> | null>(null);
|
const loginSearchBarRef = ref<InstanceType<typeof FaSearchBar> | null>(null);
|
||||||
@@ -522,6 +539,8 @@ function buildLoginReplaceParams(p: LoginSearchForm): Record<string, unknown> {
|
|||||||
: undefined,
|
: undefined,
|
||||||
created_time:
|
created_time:
|
||||||
Array.isArray(p.created_time) && p.created_time.length === 2 ? p.created_time : undefined,
|
Array.isArray(p.created_time) && p.created_time.length === 2 ? p.created_time : undefined,
|
||||||
|
updated_time:
|
||||||
|
Array.isArray(p.updated_time) && p.updated_time.length === 2 ? p.updated_time : undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -586,7 +605,13 @@ const {
|
|||||||
{ prop: "request_os", label: "操作系统", minWidth: 120 },
|
{ prop: "request_os", label: "操作系统", minWidth: 120 },
|
||||||
{ prop: "request_browser", label: "浏览器", minWidth: 180, showOverflowTooltip: true },
|
{ prop: "request_browser", label: "浏览器", minWidth: 180, showOverflowTooltip: true },
|
||||||
{ prop: "msg", label: "提示消息", minWidth: 200, showOverflowTooltip: true },
|
{ prop: "msg", label: "提示消息", minWidth: 200, showOverflowTooltip: true },
|
||||||
{ prop: "created_time", label: "登录时间", width: 168, showOverflowTooltip: true },
|
{
|
||||||
|
prop: "created_time",
|
||||||
|
label: "登录时间",
|
||||||
|
width: 168,
|
||||||
|
sortable: true,
|
||||||
|
showOverflowTooltip: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
prop: "operation",
|
prop: "operation",
|
||||||
label: "操作",
|
label: "操作",
|
||||||
@@ -621,7 +646,12 @@ async function handleLoginSearch(params: LoginSearchForm) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function onLoginResetSearch() {
|
function onLoginResetSearch() {
|
||||||
loginSearchForm.value = { username: undefined, status: undefined, created_time: undefined };
|
loginSearchForm.value = {
|
||||||
|
username: undefined,
|
||||||
|
status: undefined,
|
||||||
|
created_time: undefined,
|
||||||
|
updated_time: undefined,
|
||||||
|
};
|
||||||
void loginResetSearchParams();
|
void loginResetSearchParams();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -374,6 +374,7 @@ type MenuSearchForm = {
|
|||||||
name?: string;
|
name?: string;
|
||||||
status?: number;
|
status?: number;
|
||||||
created_time?: string[];
|
created_time?: string[];
|
||||||
|
updated_time?: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
function buildMenuListQuery(p: MenuSearchForm): MenuPageQuery {
|
function buildMenuListQuery(p: MenuSearchForm): MenuPageQuery {
|
||||||
@@ -382,6 +383,8 @@ function buildMenuListQuery(p: MenuSearchForm): MenuPageQuery {
|
|||||||
status: p.status,
|
status: p.status,
|
||||||
created_time:
|
created_time:
|
||||||
Array.isArray(p.created_time) && p.created_time.length === 2 ? p.created_time : undefined,
|
Array.isArray(p.created_time) && p.created_time.length === 2 ? p.created_time : undefined,
|
||||||
|
updated_time:
|
||||||
|
Array.isArray(p.updated_time) && p.updated_time.length === 2 ? p.updated_time : undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -440,6 +443,7 @@ const searchForm = ref<MenuSearchForm>({
|
|||||||
name: undefined,
|
name: undefined,
|
||||||
status: undefined,
|
status: undefined,
|
||||||
created_time: undefined,
|
created_time: undefined,
|
||||||
|
updated_time: undefined,
|
||||||
});
|
});
|
||||||
const showSearchBar = ref(true);
|
const showSearchBar = ref(true);
|
||||||
const searchBarRef = ref<InstanceType<typeof FaSearchBar> | null>(null);
|
const searchBarRef = ref<InstanceType<typeof FaSearchBar> | null>(null);
|
||||||
@@ -804,6 +808,7 @@ async function onResetSearch() {
|
|||||||
name: undefined,
|
name: undefined,
|
||||||
status: undefined,
|
status: undefined,
|
||||||
created_time: undefined,
|
created_time: undefined,
|
||||||
|
updated_time: undefined,
|
||||||
};
|
};
|
||||||
await loadMenuData();
|
await loadMenuData();
|
||||||
}
|
}
|
||||||
@@ -1045,6 +1050,7 @@ const { columnChecks, columns } = useTableColumns<MenuTable>(
|
|||||||
prop: "created_time",
|
prop: "created_time",
|
||||||
label: "创建时间",
|
label: "创建时间",
|
||||||
width: 168,
|
width: 168,
|
||||||
|
sortable: true,
|
||||||
showOverflowTooltip: true,
|
showOverflowTooltip: true,
|
||||||
visible: false,
|
visible: false,
|
||||||
},
|
},
|
||||||
@@ -1052,6 +1058,7 @@ const { columnChecks, columns } = useTableColumns<MenuTable>(
|
|||||||
prop: "updated_time",
|
prop: "updated_time",
|
||||||
label: "更新时间",
|
label: "更新时间",
|
||||||
width: 168,
|
width: 168,
|
||||||
|
sortable: true,
|
||||||
showOverflowTooltip: true,
|
showOverflowTooltip: true,
|
||||||
visible: false,
|
visible: false,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -158,8 +158,10 @@ type NoticeSearchForm = {
|
|||||||
notice_title?: string;
|
notice_title?: string;
|
||||||
notice_type?: string;
|
notice_type?: string;
|
||||||
status?: number;
|
status?: number;
|
||||||
created_time?: string[];
|
|
||||||
created_id?: number;
|
created_id?: number;
|
||||||
|
updated_id?: number;
|
||||||
|
created_time?: string[];
|
||||||
|
updated_time?: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
function noticeTypeLabel(val?: string) {
|
function noticeTypeLabel(val?: string) {
|
||||||
@@ -173,8 +175,10 @@ const searchForm = ref<NoticeSearchForm>({
|
|||||||
notice_title: undefined,
|
notice_title: undefined,
|
||||||
notice_type: undefined,
|
notice_type: undefined,
|
||||||
status: undefined,
|
status: undefined,
|
||||||
created_time: undefined,
|
|
||||||
created_id: undefined,
|
created_id: undefined,
|
||||||
|
updated_id: undefined,
|
||||||
|
created_time: undefined,
|
||||||
|
updated_time: undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
const showSearchBar = ref(true);
|
const showSearchBar = ref(true);
|
||||||
@@ -306,7 +310,7 @@ const { submitLoading, handleCloseDialog, handleOpenDialog, handleSubmit } =
|
|||||||
await refreshUpdate();
|
await refreshUpdate();
|
||||||
},
|
},
|
||||||
onSubmitSuccess: async () => {
|
onSubmitSuccess: async () => {
|
||||||
await noticeStore.getNotice();
|
await noticeStore.getNotice(true);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -410,8 +414,20 @@ const {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{ prop: "description", label: "描述", minWidth: 140, showOverflowTooltip: true },
|
{ prop: "description", label: "描述", minWidth: 140, showOverflowTooltip: true },
|
||||||
{ prop: "created_time", label: "创建时间", width: 168, showOverflowTooltip: true },
|
{
|
||||||
{ prop: "updated_time", label: "更新时间", width: 168, showOverflowTooltip: true },
|
prop: "created_time",
|
||||||
|
label: "创建时间",
|
||||||
|
width: 168,
|
||||||
|
sortable: true,
|
||||||
|
showOverflowTooltip: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: "updated_time",
|
||||||
|
label: "更新时间",
|
||||||
|
width: 168,
|
||||||
|
sortable: true,
|
||||||
|
showOverflowTooltip: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
prop: "created_id",
|
prop: "created_id",
|
||||||
label: "创建人",
|
label: "创建人",
|
||||||
@@ -442,8 +458,11 @@ function buildNoticeReplaceParams(p: NoticeSearchForm): Record<string, unknown>
|
|||||||
notice_type: p.notice_type,
|
notice_type: p.notice_type,
|
||||||
status: p.status,
|
status: p.status,
|
||||||
created_id: p.created_id,
|
created_id: p.created_id,
|
||||||
|
updated_id: p.updated_id,
|
||||||
created_time:
|
created_time:
|
||||||
Array.isArray(p.created_time) && p.created_time.length === 2 ? p.created_time : undefined,
|
Array.isArray(p.created_time) && p.created_time.length === 2 ? p.created_time : undefined,
|
||||||
|
updated_time:
|
||||||
|
Array.isArray(p.updated_time) && p.updated_time.length === 2 ? p.updated_time : undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -469,8 +488,10 @@ async function onResetSearch() {
|
|||||||
notice_title: undefined,
|
notice_title: undefined,
|
||||||
notice_type: undefined,
|
notice_type: undefined,
|
||||||
status: undefined,
|
status: undefined,
|
||||||
created_time: undefined,
|
|
||||||
created_id: undefined,
|
created_id: undefined,
|
||||||
|
updated_id: undefined,
|
||||||
|
created_time: undefined,
|
||||||
|
updated_time: undefined,
|
||||||
};
|
};
|
||||||
await resetSearchParams();
|
await resetSearchParams();
|
||||||
}
|
}
|
||||||
@@ -479,7 +500,7 @@ async function deleteNoticeRow(id: number, name: string) {
|
|||||||
try {
|
try {
|
||||||
await confirmDelete(`确定删除「${name}」吗?`);
|
await confirmDelete(`确定删除「${name}」吗?`);
|
||||||
await NoticeAPI.deleteNotice([id]);
|
await NoticeAPI.deleteNotice([id]);
|
||||||
await noticeStore.getNotice();
|
await noticeStore.getNotice(true);
|
||||||
faTableRef.value?.elTableRef?.clearSelection();
|
faTableRef.value?.elTableRef?.clearSelection();
|
||||||
await refreshRemove();
|
await refreshRemove();
|
||||||
} catch {
|
} catch {
|
||||||
@@ -540,7 +561,7 @@ async function handleBatchDelete() {
|
|||||||
);
|
);
|
||||||
batchDeleting.value = true;
|
batchDeleting.value = true;
|
||||||
await NoticeAPI.deleteNotice(ids);
|
await NoticeAPI.deleteNotice(ids);
|
||||||
await noticeStore.getNotice();
|
await noticeStore.getNotice(true);
|
||||||
faTableRef.value?.elTableRef?.clearSelection();
|
faTableRef.value?.elTableRef?.clearSelection();
|
||||||
await refreshRemove();
|
await refreshRemove();
|
||||||
} catch {
|
} catch {
|
||||||
@@ -562,7 +583,7 @@ async function handleMoreClick(value: "enable" | "disable") {
|
|||||||
const status = value === "enable" ? 0 : 1;
|
const status = value === "enable" ? 0 : 1;
|
||||||
await NoticeAPI.batchNotice({ ids, status });
|
await NoticeAPI.batchNotice({ ids, status });
|
||||||
await refreshData();
|
await refreshData();
|
||||||
await noticeStore.getNotice();
|
await noticeStore.getNotice(true);
|
||||||
} catch {
|
} catch {
|
||||||
// 用户取消或操作失败
|
// 用户取消或操作失败
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -440,15 +440,15 @@ onMounted(async () => {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
:deep(.el-card) {
|
:deep(.el-card) {
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.el-card__body) {
|
:deep(.el-card__body) {
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -460,8 +460,8 @@ onMounted(async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
:deep(.el-tabs__content) {
|
:deep(.el-tabs__content) {
|
||||||
overflow-y: auto;
|
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -147,8 +147,10 @@ const userStore = useUserStore();
|
|||||||
type PositionSearchForm = {
|
type PositionSearchForm = {
|
||||||
name?: string;
|
name?: string;
|
||||||
status?: number;
|
status?: number;
|
||||||
created_time?: string[];
|
|
||||||
created_id?: number;
|
created_id?: number;
|
||||||
|
updated_id?: number;
|
||||||
|
created_time?: string[];
|
||||||
|
updated_time?: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
function normalizePositionQuery(params: Record<string, unknown>): PositionPageQuery {
|
function normalizePositionQuery(params: Record<string, unknown>): PositionPageQuery {
|
||||||
@@ -160,8 +162,11 @@ function buildPositionReplaceParams(p: PositionSearchForm): Record<string, unkno
|
|||||||
name: p.name,
|
name: p.name,
|
||||||
status: p.status,
|
status: p.status,
|
||||||
created_id: p.created_id,
|
created_id: p.created_id,
|
||||||
|
updated_id: p.updated_id,
|
||||||
created_time:
|
created_time:
|
||||||
Array.isArray(p.created_time) && p.created_time.length === 2 ? p.created_time : undefined,
|
Array.isArray(p.created_time) && p.created_time.length === 2 ? p.created_time : undefined,
|
||||||
|
updated_time:
|
||||||
|
Array.isArray(p.updated_time) && p.updated_time.length === 2 ? p.updated_time : undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,8 +227,10 @@ function formatPositionOperationCell(
|
|||||||
const searchForm = ref<PositionSearchForm>({
|
const searchForm = ref<PositionSearchForm>({
|
||||||
name: undefined,
|
name: undefined,
|
||||||
status: undefined,
|
status: undefined,
|
||||||
created_time: undefined,
|
|
||||||
created_id: undefined,
|
created_id: undefined,
|
||||||
|
updated_id: undefined,
|
||||||
|
created_time: undefined,
|
||||||
|
updated_time: undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
const showSearchBar = ref(true);
|
const showSearchBar = ref(true);
|
||||||
@@ -308,8 +315,20 @@ const {
|
|||||||
},
|
},
|
||||||
{ prop: "order", label: "岗位排序", width: 100, showOverflowTooltip: true },
|
{ prop: "order", label: "岗位排序", width: 100, showOverflowTooltip: true },
|
||||||
{ prop: "description", label: "描述", minWidth: 120, showOverflowTooltip: true },
|
{ prop: "description", label: "描述", minWidth: 120, showOverflowTooltip: true },
|
||||||
{ prop: "created_time", label: "创建时间", width: 168, showOverflowTooltip: true },
|
{
|
||||||
{ prop: "updated_time", label: "更新时间", width: 168, showOverflowTooltip: true },
|
prop: "created_time",
|
||||||
|
label: "创建时间",
|
||||||
|
width: 168,
|
||||||
|
sortable: true,
|
||||||
|
showOverflowTooltip: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: "updated_time",
|
||||||
|
label: "更新时间",
|
||||||
|
width: 168,
|
||||||
|
sortable: true,
|
||||||
|
showOverflowTooltip: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
prop: "created_id",
|
prop: "created_id",
|
||||||
label: "创建人",
|
label: "创建人",
|
||||||
@@ -488,8 +507,10 @@ async function onResetSearch() {
|
|||||||
searchForm.value = {
|
searchForm.value = {
|
||||||
name: undefined,
|
name: undefined,
|
||||||
status: undefined,
|
status: undefined,
|
||||||
created_time: undefined,
|
|
||||||
created_id: undefined,
|
created_id: undefined,
|
||||||
|
updated_id: undefined,
|
||||||
|
created_time: undefined,
|
||||||
|
updated_time: undefined,
|
||||||
};
|
};
|
||||||
await resetSearchParams();
|
await resetSearchParams();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -186,7 +186,10 @@ defineOptions({
|
|||||||
type RoleSearchForm = {
|
type RoleSearchForm = {
|
||||||
name?: string;
|
name?: string;
|
||||||
status?: number;
|
status?: number;
|
||||||
|
created_id?: number;
|
||||||
|
updated_id?: number;
|
||||||
created_time?: string[];
|
created_time?: string[];
|
||||||
|
updated_time?: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
function normalizeRoleQuery(params: Record<string, unknown>): TablePageQuery {
|
function normalizeRoleQuery(params: Record<string, unknown>): TablePageQuery {
|
||||||
@@ -198,8 +201,12 @@ function buildRoleReplaceParams(p: RoleSearchForm): Record<string, unknown> {
|
|||||||
return {
|
return {
|
||||||
name: p.name,
|
name: p.name,
|
||||||
status: p.status,
|
status: p.status,
|
||||||
|
created_id: p.created_id,
|
||||||
|
updated_id: p.updated_id,
|
||||||
created_time:
|
created_time:
|
||||||
Array.isArray(p.created_time) && p.created_time.length === 2 ? p.created_time : undefined,
|
Array.isArray(p.created_time) && p.created_time.length === 2 ? p.created_time : undefined,
|
||||||
|
updated_time:
|
||||||
|
Array.isArray(p.updated_time) && p.updated_time.length === 2 ? p.updated_time : undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -303,7 +310,10 @@ function formatRoleOperationCell(row: RoleTable, ctx: Parameters<typeof buildRol
|
|||||||
const searchForm = ref<RoleSearchForm>({
|
const searchForm = ref<RoleSearchForm>({
|
||||||
name: undefined,
|
name: undefined,
|
||||||
status: undefined,
|
status: undefined,
|
||||||
|
created_id: undefined,
|
||||||
|
updated_id: undefined,
|
||||||
created_time: undefined,
|
created_time: undefined,
|
||||||
|
updated_time: undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
const showSearchBar = ref(true);
|
const showSearchBar = ref(true);
|
||||||
@@ -576,8 +586,20 @@ const {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{ prop: "description", label: "描述", minWidth: 120, showOverflowTooltip: true },
|
{ prop: "description", label: "描述", minWidth: 120, showOverflowTooltip: true },
|
||||||
{ prop: "created_time", label: "创建时间", width: 168, showOverflowTooltip: true },
|
{
|
||||||
{ prop: "updated_time", label: "更新时间", width: 168, showOverflowTooltip: true },
|
prop: "created_time",
|
||||||
|
label: "创建时间",
|
||||||
|
width: 168,
|
||||||
|
sortable: true,
|
||||||
|
showOverflowTooltip: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: "updated_time",
|
||||||
|
label: "更新时间",
|
||||||
|
width: 168,
|
||||||
|
sortable: true,
|
||||||
|
showOverflowTooltip: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
prop: "operation",
|
prop: "operation",
|
||||||
label: "操作",
|
label: "操作",
|
||||||
@@ -620,7 +642,10 @@ async function onResetSearch() {
|
|||||||
searchForm.value = {
|
searchForm.value = {
|
||||||
name: undefined,
|
name: undefined,
|
||||||
status: undefined,
|
status: undefined,
|
||||||
|
created_id: undefined,
|
||||||
|
updated_id: undefined,
|
||||||
created_time: undefined,
|
created_time: undefined,
|
||||||
|
updated_time: undefined,
|
||||||
};
|
};
|
||||||
await resetSearchParams();
|
await resetSearchParams();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,7 +62,12 @@
|
|||||||
<FaSvgIcon :icon="typeIcon(item.ticket_type!)" />
|
<FaSvgIcon :icon="typeIcon(item.ticket_type!)" />
|
||||||
</span>
|
</span>
|
||||||
<span class="flex-1 truncate text-sm font-semibold">{{ item.title }}</span>
|
<span class="flex-1 truncate text-sm font-semibold">{{ item.title }}</span>
|
||||||
<ElTag size="small" :type="statusTagType(String(item.status))" effect="dark" class="shrink-0">
|
<ElTag
|
||||||
|
size="small"
|
||||||
|
:type="statusTagType(String(item.status))"
|
||||||
|
effect="dark"
|
||||||
|
class="shrink-0"
|
||||||
|
>
|
||||||
{{ statusLabel(item.status ?? 0) }}
|
{{ statusLabel(item.status ?? 0) }}
|
||||||
</ElTag>
|
</ElTag>
|
||||||
</div>
|
</div>
|
||||||
@@ -70,12 +75,21 @@
|
|||||||
|
|
||||||
<template #default="{ item }">
|
<template #default="{ item }">
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<div class="flex items-center gap-1.5 text-xs" style="color: var(--el-text-color-secondary)">
|
<div
|
||||||
|
class="flex items-center gap-1.5 text-xs"
|
||||||
|
style="color: var(--el-text-color-secondary)"
|
||||||
|
>
|
||||||
<FaSvgIcon icon="ri:user-3-line" class="shrink-0" />
|
<FaSvgIcon icon="ri:user-3-line" class="shrink-0" />
|
||||||
<span>{{ item.created_by?.name ?? "—" }} · {{ item.created_time?.slice(0, 10) ?? "" }}</span>
|
<span
|
||||||
|
>{{ item.created_by?.name ?? "—" }} ·
|
||||||
|
{{ item.created_time?.slice(0, 10) ?? "" }}</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div class="flex items-center gap-1.5 text-xs" style="color: var(--el-text-color-secondary)">
|
<div
|
||||||
|
class="flex items-center gap-1.5 text-xs"
|
||||||
|
style="color: var(--el-text-color-secondary)"
|
||||||
|
>
|
||||||
<FaSvgIcon icon="ri:user-add-line" class="shrink-0" />
|
<FaSvgIcon icon="ri:user-add-line" class="shrink-0" />
|
||||||
<span>{{ item.assigned_by?.name ?? "未分配" }}</span>
|
<span>{{ item.assigned_by?.name ?? "未分配" }}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -88,7 +102,9 @@
|
|||||||
|
|
||||||
<template #footer="{ item }">
|
<template #footer="{ item }">
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
<ElButton size="small" link type="primary" @click="handleOpenDialog('detail', item.id!)">详情</ElButton>
|
<ElButton size="small" link type="primary" @click="handleOpenDialog('detail', item.id!)"
|
||||||
|
>详情</ElButton
|
||||||
|
>
|
||||||
<ElButton
|
<ElButton
|
||||||
v-if="item.status! < 3"
|
v-if="item.status! < 3"
|
||||||
v-hasPerm="['module_system:ticket:update']"
|
v-hasPerm="['module_system:ticket:update']"
|
||||||
@@ -96,7 +112,8 @@
|
|||||||
link
|
link
|
||||||
type="primary"
|
type="primary"
|
||||||
@click="handleOpenDialog('update', item.id!)"
|
@click="handleOpenDialog('update', item.id!)"
|
||||||
>处理</ElButton>
|
>处理</ElButton
|
||||||
|
>
|
||||||
<ElDropdown v-if="showCardMore(item)" trigger="click">
|
<ElDropdown v-if="showCardMore(item)" trigger="click">
|
||||||
<ElButton size="small" link type="primary" class="px-1 py-0.5 text-base">
|
<ElButton size="small" link type="primary" class="px-1 py-0.5 text-base">
|
||||||
<ElIcon><MoreFilled /></ElIcon>
|
<ElIcon><MoreFilled /></ElIcon>
|
||||||
@@ -394,6 +411,7 @@ type TicketSearchForm = {
|
|||||||
ticket_type?: string;
|
ticket_type?: string;
|
||||||
status?: number;
|
status?: number;
|
||||||
created_id?: number;
|
created_id?: number;
|
||||||
|
updated_id?: number;
|
||||||
assigned_id?: number;
|
assigned_id?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -402,6 +420,7 @@ const searchForm = ref<TicketSearchForm>({
|
|||||||
ticket_type: "",
|
ticket_type: "",
|
||||||
status: undefined,
|
status: undefined,
|
||||||
created_id: undefined,
|
created_id: undefined,
|
||||||
|
updated_id: undefined,
|
||||||
assigned_id: undefined,
|
assigned_id: undefined,
|
||||||
});
|
});
|
||||||
const showSearchBar = ref(true);
|
const showSearchBar = ref(true);
|
||||||
@@ -513,6 +532,7 @@ async function handleSearchBarSearch(params: Record<string, unknown>) {
|
|||||||
ticket_type: (params.ticket_type as string) ?? "",
|
ticket_type: (params.ticket_type as string) ?? "",
|
||||||
status: params.status !== undefined ? Number(params.status) : undefined,
|
status: params.status !== undefined ? Number(params.status) : undefined,
|
||||||
created_id: params.created_id as number | undefined,
|
created_id: params.created_id as number | undefined,
|
||||||
|
updated_id: params.updated_id as number | undefined,
|
||||||
assigned_id: params.assigned_id as number | undefined,
|
assigned_id: params.assigned_id as number | undefined,
|
||||||
};
|
};
|
||||||
pageNo.value = 1;
|
pageNo.value = 1;
|
||||||
@@ -525,6 +545,7 @@ async function onResetSearch() {
|
|||||||
ticket_type: "",
|
ticket_type: "",
|
||||||
status: undefined,
|
status: undefined,
|
||||||
created_id: undefined,
|
created_id: undefined,
|
||||||
|
updated_id: undefined,
|
||||||
assigned_id: undefined,
|
assigned_id: undefined,
|
||||||
};
|
};
|
||||||
pageNo.value = 1;
|
pageNo.value = 1;
|
||||||
|
|||||||
@@ -109,21 +109,13 @@
|
|||||||
<FaStatusTag v-else-if="row?.gender === '1'" type="warning" label="女" />
|
<FaStatusTag v-else-if="row?.gender === '1'" type="warning" label="女" />
|
||||||
<FaStatusTag v-else type="info" label="未知" />
|
<FaStatusTag v-else type="info" label="未知" />
|
||||||
</template>
|
</template>
|
||||||
<!-- 角色 → 数组 join 渲染 -->
|
<!-- 角色 → 根据 IDs 从选项解析名称 -->
|
||||||
<template #roles="{ row }">
|
<template #roles="{ row }">
|
||||||
{{
|
{{ resolveLabels((row as UserInfo).role_ids, roleOptions) }}
|
||||||
(row as unknown as UserInfo)?.roles
|
|
||||||
? (row as unknown as UserInfo).roles!.map((item) => item.name).join("、")
|
|
||||||
: ""
|
|
||||||
}}
|
|
||||||
</template>
|
</template>
|
||||||
<!-- 岗位 → 数组 join 渲染 -->
|
<!-- 岗位 → 根据 IDs 从选项解析名称 -->
|
||||||
<template #positions="{ row }">
|
<template #positions="{ row }">
|
||||||
{{
|
{{ resolveLabels((row as UserInfo).position_ids, positionOptions) }}
|
||||||
(row as unknown as UserInfo)?.positions
|
|
||||||
? (row as unknown as UserInfo).positions!.map((item) => item.name).join("、")
|
|
||||||
: ""
|
|
||||||
}}
|
|
||||||
</template>
|
</template>
|
||||||
</FaDescriptions>
|
</FaDescriptions>
|
||||||
</template>
|
</template>
|
||||||
@@ -249,7 +241,9 @@ type UserSearchForm = {
|
|||||||
name?: string;
|
name?: string;
|
||||||
status?: number;
|
status?: number;
|
||||||
created_id?: number;
|
created_id?: number;
|
||||||
|
updated_id?: number;
|
||||||
created_time?: string[];
|
created_time?: string[];
|
||||||
|
updated_time?: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
function buildUserReplaceParams(u: UserSearchForm): Record<string, unknown> {
|
function buildUserReplaceParams(u: UserSearchForm): Record<string, unknown> {
|
||||||
@@ -258,8 +252,11 @@ function buildUserReplaceParams(u: UserSearchForm): Record<string, unknown> {
|
|||||||
name: u.name,
|
name: u.name,
|
||||||
status: u.status,
|
status: u.status,
|
||||||
created_id: u.created_id,
|
created_id: u.created_id,
|
||||||
|
updated_id: u.updated_id,
|
||||||
created_time:
|
created_time:
|
||||||
Array.isArray(u.created_time) && u.created_time.length === 2 ? u.created_time : undefined,
|
Array.isArray(u.created_time) && u.created_time.length === 2 ? u.created_time : undefined,
|
||||||
|
updated_time:
|
||||||
|
Array.isArray(u.updated_time) && u.updated_time.length === 2 ? u.updated_time : undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -354,6 +351,23 @@ const positionOptions = ref<Array<{ value: number; label: string; disabled?: boo
|
|||||||
const { importVisible, exportVisible, openImport, openExport } = useImportExport();
|
const { importVisible, exportVisible, openImport, openExport } = useImportExport();
|
||||||
const detailFormData = ref<UserInfo>({});
|
const detailFormData = ref<UserInfo>({});
|
||||||
|
|
||||||
|
interface OptionItem {
|
||||||
|
value: number;
|
||||||
|
label: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveLabels(
|
||||||
|
ids: (number | undefined)[] | undefined,
|
||||||
|
options: OptionItem[] | undefined
|
||||||
|
): string {
|
||||||
|
if (!ids || !Array.isArray(ids) || !options) return "";
|
||||||
|
return ids
|
||||||
|
.filter((id): id is number => id !== undefined && id !== null)
|
||||||
|
.map((id) => options.find((o) => o.value === id)?.label ?? String(id))
|
||||||
|
.join("、");
|
||||||
|
}
|
||||||
|
|
||||||
// 用户详情描述项配置 —— 数据驱动 + 关键字段用具名插槽覆盖
|
// 用户详情描述项配置 —— 数据驱动 + 关键字段用具名插槽覆盖
|
||||||
const userDetailItems: DescriptionsItem[] = [
|
const userDetailItems: DescriptionsItem[] = [
|
||||||
{ label: "编号", prop: "id" },
|
{ label: "编号", prop: "id" },
|
||||||
@@ -361,7 +375,7 @@ const userDetailItems: DescriptionsItem[] = [
|
|||||||
{ label: "账号", prop: "username" },
|
{ label: "账号", prop: "username" },
|
||||||
{ label: "用户名", prop: "name" },
|
{ label: "用户名", prop: "name" },
|
||||||
{ label: "性别", prop: "gender", slot: "gender" }, // 三种状态 Tag
|
{ label: "性别", prop: "gender", slot: "gender" }, // 三种状态 Tag
|
||||||
{ label: "部门", prop: "dept.name" }, // 嵌套属性 a.b.c
|
{ label: "部门", prop: "dept_name" },
|
||||||
{ label: "角色", prop: "roles", slot: "roles" }, // 数组 join 渲染
|
{ label: "角色", prop: "roles", slot: "roles" }, // 数组 join 渲染
|
||||||
{ label: "岗位", prop: "positions", slot: "positions" }, // 数组 join 渲染
|
{ label: "岗位", prop: "positions", slot: "positions" }, // 数组 join 渲染
|
||||||
{ label: "邮箱", prop: "email" },
|
{ label: "邮箱", prop: "email" },
|
||||||
@@ -463,7 +477,9 @@ const searchForm = ref<UserSearchForm>({
|
|||||||
name: undefined,
|
name: undefined,
|
||||||
status: undefined,
|
status: undefined,
|
||||||
created_id: undefined,
|
created_id: undefined,
|
||||||
|
updated_id: undefined,
|
||||||
created_time: undefined,
|
created_time: undefined,
|
||||||
|
updated_time: undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
const showSearchBar = ref(true);
|
const showSearchBar = ref(true);
|
||||||
@@ -611,7 +627,7 @@ const {
|
|||||||
prop: "dept",
|
prop: "dept",
|
||||||
label: "部门",
|
label: "部门",
|
||||||
minWidth: 100,
|
minWidth: 100,
|
||||||
formatter: (row: UserInfo) => row.dept?.name ?? "—",
|
formatter: (row: UserInfo) => row.dept_name ?? "—",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
prop: "gender",
|
prop: "gender",
|
||||||
@@ -622,8 +638,20 @@ const {
|
|||||||
"1": { type: "warning", text: "女" },
|
"1": { type: "warning", text: "女" },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{ prop: "created_time", label: "创建时间", width: 168, showOverflowTooltip: true },
|
{
|
||||||
{ prop: "updated_time", label: "更新时间", width: 168, showOverflowTooltip: true },
|
prop: "created_time",
|
||||||
|
label: "创建时间",
|
||||||
|
width: 168,
|
||||||
|
sortable: true,
|
||||||
|
showOverflowTooltip: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: "updated_time",
|
||||||
|
label: "更新时间",
|
||||||
|
width: 168,
|
||||||
|
sortable: true,
|
||||||
|
showOverflowTooltip: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
prop: "operation",
|
prop: "operation",
|
||||||
label: "操作",
|
label: "操作",
|
||||||
@@ -771,7 +799,9 @@ async function onResetSearch() {
|
|||||||
name: undefined,
|
name: undefined,
|
||||||
status: undefined,
|
status: undefined,
|
||||||
created_id: undefined,
|
created_id: undefined,
|
||||||
|
updated_id: undefined,
|
||||||
created_time: undefined,
|
created_time: undefined,
|
||||||
|
updated_time: undefined,
|
||||||
};
|
};
|
||||||
deptFilterId.value = undefined;
|
deptFilterId.value = undefined;
|
||||||
await resetSearchParams();
|
await resetSearchParams();
|
||||||
@@ -828,10 +858,8 @@ async function handleOpenDialog(type: "create" | "update" | "detail", id?: numbe
|
|||||||
} else if (type === "update") {
|
} else if (type === "update") {
|
||||||
dialogVisible.title = "修改用户";
|
dialogVisible.title = "修改用户";
|
||||||
Object.assign(formData.value, response.data.data);
|
Object.assign(formData.value, response.data.data);
|
||||||
formData.value.role_ids = (response.data.data.roles || []).map((item) => item.id as number);
|
formData.value.role_ids = (response.data.data.role_ids ?? []) as number[];
|
||||||
formData.value.position_ids = (response.data.data.positions || []).map(
|
formData.value.position_ids = (response.data.data.position_ids ?? []) as number[];
|
||||||
(item) => item.id as number
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
dialogVisible.title = "新增用户";
|
dialogVisible.title = "新增用户";
|
||||||
|
|||||||
@@ -251,8 +251,20 @@ const {
|
|||||||
},
|
},
|
||||||
{ prop: "date", label: "发布日期", width: 120, showOverflowTooltip: true },
|
{ prop: "date", label: "发布日期", width: 120, showOverflowTooltip: true },
|
||||||
{ prop: "sort", label: "排序", width: 80 },
|
{ prop: "sort", label: "排序", width: 80 },
|
||||||
{ prop: "created_time", label: "创建时间", width: 168, showOverflowTooltip: true },
|
{
|
||||||
{ prop: "updated_time", label: "更新时间", width: 168, showOverflowTooltip: true },
|
prop: "created_time",
|
||||||
|
label: "创建时间",
|
||||||
|
width: 168,
|
||||||
|
sortable: true,
|
||||||
|
showOverflowTooltip: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: "updated_time",
|
||||||
|
label: "更新时间",
|
||||||
|
width: 168,
|
||||||
|
sortable: true,
|
||||||
|
showOverflowTooltip: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
prop: "operation",
|
prop: "operation",
|
||||||
label: "操作",
|
label: "操作",
|
||||||
|
|||||||
@@ -528,18 +528,26 @@ onMounted(() => {
|
|||||||
type LogSearchForm = {
|
type LogSearchForm = {
|
||||||
status?: number;
|
status?: number;
|
||||||
trigger_type?: string;
|
trigger_type?: string;
|
||||||
|
created_time?: string[];
|
||||||
|
updated_time?: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
function buildLogReplaceParams(u: LogSearchForm): Record<string, unknown> {
|
function buildLogReplaceParams(u: LogSearchForm): Record<string, unknown> {
|
||||||
return {
|
return {
|
||||||
status: u.status,
|
status: u.status,
|
||||||
trigger_type: u.trigger_type,
|
trigger_type: u.trigger_type,
|
||||||
|
created_time:
|
||||||
|
Array.isArray(u.created_time) && u.created_time.length === 2 ? u.created_time : undefined,
|
||||||
|
updated_time:
|
||||||
|
Array.isArray(u.updated_time) && u.updated_time.length === 2 ? u.updated_time : undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const logSearchForm = ref<LogSearchForm>({
|
const logSearchForm = ref<LogSearchForm>({
|
||||||
status: undefined,
|
status: undefined,
|
||||||
trigger_type: undefined,
|
trigger_type: undefined,
|
||||||
|
created_time: undefined,
|
||||||
|
updated_time: undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
const logSearchBarRef = ref<InstanceType<typeof FaSearchBar> | null>(null);
|
const logSearchBarRef = ref<InstanceType<typeof FaSearchBar> | null>(null);
|
||||||
@@ -716,12 +724,14 @@ const {
|
|||||||
prop: "created_time",
|
prop: "created_time",
|
||||||
label: "创建时间",
|
label: "创建时间",
|
||||||
minWidth: 160,
|
minWidth: 160,
|
||||||
|
sortable: true,
|
||||||
showOverflowTooltip: true,
|
showOverflowTooltip: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
prop: "updated_time",
|
prop: "updated_time",
|
||||||
label: "更新时间",
|
label: "更新时间",
|
||||||
minWidth: 160,
|
minWidth: 160,
|
||||||
|
sortable: true,
|
||||||
showOverflowTooltip: true,
|
showOverflowTooltip: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -796,6 +806,8 @@ async function onLogResetSearch() {
|
|||||||
logSearchForm.value = {
|
logSearchForm.value = {
|
||||||
status: undefined,
|
status: undefined,
|
||||||
trigger_type: undefined,
|
trigger_type: undefined,
|
||||||
|
created_time: undefined,
|
||||||
|
updated_time: undefined,
|
||||||
};
|
};
|
||||||
await resetLogSearchParams();
|
await resetLogSearchParams();
|
||||||
if (currentLogJobId.value) {
|
if (currentLogJobId.value) {
|
||||||
|
|||||||
@@ -220,7 +220,12 @@
|
|||||||
readonly
|
readonly
|
||||||
@click="openCron = true"
|
@click="openCron = true"
|
||||||
/>
|
/>
|
||||||
<FaDialog v-model="openCron" title="Cron 表达式" width="min(700px, calc(100vw - 48px))" append-to-body>
|
<FaDialog
|
||||||
|
v-model="openCron"
|
||||||
|
title="Cron 表达式"
|
||||||
|
width="min(700px, calc(100vw - 48px))"
|
||||||
|
append-to-body
|
||||||
|
>
|
||||||
<FaCron v-model="cronTempValue" />
|
<FaCron v-model="cronTempValue" />
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<ElButton @click="openCron = false">取消</ElButton>
|
<ElButton @click="openCron = false">取消</ElButton>
|
||||||
@@ -302,18 +307,32 @@ const dictStore = useDictStore();
|
|||||||
type NodeSearchForm = {
|
type NodeSearchForm = {
|
||||||
name?: string;
|
name?: string;
|
||||||
code?: string;
|
code?: string;
|
||||||
|
created_id?: number;
|
||||||
|
updated_id?: number;
|
||||||
|
created_time?: string[];
|
||||||
|
updated_time?: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
function buildNodeReplaceParams(u: NodeSearchForm): Record<string, unknown> {
|
function buildNodeReplaceParams(u: NodeSearchForm): Record<string, unknown> {
|
||||||
return {
|
return {
|
||||||
name: u.name,
|
name: u.name,
|
||||||
code: u.code,
|
code: u.code,
|
||||||
|
created_id: u.created_id,
|
||||||
|
updated_id: u.updated_id,
|
||||||
|
created_time:
|
||||||
|
Array.isArray(u.created_time) && u.created_time.length === 2 ? u.created_time : undefined,
|
||||||
|
updated_time:
|
||||||
|
Array.isArray(u.updated_time) && u.updated_time.length === 2 ? u.updated_time : undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const searchForm = ref<NodeSearchForm>({
|
const searchForm = ref<NodeSearchForm>({
|
||||||
name: undefined,
|
name: undefined,
|
||||||
code: undefined,
|
code: undefined,
|
||||||
|
created_id: undefined,
|
||||||
|
updated_id: undefined,
|
||||||
|
created_time: undefined,
|
||||||
|
updated_time: undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
const showSearchBar = ref(true);
|
const showSearchBar = ref(true);
|
||||||
@@ -512,6 +531,10 @@ async function onResetSearch() {
|
|||||||
searchForm.value = {
|
searchForm.value = {
|
||||||
name: undefined,
|
name: undefined,
|
||||||
code: undefined,
|
code: undefined,
|
||||||
|
created_id: undefined,
|
||||||
|
updated_id: undefined,
|
||||||
|
created_time: undefined,
|
||||||
|
updated_time: undefined,
|
||||||
};
|
};
|
||||||
await resetSearchParams();
|
await resetSearchParams();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,6 +81,10 @@ type WorkflowSearchForm = {
|
|||||||
name?: string;
|
name?: string;
|
||||||
code?: string;
|
code?: string;
|
||||||
status?: number;
|
status?: number;
|
||||||
|
created_id?: number;
|
||||||
|
updated_id?: number;
|
||||||
|
created_time?: string[];
|
||||||
|
updated_time?: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
function buildWorkflowReplaceParams(u: WorkflowSearchForm): Record<string, unknown> {
|
function buildWorkflowReplaceParams(u: WorkflowSearchForm): Record<string, unknown> {
|
||||||
@@ -88,6 +92,12 @@ function buildWorkflowReplaceParams(u: WorkflowSearchForm): Record<string, unkno
|
|||||||
name: u.name,
|
name: u.name,
|
||||||
code: u.code,
|
code: u.code,
|
||||||
status: u.status,
|
status: u.status,
|
||||||
|
created_id: u.created_id,
|
||||||
|
updated_id: u.updated_id,
|
||||||
|
created_time:
|
||||||
|
Array.isArray(u.created_time) && u.created_time.length === 2 ? u.created_time : undefined,
|
||||||
|
updated_time:
|
||||||
|
Array.isArray(u.updated_time) && u.updated_time.length === 2 ? u.updated_time : undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,6 +105,10 @@ const searchForm = ref<WorkflowSearchForm>({
|
|||||||
name: undefined,
|
name: undefined,
|
||||||
code: undefined,
|
code: undefined,
|
||||||
status: undefined,
|
status: undefined,
|
||||||
|
created_id: undefined,
|
||||||
|
updated_id: undefined,
|
||||||
|
created_time: undefined,
|
||||||
|
updated_time: undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
const showSearchBar = ref(true);
|
const showSearchBar = ref(true);
|
||||||
@@ -284,6 +298,7 @@ const {
|
|||||||
prop: "created_time",
|
prop: "created_time",
|
||||||
label: "创建时间",
|
label: "创建时间",
|
||||||
minWidth: 180,
|
minWidth: 180,
|
||||||
|
sortable: true,
|
||||||
showOverflowTooltip: true,
|
showOverflowTooltip: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -309,6 +324,10 @@ async function onResetSearch() {
|
|||||||
name: undefined,
|
name: undefined,
|
||||||
code: undefined,
|
code: undefined,
|
||||||
status: undefined,
|
status: undefined,
|
||||||
|
created_id: undefined,
|
||||||
|
updated_id: undefined,
|
||||||
|
created_time: undefined,
|
||||||
|
updated_time: undefined,
|
||||||
};
|
};
|
||||||
await resetSearchParams();
|
await resetSearchParams();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -190,6 +190,10 @@ type NodeTypeSearchForm = {
|
|||||||
name?: string;
|
name?: string;
|
||||||
code?: string;
|
code?: string;
|
||||||
category?: string;
|
category?: string;
|
||||||
|
created_id?: number;
|
||||||
|
updated_id?: number;
|
||||||
|
created_time?: string[];
|
||||||
|
updated_time?: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
function buildNodeTypeReplaceParams(u: NodeTypeSearchForm): Record<string, unknown> {
|
function buildNodeTypeReplaceParams(u: NodeTypeSearchForm): Record<string, unknown> {
|
||||||
@@ -197,6 +201,12 @@ function buildNodeTypeReplaceParams(u: NodeTypeSearchForm): Record<string, unkno
|
|||||||
name: u.name,
|
name: u.name,
|
||||||
code: u.code,
|
code: u.code,
|
||||||
category: u.category,
|
category: u.category,
|
||||||
|
created_id: u.created_id,
|
||||||
|
updated_id: u.updated_id,
|
||||||
|
created_time:
|
||||||
|
Array.isArray(u.created_time) && u.created_time.length === 2 ? u.created_time : undefined,
|
||||||
|
updated_time:
|
||||||
|
Array.isArray(u.updated_time) && u.updated_time.length === 2 ? u.updated_time : undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -204,6 +214,10 @@ const searchForm = ref<NodeTypeSearchForm>({
|
|||||||
name: undefined,
|
name: undefined,
|
||||||
code: undefined,
|
code: undefined,
|
||||||
category: undefined,
|
category: undefined,
|
||||||
|
created_id: undefined,
|
||||||
|
updated_id: undefined,
|
||||||
|
created_time: undefined,
|
||||||
|
updated_time: undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
const showSearchBar = ref(true);
|
const showSearchBar = ref(true);
|
||||||
@@ -419,6 +433,7 @@ const {
|
|||||||
prop: "created_time",
|
prop: "created_time",
|
||||||
label: "创建时间",
|
label: "创建时间",
|
||||||
minWidth: 170,
|
minWidth: 170,
|
||||||
|
sortable: true,
|
||||||
showOverflowTooltip: true,
|
showOverflowTooltip: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -444,6 +459,10 @@ async function onResetSearch() {
|
|||||||
name: undefined,
|
name: undefined,
|
||||||
code: undefined,
|
code: undefined,
|
||||||
category: undefined,
|
category: undefined,
|
||||||
|
created_id: undefined,
|
||||||
|
updated_id: undefined,
|
||||||
|
created_time: undefined,
|
||||||
|
updated_time: undefined,
|
||||||
};
|
};
|
||||||
await resetSearchParams();
|
await resetSearchParams();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,6 +78,14 @@ export default ({ mode }: { mode: string }) => {
|
|||||||
}
|
}
|
||||||
: {},
|
: {},
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
|
onwarn(warning, warn) {
|
||||||
|
// @vueuse/core 14.x 的 /* #__PURE__ */ 注释位置 Rollup 无法解析,
|
||||||
|
// Rollup 会自动移除这些注释(见构建日志 "The comment will be removed to avoid issues")
|
||||||
|
if (warning.message?.includes("@vueuse/core") && warning.message?.includes("#__PURE__")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
warn(warning);
|
||||||
|
},
|
||||||
output: {
|
output: {
|
||||||
manualChunks(id) {
|
manualChunks(id) {
|
||||||
if (!id.includes("node_modules")) return;
|
if (!id.includes("node_modules")) return;
|
||||||
|
|||||||
Reference in New Issue
Block a user