Merge pull request #455 from fastapiadmin/dev

Dev
This commit is contained in:
fastapiadmin
2026-06-18 01:08:28 +08:00
committed by GitHub
33 changed files with 286 additions and 237 deletions
+1
View File
@@ -79,3 +79,4 @@ frontend/app/*
.plan/* .plan/*
CLAUDE.md CLAUDE.md
AGENTS.md AGENTS.md
.codebuddy
@@ -23,7 +23,7 @@ class MenuModel(ModelMixin):
__tablename__: str = "platform_menu" __tablename__: str = "platform_menu"
__table_args__: dict[str, str] = {"comment": "平台菜单表"} __table_args__: dict[str, str] = {"comment": "平台菜单表"}
__loader_options__: list[str] = ["roles"] __loader_options__: list[str] = ["roles", "children"]
__permission_strategy__: PermissionFilterStrategy = PermissionFilterStrategy.ROLE_BASED __permission_strategy__: PermissionFilterStrategy = PermissionFilterStrategy.ROLE_BASED
name: Mapped[str] = mapped_column(String(50), nullable=False, comment="菜单名称") name: Mapped[str] = mapped_column(String(50), nullable=False, comment="菜单名称")
@@ -115,6 +115,7 @@ class MenuModel(ModelMixin):
back_populates="parent", back_populates="parent",
foreign_keys="MenuModel.parent_id", foreign_keys="MenuModel.parent_id",
order_by="MenuModel.order", order_by="MenuModel.order",
lazy="selectin",
) )
roles: Mapped[list["RoleModel"]] = relationship( roles: Mapped[list["RoleModel"]] = relationship(
secondary="sys_role_menus", back_populates="menus", lazy="selectin" secondary="sys_role_menus", back_populates="menus", lazy="selectin"
@@ -181,13 +181,22 @@ class MenuUpdateSchema(BaseModel):
return menu_request_validator(self) return menu_request_validator(self)
class MenuOutSchema(MenuCreateSchema, BaseSchema): class MenuDetailOutSchema(MenuCreateSchema, BaseSchema):
"""菜单响应模型""" """菜单详情响应模型(不含 children,用于详情和更新)"""
model_config = ConfigDict(from_attributes=True) model_config = ConfigDict(from_attributes=True)
parent_name: str | None = Field(default=None, max_length=50, description="父菜单名称") parent_name: str | None = Field(default=None, max_length=50, description="父菜单名称")
children: list["MenuOutSchema"] | None = Field(default=None, description="子菜单列表")
class MenuTreeOutSchema(MenuDetailOutSchema):
"""菜单树形响应模型(含 children,用于树形列表)"""
children: list["MenuTreeOutSchema"] | None = Field(default=None, description="子菜单列表")
# 兼容旧代码的别名(后续可逐步移除)
MenuOutSchema = MenuDetailOutSchema
class MenuQueryParam: class MenuQueryParam:
@@ -13,8 +13,9 @@ from app.utils.common_util import (
from .crud import MenuCRUD from .crud import MenuCRUD
from .schema import ( from .schema import (
MenuCreateSchema, MenuCreateSchema,
MenuOutSchema, MenuDetailOutSchema,
MenuQueryParam, MenuQueryParam,
MenuTreeOutSchema,
MenuUpdateSchema, MenuUpdateSchema,
) )
@@ -66,7 +67,7 @@ class MenuService:
raise CustomException(msg="子菜单终端须与父菜单一致(均为 pc 或均为 app)") raise CustomException(msg="子菜单终端须与父菜单一致(均为 pc 或均为 app)")
@classmethod @classmethod
async def get_menu_detail_service(cls, auth: AuthSchema, id: int) -> MenuOutSchema: async def get_menu_detail_service(cls, auth: AuthSchema, id: int) -> MenuDetailOutSchema:
""" """
获取菜单详情。 获取菜单详情。
@@ -75,16 +76,16 @@ class MenuService:
- id (int): 菜单ID。 - id (int): 菜单ID。
返回: 返回:
- dict: 菜单详情对象。 - MenuDetailOutSchema: 菜单详情对象。
""" """
menu = await MenuCRUD(auth).get(id=id) menu = await MenuCRUD(auth).get(id=id, preload=["roles"])
# 创建实例后再设置parent_name属性 if not menu:
menu_out = MenuOutSchema.model_validate(menu) raise CustomException(msg="菜单不存在")
if menu and menu.parent_id: menu_out = MenuDetailOutSchema.model_validate(menu)
if menu.parent_id:
parent = await MenuCRUD(auth).get(id=menu.parent_id) parent = await MenuCRUD(auth).get(id=menu.parent_id)
if parent: if parent:
menu_out.parent_name = parent.name menu_out.parent_name = parent.name
return menu_out return menu_out
@classmethod @classmethod
@@ -109,13 +110,13 @@ class MenuService:
menu_list = await MenuCRUD(auth).get_tree_list( menu_list = await MenuCRUD(auth).get_tree_list(
search=search.__dict__, order_by=order_by search=search.__dict__, order_by=order_by
) )
# 转换为字典列表 # 转换为字典列表(使用树形 Schema)
menu_dict_list = [MenuOutSchema.model_validate(menu).model_dump() for menu in menu_list] menu_dict_list = [MenuTreeOutSchema.model_validate(menu).model_dump() for menu in menu_list]
# 使用traversal_to_tree构建树形结构 # 使用traversal_to_tree构建树形结构
return traversal_to_tree(menu_dict_list) return traversal_to_tree(menu_dict_list)
@classmethod @classmethod
async def create_menu_service(cls, auth: AuthSchema, data: MenuCreateSchema) -> MenuOutSchema: async def create_menu_service(cls, auth: AuthSchema, data: MenuCreateSchema) -> MenuDetailOutSchema:
""" """
创建菜单。 创建菜单。
@@ -124,7 +125,7 @@ class MenuService:
- data (MenuCreateSchema): 创建参数对象。 - data (MenuCreateSchema): 创建参数对象。
返回: 返回:
- dict: 创建的菜单对象。 - MenuDetailOutSchema: 创建的菜单对象。
""" """
search: dict[str, Any] = {} search: dict[str, Any] = {}
if data.title is not None: if data.title is not None:
@@ -139,11 +140,10 @@ class MenuService:
await cls._validate_parent_child_client(auth, data.parent_id, data.client) await cls._validate_parent_child_client(auth, data.parent_id, data.client)
new_menu = await MenuCRUD(auth).create(data=data) new_menu = await MenuCRUD(auth).create(data=data)
new_menu_dict = MenuOutSchema.model_validate(new_menu) return MenuDetailOutSchema.model_validate(new_menu)
return new_menu_dict
@classmethod @classmethod
async def update_menu_service(cls, auth: AuthSchema, id: int, data: MenuUpdateSchema) -> MenuOutSchema: async def update_menu_service(cls, auth: AuthSchema, id: int, data: MenuUpdateSchema) -> MenuDetailOutSchema:
""" """
更新菜单。 更新菜单。
@@ -153,7 +153,7 @@ class MenuService:
- data (MenuUpdateSchema): 更新参数对象。 - data (MenuUpdateSchema): 更新参数对象。
返回: 返回:
- dict: 更新的菜单对象。 - MenuDetailOutSchema: 更新的菜单对象。
""" """
menu = await MenuCRUD(auth).get(id=id) menu = await MenuCRUD(auth).get(id=id)
if not menu: if not menu:
@@ -172,6 +172,7 @@ class MenuService:
parent_menu = await MenuCRUD(auth).get(id=data.parent_id) parent_menu = await MenuCRUD(auth).get(id=data.parent_id)
if not parent_menu: if not parent_menu:
raise CustomException(msg="更新失败,父级菜单不存在") raise CustomException(msg="更新失败,父级菜单不存在")
new_menu = await MenuCRUD(auth).update(id=id, data=data) new_menu = await MenuCRUD(auth).update(id=id, data=data)
if data.status is not None: if data.status is not None:
@@ -179,8 +180,12 @@ class MenuService:
auth=auth, data=BatchSetAvailable(ids=[id], status=data.status) auth=auth, data=BatchSetAvailable(ids=[id], status=data.status)
) )
new_menu_dict = MenuOutSchema.model_validate(new_menu) menu_out = MenuDetailOutSchema.model_validate(new_menu)
return new_menu_dict if menu_out.parent_id:
parent = await MenuCRUD(auth).get(id=menu_out.parent_id)
if parent:
menu_out.parent_name = parent.name
return menu_out
@classmethod @classmethod
async def delete_menu_service(cls, auth: AuthSchema, ids: list[int]) -> None: async def delete_menu_service(cls, auth: AuthSchema, ids: list[int]) -> None:
@@ -83,7 +83,7 @@ class TenantService:
"password": PwdUtil.set_password_hash(password=password), "password": PwdUtil.set_password_hash(password=password),
"name": f"{tenant_obj.name}管理员", "name": f"{tenant_obj.name}管理员",
"tenant_id": tenant_obj.id, "tenant_id": tenant_obj.id,
"status": "0", "status": 0,
"is_superuser": False, "is_superuser": False,
} }
try: try:
@@ -18,7 +18,7 @@ class DeptModel(ModelMixin, TenantMixin):
__tablename__: str = "sys_dept" __tablename__: str = "sys_dept"
__table_args__ = (UniqueConstraint("tenant_id", "code"), {"comment": "部门表"}) __table_args__ = (UniqueConstraint("tenant_id", "code"), {"comment": "部门表"})
__loader_options__: list[str] = [] __loader_options__: list[str] = ["children"]
__permission_strategy__: PermissionFilterStrategy = PermissionFilterStrategy.DEPT_BASED __permission_strategy__: PermissionFilterStrategy = PermissionFilterStrategy.DEPT_BASED
name: Mapped[str] = mapped_column(String(64), nullable=False, comment="部门名称") name: Mapped[str] = mapped_column(String(64), nullable=False, comment="部门名称")
@@ -46,13 +46,22 @@ class DeptUpdateSchema(DeptCreateSchema):
"""部门更新模型""" """部门更新模型"""
class DeptOutSchema(DeptCreateSchema, BaseSchema): class DeptDetailOutSchema(DeptCreateSchema, BaseSchema):
"""部门响应模型""" """部门详情响应模型(不含 children,用于详情和更新)"""
model_config = ConfigDict(from_attributes=True) model_config = ConfigDict(from_attributes=True)
parent_name: str | None = Field(default=None, max_length=64, description="父部门名称") parent_name: str | None = Field(default=None, max_length=64, description="父部门名称")
children: list["DeptOutSchema"] | None = Field(default=None, description="子部门列表")
class DeptTreeOutSchema(DeptDetailOutSchema):
"""部门树形响应模型(含 children,用于树形列表)"""
children: list["DeptTreeOutSchema"] | None = Field(default=None, description="子部门列表")
# 兼容旧代码的别名(后续可逐步移除)
DeptOutSchema = DeptDetailOutSchema
class DeptQueryParam: class DeptQueryParam:
@@ -10,8 +10,9 @@ from app.utils.common_util import (
from .crud import DeptCRUD from .crud import DeptCRUD
from .schema import ( from .schema import (
DeptCreateSchema, DeptCreateSchema,
DeptOutSchema, DeptDetailOutSchema,
DeptQueryParam, DeptQueryParam,
DeptTreeOutSchema,
DeptUpdateSchema, DeptUpdateSchema,
) )
@@ -22,7 +23,7 @@ class DeptService:
""" """
@classmethod @classmethod
async def get_dept_detail_service(cls, auth: AuthSchema, id: int) -> DeptOutSchema: async def get_dept_detail_service(cls, auth: AuthSchema, id: int) -> DeptDetailOutSchema:
""" """
获取部门详情。 获取部门详情。
@@ -31,20 +32,17 @@ class DeptService:
- id (int): 部门 ID。 - id (int): 部门 ID。
返回: 返回:
- dict: 部门详情对象。 - DeptDetailOutSchema: 部门详情对象。
""" """
dept = await DeptCRUD(auth).get(id=id) dept = await DeptCRUD(auth).get(id=id)
if not dept: if not dept:
raise CustomException(msg="部门不存在") raise CustomException(msg="部门不存在")
# 从列属性构建 dict,避免 Pydantic 访问 ORM 关系触发 async 惰性加载 dept_out = DeptDetailOutSchema.model_validate(dept)
dept_dict = {c.name: getattr(dept, c.name) for c in dept.__table__.columns}
dept_dict["children"] = None
dept_dict["parent_name"] = None
if dept.parent_id: if dept.parent_id:
parent = await DeptCRUD(auth).get(id=dept.parent_id) parent = await DeptCRUD(auth).get(id=dept.parent_id)
if parent: if parent:
dept_dict["parent_name"] = parent.name dept_out.parent_name = parent.name
return DeptOutSchema(**dept_dict) return dept_out
@classmethod @classmethod
async def get_dept_tree_service( async def get_dept_tree_service(
@@ -68,13 +66,13 @@ class DeptService:
dept_list = await DeptCRUD(auth).get_tree_list( dept_list = await DeptCRUD(auth).get_tree_list(
search=search.__dict__ if search else {}, order_by=order_by search=search.__dict__ if search else {}, order_by=order_by
) )
# 转换为字典列表,tree_list 已通过 selectin 预加载 children # 转换为字典列表(使用树形 Schema),tree_list 已通过 selectin 预加载 children
dept_dict_list = [DeptOutSchema.model_validate(dept).model_dump() for dept in dept_list] dept_dict_list = [DeptTreeOutSchema.model_validate(dept).model_dump() for dept in dept_list]
# 仅保留根节点,子树已在 model_dump 中递归序列化 # 仅保留根节点,子树已在 model_dump 中递归序列化
return [d for d in dept_dict_list if d.get("parent_id") is None] return [d for d in dept_dict_list if d.get("parent_id") is None]
@classmethod @classmethod
async def create_dept_service(cls, auth: AuthSchema, data: DeptCreateSchema) -> DeptOutSchema: async def create_dept_service(cls, auth: AuthSchema, data: DeptCreateSchema) -> DeptDetailOutSchema:
""" """
创建部门。 创建部门。
@@ -83,7 +81,7 @@ class DeptService:
- data (DeptCreateSchema): 部门创建对象。 - data (DeptCreateSchema): 部门创建对象。
返回: 返回:
- dict: 新创建的部门对象。 - DeptDetailOutSchema: 新创建的部门对象。
异常: 异常:
- CustomException: 当部门已存在时抛出。 - CustomException: 当部门已存在时抛出。
@@ -100,10 +98,10 @@ class DeptService:
await TenantService.check_quota_service(auth, auth.tenant_id, "dept") await TenantService.check_quota_service(auth, auth.tenant_id, "dept")
dept = await DeptCRUD(auth).create(data=data) dept = await DeptCRUD(auth).create(data=data)
return DeptOutSchema.model_validate(dept) return DeptDetailOutSchema.model_validate(dept)
@classmethod @classmethod
async def update_dept_service(cls, auth: AuthSchema, id: int, data: DeptUpdateSchema) -> DeptOutSchema: async def update_dept_service(cls, auth: AuthSchema, id: int, data: DeptUpdateSchema) -> DeptDetailOutSchema:
""" """
更新部门。 更新部门。
@@ -113,7 +111,7 @@ class DeptService:
- data (DeptUpdateSchema): 部门更新对象。 - data (DeptUpdateSchema): 部门更新对象。
返回: 返回:
- dict: 更新后的部门对象。 - DeptDetailOutSchema: 更新后的部门对象。
异常: 异常:
- CustomException: 当部门不存在或名称重复时抛出。 - CustomException: 当部门不存在或名称重复时抛出。
@@ -127,8 +125,14 @@ class DeptService:
exist_code = await DeptCRUD(auth).get(code=data.code) exist_code = await DeptCRUD(auth).get(code=data.code)
if exist_code and exist_code.id != id: if exist_code and exist_code.id != id:
raise CustomException(msg="更新失败,部门编码已存在") raise CustomException(msg="更新失败,部门编码已存在")
dept = await DeptCRUD(auth).update(id=id, data=data) dept = await DeptCRUD(auth).update(id=id, data=data)
return DeptOutSchema.model_validate(dept) dept_out = DeptDetailOutSchema.model_validate(dept)
if dept_out.parent_id:
parent = await DeptCRUD(auth).get(id=dept_out.parent_id)
if parent:
dept_out.parent_name = parent.name
return dept_out
@classmethod @classmethod
async def delete_dept_service(cls, auth: AuthSchema, ids: list[int]) -> None: async def delete_dept_service(cls, auth: AuthSchema, ids: list[int]) -> None:
@@ -109,7 +109,7 @@ class NoticeService:
offset=0, offset=0,
limit=10, limit=10,
order_by=[{"id": "asc"}], order_by=[{"id": "asc"}],
search={"status": "0"}, search={"status": 0},
out_schema=NoticeOutSchema, out_schema=NoticeOutSchema,
) )
@@ -198,7 +198,7 @@ class PositionService:
# 复制数据并转换状态 # 复制数据并转换状态
data = position_list.copy() data = position_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 "停用"
item["creator"] = ( item["creator"] = (
item.get("created_by", {}).get("name", "未知") item.get("created_by", {}).get("name", "未知")
if isinstance(item.get("created_by"), dict) if isinstance(item.get("created_by"), dict)
@@ -245,7 +245,7 @@ class RoleService:
# 处理数据 # 处理数据
data = role_list.copy() data = role_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 "停用"
item["data_scope"] = data_scope_map.get(item.get("data_scope", 1), "") item["data_scope"] = data_scope_map.get(item.get("data_scope", 1), "")
return ExcelUtil.export_list2excel(list_data=data, mapping_dict=mapping_dict) return ExcelUtil.export_list2excel(list_data=data, mapping_dict=mapping_dict)
@@ -234,12 +234,37 @@ class UserCreateSchema(CurrentUserUpdateSchema):
return value return value
class UserUpdateSchema(UserCreateSchema): class UserUpdateSchema(CurrentUserUpdateSchema):
"""更新""" """更新"""
model_config = ConfigDict(from_attributes=True) model_config = ConfigDict(from_attributes=True)
last_login: DateTimeStr | None = Field(default=None, description="最后登录时间") username: str | None = Field(default=None, max_length=32, description="用户名")
status: int | None = Field(default=None, ge=0, le=1, description="状态(0:正常 1:禁用)")
description: str | None = Field(default=None, max_length=255, description="备注")
dept_id: int | None = Field(default=None, description="部门ID")
role_ids: list[int] | None = Field(default=[], description="角色ID列表")
position_ids: list[int] | None = Field(default=[], description="岗位ID列表")
@field_validator("status")
@classmethod
def validate_status(cls, value: int | None):
"""校验状态:仅支持 0(正常)、1(禁用)"""
if value is not None and value not in {0, 1}:
raise ValueError("状态仅支持 0(正常) 或 1(禁用)")
return value
@field_validator("username")
@classmethod
def validate_username(cls, value: str | None):
"""校验账号:字母开头,2-32 位"""
if not value:
return value
v = value.strip()
import re
if not re.match(r"^[A-Za-z][A-Za-z0-9_.-]{1,31}$", v):
raise ValueError("账号需以字母开头,2-32 位,仅允许字母、数字、_ . -")
return v
class UserOutSchema(UserUpdateSchema, BaseSchema, UserBySchema, TenantBySchema): class UserOutSchema(UserUpdateSchema, BaseSchema, UserBySchema, TenantBySchema):
@@ -199,12 +199,8 @@ class UserService:
if dept.status == 1: if dept.status == 1:
raise CustomException(msg="部门已被禁用") raise CustomException(msg="部门已被禁用")
# 更新用户 - 排除不应被修改的字段, 更新不更新密码 # 更新用户
user_dict = data.model_dump( new_user = await UserCRUD(auth).update(id=id, data=data)
exclude_unset=True,
exclude={"role_ids", "position_ids", "last_login", "password"},
)
new_user = await UserCRUD(auth).update(id=id, data=user_dict)
# 更新角色和岗位 # 更新角色和岗位
if data.role_ids and len(data.role_ids) > 0: if data.role_ids and len(data.role_ids) > 0:
@@ -293,7 +289,7 @@ class UserService:
if auth.user and auth.user.is_superuser: if auth.user and auth.user.is_superuser:
# 使用树形结构查询,预加载children关系(含 type=3 按钮,供前端权限列表使用) # 使用树形结构查询,预加载children关系(含 type=3 按钮,供前端权限列表使用)
menu_all = await MenuCRUD(auth).get_tree_list( menu_all = await MenuCRUD(auth).get_tree_list(
search={"type": ("in", [1, 2, 3, 4]), "status": "0", **_pc_only}, search={"type": ("in", [1, 2, 3, 4]), "status": 0, **_pc_only},
order_by=[{"order": "asc"}], order_by=[{"order": "asc"}],
) )
menus = [MenuOutSchema.model_validate(menu) for menu in menu_all] menus = [MenuOutSchema.model_validate(menu) for menu in menu_all]
@@ -703,7 +699,7 @@ class UserService:
# creator = {'id': 1, 'name': '管理员', 'username': 'admin'} # creator = {'id': 1, 'name': '管理员', 'username': 'admin'}
data = user_list.copy() data = user_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 "停用"
gender = item.get("gender") gender = item.get("gender")
item["gender"] = "男" if gender == "1" else ("女" if gender == "2" else "未知") item["gender"] = "男" if gender == "1" else ("女" if gender == "2" else "未知")
item["is_superuser"] = "是" if item.get("is_superuser") else "否" item["is_superuser"] = "是" if item.get("is_superuser") else "否"
+1 -1
View File
@@ -233,9 +233,9 @@ class Settings(BaseSettings):
# ================================================= # # ================================================= #
# ******************* AI大模型配置 ****************** # # ******************* AI大模型配置 ****************** #
# ================================================= # # ================================================= #
OPENAI_BASE_URL: str = ""
OPENAI_API_KEY: str = "" OPENAI_API_KEY: str = ""
OPENAI_MODEL: str = "" OPENAI_MODEL: str = ""
OPENAI_BASE_URL: str = "" # API Base URL,如 https://api.minimax.chat/v1
# ================================================= # # ================================================= #
# ******************* ChromaDB配置 ****************** # # ******************* ChromaDB配置 ****************** #
+2 -1
View File
@@ -359,7 +359,8 @@ class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]):
""" """
try: try:
obj_dict = data.model_dump(exclude_unset=True, exclude={"id"}) obj_dict = data.model_dump(exclude_unset=True, exclude={"id"})
obj = await self._get_one(self.db, id=id, preload=[]) model_defaults = getattr(self.model, "__loader_options__", [])
obj = await self._get_one(self.db, id=id, preload=model_defaults)
if not obj: if not obj:
raise CustomException(msg="更新对象不存在") raise CustomException(msg="更新对象不存在")
@@ -216,8 +216,8 @@ const searchBarRef = ref<InstanceType<typeof FaSearchBarWithAudit> | null>(null)
const searchBarRules: Record<string, unknown> = {}; const searchBarRules: Record<string, unknown> = {};
const statusOptions = ref([ const statusOptions = ref([
{ label: "启用", value: "0" }, { label: "启用", value: 0 },
{ label: "停用", value: "1" }, { label: "停用", value: 1 },
]); ]);
/** 业务搜索项(审计四字段由 FaSearchBarWithAudit 自动追加) */ /** 业务搜索项(审计四字段由 FaSearchBarWithAudit 自动追加) */
@@ -504,7 +504,7 @@ const dialogFormItems = computed<FormItem[]>(() => [
{% set comment = column_comment[:parentheseIndex] if parentheseIndex != -1 else column_comment %} {% set comment = column_comment[:parentheseIndex] if parentheseIndex != -1 else column_comment %}
{% if column.column_name not in ['uuid', 'created_time', 'updated_time', 'created_id', 'updated_id', 'is_deleted', 'deleted_time', 'deleted_id', 'tenant_id'] and column.column_name != pk_column_name %} {% if column.column_name not in ['uuid', 'created_time', 'updated_time', 'created_id', 'updated_id', 'is_deleted', 'deleted_time', 'deleted_id', 'tenant_id'] and column.column_name != pk_column_name %}
{% if column.column_name == "status" %} {% if column.column_name == "status" %}
{ key: "status", label: "状态", type: "radio", props: { options: [{ label: "启用", value: "0" }, { label: "停用", value: "1" }] } }, { key: "status", label: "状态", type: "radio", props: { options: [{ label: "启用", value: 0 }, { label: "停用", value: 1 }] } },
{% elif column.column_name == "description" %} {% elif column.column_name == "description" %}
{ key: "description", label: "描述", type: "textarea", placeholder: "请输入描述" }, { key: "description", label: "描述", type: "textarea", placeholder: "请输入描述" },
{% elif column.python_type == "int" or column.python_type == "float" or column.python_type == "Decimal" %} {% elif column.python_type == "int" or column.python_type == "float" or column.python_type == "Decimal" %}
-15
View File
@@ -44,23 +44,8 @@ REDIS_DB_NAME = 1
LOGGER_LEVEL = "DEBUG" # 日志级别 LOGGER_LEVEL = "DEBUG" # 日志级别
# 大模型配置 # 大模型配置
OPENAI_BASE_URL = "https://api.deepseek.com"
OPENAI_API_KEY = "your_api_key" OPENAI_API_KEY = "your_api_key"
OPENAI_MODEL = "deepseek-v4-flash" OPENAI_MODEL = "deepseek-v4-flash"
# IP 归属地查询(登录时对外请求第三方 API,生产建议关闭) # IP 归属地查询(登录时对外请求第三方 API,生产建议关闭)
IP_LOCATION_ENABLE=false IP_LOCATION_ENABLE=false
# 支付配置
PAYMENT_GATEWAY = "mock" # alipay / wxpay / mock(开发环境默认 mock)
PAYMENT_ALIPAY_APP_ID = ""
PAYMENT_ALIPAY_PRIVATE_KEY = ""
PAYMENT_ALIPAY_PUBLIC_KEY = ""
PAYMENT_ALIPAY_SANDBOX = True
PAYMENT_WXPAY_APP_ID = ""
PAYMENT_WXPAY_MCH_ID = ""
PAYMENT_WXPAY_API_KEY = ""
SITE_URL = "http://localhost:8001"
# 安全配置(生产环境务必覆盖)
SECRET_KEY = "your-secret-key-change-in-production"
ALLOW_ORIGINS = ["http://localhost:5173", "http://localhost:4173", "http://127.0.0.1:5173"]
-17
View File
@@ -44,25 +44,8 @@ REDIS_DB_NAME = 1
LOGGER_LEVEL = 'INFO' LOGGER_LEVEL = 'INFO'
# 大模型配置 # 大模型配置
OPENAI_BASE_URL = "https://api.deepseek.com"
OPENAI_API_KEY = "your_api_key" OPENAI_API_KEY = "your_api_key"
OPENAI_MODEL = "deepseek-v4-flash" OPENAI_MODEL = "deepseek-v4-flash"
# IP 归属地查询(登录时对外请求第三方 API,生产建议关闭) # IP 归属地查询(登录时对外请求第三方 API,生产建议关闭)
IP_LOCATION_ENABLE=false IP_LOCATION_ENABLE=false
# 支付配置
PAYMENT_GATEWAY = "alipay" # alipay / wxpay / mock(生产环境使用真实支付)
PAYMENT_ALIPAY_APP_ID = ""
PAYMENT_ALIPAY_PRIVATE_KEY = ""
PAYMENT_ALIPAY_PUBLIC_KEY = ""
PAYMENT_ALIPAY_SANDBOX = False
PAYMENT_WXPAY_APP_ID = ""
PAYMENT_WXPAY_MCH_ID = ""
PAYMENT_WXPAY_API_KEY = ""
SITE_URL = "https://your-domain.com"
# CORS 配置(生产环境需要配置具体域名)
ALLOW_ORIGINS = ["https://your-domain.com", "https://admin.your-domain.com"]
# 安全配置(生产环境务必修改)
SECRET_KEY = "your-secret-key-change-in-production"
+116
View File
@@ -160,3 +160,119 @@ src/
- **格式化与校验**:`pnpm lint` - **格式化与校验**:`pnpm lint`
- **提交**:husky、lint-staged、commitlint;可使用 **`pnpm commit`**(Commitizen / cz-git) - **提交**:husky、lint-staged、commitlint;可使用 **`pnpm commit`**(Commitizen / cz-git)
/\*\*
- 浅蓝色系主题 — #1975FC
-
- 来源:花瓣网 B端淡蓝色配色参考
- 主色 #1975FC — 明快透亮、B端专业感
-
- 【启用方式】在本文件中取消注释对应主题模式的变量块,
- 并在 src/styles/index.scss 中 @use "./core/theme-blue";
- 同时在 src/config/setting.ts 中将 themeColor 改为 "#1975FC"。
-
- 【色阶 palette】
- #f8fcff 最浅底色(页面背景)
- #f0f5ff 浅蓝悬停态
- #e5f2ff 选中态 / tag 背景
- #c5ddff 浅蓝边框 / 分割线点缀
- #82b8ff 中浅蓝(图标辅助色)
- #1975FC ★ 主色
- #115ac8 深蓝(hover 按下态)
-
- 【功能色】
- success: #16a34a
- warning: #d97706
- danger: #dc2626
- error: #fa896b
- info: #8b92a0
-
- ================================================================
\*/
// ================================================================
// 亮色主题 — 浅蓝色系
// ================================================================
// :root {
// /_ ---- 侧边栏 ---- _/
// --sidebar-bg-color: #ffffff;
// --sidebar-border-color: #edf0f2;
// --sidebar-text-color: #485260;
// --sidebar-text-hover-color: #1975fc;
// --sidebar-text-active-color: #1975fc;
// --sidebar-bg-hover-color: #f0f5ff;
// --sidebar-bg-active-color: #e5f2ff;
// --sidebar-logo-color: #121926;
// --sidebar-section-title-color: #8b92a0;
//
// /_ ---- 页面 ---- _/
// --main-bg-color: #f4f6f9;
// --header-bg-color: #ffffff;
// --header-border-color: #edf0f2;
//
// /_ ---- 卡片 ---- _/
// --card-bg-color: #ffffff;
// --card-border-color: #edf0f2;
//
// /_ ---- 表格 ---- _/
// --table-header-bg: #f8fafc;
// --table-header-color: #8b92a0;
// --table-row-hover-bg: #f8fafc;
// --table-border-color: #edf0f2;
//
// /_ ---- 文字 ---- _/
// --text-primary: #121926;
// --text-regular: #334155;
// --text-secondary: #8b92a0;
//
// /_ ---- 标签 ---- _/
// --tag-bg-color: #e5f2ff;
// --tag-text-color: #1975fc;
//
// /_ ---- 按钮 —— 由 Element Plus 通过 primary 色阶自动生成 ---- _/
// /_ ---- 进度条 ---- _/
// --nprogress-color: #1975fc;
//
// /_ ---- 边框模式 ---- _/
// --fa-card-border: #edf0f2;
// --default-box-color: #ffffff;
// --default-border: #edf0f2;
// --custom-radius: 0.75rem;
// }
// ================================================================
// 暗色主题 — 浅蓝色系(可选)
// ================================================================
// html.dark {
// --sidebar-bg-color: #0c1031;
// --sidebar-border-color: #1a1e3d;
// --sidebar-text-color: #a8adcc;
// --sidebar-text-hover-color: #5c9fff;
// --sidebar-text-active-color: #1975fc;
// --sidebar-bg-hover-color: rgba(25, 117, 252, 0.15);
// --sidebar-bg-active-color: rgba(25, 117, 252, 0.25);
// --sidebar-logo-color: #ffffff;
// --sidebar-section-title-color: #6b7094;
//
// --main-bg-color: #0f1420;
// --header-bg-color: #151a2d;
// --header-border-color: #1a1e3d;
//
// --card-bg-color: #151a2d;
// --card-border-color: #1a1e3d;
//
// --table-header-bg: #1a1e3d;
// --table-header-color: #6b7094;
// --table-row-hover-bg: #1a1e3d;
// --table-border-color: #1a1e3d;
//
// --text-primary: #e5e7eb;
// --text-regular: #a8adcc;
// --text-secondary: #6b7094;
//
// --tag-bg-color: rgba(25, 117, 252, 0.2);
// --tag-text-color: #5c9fff;
// }
@@ -1,114 +0,0 @@
/**
* 浅蓝色系主题 — #1975FC
*
* 来源:花瓣网 B端淡蓝色配色参考
* 主色 #1975FC — 明快透亮、B端专业感
*
* 【启用方式】在本文件中取消注释对应主题模式的变量块,
* 并在 src/styles/index.scss 中 @use "./core/theme-blue";
* 同时在 src/config/setting.ts 中将 themeColor 改为 "#1975FC"。
*
* 【色阶 palette】
* #f8fcff 最浅底色(页面背景)
* #f0f5ff 浅蓝悬停态
* #e5f2ff 选中态 / tag 背景
* #c5ddff 浅蓝边框 / 分割线点缀
* #82b8ff 中浅蓝(图标辅助色)
* #1975FC ★ 主色
* #115ac8 深蓝(hover 按下态)
*
* 【功能色】
* success: #16a34a
* warning: #d97706
* danger: #dc2626
* error: #fa896b
* info: #8b92a0
*
* ================================================================
*/
// ================================================================
// 亮色主题 — 浅蓝色系
// ================================================================
// :root {
// /* ---- 侧边栏 ---- */
// --sidebar-bg-color: #ffffff;
// --sidebar-border-color: #edf0f2;
// --sidebar-text-color: #485260;
// --sidebar-text-hover-color: #1975fc;
// --sidebar-text-active-color: #1975fc;
// --sidebar-bg-hover-color: #f0f5ff;
// --sidebar-bg-active-color: #e5f2ff;
// --sidebar-logo-color: #121926;
// --sidebar-section-title-color: #8b92a0;
//
// /* ---- 页面 ---- */
// --main-bg-color: #f4f6f9;
// --header-bg-color: #ffffff;
// --header-border-color: #edf0f2;
//
// /* ---- 卡片 ---- */
// --card-bg-color: #ffffff;
// --card-border-color: #edf0f2;
//
// /* ---- 表格 ---- */
// --table-header-bg: #f8fafc;
// --table-header-color: #8b92a0;
// --table-row-hover-bg: #f8fafc;
// --table-border-color: #edf0f2;
//
// /* ---- 文字 ---- */
// --text-primary: #121926;
// --text-regular: #334155;
// --text-secondary: #8b92a0;
//
// /* ---- 标签 ---- */
// --tag-bg-color: #e5f2ff;
// --tag-text-color: #1975fc;
//
// /* ---- 按钮 —— 由 Element Plus 通过 primary 色阶自动生成 ---- */
// /* ---- 进度条 ---- */
// --nprogress-color: #1975fc;
//
// /* ---- 边框模式 ---- */
// --fa-card-border: #edf0f2;
// --default-box-color: #ffffff;
// --default-border: #edf0f2;
// --custom-radius: 0.75rem;
// }
// ================================================================
// 暗色主题 — 浅蓝色系(可选)
// ================================================================
// html.dark {
// --sidebar-bg-color: #0c1031;
// --sidebar-border-color: #1a1e3d;
// --sidebar-text-color: #a8adcc;
// --sidebar-text-hover-color: #5c9fff;
// --sidebar-text-active-color: #1975fc;
// --sidebar-bg-hover-color: rgba(25, 117, 252, 0.15);
// --sidebar-bg-active-color: rgba(25, 117, 252, 0.25);
// --sidebar-logo-color: #ffffff;
// --sidebar-section-title-color: #6b7094;
//
// --main-bg-color: #0f1420;
// --header-bg-color: #151a2d;
// --header-border-color: #1a1e3d;
//
// --card-bg-color: #151a2d;
// --card-border-color: #1a1e3d;
//
// --table-header-bg: #1a1e3d;
// --table-header-color: #6b7094;
// --table-row-hover-bg: #1a1e3d;
// --table-border-color: #1a1e3d;
//
// --text-primary: #e5e7eb;
// --text-regular: #a8adcc;
// --text-secondary: #6b7094;
//
// --tag-bg-color: rgba(25, 117, 252, 0.2);
// --tag-text-color: #5c9fff;
// }
@@ -1,5 +1,5 @@
<template> <template>
<div class="fa-card p-5 pb-3 h-53 max-sm:h-48 flex flex-col"> <div class="fa-card p-5 pb-3 h-53 max-sm:h-53 flex flex-col">
<div class="fa-card-header"> <div class="fa-card-header">
<div class="title"> <div class="title">
<h4> <h4>
@@ -458,8 +458,8 @@ const searchBarRef = ref<InstanceType<typeof FaSearchBar> | null>(null);
const searchBarRules: Record<string, unknown> = {}; const searchBarRules: Record<string, unknown> = {};
const statusOptions = ref([ const statusOptions = ref([
{ label: "启用", value: "0" }, { label: "启用", value: 0 },
{ label: "停用", value: "1" }, { label: "停用", value: 1 },
]); ]);
const menuSearchItems = computed<SearchFormItem[]>(() => [ const menuSearchItems = computed<SearchFormItem[]>(() => [
@@ -107,7 +107,13 @@
<!-- 订单详情弹窗 --> <!-- 订单详情弹窗 -->
<FaDialog v-model="detailVisible" title="订单详情" width="560px" :show-footer="false"> <FaDialog v-model="detailVisible" title="订单详情" width="560px" :show-footer="false">
<FaDescriptions v-if="orderDetail" :column="2" border :items="detailItems" /> <FaDescriptions
v-if="orderDetail"
:column="2"
border
:items="detailItems"
:data="orderDetail"
/>
</FaDialog> </FaDialog>
<!-- 驳回退款弹窗 --> <!-- 驳回退款弹窗 -->
@@ -345,7 +351,13 @@ const orderDetail = ref<OrderTable | null>(null);
async function showOrderDetail(row: OrderTable) { async function showOrderDetail(row: OrderTable) {
try { try {
const res = await OrderAPI.detailOrder(row.id); const res = await OrderAPI.detailOrder(row.id);
orderDetail.value = res.data.data as OrderTable; const data = res.data.data as OrderTable;
if (data) {
data.amount = data.amount / 100;
data.order_type = orderTypeLabel(data.order_type);
data.pay_method = data.pay_method ? payMethodLabel(data.pay_method) : "";
}
orderDetail.value = data;
detailVisible.value = true; detailVisible.value = true;
} catch { } catch {
/* ignore */ /* ignore */
@@ -362,16 +374,30 @@ async function cancelOrder(orderId: number) {
} }
} }
const detailItems = computed(() => { const detailItems = computed<
import("@/components/others/fa-descriptions/index.vue").DescriptionsItem[]
>(() => {
if (!orderDetail.value) return []; if (!orderDetail.value) return [];
return [ return [
{ label: "订单ID", prop: "id" }, { label: "订单ID", prop: "id" },
{ label: "订单号", prop: "order_no" }, { label: "订单号", prop: "order_no" },
{ label: "租户ID", prop: "tenant_id" }, { label: "租户ID", prop: "tenant_id" },
{ label: "套餐ID", prop: "package_id" }, { label: "套餐ID", prop: "package_id" },
{ label: "订单类型", prop: "order_type", slot: "order_type" }, { label: "订单类型", prop: "order_type" },
{ label: "金额", prop: "amount", slot: "amount" }, { label: "金额", prop: "amount" },
{ label: "状态", prop: "status", slot: "status" }, {
label: "状态",
prop: "status",
tag: {
map: {
"0": { type: "warning", text: "待支付" },
"1": { type: "success", text: "已支付" },
"2": { type: "info", text: "已取消" },
"3": { type: "warning", text: "已退款" },
"4": { type: "danger", text: "已过期" },
},
},
},
{ label: "支付方式", prop: "pay_method" }, { label: "支付方式", prop: "pay_method" },
{ label: "支付时间", prop: "pay_time" }, { label: "支付时间", prop: "pay_time" },
{ label: "过期时间", prop: "expire_time" }, { label: "过期时间", prop: "expire_time" },
@@ -245,8 +245,8 @@ const searchBarRef = ref<InstanceType<typeof FaSearchBar> | null>(null);
const searchBarRules: Record<string, unknown> = {}; const searchBarRules: Record<string, unknown> = {};
const statusOptions = ref([ const statusOptions = ref([
{ label: "正常", value: "0" }, { label: "正常", value: 0 },
{ label: "禁用", value: "1" }, { label: "禁用", value: 1 },
]); ]);
const pkgSearchItems = computed<SearchFormItem[]>(() => [ const pkgSearchItems = computed<SearchFormItem[]>(() => [
@@ -291,8 +291,8 @@ const categoryOptions = ref([
]); ]);
const statusOptions = ref([ const statusOptions = ref([
{ label: "启用", value: "0" }, { label: "启用", value: 0 },
{ label: "停用", value: "1" }, { label: "停用", value: 1 },
]); ]);
const pluginSearchItems = computed<SearchFormItem[]>(() => [ const pluginSearchItems = computed<SearchFormItem[]>(() => [
@@ -209,8 +209,8 @@ const searchBarRef = ref<InstanceType<typeof FaSearchBar> | null>(null);
const searchBarRules: Record<string, unknown> = {}; const searchBarRules: Record<string, unknown> = {};
const statusOptions = ref([ const statusOptions = ref([
{ label: "启用", value: "0" }, { label: "启用", value: 0 },
{ label: "停用", value: "1" }, { label: "停用", value: 1 },
]); ]);
const deptSearchItems = computed<SearchFormItem[]>(() => [ const deptSearchItems = computed<SearchFormItem[]>(() => [
@@ -322,8 +322,8 @@ const searchBarRef = ref<InstanceType<typeof FaSearchBar> | null>(null);
const searchBarRules: Record<string, unknown> = {}; const searchBarRules: Record<string, unknown> = {};
const statusOptions = ref([ const statusOptions = ref([
{ label: "启用", value: "0" }, { label: "启用", value: 0 },
{ label: "停用", value: "1" }, { label: "停用", value: 1 },
]); ]);
const dictDataSearchItems = computed<SearchFormItem[]>(() => [ const dictDataSearchItems = computed<SearchFormItem[]>(() => [
@@ -177,8 +177,8 @@ const searchBarRef = ref<InstanceType<typeof FaSearchBar> | null>(null);
const searchBarRules: Record<string, unknown> = {}; const searchBarRules: Record<string, unknown> = {};
const statusOptions = ref([ const statusOptions = ref([
{ label: "启用", value: "0" }, { label: "启用", value: 0 },
{ label: "停用", value: "1" }, { label: "停用", value: 1 },
]); ]);
const dictTypeSearchItems = computed<SearchFormItem[]>(() => [ const dictTypeSearchItems = computed<SearchFormItem[]>(() => [
@@ -213,8 +213,8 @@ const searchBarRef = ref<InstanceType<typeof FaSearchBar> | null>(null);
const searchBarRules: Record<string, unknown> = {}; const searchBarRules: Record<string, unknown> = {};
const statusOptions = ref([ const statusOptions = ref([
{ label: "启用", value: "0" }, { label: "启用", value: 0 },
{ label: "停用", value: "1" }, { label: "停用", value: 1 },
]); ]);
const noticeTypeSearchOptions = computed(() => const noticeTypeSearchOptions = computed(() =>
@@ -310,8 +310,8 @@ const searchBarRef = ref<InstanceType<typeof FaSearchBar> | null>(null);
const searchBarRules: Record<string, unknown> = {}; const searchBarRules: Record<string, unknown> = {};
const statusOptions = ref([ const statusOptions = ref([
{ label: "启用", value: "0" }, { label: "启用", value: 0 },
{ label: "停用", value: "1" }, { label: "停用", value: 1 },
]); ]);
const positionSearchItems = computed<SearchFormItem[]>(() => [ const positionSearchItems = computed<SearchFormItem[]>(() => [
@@ -375,7 +375,7 @@ const userDetailItems: DescriptionsItem[] = [
label: "状态", label: "状态",
prop: "status", prop: "status",
tag: { tag: {
map: { "0": { type: "success", text: "启用" }, "1": { type: "danger", text: "停用" } }, map: { 0: { type: "success", text: "启用" }, 1: { type: "danger", text: "停用" } },
}, },
}, },
{ label: "上次登录时间", prop: "last_login" }, { label: "上次登录时间", prop: "last_login" },
@@ -437,8 +437,8 @@ const userDialogFormItems = computed<FormItem[]>(() => [
type: "radiogroup", type: "radiogroup",
props: { props: {
options: [ options: [
{ label: "启用", value: "0" }, { label: "启用", value: 0 },
{ label: "停用", value: "1" }, { label: "停用", value: 1 },
], ],
}, },
}, },
@@ -469,8 +469,8 @@ const searchBarRef = ref<InstanceType<typeof FaSearchBar> | null>(null);
const searchBarRules: Record<string, unknown> = {}; const searchBarRules: Record<string, unknown> = {};
const statusOptions = ref([ const statusOptions = ref([
{ label: "启用", value: "0" }, { label: "启用", value: 0 },
{ label: "停用", value: "1" }, { label: "停用", value: 1 },
]); ]);
const userSearchItems = computed<SearchFormItem[]>(() => [ const userSearchItems = computed<SearchFormItem[]>(() => [
@@ -13,6 +13,7 @@
:show-search="true" :show-search="true"
:disabled-search="false" :disabled-search="false"
:default-expanded="false" :default-expanded="false"
:button-left-limit="0"
@search="handleJobSearchBarSearch" @search="handleJobSearchBarSearch"
@reset="onJobResetSearch" @reset="onJobResetSearch"
/> />
@@ -13,6 +13,7 @@
:show-search="true" :show-search="true"
:disabled-search="false" :disabled-search="false"
:default-expanded="false" :default-expanded="false"
:button-left-limit="0"
@search="handleSearchBarSearch" @search="handleSearchBarSearch"
@reset="onResetSearch" @reset="onResetSearch"
/> />