chore: 整理项目配置与代码优化

1.  统一前端环境变量命名,新增通用项目标题配置
2.  修复后端接口参数绑定方式,将Depends改为Query适配FastAPI解析
3.  优化表格列排序、搜索过滤功能与页面布局样式
4.  重构路由与状态管理代码,移除循环依赖
5.  更新依赖版本与脚本命令,修复构建警告
6.  调整多语言文案与快捷入口配置
7.  修复通知中心样式与接口返回格式
This commit is contained in:
zhangtao
2026-07-26 23:33:17 +08:00
parent df00863b6c
commit 698e198280
78 changed files with 1142 additions and 1134 deletions
+4 -5
View File
@@ -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(
+40 -168
View File
@@ -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),
+12 -4
View File
@@ -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 %}
+38 -3
View File
@@ -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 %}
+90 -209
View File
@@ -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,
) )
+1 -1
View File
@@ -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",
+59 -126
View File
@@ -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);
/* 接口错误已由拦截器提示 */ /* 接口错误已由拦截器提示 */
} }
} }
+3
View File
@@ -1,5 +1,8 @@
# 【通用】环境变量 - 所有环境共享 # 【通用】环境变量 - 所有环境共享
# 项目名称
VITE_APP_TITLE = FastapiAdmin
# 应用版本号 # 应用版本号
VITE_VERSION = 3.0.0 VITE_VERSION = 3.0.0
-19
View File
@@ -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
+8 -10
View File
@@ -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",
+5 -31
View File
@@ -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;
} }
+18 -19
View File
@@ -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" },
}, },
], ],
}; };
+2 -2
View File
@@ -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);
}; };
+2 -2
View File
@@ -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",
+2 -2
View File
@@ -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": "请再次输入密码",
+6 -6
View File
@@ -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()));
+10 -69
View File
@@ -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 -4
View File
@@ -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";
/** 菜单处理(获取、过滤、壳层补全) */ /** 菜单处理(获取、过滤、壳层补全) */
+59
View File
@@ -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();
}
+7 -11
View File
@@ -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: [
+36 -23
View File
@@ -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" },
}, },
]; ];
+4 -3
View File
@@ -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) {
+11 -8
View File
@@ -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)
+3 -9
View File
@@ -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;
+3 -8
View File
@@ -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: "创建人",
@@ -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
View File
@@ -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);
@@ -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();
} }
+8
View File
@@ -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;