Compare commits
3 Commits
0545d68018
...
6751a5c2b2
| Author | SHA1 | Date | |
|---|---|---|---|
| 6751a5c2b2 | |||
|
|
677d8a53b5 | ||
|
|
9586c7467a |
@ -44,7 +44,9 @@ async def create_permission(
|
||||
name=permission_data.name,
|
||||
code=permission_data.code,
|
||||
parentid=permission_data.parentid,
|
||||
route=permission_data.route,
|
||||
description=permission_data.description,
|
||||
sort=permission_data.sort,
|
||||
)
|
||||
|
||||
db.add(db_permission)
|
||||
@ -112,6 +114,7 @@ async def get_permission_tree(
|
||||
"parentid": perm.parentid,
|
||||
"route": perm.route,
|
||||
"description": perm.description,
|
||||
"sort": perm.sort or 0,
|
||||
"createtime": perm.createtime,
|
||||
"updatetime": perm.updatetime,
|
||||
"children": []
|
||||
@ -128,6 +131,16 @@ async def get_permission_tree(
|
||||
# 子节点,添加到父节点的 children
|
||||
perm_dict[parentid]["children"].append(perm_data)
|
||||
|
||||
# 递归排序:按 sort 字段升序排列
|
||||
def sort_tree(nodes):
|
||||
nodes.sort(key=lambda n: n.get("sort", 0))
|
||||
for node in nodes:
|
||||
if node.get("children"):
|
||||
sort_tree(node["children"])
|
||||
return nodes
|
||||
|
||||
tree = sort_tree(tree)
|
||||
|
||||
# 记录查询日志
|
||||
await create_operation_log(
|
||||
db, current_user.id, current_user.username, "search", "permission",
|
||||
@ -187,6 +200,7 @@ async def get_menu(
|
||||
"parentid": perm.parentid,
|
||||
"route": perm.route,
|
||||
"description": perm.description,
|
||||
"sort": perm.sort or 0,
|
||||
"createtime": perm.createtime,
|
||||
"updatetime": perm.updatetime,
|
||||
"children": []
|
||||
@ -203,6 +217,16 @@ async def get_menu(
|
||||
# 子节点,添加到父节点的 children
|
||||
perm_dict[parentid]["children"].append(perm_data)
|
||||
|
||||
# 递归排序:按 sort 字段升序排列
|
||||
def sort_tree(nodes):
|
||||
nodes.sort(key=lambda n: n.get("sort", 0))
|
||||
for node in nodes:
|
||||
if node.get("children"):
|
||||
sort_tree(node["children"])
|
||||
return nodes
|
||||
|
||||
tree = sort_tree(tree)
|
||||
|
||||
# 记录查询日志
|
||||
await create_operation_log(
|
||||
db, current_user.id, current_user.username, "search", "permission",
|
||||
@ -222,7 +246,7 @@ async def get_permission(
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""获取单个权限"""
|
||||
permission = await permission_crud.get(db, id=permission_id)
|
||||
permission = await permission_crud.get(db, id=permission_id, schema_to_select=PermissionResponse, return_as_model=True)
|
||||
if not permission:
|
||||
raise HTTPException(status_code=404, detail="权限不存在")
|
||||
|
||||
@ -271,7 +295,7 @@ async def update_permission(
|
||||
raise HTTPException(status_code=404, detail="权限不存在")
|
||||
# 记录操作日志
|
||||
changed = {}
|
||||
for field in ["name", "code", "parentid", "description"]:
|
||||
for field in ["name", "code", "parentid", "route", "sort", "description"]:
|
||||
old_val = getattr(old_permission, field, None)
|
||||
new_val = update_data.get(field)
|
||||
if old_val != new_val:
|
||||
@ -452,6 +476,7 @@ async def get_current_user_permissions(
|
||||
"parentid": perm.parentid,
|
||||
"route": perm.route,
|
||||
"description": perm.description,
|
||||
"sort": perm.sort or 0,
|
||||
"createtime": perm.createtime,
|
||||
"updatetime": perm.updatetime,
|
||||
"children": []
|
||||
@ -468,6 +493,16 @@ async def get_current_user_permissions(
|
||||
# 子节点,添加到父节点的 children
|
||||
perm_dict[parentid]["children"].append(perm_data)
|
||||
|
||||
# 递归排序:按 sort 字段升序排列
|
||||
def sort_tree(nodes):
|
||||
nodes.sort(key=lambda n: n.get("sort", 0))
|
||||
for node in nodes:
|
||||
if node.get("children"):
|
||||
sort_tree(node["children"])
|
||||
return nodes
|
||||
|
||||
menu_tree = sort_tree(menu_tree)
|
||||
|
||||
# 记录查询日志
|
||||
await create_operation_log(
|
||||
db, current_user.id, current_user.username, "search", "permission",
|
||||
|
||||
@ -1,452 +1,451 @@
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, text
|
||||
from models import Role, User, Permission, SystemSetting
|
||||
from utils.password import get_password_hash
|
||||
|
||||
|
||||
async def init_permissions(db: AsyncSession):
|
||||
"""初始化权限数据(树形结构)"""
|
||||
# 定义需要初始化的权限(树形结构)
|
||||
# parentid: 0 = 顶级模块, >0 = 子权限
|
||||
permission_tree = [
|
||||
# ========================================
|
||||
# 顶级模块 (DB ID: 1,2,3)
|
||||
# ========================================
|
||||
{"name": "权限管理", "code": "perm", "parentid": 0, "route": "/admin/permissions", "description": "权限管理模块"},
|
||||
{"name": "系统设置", "code": "system", "parentid": 0, "route": "/admin/settings", "description": "系统设置模块"},
|
||||
{"name": "日志管理", "code": "log", "parentid": 0, "route": "/admin/logs", "description": "日志管理模块"},
|
||||
|
||||
# ========================================
|
||||
# 权限管理 -> 二级子模块 (parentid=1, DB ID: 4,5,6)
|
||||
# ========================================
|
||||
{"name": "用户管理", "code": "perm:user", "parentid": 1, "route": "/admin/users", "description": "用户管理子模块"},
|
||||
{"name": "角色管理", "code": "perm:role", "parentid": 1, "route": "/admin/roles", "description": "角色管理子模块"},
|
||||
{"name": "菜单管理", "code": "perm:menu", "parentid": 1, "route": "/admin/menu", "description": "菜单管理子模块"},
|
||||
|
||||
# ========================================
|
||||
# 用户管理操作 (parentid=4, DB ID: 7-14)
|
||||
# ========================================
|
||||
{"name": "查看列表", "code": "perm:user:list", "parentid": 4, "description": "查看用户列表"},
|
||||
{"name": "查看详情", "code": "perm:user:detail", "parentid": 4, "description": "查看用户详情"},
|
||||
{"name": "搜索", "code": "perm:user:search", "parentid": 4, "description": "搜索用户"},
|
||||
{"name": "创建", "code": "perm:user:create", "parentid": 4, "description": "创建新用户"},
|
||||
{"name": "编辑", "code": "perm:user:edit", "parentid": 4, "description": "编辑用户信息"},
|
||||
{"name": "删除", "code": "perm:user:delete", "parentid": 4, "description": "删除用户"},
|
||||
{"name": "导出", "code": "perm:user:export", "parentid": 4, "description": "导出用户数据"},
|
||||
{"name": "批量操作", "code": "perm:user:batch", "parentid": 4, "description": "批量操作用户"},
|
||||
|
||||
# ========================================
|
||||
# 角色管理操作 (parentid=5, DB ID: 15-22)
|
||||
# ========================================
|
||||
{"name": "查看列表", "code": "perm:role:list", "parentid": 5, "description": "查看角色列表"},
|
||||
{"name": "查看详情", "code": "perm:role:detail", "parentid": 5, "description": "查看角色详情"},
|
||||
{"name": "搜索", "code": "perm:role:search", "parentid": 5, "description": "搜索角色"},
|
||||
{"name": "创建", "code": "perm:role:create", "parentid": 5, "description": "创建新角色"},
|
||||
{"name": "编辑", "code": "perm:role:edit", "parentid": 5, "description": "编辑角色信息"},
|
||||
{"name": "删除", "code": "perm:role:delete", "parentid": 5, "description": "删除角色"},
|
||||
{"name": "导出", "code": "perm:role:export", "parentid": 5, "description": "导出角色数据"},
|
||||
{"name": "分配权限", "code": "perm:role:assign", "parentid": 5, "description": "为角色分配权限"},
|
||||
|
||||
# ========================================
|
||||
# 菜单管理操作 (parentid=6, DB ID: 23-29)
|
||||
# ========================================
|
||||
{"name": "查看列表", "code": "perm:menu:list", "parentid": 6, "description": "查看菜单列表"},
|
||||
{"name": "查看详情", "code": "perm:menu:detail", "parentid": 6, "description": "查看菜单详情"},
|
||||
{"name": "搜索", "code": "perm:menu:search", "parentid": 6, "description": "搜索菜单"},
|
||||
{"name": "创建", "code": "perm:menu:create", "parentid": 6, "description": "创建新菜单"},
|
||||
{"name": "编辑", "code": "perm:menu:edit", "parentid": 6, "description": "编辑菜单信息"},
|
||||
{"name": "删除", "code": "perm:menu:delete", "parentid": 6, "description": "删除菜单"},
|
||||
{"name": "导出", "code": "perm:menu:export", "parentid": 6, "description": "导出菜单数据"},
|
||||
|
||||
# ========================================
|
||||
# 系统设置 -> 子权限 (parentid=2, DB ID: 30)
|
||||
# ========================================
|
||||
{"name": "系统设置", "code": "system:view", "parentid": 2, "description": "查看和修改系统设置"},
|
||||
|
||||
# ========================================
|
||||
# 日志管理 -> 子模块 (parentid=3, DB ID: 31,32)
|
||||
# ========================================
|
||||
{"name": "登录日志", "code": "log:login", "parentid": 3, "route": "/admin/login-logs", "description": "登录日志查看"},
|
||||
{"name": "操作日志", "code": "log:operation", "parentid": 3, "route": "/admin/operation-logs", "description": "操作日志查看"},
|
||||
|
||||
# ========================================
|
||||
# 登录日志操作 (parentid=31, DB ID: 33,34)
|
||||
# ========================================
|
||||
{"name": "查看列表", "code": "log:login:list", "parentid": 31, "description": "查看登录日志列表"},
|
||||
{"name": "搜索", "code": "log:login:search", "parentid": 31, "description": "搜索登录日志"},
|
||||
|
||||
# ========================================
|
||||
# 操作日志操作 (parentid=32, DB ID: 35,36)
|
||||
# ========================================
|
||||
{"name": "查看列表", "code": "log:operation:list", "parentid": 32, "description": "查看操作日志列表"},
|
||||
{"name": "搜索", "code": "log:operation:search", "parentid": 32, "description": "搜索操作日志"},
|
||||
|
||||
# ========================================
|
||||
# 日志清理 (parentid=3, DB ID: 37)
|
||||
# ========================================
|
||||
{"name": "清理日志", "code": "log:clean", "parentid": 3, "description": "清理旧日志"},
|
||||
]
|
||||
|
||||
# 检查现有权限
|
||||
result = await db.execute(select(Permission))
|
||||
existing_permissions = result.scalars().all()
|
||||
existing_permission_codes = {perm.code: perm for perm in existing_permissions}
|
||||
|
||||
# 创建或更新权限
|
||||
created_count = 0
|
||||
updated_count = 0
|
||||
|
||||
for perm_data in permission_tree:
|
||||
perm_code = perm_data["code"]
|
||||
if perm_code not in existing_permission_codes:
|
||||
# 权限不存在,需要创建
|
||||
db.add(Permission(**perm_data))
|
||||
created_count += 1
|
||||
else:
|
||||
# 权限存在,检查是否需要更新
|
||||
existing_perm = existing_permission_codes[perm_code]
|
||||
update_needed = False
|
||||
|
||||
if existing_perm.name != perm_data["name"]:
|
||||
existing_perm.name = perm_data["name"]
|
||||
update_needed = True
|
||||
if existing_perm.parentid != perm_data["parentid"]:
|
||||
existing_perm.parentid = perm_data["parentid"]
|
||||
update_needed = True
|
||||
if existing_perm.route != perm_data.get("route"):
|
||||
existing_perm.route = perm_data.get("route")
|
||||
update_needed = True
|
||||
if existing_perm.description != perm_data["description"]:
|
||||
existing_perm.description = perm_data["description"]
|
||||
update_needed = True
|
||||
|
||||
if update_needed:
|
||||
updated_count += 1
|
||||
|
||||
if created_count > 0 or updated_count > 0:
|
||||
await db.commit()
|
||||
print(f"创建了 {created_count} 个权限,更新了 {updated_count} 个权限")
|
||||
else:
|
||||
print("权限数据已存在且无需更新,跳过初始化")
|
||||
|
||||
print("权限数据初始化/更新完成")
|
||||
|
||||
|
||||
async def init_role_permissions(db: AsyncSession):
|
||||
"""初始化角色权限分配"""
|
||||
from sqlalchemy.orm import selectinload
|
||||
|
||||
# 获取管理员角色(使用预加载避免懒加载问题)
|
||||
result = await db.execute(
|
||||
select(Role)
|
||||
.filter(Role.name == "管理员")
|
||||
.options(selectinload(Role.permissions))
|
||||
)
|
||||
admin_role = result.scalar_one_or_none()
|
||||
if not admin_role:
|
||||
print("管理员角色不存在,请先初始化角色数据")
|
||||
return
|
||||
|
||||
# 获取所有权限
|
||||
result = await db.execute(select(Permission))
|
||||
all_permissions = result.scalars().all()
|
||||
|
||||
if not all_permissions:
|
||||
print("没有找到任何权限,请先初始化权限数据")
|
||||
return
|
||||
|
||||
# 获取管理员角色当前的权限(使用预加载的数据)
|
||||
admin_permission_codes = set()
|
||||
if admin_role.permissions:
|
||||
admin_permission_codes = {perm.code for perm in admin_role.permissions}
|
||||
|
||||
# 检查是否需要添加权限
|
||||
permissions_to_add = []
|
||||
for perm in all_permissions:
|
||||
if perm.code not in admin_permission_codes:
|
||||
permissions_to_add.append(perm)
|
||||
|
||||
if permissions_to_add:
|
||||
# 直接设置权限(替换而非追加,确保权限完整)
|
||||
admin_role.permissions = all_permissions
|
||||
await db.commit()
|
||||
print(f"为管理员角色分配了 {len(permissions_to_add)} 个权限")
|
||||
else:
|
||||
print("管理员角色权限已完整,无需更新")
|
||||
|
||||
print("角色权限初始化/更新完成")
|
||||
|
||||
|
||||
async def init_roles(db: AsyncSession):
|
||||
"""初始化角色数据"""
|
||||
# 定义需要初始化的角色
|
||||
required_roles = [
|
||||
{"name": "管理员", "creator": "system"},
|
||||
{"name": "普通用户", "creator": "system"},
|
||||
{"name": "访客", "creator": "system"},
|
||||
{"name": "会员", "creator": "system"},
|
||||
]
|
||||
|
||||
# 检查现有角色
|
||||
result = await db.execute(select(Role))
|
||||
existing_roles = result.scalars().all()
|
||||
existing_role_names = {role.name for role in existing_roles}
|
||||
|
||||
# 检查需要创建或更新的角色
|
||||
roles_to_add = []
|
||||
roles_to_update = []
|
||||
|
||||
for role_data in required_roles:
|
||||
role_name = role_data["name"]
|
||||
if role_name not in existing_role_names:
|
||||
# 角色不存在,需要创建
|
||||
roles_to_add.append(Role(**role_data))
|
||||
else:
|
||||
# 角色存在,检查是否需要更新
|
||||
existing_role = next(
|
||||
role for role in existing_roles if role.name == role_name
|
||||
)
|
||||
if existing_role.creator != role_data["creator"]:
|
||||
# 需要更新
|
||||
existing_role.creator = role_data["creator"]
|
||||
roles_to_update.append(existing_role)
|
||||
|
||||
# 执行创建和更新操作
|
||||
if roles_to_add:
|
||||
for role in roles_to_add:
|
||||
db.add(role)
|
||||
await db.commit()
|
||||
print(f"创建了 {len(roles_to_add)} 个角色")
|
||||
|
||||
if roles_to_update:
|
||||
await db.commit()
|
||||
print(f"更新了 {len(roles_to_update)} 个角色")
|
||||
|
||||
if not roles_to_add and not roles_to_update:
|
||||
print("角色数据已存在且无需更新,跳过初始化")
|
||||
|
||||
print("角色数据初始化/更新完成")
|
||||
|
||||
|
||||
async def init_admin_user(db: AsyncSession):
|
||||
"""初始化管理员用户"""
|
||||
# 获取管理员角色
|
||||
result = await db.execute(select(Role).filter(Role.name == "管理员"))
|
||||
admin_role = result.scalar_one_or_none()
|
||||
if not admin_role:
|
||||
print("管理员角色不存在,请先初始化角色数据")
|
||||
return
|
||||
|
||||
# 定义需要的管理员用户数据
|
||||
admin_data = {
|
||||
"username": "admin",
|
||||
"nickname": "系统管理员",
|
||||
"email": "admin@example.com",
|
||||
"phone": "13800138000",
|
||||
"password_hash": get_password_hash("admin"),
|
||||
"role_id": admin_role.id,
|
||||
"avatar": "",
|
||||
"wx_openid": "",
|
||||
}
|
||||
|
||||
# 检查管理员用户是否存在
|
||||
result = await db.execute(select(User).filter(User.username == "admin"))
|
||||
existing_admin = result.scalar_one_or_none()
|
||||
|
||||
if not existing_admin:
|
||||
# 管理员不存在,创建
|
||||
admin_user = User(**admin_data)
|
||||
db.add(admin_user)
|
||||
await db.commit()
|
||||
print("创建了管理员用户")
|
||||
else:
|
||||
# 管理员存在,检查是否需要更新
|
||||
update_needed = False
|
||||
|
||||
# 检查字段是否需要更新(不包括密码,因为密码只在首次创建时设置)
|
||||
if existing_admin.nickname != admin_data["nickname"]:
|
||||
existing_admin.nickname = admin_data["nickname"]
|
||||
update_needed = True
|
||||
if existing_admin.email != admin_data["email"]:
|
||||
existing_admin.email = admin_data["email"]
|
||||
update_needed = True
|
||||
if existing_admin.phone != admin_data["phone"]:
|
||||
existing_admin.phone = admin_data["phone"]
|
||||
update_needed = True
|
||||
if existing_admin.role_id != admin_data["role_id"]:
|
||||
existing_admin.role_id = admin_data["role_id"]
|
||||
update_needed = True
|
||||
if existing_admin.avatar != admin_data["avatar"]:
|
||||
existing_admin.avatar = admin_data["avatar"]
|
||||
update_needed = True
|
||||
if existing_admin.wx_openid != admin_data["wx_openid"]:
|
||||
existing_admin.wx_openid = admin_data["wx_openid"]
|
||||
update_needed = True
|
||||
|
||||
if update_needed:
|
||||
await db.commit()
|
||||
print("更新了管理员用户")
|
||||
else:
|
||||
print("管理员用户已存在且无需更新,跳过初始化")
|
||||
|
||||
print("管理员用户初始化/更新完成")
|
||||
|
||||
|
||||
async def init_test_user(db: AsyncSession):
|
||||
"""初始化测试用户"""
|
||||
# 获取普通用户角色
|
||||
result = await db.execute(select(Role).filter(Role.name == "普通用户"))
|
||||
user_role = result.scalar_one_or_none()
|
||||
if not user_role:
|
||||
print("普通用户角色不存在,请先初始化角色数据")
|
||||
return
|
||||
|
||||
# 定义需要的测试用户数据
|
||||
test_data = {
|
||||
"username": "test",
|
||||
"nickname": "测试用户",
|
||||
"email": "test@example.com",
|
||||
"phone": "13800138001",
|
||||
"password_hash": get_password_hash("test"),
|
||||
"role_id": user_role.id,
|
||||
"avatar": "",
|
||||
"wx_openid": "",
|
||||
}
|
||||
|
||||
# 检查测试用户是否存在
|
||||
result = await db.execute(select(User).filter(User.username == "test"))
|
||||
existing_test_user = result.scalar_one_or_none()
|
||||
|
||||
if not existing_test_user:
|
||||
# 测试用户不存在,创建
|
||||
test_user = User(**test_data)
|
||||
db.add(test_user)
|
||||
await db.commit()
|
||||
print("创建了测试用户")
|
||||
else:
|
||||
# 测试用户存在,检查是否需要更新
|
||||
update_needed = False
|
||||
|
||||
# 检查字段是否需要更新(不包括密码,因为密码只在首次创建时设置)
|
||||
if existing_test_user.nickname != test_data["nickname"]:
|
||||
existing_test_user.nickname = test_data["nickname"]
|
||||
update_needed = True
|
||||
if existing_test_user.email != test_data["email"]:
|
||||
existing_test_user.email = test_data["email"]
|
||||
update_needed = True
|
||||
if existing_test_user.phone != test_data["phone"]:
|
||||
existing_test_user.phone = test_data["phone"]
|
||||
update_needed = True
|
||||
if existing_test_user.role_id != test_data["role_id"]:
|
||||
existing_test_user.role_id = test_data["role_id"]
|
||||
update_needed = True
|
||||
if existing_test_user.avatar != test_data["avatar"]:
|
||||
existing_test_user.avatar = test_data["avatar"]
|
||||
update_needed = True
|
||||
if existing_test_user.wx_openid != test_data["wx_openid"]:
|
||||
existing_test_user.wx_openid = test_data["wx_openid"]
|
||||
update_needed = True
|
||||
|
||||
if update_needed:
|
||||
await db.commit()
|
||||
print("更新了测试用户")
|
||||
else:
|
||||
print("测试用户已存在且无需更新,跳过初始化")
|
||||
|
||||
print("测试用户初始化/更新完成")
|
||||
|
||||
|
||||
async def init_system_settings(db: AsyncSession):
|
||||
"""初始化系统设置数据"""
|
||||
# 定义需要初始化的系统设置
|
||||
system_settings = [
|
||||
{
|
||||
"key": "系统标题",
|
||||
"value": "图书管理系统",
|
||||
"type": "string",
|
||||
"description": "系统显示标题"
|
||||
},
|
||||
{
|
||||
"key": "页脚信息",
|
||||
"value": "Copyright © 2026 图书管理系统 版权所有 | 技术支持:强蕊科技",
|
||||
"type": "string",
|
||||
"description": "页脚信息"
|
||||
},
|
||||
{
|
||||
"key": "ICP信息",
|
||||
"value": "京ICP备2023000000号",
|
||||
"type": "string",
|
||||
"description": "ICP备案信息"
|
||||
},
|
||||
{
|
||||
"key": "启用手机短信注册",
|
||||
"value": "false",
|
||||
"type": "boolean",
|
||||
"description": "是否允许通过手机短信验证码注册"
|
||||
},
|
||||
{
|
||||
"key": "启用邮箱注册",
|
||||
"value": "false",
|
||||
"type": "boolean",
|
||||
"description": "是否允许通过邮箱验证链接注册"
|
||||
},
|
||||
{
|
||||
"key": "系统Logo",
|
||||
"value": "",
|
||||
"type": "image",
|
||||
"description": "系统Logo图片"
|
||||
}
|
||||
]
|
||||
|
||||
# 检查现有系统设置
|
||||
result = await db.execute(select(SystemSetting))
|
||||
existing_settings = result.scalars().all()
|
||||
existing_setting_keys = {setting.key: setting for setting in existing_settings}
|
||||
|
||||
# 创建或更新系统设置
|
||||
created_count = 0
|
||||
updated_count = 0
|
||||
|
||||
for setting_data in system_settings:
|
||||
setting_key = setting_data["key"]
|
||||
if setting_key not in existing_setting_keys:
|
||||
# 设置不存在,需要创建
|
||||
db.add(SystemSetting(**setting_data))
|
||||
created_count += 1
|
||||
else:
|
||||
# 设置存在,检查是否需要更新
|
||||
existing_setting = existing_setting_keys[setting_key]
|
||||
update_needed = False
|
||||
|
||||
if existing_setting.value != setting_data["value"]:
|
||||
existing_setting.value = setting_data["value"]
|
||||
update_needed = True
|
||||
if existing_setting.type != setting_data["type"]:
|
||||
existing_setting.type = setting_data["type"]
|
||||
update_needed = True
|
||||
if existing_setting.description != setting_data.get("description"):
|
||||
existing_setting.description = setting_data.get("description")
|
||||
update_needed = True
|
||||
|
||||
if update_needed:
|
||||
updated_count += 1
|
||||
|
||||
if created_count > 0 or updated_count > 0:
|
||||
await db.commit()
|
||||
print(f"创建了 {created_count} 个系统设置,更新了 {updated_count} 个系统设置")
|
||||
else:
|
||||
print("系统设置数据已存在且无需更新,跳过初始化")
|
||||
|
||||
print("系统设置数据初始化/更新完成")
|
||||
|
||||
|
||||
async def init_all_data(db: AsyncSession):
|
||||
"""初始化所有数据"""
|
||||
print("开始初始化数据库数据...")
|
||||
await init_roles(db)
|
||||
await init_permissions(db)
|
||||
await init_role_permissions(db)
|
||||
await init_admin_user(db)
|
||||
await init_test_user(db)
|
||||
await init_system_settings(db)
|
||||
print("数据库数据初始化完成")
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, text
|
||||
from models import Role, User, Permission, SystemSetting
|
||||
from utils.password import get_password_hash
|
||||
|
||||
|
||||
async def init_permissions(db: AsyncSession):
|
||||
"""初始化权限数据(树形结构)"""
|
||||
# 定义需要初始化的权限(树形结构)
|
||||
# parentid: 0 = 顶级模块, >0 = 子权限
|
||||
permission_tree = [
|
||||
# ========================================
|
||||
# 顶级模块 (DB ID: 1,2,3)
|
||||
# ========================================
|
||||
{"name": "权限管理", "code": "perm", "parentid": 0, "route": "/admin/permissions", "description": "权限管理模块", "sort": 1},
|
||||
{"name": "系统设置", "code": "system", "parentid": 0, "route": "/admin/settings", "description": "系统设置模块", "sort": 2},
|
||||
{"name": "日志管理", "code": "log", "parentid": 0, "route": "/admin/logs", "description": "日志管理模块", "sort": 3},
|
||||
|
||||
# ========================================
|
||||
# 权限管理 -> 二级子模块 (parentid=1, DB ID: 4,5,6)
|
||||
# ========================================
|
||||
{"name": "用户管理", "code": "perm:user", "parentid": 1, "route": "/admin/users", "description": "用户管理子模块", "sort": 1},
|
||||
{"name": "角色管理", "code": "perm:role", "parentid": 1, "route": "/admin/roles", "description": "角色管理子模块", "sort": 2},
|
||||
{"name": "菜单管理", "code": "perm:menu", "parentid": 1, "route": "/admin/menu", "description": "菜单管理子模块", "sort": 3},
|
||||
|
||||
# ========================================
|
||||
# 用户管理操作 (parentid=4, DB ID: 7-14)
|
||||
# ========================================
|
||||
{"name": "查看列表", "code": "perm:user:list", "parentid": 4, "description": "查看用户列表", "sort": 1},
|
||||
{"name": "查看详情", "code": "perm:user:detail", "parentid": 4, "description": "查看用户详情", "sort": 2},
|
||||
{"name": "搜索", "code": "perm:user:search", "parentid": 4, "description": "搜索用户", "sort": 3},
|
||||
{"name": "创建", "code": "perm:user:create", "parentid": 4, "description": "创建新用户", "sort": 4},
|
||||
{"name": "编辑", "code": "perm:user:edit", "parentid": 4, "description": "编辑用户信息", "sort": 5},
|
||||
{"name": "删除", "code": "perm:user:delete", "parentid": 4, "description": "删除用户", "sort": 6},
|
||||
{"name": "导出", "code": "perm:user:export", "parentid": 4, "description": "导出用户数据", "sort": 7},
|
||||
{"name": "批量操作", "code": "perm:user:batch", "parentid": 4, "description": "批量操作用户", "sort": 8},
|
||||
|
||||
# ========================================
|
||||
# 角色管理操作 (parentid=5, DB ID: 15-22)
|
||||
# ========================================
|
||||
{"name": "查看列表", "code": "perm:role:list", "parentid": 5, "description": "查看角色列表", "sort": 1},
|
||||
{"name": "查看详情", "code": "perm:role:detail", "parentid": 5, "description": "查看角色详情", "sort": 2},
|
||||
{"name": "搜索", "code": "perm:role:search", "parentid": 5, "description": "搜索角色", "sort": 3},
|
||||
{"name": "创建", "code": "perm:role:create", "parentid": 5, "description": "创建新角色", "sort": 4},
|
||||
{"name": "编辑", "code": "perm:role:edit", "parentid": 5, "description": "编辑角色信息", "sort": 5},
|
||||
{"name": "删除", "code": "perm:role:delete", "parentid": 5, "description": "删除角色", "sort": 6},
|
||||
{"name": "导出", "code": "perm:role:export", "parentid": 5, "description": "导出角色数据", "sort": 7},
|
||||
{"name": "分配权限", "code": "perm:role:assign", "parentid": 5, "description": "为角色分配权限", "sort": 8},
|
||||
|
||||
# ========================================
|
||||
# 菜单管理操作 (parentid=6, DB ID: 23-29)
|
||||
# ========================================
|
||||
{"name": "查看列表", "code": "perm:menu:list", "parentid": 6, "description": "查看菜单列表", "sort": 1},
|
||||
{"name": "查看详情", "code": "perm:menu:detail", "parentid": 6, "description": "查看菜单详情", "sort": 2},
|
||||
{"name": "搜索", "code": "perm:menu:search", "parentid": 6, "description": "搜索菜单", "sort": 3},
|
||||
{"name": "创建", "code": "perm:menu:create", "parentid": 6, "description": "创建新菜单", "sort": 4},
|
||||
{"name": "编辑", "code": "perm:menu:edit", "parentid": 6, "description": "编辑菜单信息", "sort": 5},
|
||||
{"name": "删除", "code": "perm:menu:delete", "parentid": 6, "description": "删除菜单", "sort": 6},
|
||||
{"name": "导出", "code": "perm:menu:export", "parentid": 6, "description": "导出菜单数据", "sort": 7},
|
||||
|
||||
# ========================================
|
||||
# 系统设置 -> 子权限 (parentid=2, DB ID: 30)
|
||||
# ========================================
|
||||
{"name": "系统设置", "code": "system:view", "parentid": 2, "description": "查看和修改系统设置", "sort": 1},
|
||||
|
||||
# ========================================
|
||||
# 日志管理 -> 子模块 (parentid=3, DB ID: 31,32,37)
|
||||
# ========================================
|
||||
{"name": "登录日志", "code": "log:login", "parentid": 3, "route": "/admin/login-logs", "description": "登录日志查看", "sort": 1},
|
||||
{"name": "操作日志", "code": "log:operation", "parentid": 3, "route": "/admin/operation-logs", "description": "操作日志查看", "sort": 2},
|
||||
{"name": "清理日志", "code": "log:clean", "parentid": 3, "description": "清理旧日志", "sort": 3},
|
||||
|
||||
# ========================================
|
||||
# 登录日志操作 (parentid=31, DB ID: 33,34)
|
||||
# ========================================
|
||||
{"name": "查看列表", "code": "log:login:list", "parentid": 31, "description": "查看登录日志列表", "sort": 1},
|
||||
{"name": "搜索", "code": "log:login:search", "parentid": 31, "description": "搜索登录日志", "sort": 2},
|
||||
|
||||
# ========================================
|
||||
# 操作日志操作 (parentid=32, DB ID: 35,36)
|
||||
# ========================================
|
||||
{"name": "查看列表", "code": "log:operation:list", "parentid": 32, "description": "查看操作日志列表", "sort": 1},
|
||||
{"name": "搜索", "code": "log:operation:search", "parentid": 32, "description": "搜索操作日志", "sort": 2},
|
||||
]
|
||||
|
||||
# 检查现有权限
|
||||
result = await db.execute(select(Permission))
|
||||
existing_permissions = result.scalars().all()
|
||||
existing_permission_codes = {perm.code: perm for perm in existing_permissions}
|
||||
|
||||
# 创建或更新权限
|
||||
created_count = 0
|
||||
updated_count = 0
|
||||
|
||||
for perm_data in permission_tree:
|
||||
perm_code = perm_data["code"]
|
||||
if perm_code not in existing_permission_codes:
|
||||
# 权限不存在,需要创建
|
||||
db.add(Permission(**perm_data))
|
||||
created_count += 1
|
||||
else:
|
||||
# 权限存在,检查是否需要更新
|
||||
existing_perm = existing_permission_codes[perm_code]
|
||||
update_needed = False
|
||||
|
||||
if existing_perm.name != perm_data["name"]:
|
||||
existing_perm.name = perm_data["name"]
|
||||
update_needed = True
|
||||
if existing_perm.parentid != perm_data["parentid"]:
|
||||
existing_perm.parentid = perm_data["parentid"]
|
||||
update_needed = True
|
||||
if existing_perm.route != perm_data.get("route"):
|
||||
existing_perm.route = perm_data.get("route")
|
||||
update_needed = True
|
||||
if existing_perm.description != perm_data["description"]:
|
||||
existing_perm.description = perm_data["description"]
|
||||
update_needed = True
|
||||
if existing_perm.sort != perm_data.get("sort", 0):
|
||||
existing_perm.sort = perm_data.get("sort", 0)
|
||||
update_needed = True
|
||||
|
||||
if update_needed:
|
||||
updated_count += 1
|
||||
|
||||
if created_count > 0 or updated_count > 0:
|
||||
await db.commit()
|
||||
print(f"创建了 {created_count} 个权限,更新了 {updated_count} 个权限")
|
||||
else:
|
||||
print("权限数据已存在且无需更新,跳过初始化")
|
||||
|
||||
print("权限数据初始化/更新完成")
|
||||
|
||||
|
||||
async def init_role_permissions(db: AsyncSession):
|
||||
"""初始化角色权限分配"""
|
||||
from sqlalchemy.orm import selectinload
|
||||
|
||||
# 获取管理员角色(使用预加载避免懒加载问题)
|
||||
result = await db.execute(
|
||||
select(Role)
|
||||
.filter(Role.name == "管理员")
|
||||
.options(selectinload(Role.permissions))
|
||||
)
|
||||
admin_role = result.scalar_one_or_none()
|
||||
if not admin_role:
|
||||
print("管理员角色不存在,请先初始化角色数据")
|
||||
return
|
||||
|
||||
# 获取所有权限
|
||||
result = await db.execute(select(Permission))
|
||||
all_permissions = result.scalars().all()
|
||||
|
||||
if not all_permissions:
|
||||
print("没有找到任何权限,请先初始化权限数据")
|
||||
return
|
||||
|
||||
# 获取管理员角色当前的权限(使用预加载的数据)
|
||||
admin_permission_codes = set()
|
||||
if admin_role.permissions:
|
||||
admin_permission_codes = {perm.code for perm in admin_role.permissions}
|
||||
|
||||
# 检查是否需要添加权限
|
||||
permissions_to_add = []
|
||||
for perm in all_permissions:
|
||||
if perm.code not in admin_permission_codes:
|
||||
permissions_to_add.append(perm)
|
||||
|
||||
if permissions_to_add:
|
||||
# 直接设置权限(替换而非追加,确保权限完整)
|
||||
admin_role.permissions = all_permissions
|
||||
await db.commit()
|
||||
print(f"为管理员角色分配了 {len(permissions_to_add)} 个权限")
|
||||
else:
|
||||
print("管理员角色权限已完整,无需更新")
|
||||
|
||||
print("角色权限初始化/更新完成")
|
||||
|
||||
|
||||
async def init_roles(db: AsyncSession):
|
||||
"""初始化角色数据"""
|
||||
# 定义需要初始化的角色
|
||||
required_roles = [
|
||||
{"name": "管理员", "creator": "system"},
|
||||
{"name": "普通用户", "creator": "system"},
|
||||
{"name": "访客", "creator": "system"},
|
||||
{"name": "会员", "creator": "system"},
|
||||
]
|
||||
|
||||
# 检查现有角色
|
||||
result = await db.execute(select(Role))
|
||||
existing_roles = result.scalars().all()
|
||||
existing_role_names = {role.name for role in existing_roles}
|
||||
|
||||
# 检查需要创建或更新的角色
|
||||
roles_to_add = []
|
||||
roles_to_update = []
|
||||
|
||||
for role_data in required_roles:
|
||||
role_name = role_data["name"]
|
||||
if role_name not in existing_role_names:
|
||||
# 角色不存在,需要创建
|
||||
roles_to_add.append(Role(**role_data))
|
||||
else:
|
||||
# 角色存在,检查是否需要更新
|
||||
existing_role = next(
|
||||
role for role in existing_roles if role.name == role_name
|
||||
)
|
||||
if existing_role.creator != role_data["creator"]:
|
||||
# 需要更新
|
||||
existing_role.creator = role_data["creator"]
|
||||
roles_to_update.append(existing_role)
|
||||
|
||||
# 执行创建和更新操作
|
||||
if roles_to_add:
|
||||
for role in roles_to_add:
|
||||
db.add(role)
|
||||
await db.commit()
|
||||
print(f"创建了 {len(roles_to_add)} 个角色")
|
||||
|
||||
if roles_to_update:
|
||||
await db.commit()
|
||||
print(f"更新了 {len(roles_to_update)} 个角色")
|
||||
|
||||
if not roles_to_add and not roles_to_update:
|
||||
print("角色数据已存在且无需更新,跳过初始化")
|
||||
|
||||
print("角色数据初始化/更新完成")
|
||||
|
||||
|
||||
async def init_admin_user(db: AsyncSession):
|
||||
"""初始化管理员用户"""
|
||||
# 获取管理员角色
|
||||
result = await db.execute(select(Role).filter(Role.name == "管理员"))
|
||||
admin_role = result.scalar_one_or_none()
|
||||
if not admin_role:
|
||||
print("管理员角色不存在,请先初始化角色数据")
|
||||
return
|
||||
|
||||
# 定义需要的管理员用户数据
|
||||
admin_data = {
|
||||
"username": "admin",
|
||||
"nickname": "系统管理员",
|
||||
"email": "admin@example.com",
|
||||
"phone": "13800138000",
|
||||
"password_hash": get_password_hash("admin"),
|
||||
"role_id": admin_role.id,
|
||||
"avatar": "",
|
||||
"wx_openid": "",
|
||||
}
|
||||
|
||||
# 检查管理员用户是否存在
|
||||
result = await db.execute(select(User).filter(User.username == "admin"))
|
||||
existing_admin = result.scalar_one_or_none()
|
||||
|
||||
if not existing_admin:
|
||||
# 管理员不存在,创建
|
||||
admin_user = User(**admin_data)
|
||||
db.add(admin_user)
|
||||
await db.commit()
|
||||
print("创建了管理员用户")
|
||||
else:
|
||||
# 管理员存在,检查是否需要更新
|
||||
update_needed = False
|
||||
|
||||
# 检查字段是否需要更新(不包括密码,因为密码只在首次创建时设置)
|
||||
if existing_admin.nickname != admin_data["nickname"]:
|
||||
existing_admin.nickname = admin_data["nickname"]
|
||||
update_needed = True
|
||||
if existing_admin.email != admin_data["email"]:
|
||||
existing_admin.email = admin_data["email"]
|
||||
update_needed = True
|
||||
if existing_admin.phone != admin_data["phone"]:
|
||||
existing_admin.phone = admin_data["phone"]
|
||||
update_needed = True
|
||||
if existing_admin.role_id != admin_data["role_id"]:
|
||||
existing_admin.role_id = admin_data["role_id"]
|
||||
update_needed = True
|
||||
if existing_admin.avatar != admin_data["avatar"]:
|
||||
existing_admin.avatar = admin_data["avatar"]
|
||||
update_needed = True
|
||||
if existing_admin.wx_openid != admin_data["wx_openid"]:
|
||||
existing_admin.wx_openid = admin_data["wx_openid"]
|
||||
update_needed = True
|
||||
|
||||
if update_needed:
|
||||
await db.commit()
|
||||
print("更新了管理员用户")
|
||||
else:
|
||||
print("管理员用户已存在且无需更新,跳过初始化")
|
||||
|
||||
print("管理员用户初始化/更新完成")
|
||||
|
||||
|
||||
async def init_test_user(db: AsyncSession):
|
||||
"""初始化测试用户"""
|
||||
# 获取普通用户角色
|
||||
result = await db.execute(select(Role).filter(Role.name == "普通用户"))
|
||||
user_role = result.scalar_one_or_none()
|
||||
if not user_role:
|
||||
print("普通用户角色不存在,请先初始化角色数据")
|
||||
return
|
||||
|
||||
# 定义需要的测试用户数据
|
||||
test_data = {
|
||||
"username": "test",
|
||||
"nickname": "测试用户",
|
||||
"email": "test@example.com",
|
||||
"phone": "13800138001",
|
||||
"password_hash": get_password_hash("test"),
|
||||
"role_id": user_role.id,
|
||||
"avatar": "",
|
||||
"wx_openid": "",
|
||||
}
|
||||
|
||||
# 检查测试用户是否存在
|
||||
result = await db.execute(select(User).filter(User.username == "test"))
|
||||
existing_test_user = result.scalar_one_or_none()
|
||||
|
||||
if not existing_test_user:
|
||||
# 测试用户不存在,创建
|
||||
test_user = User(**test_data)
|
||||
db.add(test_user)
|
||||
await db.commit()
|
||||
print("创建了测试用户")
|
||||
else:
|
||||
# 测试用户存在,检查是否需要更新
|
||||
update_needed = False
|
||||
|
||||
# 检查字段是否需要更新(不包括密码,因为密码只在首次创建时设置)
|
||||
if existing_test_user.nickname != test_data["nickname"]:
|
||||
existing_test_user.nickname = test_data["nickname"]
|
||||
update_needed = True
|
||||
if existing_test_user.email != test_data["email"]:
|
||||
existing_test_user.email = test_data["email"]
|
||||
update_needed = True
|
||||
if existing_test_user.phone != test_data["phone"]:
|
||||
existing_test_user.phone = test_data["phone"]
|
||||
update_needed = True
|
||||
if existing_test_user.role_id != test_data["role_id"]:
|
||||
existing_test_user.role_id = test_data["role_id"]
|
||||
update_needed = True
|
||||
if existing_test_user.avatar != test_data["avatar"]:
|
||||
existing_test_user.avatar = test_data["avatar"]
|
||||
update_needed = True
|
||||
if existing_test_user.wx_openid != test_data["wx_openid"]:
|
||||
existing_test_user.wx_openid = test_data["wx_openid"]
|
||||
update_needed = True
|
||||
|
||||
if update_needed:
|
||||
await db.commit()
|
||||
print("更新了测试用户")
|
||||
else:
|
||||
print("测试用户已存在且无需更新,跳过初始化")
|
||||
|
||||
print("测试用户初始化/更新完成")
|
||||
|
||||
|
||||
async def init_system_settings(db: AsyncSession):
|
||||
"""初始化系统设置数据"""
|
||||
# 定义需要初始化的系统设置
|
||||
system_settings = [
|
||||
{
|
||||
"key": "系统标题",
|
||||
"value": "图书管理系统",
|
||||
"type": "string",
|
||||
"description": "系统显示标题"
|
||||
},
|
||||
{
|
||||
"key": "页脚信息",
|
||||
"value": "Copyright © 2026 图书管理系统 版权所有 | 技术支持:强蕊科技",
|
||||
"type": "string",
|
||||
"description": "页脚信息"
|
||||
},
|
||||
{
|
||||
"key": "ICP信息",
|
||||
"value": "京ICP备2023000000号",
|
||||
"type": "string",
|
||||
"description": "ICP备案信息"
|
||||
},
|
||||
{
|
||||
"key": "启用手机短信注册",
|
||||
"value": "false",
|
||||
"type": "boolean",
|
||||
"description": "是否允许通过手机短信验证码注册"
|
||||
},
|
||||
{
|
||||
"key": "启用邮箱注册",
|
||||
"value": "false",
|
||||
"type": "boolean",
|
||||
"description": "是否允许通过邮箱验证链接注册"
|
||||
},
|
||||
{
|
||||
"key": "系统Logo",
|
||||
"value": "",
|
||||
"type": "image",
|
||||
"description": "系统Logo图片"
|
||||
}
|
||||
]
|
||||
|
||||
# 检查现有系统设置
|
||||
result = await db.execute(select(SystemSetting))
|
||||
existing_settings = result.scalars().all()
|
||||
existing_setting_keys = {setting.key: setting for setting in existing_settings}
|
||||
|
||||
# 创建或更新系统设置
|
||||
created_count = 0
|
||||
updated_count = 0
|
||||
|
||||
for setting_data in system_settings:
|
||||
setting_key = setting_data["key"]
|
||||
if setting_key not in existing_setting_keys:
|
||||
# 设置不存在,需要创建
|
||||
db.add(SystemSetting(**setting_data))
|
||||
created_count += 1
|
||||
else:
|
||||
# 设置存在,检查是否需要更新
|
||||
existing_setting = existing_setting_keys[setting_key]
|
||||
update_needed = False
|
||||
|
||||
if existing_setting.value != setting_data["value"]:
|
||||
existing_setting.value = setting_data["value"]
|
||||
update_needed = True
|
||||
if existing_setting.type != setting_data["type"]:
|
||||
existing_setting.type = setting_data["type"]
|
||||
update_needed = True
|
||||
if existing_setting.description != setting_data.get("description"):
|
||||
existing_setting.description = setting_data.get("description")
|
||||
update_needed = True
|
||||
|
||||
if update_needed:
|
||||
updated_count += 1
|
||||
|
||||
if created_count > 0 or updated_count > 0:
|
||||
await db.commit()
|
||||
print(f"创建了 {created_count} 个系统设置,更新了 {updated_count} 个系统设置")
|
||||
else:
|
||||
print("系统设置数据已存在且无需更新,跳过初始化")
|
||||
|
||||
print("系统设置数据初始化/更新完成")
|
||||
|
||||
|
||||
async def init_all_data(db: AsyncSession):
|
||||
"""初始化所有数据"""
|
||||
print("开始初始化数据库数据...")
|
||||
await init_roles(db)
|
||||
await init_permissions(db)
|
||||
await init_role_permissions(db)
|
||||
await init_admin_user(db)
|
||||
await init_test_user(db)
|
||||
await init_system_settings(db)
|
||||
print("数据库数据初始化完成")
|
||||
|
||||
@ -1,172 +1,182 @@
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.responses import HTMLResponse, FileResponse
|
||||
from fastapi.exceptions import RequestValidationError
|
||||
from contextlib import asynccontextmanager
|
||||
from apps.urls import api_router
|
||||
from apps.settings.urls import UPLOAD_DIR
|
||||
from database import engine, get_db
|
||||
from models import Base
|
||||
from init_data import init_all_data
|
||||
from pathlib import Path
|
||||
from starlette.requests import Request
|
||||
from starlette.responses import JSONResponse
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
# 启动时初始化数据库
|
||||
async with engine.begin() as conn:
|
||||
# 创建所有表
|
||||
await conn.run_sync(Base.metadata.create_all)
|
||||
|
||||
# 初始化数据
|
||||
async for db in get_db():
|
||||
await init_all_data(db)
|
||||
break
|
||||
|
||||
yield
|
||||
# 关闭时的清理工作
|
||||
await engine.dispose()
|
||||
|
||||
|
||||
# 实例化FastAPI
|
||||
app = FastAPI(
|
||||
title="图书系统授权服务API",
|
||||
description="""图书系统授权服务API是一套用于用户认证和授权的服务,提供用户注册、登录、权限校验等功能。
|
||||
同时实现了基于角色的访问控制(RBAC),支持自定义角色和权限。还增加了日志记录功能,方便监控和调试。""",
|
||||
version="1.0.0",
|
||||
lifespan=lifespan,
|
||||
docs_url=None,
|
||||
redoc_url="/redoc",
|
||||
)
|
||||
|
||||
# 开发环境前后端分离时的跨域(生产环境请收窄 allow_origins)
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=[
|
||||
"http://localhost:5173",
|
||||
"http://127.0.0.1:5173",
|
||||
"http://localhost:3000",
|
||||
"http://127.0.0.1:3000",
|
||||
],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
expose_headers=["X-Captcha-ID"],
|
||||
)
|
||||
|
||||
# 挂载swagger-ui静态文件目录
|
||||
swagger_ui_path = Path(__file__).parent.parent / "swagger-ui"
|
||||
app.mount("/static", StaticFiles(directory=str(swagger_ui_path)), name="static")
|
||||
|
||||
# 上传图片通过自定义端点提供(和 settings/urls.py 中的 UPLOAD_DIR 保持一致)
|
||||
@app.get("/uploads/{filename}")
|
||||
async def serve_upload(filename: str):
|
||||
"""提供上传的图片文件"""
|
||||
file_path = UPLOAD_DIR / filename
|
||||
if not file_path.exists():
|
||||
raise HTTPException(status_code=404, detail="文件不存在")
|
||||
return FileResponse(str(file_path))
|
||||
|
||||
|
||||
# 自定义Swagger UI页面
|
||||
@app.get("/docs", include_in_schema=False)
|
||||
async def custom_swagger_ui_html():
|
||||
html_content = """
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Book System API - Swagger UI</title>
|
||||
<link rel="stylesheet" type="text/css" href="/static/swagger-ui.css">
|
||||
<link rel="icon" type="image/png" href="/static/favicon-32x32.png" sizes="32x32"/>
|
||||
<style>
|
||||
html {
|
||||
box-sizing: border-box;
|
||||
overflow: -moz-scrollbars-vertical;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
*, *:before, *:after {
|
||||
box-sizing: inherit;
|
||||
}
|
||||
body {
|
||||
margin: 0;
|
||||
background: #fafafa;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="swagger-ui"></div>
|
||||
<script src="/static/swagger-ui-bundle.js"></script>
|
||||
<script>
|
||||
window.onload = function() {
|
||||
const ui = SwaggerUIBundle({
|
||||
url: "/openapi.json",
|
||||
dom_id: '#swagger-ui',
|
||||
deepLinking: true,
|
||||
presets: [
|
||||
SwaggerUIBundle.presets.apis,
|
||||
SwaggerUIBundle.SwaggerUIStandalonePreset
|
||||
],
|
||||
layout: "BaseLayout",
|
||||
persistAuthorization: true,
|
||||
docExpansion: "list"
|
||||
});
|
||||
window.ui = ui;
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
return HTMLResponse(content=html_content)
|
||||
|
||||
|
||||
# 包含路由
|
||||
app.include_router(api_router)
|
||||
|
||||
|
||||
# 声明装饰器方法和路径
|
||||
@app.get("/")
|
||||
# 声明装饰器函数
|
||||
async def home():
|
||||
return {"message": "Hello World!!!"}
|
||||
|
||||
|
||||
# 自定义验证错误处理器
|
||||
@app.exception_handler(RequestValidationError)
|
||||
async def validation_exception_handler(request: Request, exc: RequestValidationError):
|
||||
errors = []
|
||||
for err in exc.errors():
|
||||
field = err.get("loc", ["unknown"])[-1]
|
||||
msg = err.get("msg", "验证失败")
|
||||
|
||||
if "min_length" in msg.lower():
|
||||
if field == "username":
|
||||
msg = "用户名长度至少为3个字符"
|
||||
elif field == "password":
|
||||
msg = "密码长度至少为6个字符"
|
||||
elif "max_length" in msg.lower():
|
||||
if field == "username":
|
||||
msg = "用户名长度不能超过50个字符"
|
||||
elif field == "password":
|
||||
msg = "密码长度不能超过50个字符"
|
||||
|
||||
errors.append({"field": field, "message": msg})
|
||||
|
||||
return JSONResponse(
|
||||
status_code=422,
|
||||
content={"code": 422, "message": "验证失败", "errors": errors}
|
||||
)
|
||||
|
||||
|
||||
# 如果使用命令行启动,使用uvicorn 文件名:app --reload启动即可,下面命令就不用写
|
||||
# 如果写下面的命令,就不需要命令行启动了,直接用IDE运行即可
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
import os
|
||||
|
||||
name = f"{os.path.splitext(os.path.basename(os.path.abspath(__file__)))[0]}:app"
|
||||
uvicorn.run(name, host="0.0.0.0", port=8000, reload=True, reload_dirs=["_"])
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.responses import HTMLResponse, FileResponse
|
||||
from fastapi.exceptions import RequestValidationError
|
||||
from contextlib import asynccontextmanager
|
||||
from apps.urls import api_router
|
||||
from apps.settings.urls import UPLOAD_DIR
|
||||
from database import engine, get_db
|
||||
from models import Base
|
||||
from init_data import init_all_data
|
||||
from pathlib import Path
|
||||
from starlette.requests import Request
|
||||
from starlette.responses import JSONResponse
|
||||
from sqlalchemy import text
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
# 启动时初始化数据库
|
||||
async with engine.begin() as conn:
|
||||
# 创建所有表
|
||||
await conn.run_sync(Base.metadata.create_all)
|
||||
# 为已有表添加 sort 列(兼容 PostgreSQL 和 SQLite)
|
||||
try:
|
||||
await conn.execute(text("ALTER TABLE permission ADD COLUMN IF NOT EXISTS sort INTEGER DEFAULT 0"))
|
||||
except Exception:
|
||||
# SQLite 不支持 IF NOT EXISTS
|
||||
try:
|
||||
await conn.execute(text("ALTER TABLE permission ADD COLUMN sort INTEGER DEFAULT 0"))
|
||||
except Exception:
|
||||
pass # 列已存在则忽略
|
||||
|
||||
# 初始化数据
|
||||
async for db in get_db():
|
||||
await init_all_data(db)
|
||||
break
|
||||
|
||||
yield
|
||||
# 关闭时的清理工作
|
||||
await engine.dispose()
|
||||
|
||||
|
||||
# 实例化FastAPI
|
||||
app = FastAPI(
|
||||
title="图书系统授权服务API",
|
||||
description="""图书系统授权服务API是一套用于用户认证和授权的服务,提供用户注册、登录、权限校验等功能。
|
||||
同时实现了基于角色的访问控制(RBAC),支持自定义角色和权限。还增加了日志记录功能,方便监控和调试。""",
|
||||
version="1.0.0",
|
||||
lifespan=lifespan,
|
||||
docs_url=None,
|
||||
redoc_url="/redoc",
|
||||
)
|
||||
|
||||
# 开发环境前后端分离时的跨域(生产环境请收窄 allow_origins)
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=[
|
||||
"http://localhost:5173",
|
||||
"http://127.0.0.1:5173",
|
||||
"http://localhost:3000",
|
||||
"http://127.0.0.1:3000",
|
||||
],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
expose_headers=["X-Captcha-ID"],
|
||||
)
|
||||
|
||||
# 挂载swagger-ui静态文件目录
|
||||
swagger_ui_path = Path(__file__).parent.parent / "swagger-ui"
|
||||
app.mount("/static", StaticFiles(directory=str(swagger_ui_path)), name="static")
|
||||
|
||||
# 上传图片通过自定义端点提供(和 settings/urls.py 中的 UPLOAD_DIR 保持一致)
|
||||
@app.get("/uploads/{filename}")
|
||||
async def serve_upload(filename: str):
|
||||
"""提供上传的图片文件"""
|
||||
file_path = UPLOAD_DIR / filename
|
||||
if not file_path.exists():
|
||||
raise HTTPException(status_code=404, detail="文件不存在")
|
||||
return FileResponse(str(file_path))
|
||||
|
||||
|
||||
# 自定义Swagger UI页面
|
||||
@app.get("/docs", include_in_schema=False)
|
||||
async def custom_swagger_ui_html():
|
||||
html_content = """
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Book System API - Swagger UI</title>
|
||||
<link rel="stylesheet" type="text/css" href="/static/swagger-ui.css">
|
||||
<link rel="icon" type="image/png" href="/static/favicon-32x32.png" sizes="32x32"/>
|
||||
<style>
|
||||
html {
|
||||
box-sizing: border-box;
|
||||
overflow: -moz-scrollbars-vertical;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
*, *:before, *:after {
|
||||
box-sizing: inherit;
|
||||
}
|
||||
body {
|
||||
margin: 0;
|
||||
background: #fafafa;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="swagger-ui"></div>
|
||||
<script src="/static/swagger-ui-bundle.js"></script>
|
||||
<script>
|
||||
window.onload = function() {
|
||||
const ui = SwaggerUIBundle({
|
||||
url: "/openapi.json",
|
||||
dom_id: '#swagger-ui',
|
||||
deepLinking: true,
|
||||
presets: [
|
||||
SwaggerUIBundle.presets.apis,
|
||||
SwaggerUIBundle.SwaggerUIStandalonePreset
|
||||
],
|
||||
layout: "BaseLayout",
|
||||
persistAuthorization: true,
|
||||
docExpansion: "list"
|
||||
});
|
||||
window.ui = ui;
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
return HTMLResponse(content=html_content)
|
||||
|
||||
|
||||
# 包含路由
|
||||
app.include_router(api_router)
|
||||
|
||||
|
||||
# 声明装饰器方法和路径
|
||||
@app.get("/")
|
||||
# 声明装饰器函数
|
||||
async def home():
|
||||
return {"message": "Hello World!!!"}
|
||||
|
||||
|
||||
# 自定义验证错误处理器
|
||||
@app.exception_handler(RequestValidationError)
|
||||
async def validation_exception_handler(request: Request, exc: RequestValidationError):
|
||||
errors = []
|
||||
for err in exc.errors():
|
||||
field = err.get("loc", ["unknown"])[-1]
|
||||
msg = err.get("msg", "验证失败")
|
||||
|
||||
if "min_length" in msg.lower():
|
||||
if field == "username":
|
||||
msg = "用户名长度至少为3个字符"
|
||||
elif field == "password":
|
||||
msg = "密码长度至少为6个字符"
|
||||
elif "max_length" in msg.lower():
|
||||
if field == "username":
|
||||
msg = "用户名长度不能超过50个字符"
|
||||
elif field == "password":
|
||||
msg = "密码长度不能超过50个字符"
|
||||
|
||||
errors.append({"field": field, "message": msg})
|
||||
|
||||
return JSONResponse(
|
||||
status_code=422,
|
||||
content={"code": 422, "message": "验证失败", "errors": errors}
|
||||
)
|
||||
|
||||
|
||||
# 如果使用命令行启动,使用uvicorn 文件名:app --reload启动即可,下面命令就不用写
|
||||
# 如果写下面的命令,就不需要命令行启动了,直接用IDE运行即可
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
import os
|
||||
|
||||
name = f"{os.path.splitext(os.path.basename(os.path.abspath(__file__)))[0]}:app"
|
||||
uvicorn.run(name, host="0.0.0.0", port=8000, reload=True, reload_dirs=["_"])
|
||||
|
||||
@ -1,140 +1,141 @@
|
||||
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, func, Table, Index, Text
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from datetime import datetime
|
||||
|
||||
Base = declarative_base()
|
||||
|
||||
|
||||
class Role(Base):
|
||||
"""角色数据库模型"""
|
||||
|
||||
__tablename__ = "role"
|
||||
id = Column(Integer, primary_key=True, index=True, comment="角色ID")
|
||||
name = Column(String(50), nullable=False, unique=True, comment="角色名称")
|
||||
createtime = Column(DateTime, default=datetime.utcnow, comment="创建时间")
|
||||
updatetime = Column(
|
||||
DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, comment="更新时间"
|
||||
)
|
||||
creator = Column(String(50), nullable=True, comment="创建人")
|
||||
|
||||
# 关联到User
|
||||
users = relationship("User", back_populates="role_rel")
|
||||
# 关联到Permission(多对多)
|
||||
permissions = relationship(
|
||||
"Permission",
|
||||
secondary="role_permission",
|
||||
back_populates="roles"
|
||||
)
|
||||
|
||||
|
||||
class User(Base):
|
||||
"""用户数据库模型"""
|
||||
|
||||
__tablename__ = "users"
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
username = Column(String(50), unique=True, nullable=False, comment="登录名")
|
||||
nickname = Column(String(50), nullable=False, comment="昵称")
|
||||
email = Column(String(50), nullable=True, comment="电子邮箱")
|
||||
phone = Column(String(11), nullable=True, comment="手机号码")
|
||||
wx_openid = Column(String(100), nullable=True, comment="微信OpenID")
|
||||
password_hash = Column(String(128), nullable=False, comment="密码哈希值")
|
||||
avatar = Column(String(255), nullable=True, comment="头像路径")
|
||||
role_id = Column(Integer, ForeignKey("role.id"), default=2, comment="角色ID")
|
||||
createtime = Column(DateTime, default=datetime.utcnow, comment="创建时间")
|
||||
updatetime = Column(
|
||||
DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, comment="更新时间"
|
||||
)
|
||||
lastlogintime = Column(DateTime, comment="最后登录时间")
|
||||
|
||||
# 关联到Role
|
||||
role_rel = relationship("Role", back_populates="users")
|
||||
|
||||
|
||||
class Permission(Base):
|
||||
"""权限数据库模型"""
|
||||
|
||||
__tablename__ = "permission"
|
||||
id = Column(Integer, primary_key=True, index=True, comment="权限ID")
|
||||
name = Column(String(100), nullable=False, comment="权限名称")
|
||||
code = Column(String(100), nullable=False, unique=True, comment="权限编码")
|
||||
parentid = Column(Integer, default=0, comment="父权限ID(0表示顶级模块)")
|
||||
route = Column(String(255), nullable=True, comment="前端路由路径")
|
||||
description = Column(String(255), nullable=True, comment="权限描述")
|
||||
createtime = Column(DateTime, default=datetime.utcnow, comment="创建时间")
|
||||
updatetime = Column(
|
||||
DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, comment="更新时间"
|
||||
)
|
||||
|
||||
# 关联到Role(多对多)
|
||||
roles = relationship(
|
||||
"Role",
|
||||
secondary="role_permission",
|
||||
back_populates="permissions"
|
||||
)
|
||||
|
||||
|
||||
# 角色权限关联表(多对多)
|
||||
role_permission = Table(
|
||||
"role_permission",
|
||||
Base.metadata,
|
||||
Column("role_id", Integer, ForeignKey("role.id"), primary_key=True, comment="角色ID"),
|
||||
Column("permission_id", Integer, ForeignKey("permission.id"), primary_key=True, comment="权限ID")
|
||||
)
|
||||
|
||||
|
||||
class SystemSetting(Base):
|
||||
"""系统设置数据库模型"""
|
||||
|
||||
__tablename__ = "system_setting"
|
||||
id = Column(Integer, primary_key=True, index=True, comment="设置ID")
|
||||
key = Column(String(100), nullable=False, unique=True, comment="设置键名")
|
||||
value = Column(String(2000), nullable=False, comment="设置值")
|
||||
type = Column(String(20), default="string", comment="类型:string/image/select/boolean")
|
||||
options = Column(String(500), nullable=True, comment="下拉选项(用|分隔)")
|
||||
description = Column(String(255), nullable=True, comment="设置描述")
|
||||
createtime = Column(DateTime, default=datetime.utcnow, comment="创建时间")
|
||||
updatetime = Column(
|
||||
DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, comment="更新时间"
|
||||
)
|
||||
|
||||
|
||||
class LoginLog(Base):
|
||||
"""登录日志"""
|
||||
|
||||
__tablename__ = "login_log"
|
||||
id = Column(Integer, primary_key=True, index=True, comment="日志ID")
|
||||
username = Column(String(50), nullable=False, comment="用户名")
|
||||
login_time = Column(DateTime, default=datetime.utcnow, comment="登录时间")
|
||||
ip_address = Column(String(50), nullable=True, comment="IP地址")
|
||||
user_agent = Column(String(500), nullable=True, comment="User-Agent")
|
||||
status = Column(String(20), nullable=False, comment="状态: success/failure")
|
||||
failure_reason = Column(String(255), nullable=True, comment="失败原因")
|
||||
|
||||
|
||||
Index("ix_login_log_login_time", LoginLog.login_time)
|
||||
Index("ix_login_log_username", LoginLog.username)
|
||||
Index("ix_login_log_status", LoginLog.status)
|
||||
|
||||
|
||||
class OperationLog(Base):
|
||||
"""操作日志"""
|
||||
|
||||
__tablename__ = "operation_log"
|
||||
id = Column(Integer, primary_key=True, index=True, comment="日志ID")
|
||||
user_id = Column(Integer, nullable=False, comment="用户ID")
|
||||
username = Column(String(50), nullable=False, comment="用户名")
|
||||
action_type = Column(String(20), nullable=False, comment="操作类型: create/update/delete")
|
||||
target_type = Column(String(50), nullable=False, comment="目标类型: user/role/permission/setting")
|
||||
target_id = Column(Integer, nullable=True, comment="目标ID")
|
||||
target_name = Column(String(100), nullable=True, comment="目标名称")
|
||||
detail = Column(Text, nullable=True, comment="变更详情(JSON)")
|
||||
ip_address = Column(String(50), nullable=True, comment="IP地址")
|
||||
user_agent = Column(String(500), nullable=True, comment="User-Agent")
|
||||
createtime = Column(DateTime, default=datetime.utcnow, comment="创建时间")
|
||||
|
||||
|
||||
Index("ix_operation_log_createtime", OperationLog.createtime)
|
||||
Index("ix_operation_log_username", OperationLog.username)
|
||||
Index("ix_operation_log_action_type", OperationLog.action_type)
|
||||
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, func, Table, Index, Text
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from datetime import datetime
|
||||
|
||||
Base = declarative_base()
|
||||
|
||||
|
||||
class Role(Base):
|
||||
"""角色数据库模型"""
|
||||
|
||||
__tablename__ = "role"
|
||||
id = Column(Integer, primary_key=True, index=True, comment="角色ID")
|
||||
name = Column(String(50), nullable=False, unique=True, comment="角色名称")
|
||||
createtime = Column(DateTime, default=datetime.utcnow, comment="创建时间")
|
||||
updatetime = Column(
|
||||
DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, comment="更新时间"
|
||||
)
|
||||
creator = Column(String(50), nullable=True, comment="创建人")
|
||||
|
||||
# 关联到User
|
||||
users = relationship("User", back_populates="role_rel")
|
||||
# 关联到Permission(多对多)
|
||||
permissions = relationship(
|
||||
"Permission",
|
||||
secondary="role_permission",
|
||||
back_populates="roles"
|
||||
)
|
||||
|
||||
|
||||
class User(Base):
|
||||
"""用户数据库模型"""
|
||||
|
||||
__tablename__ = "users"
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
username = Column(String(50), unique=True, nullable=False, comment="登录名")
|
||||
nickname = Column(String(50), nullable=False, comment="昵称")
|
||||
email = Column(String(50), nullable=True, comment="电子邮箱")
|
||||
phone = Column(String(11), nullable=True, comment="手机号码")
|
||||
wx_openid = Column(String(100), nullable=True, comment="微信OpenID")
|
||||
password_hash = Column(String(128), nullable=False, comment="密码哈希值")
|
||||
avatar = Column(String(255), nullable=True, comment="头像路径")
|
||||
role_id = Column(Integer, ForeignKey("role.id"), default=2, comment="角色ID")
|
||||
createtime = Column(DateTime, default=datetime.utcnow, comment="创建时间")
|
||||
updatetime = Column(
|
||||
DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, comment="更新时间"
|
||||
)
|
||||
lastlogintime = Column(DateTime, comment="最后登录时间")
|
||||
|
||||
# 关联到Role
|
||||
role_rel = relationship("Role", back_populates="users")
|
||||
|
||||
|
||||
class Permission(Base):
|
||||
"""权限数据库模型"""
|
||||
|
||||
__tablename__ = "permission"
|
||||
id = Column(Integer, primary_key=True, index=True, comment="权限ID")
|
||||
name = Column(String(100), nullable=False, comment="权限名称")
|
||||
code = Column(String(100), nullable=False, unique=True, comment="权限编码")
|
||||
parentid = Column(Integer, default=0, comment="父权限ID(0表示顶级模块)")
|
||||
route = Column(String(255), nullable=True, comment="前端路由路径")
|
||||
description = Column(String(255), nullable=True, comment="权限描述")
|
||||
sort = Column(Integer, default=0, comment="排序号(升序排列)")
|
||||
createtime = Column(DateTime, default=datetime.utcnow, comment="创建时间")
|
||||
updatetime = Column(
|
||||
DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, comment="更新时间"
|
||||
)
|
||||
|
||||
# 关联到Role(多对多)
|
||||
roles = relationship(
|
||||
"Role",
|
||||
secondary="role_permission",
|
||||
back_populates="permissions"
|
||||
)
|
||||
|
||||
|
||||
# 角色权限关联表(多对多)
|
||||
role_permission = Table(
|
||||
"role_permission",
|
||||
Base.metadata,
|
||||
Column("role_id", Integer, ForeignKey("role.id"), primary_key=True, comment="角色ID"),
|
||||
Column("permission_id", Integer, ForeignKey("permission.id"), primary_key=True, comment="权限ID")
|
||||
)
|
||||
|
||||
|
||||
class SystemSetting(Base):
|
||||
"""系统设置数据库模型"""
|
||||
|
||||
__tablename__ = "system_setting"
|
||||
id = Column(Integer, primary_key=True, index=True, comment="设置ID")
|
||||
key = Column(String(100), nullable=False, unique=True, comment="设置键名")
|
||||
value = Column(String(2000), nullable=False, comment="设置值")
|
||||
type = Column(String(20), default="string", comment="类型:string/image/select/boolean")
|
||||
options = Column(String(500), nullable=True, comment="下拉选项(用|分隔)")
|
||||
description = Column(String(255), nullable=True, comment="设置描述")
|
||||
createtime = Column(DateTime, default=datetime.utcnow, comment="创建时间")
|
||||
updatetime = Column(
|
||||
DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, comment="更新时间"
|
||||
)
|
||||
|
||||
|
||||
class LoginLog(Base):
|
||||
"""登录日志"""
|
||||
|
||||
__tablename__ = "login_log"
|
||||
id = Column(Integer, primary_key=True, index=True, comment="日志ID")
|
||||
username = Column(String(50), nullable=False, comment="用户名")
|
||||
login_time = Column(DateTime, default=datetime.utcnow, comment="登录时间")
|
||||
ip_address = Column(String(50), nullable=True, comment="IP地址")
|
||||
user_agent = Column(String(500), nullable=True, comment="User-Agent")
|
||||
status = Column(String(20), nullable=False, comment="状态: success/failure")
|
||||
failure_reason = Column(String(255), nullable=True, comment="失败原因")
|
||||
|
||||
|
||||
Index("ix_login_log_login_time", LoginLog.login_time)
|
||||
Index("ix_login_log_username", LoginLog.username)
|
||||
Index("ix_login_log_status", LoginLog.status)
|
||||
|
||||
|
||||
class OperationLog(Base):
|
||||
"""操作日志"""
|
||||
|
||||
__tablename__ = "operation_log"
|
||||
id = Column(Integer, primary_key=True, index=True, comment="日志ID")
|
||||
user_id = Column(Integer, nullable=False, comment="用户ID")
|
||||
username = Column(String(50), nullable=False, comment="用户名")
|
||||
action_type = Column(String(20), nullable=False, comment="操作类型: create/update/delete")
|
||||
target_type = Column(String(50), nullable=False, comment="目标类型: user/role/permission/setting")
|
||||
target_id = Column(Integer, nullable=True, comment="目标ID")
|
||||
target_name = Column(String(100), nullable=True, comment="目标名称")
|
||||
detail = Column(Text, nullable=True, comment="变更详情(JSON)")
|
||||
ip_address = Column(String(50), nullable=True, comment="IP地址")
|
||||
user_agent = Column(String(500), nullable=True, comment="User-Agent")
|
||||
createtime = Column(DateTime, default=datetime.utcnow, comment="创建时间")
|
||||
|
||||
|
||||
Index("ix_operation_log_createtime", OperationLog.createtime)
|
||||
Index("ix_operation_log_username", OperationLog.username)
|
||||
Index("ix_operation_log_action_type", OperationLog.action_type)
|
||||
Index("ix_operation_log_target_type", OperationLog.target_type)
|
||||
@ -1,335 +1,337 @@
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Optional
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class UserBase(BaseModel):
|
||||
"""用户基础模型"""
|
||||
|
||||
username: str = Field(
|
||||
...,
|
||||
min_length=3,
|
||||
max_length=50,
|
||||
description="登录名",
|
||||
json_schema_extra={
|
||||
"error_messages": {
|
||||
"min_length": "用户名长度至少为3个字符",
|
||||
"max_length": "用户名长度不能超过50个字符"
|
||||
}
|
||||
}
|
||||
)
|
||||
nickname: str = Field(..., max_length=50, description="昵称")
|
||||
email: Optional[str] = Field(None, max_length=50, description="电子邮箱")
|
||||
phone: Optional[str] = Field(None, max_length=11, description="手机号码")
|
||||
wx_openid: Optional[str] = Field(None, max_length=100, description="微信OpenID")
|
||||
avatar: Optional[str] = Field(None, max_length=255, description="头像路径")
|
||||
role_id: Optional[int] = Field(2, description="角色ID")
|
||||
|
||||
model_config = {"extra": "ignore"}
|
||||
|
||||
|
||||
class UserCreateRequest(UserBase):
|
||||
"""用户创建请求模型"""
|
||||
|
||||
password: str = Field(
|
||||
...,
|
||||
min_length=6,
|
||||
max_length=50,
|
||||
description="密码",
|
||||
json_schema_extra={
|
||||
"error_messages": {
|
||||
"min_length": "密码长度至少为6个字符",
|
||||
"max_length": "密码长度不能超过50个字符"
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class UserCreate(UserBase):
|
||||
"""用户创建模型"""
|
||||
|
||||
password_hash: str = Field(
|
||||
..., min_length=6, max_length=128, description="密码哈希值"
|
||||
)
|
||||
|
||||
|
||||
class UserUpdate(BaseModel):
|
||||
"""用户更新模型"""
|
||||
|
||||
username: str = Field(..., min_length=3, max_length=50, description="登录名")
|
||||
nickname: str = Field(..., max_length=50, description="昵称")
|
||||
email: Optional[str] = Field(None, max_length=50, description="电子邮箱")
|
||||
phone: Optional[str] = Field(None, max_length=11, description="手机号码")
|
||||
wx_openid: Optional[str] = Field(None, max_length=100, description="微信OpenID")
|
||||
avatar: Optional[str] = Field(None, max_length=255, description="头像路径")
|
||||
role_id: Optional[int] = Field(2, description="角色ID")
|
||||
password: Optional[str] = Field(None, max_length=128, description="密码")
|
||||
|
||||
|
||||
class UserLogin(BaseModel):
|
||||
"""用户登录模型"""
|
||||
|
||||
username: str = Field(..., description="用户名/邮箱/手机号")
|
||||
password: str = Field(..., description="密码")
|
||||
captcha_id: str = Field(..., description="验证码ID")
|
||||
captcha_code: str = Field(..., description="验证码")
|
||||
|
||||
|
||||
class UserResponse(BaseModel):
|
||||
"""用户响应模型"""
|
||||
|
||||
id: int = Field(..., description="用户ID")
|
||||
username: str = Field(..., description="登录名")
|
||||
nickname: str = Field(..., description="昵称")
|
||||
email: Optional[str] = Field(None, description="电子邮箱")
|
||||
phone: Optional[str] = Field(None, description="手机号码")
|
||||
wx_openid: Optional[str] = Field(None, description="微信OpenID")
|
||||
avatar: Optional[str] = Field(None, description="头像路径")
|
||||
role_id: int = Field(..., description="角色ID")
|
||||
createtime: datetime = Field(..., description="创建时间")
|
||||
updatetime: datetime = Field(..., description="更新时间")
|
||||
lastlogintime: Optional[datetime] = Field(None, description="最后登录时间")
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class Token(BaseModel):
|
||||
"""令牌模型"""
|
||||
|
||||
access_token: str = Field(..., description="访问令牌")
|
||||
token_type: str = Field(..., description="令牌类型")
|
||||
|
||||
|
||||
class TokenData(BaseModel):
|
||||
"""令牌数据模型"""
|
||||
|
||||
user_id: Optional[int] = None
|
||||
email: Optional[str] = None
|
||||
|
||||
|
||||
class RoleBase(BaseModel):
|
||||
"""角色基础模型"""
|
||||
|
||||
name: str = Field(..., max_length=50, description="角色名称")
|
||||
|
||||
|
||||
class RoleCreate(RoleBase):
|
||||
"""角色创建模型"""
|
||||
|
||||
creator: Optional[str] = Field(None, max_length=50, description="创建人")
|
||||
|
||||
|
||||
class RoleResponse(RoleBase):
|
||||
"""角色响应模型"""
|
||||
|
||||
id: int = Field(..., description="角色ID")
|
||||
createtime: datetime = Field(..., description="创建时间")
|
||||
updatetime: datetime = Field(..., description="更新时间")
|
||||
creator: Optional[str] = Field(None, description="创建人")
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
# 权限相关模型
|
||||
class PermissionBase(BaseModel):
|
||||
"""权限基础模型"""
|
||||
|
||||
name: str = Field(..., max_length=100, description="权限名称")
|
||||
code: str = Field(..., max_length=100, description="权限编码")
|
||||
parentid: int = Field(0, description="父权限ID(0表示顶级模块)")
|
||||
route: Optional[str] = Field(None, max_length=255, description="前端路由路径")
|
||||
description: Optional[str] = Field(None, max_length=255, description="权限描述")
|
||||
|
||||
|
||||
class PermissionCreate(PermissionBase):
|
||||
"""权限创建模型"""
|
||||
pass
|
||||
|
||||
|
||||
class PermissionResponse(PermissionBase):
|
||||
"""权限响应模型"""
|
||||
|
||||
id: int = Field(..., description="权限ID")
|
||||
createtime: datetime = Field(..., description="创建时间")
|
||||
updatetime: datetime = Field(..., description="更新时间")
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class PermissionTreeResponse(BaseModel):
|
||||
"""权限树响应模型"""
|
||||
|
||||
id: int = Field(..., description="权限ID")
|
||||
name: str = Field(..., description="权限名称")
|
||||
code: str = Field(..., description="权限编码")
|
||||
parentid: int = Field(..., description="父权限ID")
|
||||
route: Optional[str] = Field(None, description="前端路由路径")
|
||||
createtime: Optional[datetime] = Field(None, description="创建时间")
|
||||
updatetime: Optional[datetime] = Field(None, description="更新时间")
|
||||
description: Optional[str] = Field(None, description="权限描述")
|
||||
children: list = Field([], description="子权限列表")
|
||||
|
||||
|
||||
# 角色权限关联相关模型
|
||||
class RolePermissionAssign(BaseModel):
|
||||
"""角色权限分配模型"""
|
||||
|
||||
role_id: int = Field(..., description="角色ID")
|
||||
permission_ids: list[int] = Field(..., description="权限ID列表")
|
||||
|
||||
|
||||
class RolePermissionResponse(BaseModel):
|
||||
"""角色权限响应模型"""
|
||||
|
||||
role_id: int = Field(..., description="角色ID")
|
||||
role_name: str = Field(..., description="角色名称")
|
||||
permissions: list[PermissionResponse] = Field(..., description="权限列表")
|
||||
menu: list = Field([], description="菜单树形结构")
|
||||
|
||||
|
||||
# 系统设置相关模型
|
||||
class SystemSettingBase(BaseModel):
|
||||
"""系统设置基础模型"""
|
||||
|
||||
key: str = Field(..., max_length=100, description="设置键名")
|
||||
value: str = Field(..., max_length=2000, description="设置值")
|
||||
type: Optional[str] = Field("string", max_length=20, description="类型:string/image/select/boolean")
|
||||
options: Optional[str] = Field(None, max_length=500, description="下拉选项(用|分隔)")
|
||||
description: Optional[str] = Field(None, max_length=255, description="设置描述")
|
||||
|
||||
model_config = {"extra": "ignore"}
|
||||
|
||||
|
||||
class SystemSettingCreate(SystemSettingBase):
|
||||
"""系统设置创建模型"""
|
||||
pass
|
||||
|
||||
|
||||
class SystemSettingUpdate(SystemSettingBase):
|
||||
"""系统设置更新模型"""
|
||||
key: Optional[str] = Field(None, max_length=100, description="设置键名")
|
||||
|
||||
|
||||
class SystemSettingResponse(BaseModel):
|
||||
"""系统设置响应模型"""
|
||||
|
||||
id: int = Field(..., description="设置ID")
|
||||
key: str = Field(..., description="设置键名")
|
||||
value: str = Field(..., description="设置值")
|
||||
type: str = Field(..., description="类型")
|
||||
options: Optional[str] = Field(None, description="下拉选项")
|
||||
description: Optional[str] = Field(None, description="设置描述")
|
||||
createtime: datetime = Field(..., description="创建时间")
|
||||
updatetime: datetime = Field(..., description="更新时间")
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class LoginLogResponse(BaseModel):
|
||||
"""登录日志响应模型"""
|
||||
|
||||
id: int = Field(..., description="日志ID")
|
||||
username: str = Field(..., description="用户名")
|
||||
login_time: datetime = Field(..., description="登录时间")
|
||||
ip_address: Optional[str] = Field(None, description="IP地址")
|
||||
user_agent: Optional[str] = Field(None, description="User-Agent")
|
||||
status: str = Field(..., description="状态: success/failure")
|
||||
failure_reason: Optional[str] = Field(None, description="失败原因")
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class LoginLogSearch(BaseModel):
|
||||
"""登录日志搜索模型"""
|
||||
|
||||
username: Optional[str] = Field(None, description="用户名(模糊搜索)")
|
||||
status: Optional[str] = Field(None, description="状态")
|
||||
start_time: Optional[datetime] = Field(None, description="开始时间")
|
||||
end_time: Optional[datetime] = Field(None, description="结束时间")
|
||||
|
||||
|
||||
class LoginLogCleanRequest(BaseModel):
|
||||
"""登录日志清理请求模型"""
|
||||
|
||||
before_date: datetime = Field(..., description="清理此日期之前的日志")
|
||||
|
||||
|
||||
class OperationLogResponse(BaseModel):
|
||||
"""操作日志响应模型"""
|
||||
|
||||
id: int = Field(..., description="日志ID")
|
||||
user_id: int = Field(..., description="用户ID")
|
||||
username: str = Field(..., description="用户名")
|
||||
action_type: str = Field(..., description="操作类型: create/update/delete")
|
||||
target_type: str = Field(..., description="目标类型: user/role/permission/setting")
|
||||
target_id: Optional[int] = Field(None, description="目标ID")
|
||||
target_name: Optional[str] = Field(None, description="目标名称")
|
||||
detail: Optional[str] = Field(None, description="变更详情(JSON)")
|
||||
ip_address: Optional[str] = Field(None, description="IP地址")
|
||||
user_agent: Optional[str] = Field(None, description="User-Agent")
|
||||
createtime: datetime = Field(..., description="创建时间")
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class OperationLogSearch(BaseModel):
|
||||
"""操作日志搜索模型"""
|
||||
|
||||
username: Optional[str] = Field(None, description="用户名(模糊搜索)")
|
||||
action_type: Optional[str] = Field(None, description="操作类型")
|
||||
target_type: Optional[str] = Field(None, description="目标类型")
|
||||
start_time: Optional[datetime] = Field(None, description="开始时间")
|
||||
end_time: Optional[datetime] = Field(None, description="结束时间")
|
||||
|
||||
|
||||
# ========================================
|
||||
# 用户注册相关模型
|
||||
# ========================================
|
||||
|
||||
|
||||
class PhoneSendCodeRequest(BaseModel):
|
||||
"""发送短信验证码请求"""
|
||||
phone: str = Field(..., pattern=r"^\d{11}$", description="手机号(11位数字)")
|
||||
|
||||
|
||||
class PhoneSendCodeResponse(BaseModel):
|
||||
"""发送短信验证码响应"""
|
||||
message: str = "验证码已发送"
|
||||
expire_seconds: int = 300
|
||||
|
||||
|
||||
class PhoneRegisterRequest(BaseModel):
|
||||
"""手机号注册请求"""
|
||||
username: str = Field(..., min_length=3, max_length=50, description="用户名")
|
||||
phone: str = Field(..., pattern=r"^\d{11}$", description="手机号")
|
||||
code: str = Field(..., min_length=6, max_length=6, description="短信验证码")
|
||||
password: str = Field(..., min_length=6, max_length=50, description="密码")
|
||||
nickname: str = Field(..., max_length=50, description="昵称")
|
||||
|
||||
|
||||
class EmailRegisterInitRequest(BaseModel):
|
||||
"""邮箱注册初始化请求"""
|
||||
username: str = Field(..., min_length=3, max_length=50, description="用户名")
|
||||
email: str = Field(..., max_length=50, description="电子邮箱")
|
||||
password: str = Field(..., min_length=6, max_length=50, description="密码")
|
||||
nickname: str = Field(..., max_length=50, description="昵称")
|
||||
|
||||
|
||||
class EmailRegisterInitResponse(BaseModel):
|
||||
"""邮箱注册初始化响应"""
|
||||
message: str = "验证邮件已发送,请查收并点击链接完成注册"
|
||||
expire_minutes: int = 30
|
||||
|
||||
|
||||
class RegisterSuccessResponse(BaseModel):
|
||||
"""注册成功响应"""
|
||||
id: int
|
||||
username: str
|
||||
nickname: str
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Optional
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class UserBase(BaseModel):
|
||||
"""用户基础模型"""
|
||||
|
||||
username: str = Field(
|
||||
...,
|
||||
min_length=3,
|
||||
max_length=50,
|
||||
description="登录名",
|
||||
json_schema_extra={
|
||||
"error_messages": {
|
||||
"min_length": "用户名长度至少为3个字符",
|
||||
"max_length": "用户名长度不能超过50个字符"
|
||||
}
|
||||
}
|
||||
)
|
||||
nickname: str = Field(..., max_length=50, description="昵称")
|
||||
email: Optional[str] = Field(None, max_length=50, description="电子邮箱")
|
||||
phone: Optional[str] = Field(None, max_length=11, description="手机号码")
|
||||
wx_openid: Optional[str] = Field(None, max_length=100, description="微信OpenID")
|
||||
avatar: Optional[str] = Field(None, max_length=255, description="头像路径")
|
||||
role_id: Optional[int] = Field(2, description="角色ID")
|
||||
|
||||
model_config = {"extra": "ignore"}
|
||||
|
||||
|
||||
class UserCreateRequest(UserBase):
|
||||
"""用户创建请求模型"""
|
||||
|
||||
password: str = Field(
|
||||
...,
|
||||
min_length=6,
|
||||
max_length=50,
|
||||
description="密码",
|
||||
json_schema_extra={
|
||||
"error_messages": {
|
||||
"min_length": "密码长度至少为6个字符",
|
||||
"max_length": "密码长度不能超过50个字符"
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class UserCreate(UserBase):
|
||||
"""用户创建模型"""
|
||||
|
||||
password_hash: str = Field(
|
||||
..., min_length=6, max_length=128, description="密码哈希值"
|
||||
)
|
||||
|
||||
|
||||
class UserUpdate(BaseModel):
|
||||
"""用户更新模型"""
|
||||
|
||||
username: str = Field(..., min_length=3, max_length=50, description="登录名")
|
||||
nickname: str = Field(..., max_length=50, description="昵称")
|
||||
email: Optional[str] = Field(None, max_length=50, description="电子邮箱")
|
||||
phone: Optional[str] = Field(None, max_length=11, description="手机号码")
|
||||
wx_openid: Optional[str] = Field(None, max_length=100, description="微信OpenID")
|
||||
avatar: Optional[str] = Field(None, max_length=255, description="头像路径")
|
||||
role_id: Optional[int] = Field(2, description="角色ID")
|
||||
password: Optional[str] = Field(None, max_length=128, description="密码")
|
||||
|
||||
|
||||
class UserLogin(BaseModel):
|
||||
"""用户登录模型"""
|
||||
|
||||
username: str = Field(..., description="用户名/邮箱/手机号")
|
||||
password: str = Field(..., description="密码")
|
||||
captcha_id: str = Field(..., description="验证码ID")
|
||||
captcha_code: str = Field(..., description="验证码")
|
||||
|
||||
|
||||
class UserResponse(BaseModel):
|
||||
"""用户响应模型"""
|
||||
|
||||
id: int = Field(..., description="用户ID")
|
||||
username: str = Field(..., description="登录名")
|
||||
nickname: str = Field(..., description="昵称")
|
||||
email: Optional[str] = Field(None, description="电子邮箱")
|
||||
phone: Optional[str] = Field(None, description="手机号码")
|
||||
wx_openid: Optional[str] = Field(None, description="微信OpenID")
|
||||
avatar: Optional[str] = Field(None, description="头像路径")
|
||||
role_id: int = Field(..., description="角色ID")
|
||||
createtime: datetime = Field(..., description="创建时间")
|
||||
updatetime: datetime = Field(..., description="更新时间")
|
||||
lastlogintime: Optional[datetime] = Field(None, description="最后登录时间")
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class Token(BaseModel):
|
||||
"""令牌模型"""
|
||||
|
||||
access_token: str = Field(..., description="访问令牌")
|
||||
token_type: str = Field(..., description="令牌类型")
|
||||
|
||||
|
||||
class TokenData(BaseModel):
|
||||
"""令牌数据模型"""
|
||||
|
||||
user_id: Optional[int] = None
|
||||
email: Optional[str] = None
|
||||
|
||||
|
||||
class RoleBase(BaseModel):
|
||||
"""角色基础模型"""
|
||||
|
||||
name: str = Field(..., max_length=50, description="角色名称")
|
||||
|
||||
|
||||
class RoleCreate(RoleBase):
|
||||
"""角色创建模型"""
|
||||
|
||||
creator: Optional[str] = Field(None, max_length=50, description="创建人")
|
||||
|
||||
|
||||
class RoleResponse(RoleBase):
|
||||
"""角色响应模型"""
|
||||
|
||||
id: int = Field(..., description="角色ID")
|
||||
createtime: datetime = Field(..., description="创建时间")
|
||||
updatetime: datetime = Field(..., description="更新时间")
|
||||
creator: Optional[str] = Field(None, description="创建人")
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
# 权限相关模型
|
||||
class PermissionBase(BaseModel):
|
||||
"""权限基础模型"""
|
||||
|
||||
name: str = Field(..., max_length=100, description="权限名称")
|
||||
code: str = Field(..., max_length=100, description="权限编码")
|
||||
parentid: int = Field(0, description="父权限ID(0表示顶级模块)")
|
||||
route: Optional[str] = Field(None, max_length=255, description="前端路由路径")
|
||||
description: Optional[str] = Field(None, max_length=255, description="权限描述")
|
||||
sort: int = Field(0, description="排序号(升序排列)")
|
||||
|
||||
|
||||
class PermissionCreate(PermissionBase):
|
||||
"""权限创建模型"""
|
||||
pass
|
||||
|
||||
|
||||
class PermissionResponse(PermissionBase):
|
||||
"""权限响应模型"""
|
||||
|
||||
id: int = Field(..., description="权限ID")
|
||||
createtime: datetime = Field(..., description="创建时间")
|
||||
updatetime: datetime = Field(..., description="更新时间")
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class PermissionTreeResponse(BaseModel):
|
||||
"""权限树响应模型"""
|
||||
|
||||
id: int = Field(..., description="权限ID")
|
||||
name: str = Field(..., description="权限名称")
|
||||
code: str = Field(..., description="权限编码")
|
||||
parentid: int = Field(..., description="父权限ID")
|
||||
route: Optional[str] = Field(None, description="前端路由路径")
|
||||
createtime: Optional[datetime] = Field(None, description="创建时间")
|
||||
updatetime: Optional[datetime] = Field(None, description="更新时间")
|
||||
description: Optional[str] = Field(None, description="权限描述")
|
||||
sort: int = Field(0, description="排序号")
|
||||
children: list = Field([], description="子权限列表")
|
||||
|
||||
|
||||
# 角色权限关联相关模型
|
||||
class RolePermissionAssign(BaseModel):
|
||||
"""角色权限分配模型"""
|
||||
|
||||
role_id: int = Field(..., description="角色ID")
|
||||
permission_ids: list[int] = Field(..., description="权限ID列表")
|
||||
|
||||
|
||||
class RolePermissionResponse(BaseModel):
|
||||
"""角色权限响应模型"""
|
||||
|
||||
role_id: int = Field(..., description="角色ID")
|
||||
role_name: str = Field(..., description="角色名称")
|
||||
permissions: list[PermissionResponse] = Field(..., description="权限列表")
|
||||
menu: list = Field([], description="菜单树形结构")
|
||||
|
||||
|
||||
# 系统设置相关模型
|
||||
class SystemSettingBase(BaseModel):
|
||||
"""系统设置基础模型"""
|
||||
|
||||
key: str = Field(..., max_length=100, description="设置键名")
|
||||
value: str = Field(..., max_length=2000, description="设置值")
|
||||
type: Optional[str] = Field("string", max_length=20, description="类型:string/image/select/boolean")
|
||||
options: Optional[str] = Field(None, max_length=500, description="下拉选项(用|分隔)")
|
||||
description: Optional[str] = Field(None, max_length=255, description="设置描述")
|
||||
|
||||
model_config = {"extra": "ignore"}
|
||||
|
||||
|
||||
class SystemSettingCreate(SystemSettingBase):
|
||||
"""系统设置创建模型"""
|
||||
pass
|
||||
|
||||
|
||||
class SystemSettingUpdate(SystemSettingBase):
|
||||
"""系统设置更新模型"""
|
||||
key: Optional[str] = Field(None, max_length=100, description="设置键名")
|
||||
|
||||
|
||||
class SystemSettingResponse(BaseModel):
|
||||
"""系统设置响应模型"""
|
||||
|
||||
id: int = Field(..., description="设置ID")
|
||||
key: str = Field(..., description="设置键名")
|
||||
value: str = Field(..., description="设置值")
|
||||
type: str = Field(..., description="类型")
|
||||
options: Optional[str] = Field(None, description="下拉选项")
|
||||
description: Optional[str] = Field(None, description="设置描述")
|
||||
createtime: datetime = Field(..., description="创建时间")
|
||||
updatetime: datetime = Field(..., description="更新时间")
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class LoginLogResponse(BaseModel):
|
||||
"""登录日志响应模型"""
|
||||
|
||||
id: int = Field(..., description="日志ID")
|
||||
username: str = Field(..., description="用户名")
|
||||
login_time: datetime = Field(..., description="登录时间")
|
||||
ip_address: Optional[str] = Field(None, description="IP地址")
|
||||
user_agent: Optional[str] = Field(None, description="User-Agent")
|
||||
status: str = Field(..., description="状态: success/failure")
|
||||
failure_reason: Optional[str] = Field(None, description="失败原因")
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class LoginLogSearch(BaseModel):
|
||||
"""登录日志搜索模型"""
|
||||
|
||||
username: Optional[str] = Field(None, description="用户名(模糊搜索)")
|
||||
status: Optional[str] = Field(None, description="状态")
|
||||
start_time: Optional[datetime] = Field(None, description="开始时间")
|
||||
end_time: Optional[datetime] = Field(None, description="结束时间")
|
||||
|
||||
|
||||
class LoginLogCleanRequest(BaseModel):
|
||||
"""登录日志清理请求模型"""
|
||||
|
||||
before_date: datetime = Field(..., description="清理此日期之前的日志")
|
||||
|
||||
|
||||
class OperationLogResponse(BaseModel):
|
||||
"""操作日志响应模型"""
|
||||
|
||||
id: int = Field(..., description="日志ID")
|
||||
user_id: int = Field(..., description="用户ID")
|
||||
username: str = Field(..., description="用户名")
|
||||
action_type: str = Field(..., description="操作类型: create/update/delete")
|
||||
target_type: str = Field(..., description="目标类型: user/role/permission/setting")
|
||||
target_id: Optional[int] = Field(None, description="目标ID")
|
||||
target_name: Optional[str] = Field(None, description="目标名称")
|
||||
detail: Optional[str] = Field(None, description="变更详情(JSON)")
|
||||
ip_address: Optional[str] = Field(None, description="IP地址")
|
||||
user_agent: Optional[str] = Field(None, description="User-Agent")
|
||||
createtime: datetime = Field(..., description="创建时间")
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class OperationLogSearch(BaseModel):
|
||||
"""操作日志搜索模型"""
|
||||
|
||||
username: Optional[str] = Field(None, description="用户名(模糊搜索)")
|
||||
action_type: Optional[str] = Field(None, description="操作类型")
|
||||
target_type: Optional[str] = Field(None, description="目标类型")
|
||||
start_time: Optional[datetime] = Field(None, description="开始时间")
|
||||
end_time: Optional[datetime] = Field(None, description="结束时间")
|
||||
|
||||
|
||||
# ========================================
|
||||
# 用户注册相关模型
|
||||
# ========================================
|
||||
|
||||
|
||||
class PhoneSendCodeRequest(BaseModel):
|
||||
"""发送短信验证码请求"""
|
||||
phone: str = Field(..., pattern=r"^\d{11}$", description="手机号(11位数字)")
|
||||
|
||||
|
||||
class PhoneSendCodeResponse(BaseModel):
|
||||
"""发送短信验证码响应"""
|
||||
message: str = "验证码已发送"
|
||||
expire_seconds: int = 300
|
||||
|
||||
|
||||
class PhoneRegisterRequest(BaseModel):
|
||||
"""手机号注册请求"""
|
||||
username: str = Field(..., min_length=3, max_length=50, description="用户名")
|
||||
phone: str = Field(..., pattern=r"^\d{11}$", description="手机号")
|
||||
code: str = Field(..., min_length=6, max_length=6, description="短信验证码")
|
||||
password: str = Field(..., min_length=6, max_length=50, description="密码")
|
||||
nickname: str = Field(..., max_length=50, description="昵称")
|
||||
|
||||
|
||||
class EmailRegisterInitRequest(BaseModel):
|
||||
"""邮箱注册初始化请求"""
|
||||
username: str = Field(..., min_length=3, max_length=50, description="用户名")
|
||||
email: str = Field(..., max_length=50, description="电子邮箱")
|
||||
password: str = Field(..., min_length=6, max_length=50, description="密码")
|
||||
nickname: str = Field(..., max_length=50, description="昵称")
|
||||
|
||||
|
||||
class EmailRegisterInitResponse(BaseModel):
|
||||
"""邮箱注册初始化响应"""
|
||||
message: str = "验证邮件已发送,请查收并点击链接完成注册"
|
||||
expire_minutes: int = 30
|
||||
|
||||
|
||||
class RegisterSuccessResponse(BaseModel):
|
||||
"""注册成功响应"""
|
||||
id: int
|
||||
username: str
|
||||
nickname: str
|
||||
message: str = "注册成功"
|
||||
@ -1 +1 @@
|
||||
import{f as K,u as O,p as y,g as Q,E as X,q as d,w as a,k as Y,l as Z,h as f,r as o,i as p,s as ee,v as te,x as ae,d as c,a as n,c as _,y as b,t as l,z as I,F as k,A as N,B as w,b as S,C as T,D as se,o as s,_ as ne}from"./index-ChTrSGCb.js";const oe={class:"brand"},ce=["src"],ie={class:"brand-text"},le={class:"right"},re={class:"nick"},ue={class:"page-title-bar"},de={class:"page-title"},_e={class:"page-content"},fe={key:0,class:"footer"},pe={href:"https://beian.miit.gov.cn",target:"_blank"},me={key:0},ge=K({__name:"AdminLayout",setup(he){const m=Y(),x=Z(),i=O(),L=p(""),g=p(""),h=p(""),v=p(""),V=T(()=>m.path),$=T(()=>m.meta.title||""),R={perm:y(ae),system:y(te),log:y(ee)};function C(r){for(const e in R)if(r.startsWith(e))return R[e];return se}async function D(){let e=await f.get("/api/settings/public?setting_id=1");e.data&&(L.value=e.data.value)}async function E(){try{let e=await f.get("/api/settings/public?setting_id=2");e.data&&(h.value=e.data.value)}catch{}}async function M(){try{let e=await f.get("/api/settings/public?setting_id=3");e.data&&(v.value=e.data.value)}catch{}}async function P(){try{let e=await f.get("/api/settings/public?key=系统Logo");e.data&&e.data.value&&(g.value=e.data.value)}catch{}}Q(async()=>{try{await D(),await P(),await E(),await M(),i.user||await i.fetchProfile(),await i.fetchPermissions()}catch{X.error("获取用户信息失败,请重新登录"),await i.logout(),x.replace({name:"admin-login",query:{redirect:m.fullPath}})}});async function q(){await i.logout(),x.push({name:"admin-login"})}return(r,e)=>{const U=o("el-icon"),B=o("el-menu-item"),F=o("el-sub-menu"),z=o("el-menu"),W=o("el-aside"),j=o("el-button"),G=o("el-header"),H=o("router-view"),J=o("el-main"),A=o("el-container");return s(),d(A,{class:"admin-root"},{default:a(()=>[c(W,{width:"220px",class:"aside"},{default:a(()=>[n("div",oe,[g.value?(s(),_("img",{key:0,src:g.value,class:"brand-logo"},null,8,ce)):b("",!0),n("span",ie,l(L.value),1)]),c(z,{"default-active":V.value,router:"","background-color":"#0f172a","text-color":"#e2e8f0","active-text-color":"#38bdf8"},{default:a(()=>[(s(!0),_(k,null,I(w(i).menu,t=>(s(),_(k,{key:t.id},[t.children&&t.children.length>0?(s(),d(F,{key:0,index:t.route},{title:a(()=>[c(U,null,{default:a(()=>[(s(),d(N(C(t.code))))]),_:2},1024),n("span",null,l(t.name),1)]),default:a(()=>[(s(!0),_(k,null,I(t.children,u=>(s(),d(B,{key:u.id,index:u.route},{default:a(()=>[n("span",null,l(u.name),1)]),_:2},1032,["index"]))),128))]),_:2},1032,["index"])):(s(),d(B,{key:1,index:t.route},{default:a(()=>[c(U,null,{default:a(()=>[(s(),d(N(C(t.code))))]),_:2},1024),n("span",null,l(t.name),1)]),_:2},1032,["index"]))],64))),128))]),_:1},8,["default-active"])]),_:1}),c(A,null,{default:a(()=>[c(G,{class:"header"},{default:a(()=>{var t,u;return[n("div",le,[n("span",re,l(((t=w(i).user)==null?void 0:t.nickname)||((u=w(i).user)==null?void 0:u.username)),1),c(j,{type:"primary",link:"",onClick:q},{default:a(()=>[...e[0]||(e[0]=[S("退出",-1)])]),_:1})])]}),_:1}),c(J,{class:"main"},{default:a(()=>[n("div",ue,[n("h1",de,l($.value),1)]),n("div",_e,[c(H)]),h.value?(s(),_("div",fe,[S(l(h.value)+" ",1),n("a",pe,[v.value?(s(),_("span",me," | "+l(v.value),1)):b("",!0)])])):b("",!0)]),_:1})]),_:1})]),_:1})}}}),ye=ne(ge,[["__scopeId","data-v-627faa7f"]]);export{ye as default};
|
||||
import{f as K,u as O,p as y,g as Q,E as X,q as d,w as a,k as Y,l as Z,h as f,r as o,i as p,s as ee,v as te,x as ae,d as c,a as n,c as _,y as b,t as l,z as I,F as k,A as N,B as w,b as S,C as T,D as se,o as s,_ as ne}from"./index-rb7Oshfn.js";const oe={class:"brand"},ce=["src"],ie={class:"brand-text"},le={class:"right"},re={class:"nick"},ue={class:"page-title-bar"},de={class:"page-title"},_e={class:"page-content"},fe={key:0,class:"footer"},pe={href:"https://beian.miit.gov.cn",target:"_blank"},me={key:0},ge=K({__name:"AdminLayout",setup(he){const m=Y(),x=Z(),i=O(),L=p(""),g=p(""),h=p(""),v=p(""),V=T(()=>m.path),$=T(()=>m.meta.title||""),R={perm:y(ae),system:y(te),log:y(ee)};function C(r){for(const e in R)if(r.startsWith(e))return R[e];return se}async function D(){let e=await f.get("/api/settings/public?setting_id=1");e.data&&(L.value=e.data.value)}async function E(){try{let e=await f.get("/api/settings/public?setting_id=2");e.data&&(h.value=e.data.value)}catch{}}async function M(){try{let e=await f.get("/api/settings/public?setting_id=3");e.data&&(v.value=e.data.value)}catch{}}async function P(){try{let e=await f.get("/api/settings/public?key=系统Logo");e.data&&e.data.value&&(g.value=e.data.value)}catch{}}Q(async()=>{try{await D(),await P(),await E(),await M(),i.user||await i.fetchProfile(),await i.fetchPermissions()}catch{X.error("获取用户信息失败,请重新登录"),await i.logout(),x.replace({name:"admin-login",query:{redirect:m.fullPath}})}});async function q(){await i.logout(),x.push({name:"admin-login"})}return(r,e)=>{const U=o("el-icon"),B=o("el-menu-item"),F=o("el-sub-menu"),z=o("el-menu"),W=o("el-aside"),j=o("el-button"),G=o("el-header"),H=o("router-view"),J=o("el-main"),A=o("el-container");return s(),d(A,{class:"admin-root"},{default:a(()=>[c(W,{width:"220px",class:"aside"},{default:a(()=>[n("div",oe,[g.value?(s(),_("img",{key:0,src:g.value,class:"brand-logo"},null,8,ce)):b("",!0),n("span",ie,l(L.value),1)]),c(z,{"default-active":V.value,router:"","background-color":"#0f172a","text-color":"#e2e8f0","active-text-color":"#38bdf8"},{default:a(()=>[(s(!0),_(k,null,I(w(i).menu,t=>(s(),_(k,{key:t.id},[t.children&&t.children.length>0?(s(),d(F,{key:0,index:t.route},{title:a(()=>[c(U,null,{default:a(()=>[(s(),d(N(C(t.code))))]),_:2},1024),n("span",null,l(t.name),1)]),default:a(()=>[(s(!0),_(k,null,I(t.children,u=>(s(),d(B,{key:u.id,index:u.route},{default:a(()=>[n("span",null,l(u.name),1)]),_:2},1032,["index"]))),128))]),_:2},1032,["index"])):(s(),d(B,{key:1,index:t.route},{default:a(()=>[c(U,null,{default:a(()=>[(s(),d(N(C(t.code))))]),_:2},1024),n("span",null,l(t.name),1)]),_:2},1032,["index"]))],64))),128))]),_:1},8,["default-active"])]),_:1}),c(A,null,{default:a(()=>[c(G,{class:"header"},{default:a(()=>{var t,u;return[n("div",le,[n("span",re,l(((t=w(i).user)==null?void 0:t.nickname)||((u=w(i).user)==null?void 0:u.username)),1),c(j,{type:"primary",link:"",onClick:q},{default:a(()=>[...e[0]||(e[0]=[S("退出",-1)])]),_:1})])]}),_:1}),c(J,{class:"main"},{default:a(()=>[n("div",ue,[n("h1",de,l($.value),1)]),n("div",_e,[c(H)]),h.value?(s(),_("div",fe,[S(l(h.value)+" ",1),n("a",pe,[v.value?(s(),_("span",me," | "+l(v.value),1)):b("",!0)])])):b("",!0)]),_:1})]),_:1})]),_:1})}}}),ye=ne(ge,[["__scopeId","data-v-627faa7f"]]);export{ye as default};
|
||||
@ -1 +1 @@
|
||||
import{G as y,O as M,n as w,f as V,H as Y,g as D,c as H,d as n,w as l,F as O,i as p,r as s,I as R,B as E,b as _,E as m,P as L,o as I}from"./index-ChTrSGCb.js";function P(f){return()=>({crudOptions:{pagination:{show:!1},search:{show:!0,options:{layout:"inline"},expand:!1},request:{pageRequest:async e=>{const{data:t}=await w.post("/login-logs/search",e);return t},transformQuery:({form:e})=>{const t={};return e!=null&&e.username&&(t.username=e.username),e!=null&&e.status&&(t.status=e.status),t},transformRes:({res:e})=>{const t=Array.isArray(e)?e:[];return{currentPage:1,pageSize:Math.max(t.length,1),total:t.length,records:t}}},rowHandle:{show:!1},toolbar:{buttons:{clean:{text:"清理旧日志",type:"warning",click:()=>f.onClean()}}},columns:{id:{title:"ID",key:"id",type:"number",column:{width:70},form:{show:!1}},username:{title:"用户名",key:"username",type:"text",search:{show:!0}},status:{title:"状态",key:"status",type:"dict-select",search:{show:!0},dict:M({data:[{value:"success",label:"成功"},{value:"failure",label:"失败"}],value:"value",label:"label"}),column:{width:80}},failure_reason:{title:"失败原因",key:"failure_reason",type:"text",column:{width:160}},ip_address:{title:"IP地址",key:"ip_address",type:"text",column:{width:150}},user_agent:{title:"User-Agent",key:"user_agent",type:"text",column:{width:300}},login_time:{title:"登录时间",key:"login_time",type:"text",column:{width:170},form:{show:!1},valueBuilder({value:e,row:t,key:u}){!t||e==null||(t[u]=y(e).format("YYYY-MM-DD HH:mm:ss"))}}}}})}const S=V({__name:"LoginLogManage",setup(f){const{crudBinding:e,crudRef:t,crudExpose:u}=Y({createCrudOptions:P({onClean:()=>{o.value=!0}})}),o=p(!1),r=p(),d=p(!1);async function h(){if(!r.value){m.warning("请选择要清理的日期");return}try{await L.confirm(`确定要清理 ${y(r.value).format("YYYY-MM-DD HH:mm:ss")} 之前的所有登录日志吗?此操作不可撤销。`,"确认清理",{confirmButtonText:"确认清理",cancelButtonText:"取消",type:"warning"})}catch{return}d.value=!0;try{const{data:c}=await w.delete("/login-logs/clean",{data:{before_date:r.value.toISOString()}});m.success(c.message||"清理成功"),o.value=!1,u.doRefresh()}catch{m.error("清理失败")}finally{d.value=!1}}return D(()=>{u.doRefresh()}),(c,a)=>{const v=s("fs-crud"),x=s("fs-page"),k=s("el-date-picker"),b=s("el-form-item"),C=s("el-form"),g=s("el-button"),B=s("el-dialog");return I(),H(O,null,[n(x,null,{default:l(()=>[n(v,R({ref_key:"crudRef",ref:t},E(e)),null,16)]),_:1}),n(B,{modelValue:o.value,"onUpdate:modelValue":a[2]||(a[2]=i=>o.value=i),title:"清理旧日志",width:"420px"},{footer:l(()=>[n(g,{onClick:a[1]||(a[1]=i=>o.value=!1)},{default:l(()=>[...a[3]||(a[3]=[_("取消",-1)])]),_:1}),n(g,{type:"warning",loading:d.value,onClick:h},{default:l(()=>[...a[4]||(a[4]=[_("确认清理",-1)])]),_:1},8,["loading"])]),default:l(()=>[n(C,null,{default:l(()=>[n(b,{label:"清理此日期之前的日志"},{default:l(()=>[n(k,{modelValue:r.value,"onUpdate:modelValue":a[0]||(a[0]=i=>r.value=i),type:"datetime",placeholder:"选择日期时间",style:{width:"100%"}},null,8,["modelValue"])]),_:1})]),_:1})]),_:1},8,["modelValue"])],64)}}});export{S as default};
|
||||
import{G as y,O as M,n as w,f as V,H as Y,g as D,c as H,d as n,w as l,F as O,i as p,r as s,I as R,B as E,b as _,E as m,P as L,o as I}from"./index-rb7Oshfn.js";function P(f){return()=>({crudOptions:{pagination:{show:!1},search:{show:!0,options:{layout:"inline"},expand:!1},request:{pageRequest:async e=>{const{data:t}=await w.post("/login-logs/search",e);return t},transformQuery:({form:e})=>{const t={};return e!=null&&e.username&&(t.username=e.username),e!=null&&e.status&&(t.status=e.status),t},transformRes:({res:e})=>{const t=Array.isArray(e)?e:[];return{currentPage:1,pageSize:Math.max(t.length,1),total:t.length,records:t}}},rowHandle:{show:!1},toolbar:{buttons:{clean:{text:"清理旧日志",type:"warning",click:()=>f.onClean()}}},columns:{id:{title:"ID",key:"id",type:"number",column:{width:70},form:{show:!1}},username:{title:"用户名",key:"username",type:"text",search:{show:!0}},status:{title:"状态",key:"status",type:"dict-select",search:{show:!0},dict:M({data:[{value:"success",label:"成功"},{value:"failure",label:"失败"}],value:"value",label:"label"}),column:{width:80}},failure_reason:{title:"失败原因",key:"failure_reason",type:"text",column:{width:160}},ip_address:{title:"IP地址",key:"ip_address",type:"text",column:{width:150}},user_agent:{title:"User-Agent",key:"user_agent",type:"text",column:{width:300}},login_time:{title:"登录时间",key:"login_time",type:"text",column:{width:170},form:{show:!1},valueBuilder({value:e,row:t,key:u}){!t||e==null||(t[u]=y(e).format("YYYY-MM-DD HH:mm:ss"))}}}}})}const S=V({__name:"LoginLogManage",setup(f){const{crudBinding:e,crudRef:t,crudExpose:u}=Y({createCrudOptions:P({onClean:()=>{o.value=!0}})}),o=p(!1),r=p(),d=p(!1);async function h(){if(!r.value){m.warning("请选择要清理的日期");return}try{await L.confirm(`确定要清理 ${y(r.value).format("YYYY-MM-DD HH:mm:ss")} 之前的所有登录日志吗?此操作不可撤销。`,"确认清理",{confirmButtonText:"确认清理",cancelButtonText:"取消",type:"warning"})}catch{return}d.value=!0;try{const{data:c}=await w.delete("/login-logs/clean",{data:{before_date:r.value.toISOString()}});m.success(c.message||"清理成功"),o.value=!1,u.doRefresh()}catch{m.error("清理失败")}finally{d.value=!1}}return D(()=>{u.doRefresh()}),(c,a)=>{const v=s("fs-crud"),x=s("fs-page"),k=s("el-date-picker"),b=s("el-form-item"),C=s("el-form"),g=s("el-button"),B=s("el-dialog");return I(),H(O,null,[n(x,null,{default:l(()=>[n(v,R({ref_key:"crudRef",ref:t},E(e)),null,16)]),_:1}),n(B,{modelValue:o.value,"onUpdate:modelValue":a[2]||(a[2]=i=>o.value=i),title:"清理旧日志",width:"420px"},{footer:l(()=>[n(g,{onClick:a[1]||(a[1]=i=>o.value=!1)},{default:l(()=>[...a[3]||(a[3]=[_("取消",-1)])]),_:1}),n(g,{type:"warning",loading:d.value,onClick:h},{default:l(()=>[...a[4]||(a[4]=[_("确认清理",-1)])]),_:1},8,["loading"])]),default:l(()=>[n(C,null,{default:l(()=>[n(b,{label:"清理此日期之前的日志"},{default:l(()=>[n(k,{modelValue:r.value,"onUpdate:modelValue":a[0]||(a[0]=i=>r.value=i),type:"datetime",placeholder:"选择日期时间",style:{width:"100%"}},null,8,["modelValue"])]),_:1})]),_:1})]),_:1},8,["modelValue"])],64)}}});export{S as default};
|
||||
@ -1 +1 @@
|
||||
import{f as q,u as I,g as j,c as h,d as o,w as r,h as k,E as g,r as u,i,j as B,k as D,l as E,o as w,m as M,a as v,b as N,t as O,n as P,_ as $}from"./index-ChTrSGCb.js";const A={class:"login-wrap"},W={class:"card-title"},X={class:"captcha-row"},z=["src"],F={key:1,class:"captcha-placeholder"},G=q({__name:"LoginView",setup(H){const x=D(),R=E(),m=I(),b=i(),_=i(!1),l=i(""),p=i(""),y=i(""),V="/api",a=B({username:"",password:"",captcha_code:""}),U={username:[{required:!0,message:"请输入用户名",trigger:"blur"}],password:[{required:!0,message:"请输入密码",trigger:"blur"}],captcha_code:[{required:!0,message:"请输入验证码",trigger:"blur"}]};async function C(){let t=await k.get(`${V}/settings/public?setting_id=1`);t.data&&(y.value=t.data.value)}async function f(){l.value.startsWith("blob:")&&URL.revokeObjectURL(l.value),l.value="",p.value="";const t=await k.get(`${V}/users/captcha`,{responseType:"blob",headers:{}}),e=t.headers["x-captcha-id"]||t.headers["X-Captcha-ID"];if(!e){g.error("未获取到验证码 ID,请检查后端 CORS expose_headers");return}p.value=e,l.value=URL.createObjectURL(t.data),a.captcha_code=""}async function L(){var e,n;if(!p.value){g.warning("请先加载验证码");return}const t=b.value;if(t){try{await t.validate()}catch{return}_.value=!0;try{const{data:s}=await P.post("/users/login",{username:a.username,password:a.password,captcha_id:p.value,captcha_code:a.captcha_code});m.setToken(s.access_token),await m.fetchProfile(),await m.fetchPermissions();const c=x.query.redirect||"/admin";await R.replace(c)}catch(s){const c=(n=(e=s==null?void 0:s.response)==null?void 0:e.data)==null?void 0:n.detail;g.error(typeof c=="string"?c:"登录失败"),await f()}finally{_.value=!1}}}return j(()=>{f(),C()}),(t,e)=>{const n=u("el-input"),s=u("el-form-item"),c=u("el-button"),S=u("el-form"),T=u("el-card");return w(),h("div",A,[o(T,{class:"card",shadow:"hover"},{header:r(()=>[v("span",W,O(y.value),1)]),default:r(()=>[o(S,{ref_key:"formRef",ref:b,model:a,rules:U,"label-width":"88px",onSubmit:e[3]||(e[3]=M(()=>{},["prevent"]))},{default:r(()=>[o(s,{label:"用户名",prop:"username"},{default:r(()=>[o(n,{modelValue:a.username,"onUpdate:modelValue":e[0]||(e[0]=d=>a.username=d),autocomplete:"username"},null,8,["modelValue"])]),_:1}),o(s,{label:"密码",prop:"password"},{default:r(()=>[o(n,{modelValue:a.password,"onUpdate:modelValue":e[1]||(e[1]=d=>a.password=d),type:"password","show-password":"",autocomplete:"current-password"},null,8,["modelValue"])]),_:1}),o(s,{label:"验证码",prop:"captcha_code"},{default:r(()=>[v("div",X,[o(n,{modelValue:a.captcha_code,"onUpdate:modelValue":e[2]||(e[2]=d=>a.captcha_code=d),maxlength:"8",placeholder:"请输入验证码"},null,8,["modelValue"]),v("div",{class:"captcha-img",onClick:f},[l.value?(w(),h("img",{key:0,src:l.value,alt:"captcha"},null,8,z)):(w(),h("span",F,"点击获取"))])])]),_:1}),o(s,null,{default:r(()=>[o(c,{type:"primary",loading:_.value,style:{width:"100%"},onClick:L},{default:r(()=>[...e[4]||(e[4]=[N(" 登录 ",-1)])]),_:1},8,["loading"])]),_:1})]),_:1},8,["model"])]),_:1})])}}}),K=$(G,[["__scopeId","data-v-3d2542ad"]]);export{K as default};
|
||||
import{f as q,u as I,g as j,c as h,d as o,w as r,h as k,E as g,r as u,i,j as B,k as D,l as E,o as w,m as M,a as v,b as N,t as O,n as P,_ as $}from"./index-rb7Oshfn.js";const A={class:"login-wrap"},W={class:"card-title"},X={class:"captcha-row"},z=["src"],F={key:1,class:"captcha-placeholder"},G=q({__name:"LoginView",setup(H){const x=D(),R=E(),m=I(),b=i(),_=i(!1),l=i(""),p=i(""),y=i(""),V="/api",a=B({username:"",password:"",captcha_code:""}),U={username:[{required:!0,message:"请输入用户名",trigger:"blur"}],password:[{required:!0,message:"请输入密码",trigger:"blur"}],captcha_code:[{required:!0,message:"请输入验证码",trigger:"blur"}]};async function C(){let t=await k.get(`${V}/settings/public?setting_id=1`);t.data&&(y.value=t.data.value)}async function f(){l.value.startsWith("blob:")&&URL.revokeObjectURL(l.value),l.value="",p.value="";const t=await k.get(`${V}/users/captcha`,{responseType:"blob",headers:{}}),e=t.headers["x-captcha-id"]||t.headers["X-Captcha-ID"];if(!e){g.error("未获取到验证码 ID,请检查后端 CORS expose_headers");return}p.value=e,l.value=URL.createObjectURL(t.data),a.captcha_code=""}async function L(){var e,n;if(!p.value){g.warning("请先加载验证码");return}const t=b.value;if(t){try{await t.validate()}catch{return}_.value=!0;try{const{data:s}=await P.post("/users/login",{username:a.username,password:a.password,captcha_id:p.value,captcha_code:a.captcha_code});m.setToken(s.access_token),await m.fetchProfile(),await m.fetchPermissions();const c=x.query.redirect||"/admin";await R.replace(c)}catch(s){const c=(n=(e=s==null?void 0:s.response)==null?void 0:e.data)==null?void 0:n.detail;g.error(typeof c=="string"?c:"登录失败"),await f()}finally{_.value=!1}}}return j(()=>{f(),C()}),(t,e)=>{const n=u("el-input"),s=u("el-form-item"),c=u("el-button"),S=u("el-form"),T=u("el-card");return w(),h("div",A,[o(T,{class:"card",shadow:"hover"},{header:r(()=>[v("span",W,O(y.value),1)]),default:r(()=>[o(S,{ref_key:"formRef",ref:b,model:a,rules:U,"label-width":"88px",onSubmit:e[3]||(e[3]=M(()=>{},["prevent"]))},{default:r(()=>[o(s,{label:"用户名",prop:"username"},{default:r(()=>[o(n,{modelValue:a.username,"onUpdate:modelValue":e[0]||(e[0]=d=>a.username=d),autocomplete:"username"},null,8,["modelValue"])]),_:1}),o(s,{label:"密码",prop:"password"},{default:r(()=>[o(n,{modelValue:a.password,"onUpdate:modelValue":e[1]||(e[1]=d=>a.password=d),type:"password","show-password":"",autocomplete:"current-password"},null,8,["modelValue"])]),_:1}),o(s,{label:"验证码",prop:"captcha_code"},{default:r(()=>[v("div",X,[o(n,{modelValue:a.captcha_code,"onUpdate:modelValue":e[2]||(e[2]=d=>a.captcha_code=d),maxlength:"8",placeholder:"请输入验证码"},null,8,["modelValue"]),v("div",{class:"captcha-img",onClick:f},[l.value?(w(),h("img",{key:0,src:l.value,alt:"captcha"},null,8,z)):(w(),h("span",F,"点击获取"))])])]),_:1}),o(s,null,{default:r(()=>[o(c,{type:"primary",loading:_.value,style:{width:"100%"},onClick:L},{default:r(()=>[...e[4]||(e[4]=[N(" 登录 ",-1)])]),_:1},8,["loading"])]),_:1})]),_:1},8,["model"])]),_:1})])}}}),K=$(G,[["__scopeId","data-v-3d2542ad"]]);export{K as default};
|
||||
@ -1 +1 @@
|
||||
import{G as l,E as t,n as u,f as y,H as h,g as w,q as _,w as x,r as m,d as k,I as q,B as E,o as M}from"./index-ChTrSGCb.js";import{t as D}from"./list-query-4ZZvVtjn.js";const R=()=>({crudOptions:{pagination:{show:!1},search:{show:!0,options:{layout:"inline"},expand:!1},tree:{enable:!0,childrenKey:"children",parentKey:"parentid",hasChildField:"children"},request:{transformQuery:({form:e})=>D(e,["name","code"]),pageRequest:async e=>{const{data:s}=await u.get("/permissions/tree");return s},transformRes:({res:e})=>{const s=Array.isArray(e)?e:[];return{currentPage:1,pageSize:Math.max(s.length,1),total:s.length,records:s}},addRequest:async({form:e})=>{await u.post("/permissions/",e),t.success({message:"添加成功",duration:5e3})},editRequest:async({form:e,row:s})=>{s&&(await u.put(`/permissions/${s.id}`,e),t.success({message:"修改成功",duration:5e3}))},delRequest:async({row:e})=>{e&&(await u.delete(`/permissions/${e.id}`),t.success({message:"删除成功",duration:5e3}))},onSuccess:({action:e})=>{e==="add"?t.success({message:"添加成功",duration:5e3}):e==="edit"&&t.success({message:"修改成功",duration:5e3})},onError:e=>{var r;let s="操作失败";if((r=e.response)!=null&&r.data){const a=e.response.data;a.message?s=a.message:a.detail&&(s=typeof a.detail=="string"?a.detail:"操作失败")}t.error({message:s,duration:5e3})}},table:{remove:{showSuccessNotification:!1}},rowHandle:{buttons:{edit:{text:"编辑"},remove:{text:"删除"}}},columns:{id:{title:"ID",key:"id",type:"number",column:{width:70},form:{show:!1}},name:{title:"名称",key:"name",type:"text",search:{show:!0},form:{rules:[{required:!0,message:"必填"}]}},code:{title:"编码",key:"code",type:"text",search:{show:!0},form:{rules:[{required:!0,message:"必填"}]}},parentid:{title:"父级ID",key:"parentid",type:"number",form:{value:0,rules:[{required:!0,message:"必填"}]}},description:{title:"描述",key:"description",type:"textarea",column:{show:!1}},route:{title:"路由",key:"route",type:"text",column:{width:200},form:{placeholder:"如:/admin/users"}},createtime:{title:"创建时间",key:"createtime",type:"text",form:{show:!1},column:{width:170},valueBuilder({value:e,row:s,key:r}){!s||e==null||(s[r]=l(e).format("YYYY-MM-DD HH:mm:ss"))}},updatetime:{title:"更新时间",key:"updatetime",type:"text",form:{show:!1},column:{width:170},valueBuilder({value:e,row:s,key:r}){!s||e==null||(s[r]=l(e).format("YYYY-MM-DD HH:mm:ss"))}}}}}),B=y({__name:"MenuManage",setup(e){const{crudBinding:s,crudRef:r,crudExpose:a}=h({createCrudOptions:R}),p=()=>{t.success({message:"添加成功",duration:5e3})},f=()=>{t.success({message:"修改成功",duration:5e3})},g=()=>{t.success({message:"删除成功",duration:5e3})},d=c=>{var i;let n="操作失败";if((i=c.response)!=null&&i.data){const o=c.response.data;o.message?n=o.message:o.detail&&(n=typeof o.detail=="string"?o.detail:"操作失败")}t.error({message:n,duration:5e3})};return w(()=>{a.doRefresh()}),(c,n)=>{const i=m("fs-crud"),o=m("fs-page");return M(),_(o,null,{default:x(()=>[k(i,q({ref_key:"crudRef",ref:r},E(s),{onAddSuccess:p,onEditSuccess:f,onDelSuccess:g,onAddError:d,onEditError:d,onDelError:d}),null,16)]),_:1})}}});export{B as default};
|
||||
import{G as l,E as t,n as u,f as y,H as h,g as w,q as _,w as x,r as m,d as k,I as q,B as E,o as M}from"./index-rb7Oshfn.js";import{t as D}from"./list-query-4ZZvVtjn.js";const R=()=>({crudOptions:{pagination:{show:!1},search:{show:!0,options:{layout:"inline"},expand:!1},tree:{enable:!0,childrenKey:"children",parentKey:"parentid",hasChildField:"children"},request:{transformQuery:({form:e})=>D(e,["name","code"]),pageRequest:async e=>{const{data:s}=await u.get("/permissions/tree");return s},transformRes:({res:e})=>{const s=Array.isArray(e)?e:[];return{currentPage:1,pageSize:Math.max(s.length,1),total:s.length,records:s}},addRequest:async({form:e})=>{await u.post("/permissions/",e),t.success({message:"添加成功",duration:5e3})},editRequest:async({form:e,row:s})=>{s&&(await u.put(`/permissions/${s.id}`,e),t.success({message:"修改成功",duration:5e3}))},delRequest:async({row:e})=>{e&&(await u.delete(`/permissions/${e.id}`),t.success({message:"删除成功",duration:5e3}))},onSuccess:({action:e})=>{e==="add"?t.success({message:"添加成功",duration:5e3}):e==="edit"&&t.success({message:"修改成功",duration:5e3})},onError:e=>{var r;let s="操作失败";if((r=e.response)!=null&&r.data){const a=e.response.data;a.message?s=a.message:a.detail&&(s=typeof a.detail=="string"?a.detail:"操作失败")}t.error({message:s,duration:5e3})}},table:{remove:{showSuccessNotification:!1}},rowHandle:{buttons:{edit:{text:"编辑"},remove:{text:"删除"}}},columns:{id:{title:"ID",key:"id",type:"number",column:{width:70},form:{show:!1}},name:{title:"名称",key:"name",type:"text",search:{show:!0},form:{rules:[{required:!0,message:"必填"}]}},code:{title:"编码",key:"code",type:"text",search:{show:!0},form:{rules:[{required:!0,message:"必填"}]}},parentid:{title:"父级ID",key:"parentid",type:"number",form:{value:0,rules:[{required:!0,message:"必填"}]}},description:{title:"描述",key:"description",type:"textarea",column:{show:!1}},route:{title:"路由",key:"route",type:"text",column:{width:200},form:{placeholder:"如:/admin/users"}},sort:{title:"排序",key:"sort",type:"number",column:{width:80,align:"center"},form:{value:0,rules:[{required:!0,message:"必填"}],helper:"数字越小,排序越靠前"}},createtime:{title:"创建时间",key:"createtime",type:"text",form:{show:!1},column:{width:170},valueBuilder({value:e,row:s,key:r}){!s||e==null||(s[r]=l(e).format("YYYY-MM-DD HH:mm:ss"))}},updatetime:{title:"更新时间",key:"updatetime",type:"text",form:{show:!1},column:{width:170},valueBuilder({value:e,row:s,key:r}){!s||e==null||(s[r]=l(e).format("YYYY-MM-DD HH:mm:ss"))}}}}}),b=y({__name:"MenuManage",setup(e){const{crudBinding:s,crudRef:r,crudExpose:a}=h({createCrudOptions:R}),p=()=>{t.success({message:"添加成功",duration:5e3})},f=()=>{t.success({message:"修改成功",duration:5e3})},g=()=>{t.success({message:"删除成功",duration:5e3})},d=c=>{var i;let n="操作失败";if((i=c.response)!=null&&i.data){const o=c.response.data;o.message?n=o.message:o.detail&&(n=typeof o.detail=="string"?o.detail:"操作失败")}t.error({message:n,duration:5e3})};return w(()=>{a.doRefresh()}),(c,n)=>{const i=m("fs-crud"),o=m("fs-page");return M(),_(o,null,{default:x(()=>[k(i,q({ref_key:"crudRef",ref:r},E(s),{onAddSuccess:p,onEditSuccess:f,onDelSuccess:g,onAddError:d,onEditError:d,onDelError:d}),null,16)]),_:1})}}});export{b as default};
|
||||
@ -1 +1 @@
|
||||
import{G as w,O as f,n as h,f as M,H as V,g as O,c as Y,d as l,w as n,F as H,i as p,r as o,I,B as R,b as g,E as m,P as E,o as P}from"./index-ChTrSGCb.js";function A(_){return()=>({crudOptions:{pagination:{show:!1},search:{show:!0,options:{layout:"inline"},expand:!1},request:{pageRequest:async e=>{const{data:t}=await h.post("/operation-logs/search",e);return t},transformQuery:({form:e})=>{const t={};return e!=null&&e.username&&(t.username=e.username),e!=null&&e.action_type&&(t.action_type=e.action_type),e!=null&&e.target_type&&(t.target_type=e.target_type),t},transformRes:({res:e})=>{const t=Array.isArray(e)?e:[];return{currentPage:1,pageSize:Math.max(t.length,1),total:t.length,records:t}}},rowHandle:{show:!1},toolbar:{buttons:{clean:{text:"清理旧日志",type:"warning",click:()=>_.onClean()}}},columns:{id:{title:"ID",key:"id",type:"number",column:{width:70},form:{show:!1}},user_id:{title:"用户ID",key:"user_id",type:"number",column:{width:80},form:{show:!1}},username:{title:"操作人",key:"username",type:"text",search:{show:!0}},action_type:{title:"操作类型",key:"action_type",type:"dict-select",search:{show:!0},dict:f({data:[{value:"create",label:"创建"},{value:"update",label:"更新"},{value:"delete",label:"删除"}],value:"value",label:"label"}),column:{width:90}},target_type:{title:"目标类型",key:"target_type",type:"dict-select",search:{show:!0},dict:f({data:[{value:"user",label:"用户"},{value:"role",label:"角色"},{value:"permission",label:"菜单"},{value:"setting",label:"系统设置"}],value:"value",label:"label"}),column:{width:100}},target_name:{title:"目标名称",key:"target_name",type:"text",column:{width:140}},target_id:{title:"目标ID",key:"target_id",type:"number",column:{width:80},form:{show:!1}},detail:{title:"变更详情",key:"detail",type:"text",column:{width:200},form:{show:!1}},ip_address:{title:"IP地址",key:"ip_address",type:"text",column:{width:150}},user_agent:{title:"User-Agent",key:"user_agent",type:"text",column:{width:300}},createtime:{title:"操作时间",key:"createtime",type:"text",column:{width:170},form:{show:!1},valueBuilder({value:e,row:t,key:u}){!t||e==null||(t[u]=w(e).format("YYYY-MM-DD HH:mm:ss"))}}}}})}const S=M({__name:"OperationLogManage",setup(_){const{crudBinding:e,crudRef:t,crudExpose:u}=V({createCrudOptions:A({onClean:()=>{s.value=!0}})}),s=p(!1),r=p(),d=p(!1);async function v(){if(!r.value){m.warning("请选择要清理的日期");return}try{await E.confirm(`确定要清理 ${w(r.value).format("YYYY-MM-DD HH:mm:ss")} 之前的所有操作日志吗?此操作不可撤销。`,"确认清理",{confirmButtonText:"确认清理",cancelButtonText:"取消",type:"warning"})}catch{return}d.value=!0;try{const{data:c}=await h.delete("/operation-logs/clean",{data:{before_date:r.value.toISOString()}});m.success(c.message||"清理成功"),s.value=!1,u.doRefresh()}catch{m.error("清理失败")}finally{d.value=!1}}return O(()=>{u.doRefresh()}),(c,a)=>{const b=o("fs-crud"),k=o("fs-page"),x=o("el-date-picker"),C=o("el-form-item"),B=o("el-form"),y=o("el-button"),D=o("el-dialog");return P(),Y(H,null,[l(k,null,{default:n(()=>[l(b,I({ref_key:"crudRef",ref:t},R(e)),null,16)]),_:1}),l(D,{modelValue:s.value,"onUpdate:modelValue":a[2]||(a[2]=i=>s.value=i),title:"清理旧日志",width:"420px"},{footer:n(()=>[l(y,{onClick:a[1]||(a[1]=i=>s.value=!1)},{default:n(()=>[...a[3]||(a[3]=[g("取消",-1)])]),_:1}),l(y,{type:"warning",loading:d.value,onClick:v},{default:n(()=>[...a[4]||(a[4]=[g("确认清理",-1)])]),_:1},8,["loading"])]),default:n(()=>[l(B,null,{default:n(()=>[l(C,{label:"清理此日期之前的日志"},{default:n(()=>[l(x,{modelValue:r.value,"onUpdate:modelValue":a[0]||(a[0]=i=>r.value=i),type:"datetime",placeholder:"选择日期时间",style:{width:"100%"}},null,8,["modelValue"])]),_:1})]),_:1})]),_:1},8,["modelValue"])],64)}}});export{S as default};
|
||||
import{G as w,O as f,n as h,f as M,H as V,g as O,c as Y,d as l,w as n,F as H,i as p,r as o,I,B as R,b as g,E as m,P as E,o as P}from"./index-rb7Oshfn.js";function A(_){return()=>({crudOptions:{pagination:{show:!1},search:{show:!0,options:{layout:"inline"},expand:!1},request:{pageRequest:async e=>{const{data:t}=await h.post("/operation-logs/search",e);return t},transformQuery:({form:e})=>{const t={};return e!=null&&e.username&&(t.username=e.username),e!=null&&e.action_type&&(t.action_type=e.action_type),e!=null&&e.target_type&&(t.target_type=e.target_type),t},transformRes:({res:e})=>{const t=Array.isArray(e)?e:[];return{currentPage:1,pageSize:Math.max(t.length,1),total:t.length,records:t}}},rowHandle:{show:!1},toolbar:{buttons:{clean:{text:"清理旧日志",type:"warning",click:()=>_.onClean()}}},columns:{id:{title:"ID",key:"id",type:"number",column:{width:70},form:{show:!1}},user_id:{title:"用户ID",key:"user_id",type:"number",column:{width:80},form:{show:!1}},username:{title:"操作人",key:"username",type:"text",search:{show:!0}},action_type:{title:"操作类型",key:"action_type",type:"dict-select",search:{show:!0},dict:f({data:[{value:"create",label:"创建"},{value:"update",label:"更新"},{value:"delete",label:"删除"}],value:"value",label:"label"}),column:{width:90}},target_type:{title:"目标类型",key:"target_type",type:"dict-select",search:{show:!0},dict:f({data:[{value:"user",label:"用户"},{value:"role",label:"角色"},{value:"permission",label:"菜单"},{value:"setting",label:"系统设置"}],value:"value",label:"label"}),column:{width:100}},target_name:{title:"目标名称",key:"target_name",type:"text",column:{width:140}},target_id:{title:"目标ID",key:"target_id",type:"number",column:{width:80},form:{show:!1}},detail:{title:"变更详情",key:"detail",type:"text",column:{width:200},form:{show:!1}},ip_address:{title:"IP地址",key:"ip_address",type:"text",column:{width:150}},user_agent:{title:"User-Agent",key:"user_agent",type:"text",column:{width:300}},createtime:{title:"操作时间",key:"createtime",type:"text",column:{width:170},form:{show:!1},valueBuilder({value:e,row:t,key:u}){!t||e==null||(t[u]=w(e).format("YYYY-MM-DD HH:mm:ss"))}}}}})}const S=M({__name:"OperationLogManage",setup(_){const{crudBinding:e,crudRef:t,crudExpose:u}=V({createCrudOptions:A({onClean:()=>{s.value=!0}})}),s=p(!1),r=p(),d=p(!1);async function v(){if(!r.value){m.warning("请选择要清理的日期");return}try{await E.confirm(`确定要清理 ${w(r.value).format("YYYY-MM-DD HH:mm:ss")} 之前的所有操作日志吗?此操作不可撤销。`,"确认清理",{confirmButtonText:"确认清理",cancelButtonText:"取消",type:"warning"})}catch{return}d.value=!0;try{const{data:c}=await h.delete("/operation-logs/clean",{data:{before_date:r.value.toISOString()}});m.success(c.message||"清理成功"),s.value=!1,u.doRefresh()}catch{m.error("清理失败")}finally{d.value=!1}}return O(()=>{u.doRefresh()}),(c,a)=>{const b=o("fs-crud"),k=o("fs-page"),x=o("el-date-picker"),C=o("el-form-item"),B=o("el-form"),y=o("el-button"),D=o("el-dialog");return P(),Y(H,null,[l(k,null,{default:n(()=>[l(b,I({ref_key:"crudRef",ref:t},R(e)),null,16)]),_:1}),l(D,{modelValue:s.value,"onUpdate:modelValue":a[2]||(a[2]=i=>s.value=i),title:"清理旧日志",width:"420px"},{footer:n(()=>[l(y,{onClick:a[1]||(a[1]=i=>s.value=!1)},{default:n(()=>[...a[3]||(a[3]=[g("取消",-1)])]),_:1}),l(y,{type:"warning",loading:d.value,onClick:v},{default:n(()=>[...a[4]||(a[4]=[g("确认清理",-1)])]),_:1},8,["loading"])]),default:n(()=>[l(B,null,{default:n(()=>[l(C,{label:"清理此日期之前的日志"},{default:n(()=>[l(x,{modelValue:r.value,"onUpdate:modelValue":a[0]||(a[0]=i=>r.value=i),type:"datetime",placeholder:"选择日期时间",style:{width:"100%"}},null,8,["modelValue"])]),_:1})]),_:1})]),_:1},8,["modelValue"])],64)}}});export{S as default};
|
||||
@ -1 +1 @@
|
||||
import{_ as n,c as r,a as s,b as t,d as l,w as d,r as i,o as a}from"./index-ChTrSGCb.js";const p={},u={class:"public-wrap"},m={class:"hint"};function f(_,o){const e=i("router-link");return a(),r("div",u,[o[5]||(o[5]=s("h1",null,"BookSystem",-1)),s("p",m,[o[1]||(o[1]=t(" 此处预留为",-1)),o[2]||(o[2]=s("strong",null,"无需登录",-1)),o[3]||(o[3]=t("的站点首页(图书浏览、门户等)。管理功能请前往 ",-1)),l(e,{to:"/admin/login"},{default:d(()=>[...o[0]||(o[0]=[t("管理后台",-1)])]),_:1}),o[4]||(o[4]=t("。 ",-1))])])}const x=n(p,[["render",f],["__scopeId","data-v-f5876933"]]);export{x as default};
|
||||
import{_ as n,c as r,a as s,b as t,d as l,w as d,r as i,o as a}from"./index-rb7Oshfn.js";const p={},u={class:"public-wrap"},m={class:"hint"};function f(_,o){const e=i("router-link");return a(),r("div",u,[o[5]||(o[5]=s("h1",null,"BookSystem",-1)),s("p",m,[o[1]||(o[1]=t(" 此处预留为",-1)),o[2]||(o[2]=s("strong",null,"无需登录",-1)),o[3]||(o[3]=t("的站点首页(图书浏览、门户等)。管理功能请前往 ",-1)),l(e,{to:"/admin/login"},{default:d(()=>[...o[0]||(o[0]=[t("管理后台",-1)])]),_:1}),o[4]||(o[4]=t("。 ",-1))])])}const x=n(p,[["render",f],["__scopeId","data-v-f5876933"]]);export{x as default};
|
||||
File diff suppressed because one or more lines are too long
@ -1 +0,0 @@
|
||||
.setting-container[data-v-a71db493]{padding:20px}.header[data-v-a71db493]{display:flex;justify-content:space-between;align-items:center;margin-bottom:20px}.header h2[data-v-a71db493]{margin:0;font-size:18px}.add-btn[data-v-a71db493]{background:#409eff;color:#fff;border:none;padding:8px 16px;border-radius:4px;cursor:pointer}.add-btn[data-v-a71db493]:hover{background:#66b1ff}.settings-table[data-v-a71db493]{width:100%;border-collapse:collapse}.settings-table th[data-v-a71db493],.settings-table td[data-v-a71db493]{border:1px solid #ebeef5;padding:12px;text-align:left}.settings-table th[data-v-a71db493]{background:#f5f7fa;font-weight:600}.value-cell[data-v-a71db493]{max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.cell-image[data-v-a71db493]{width:60px;height:60px;object-fit:contain;border-radius:4px;background:#f5f7fa}.actions[data-v-a71db493]{display:flex;gap:8px}.edit-btn[data-v-a71db493]{background:#67c23a;color:#fff;border:none;padding:4px 12px;border-radius:4px;cursor:pointer}.edit-btn[data-v-a71db493]:hover{background:#85ce61}.modal-overlay[data-v-a71db493]{position:fixed;top:0;left:0;right:0;bottom:0;background:#00000080;display:flex;justify-content:center;align-items:center;z-index:1000}.modal-content[data-v-a71db493]{background:#fff;padding:24px;border-radius:8px;width:480px}.modal-content h3[data-v-a71db493]{margin:0 0 20px;font-size:16px}.form-group[data-v-a71db493]{margin-bottom:16px}.form-group label[data-v-a71db493]{display:block;margin-bottom:8px;font-weight:500}.form-group input[data-v-a71db493],.form-group textarea[data-v-a71db493],.form-select[data-v-a71db493]{width:100%;padding:8px;border:1px solid #dcdfe6;border-radius:4px;box-sizing:border-box}.form-group textarea[data-v-a71db493]{resize:vertical}.form-select[data-v-a71db493]{height:36px}.image-upload-area[data-v-a71db493]{display:flex;flex-direction:column;align-items:center;gap:12px;padding:16px;border:1px dashed #dcdfe6;border-radius:4px;background:#fafafa}.preview-image[data-v-a71db493]{max-width:200px;max-height:120px;object-fit:contain;border-radius:4px}.no-image[data-v-a71db493]{color:#c0c4cc;font-size:13px;padding:12px}.upload-btn[data-v-a71db493]{background:#409eff;color:#fff;border:none;padding:6px 16px;border-radius:4px;cursor:pointer;font-size:13px}.upload-btn[data-v-a71db493]:hover{background:#66b1ff}.upload-hint[data-v-a71db493]{color:#909399;font-size:13px}.modal-footer[data-v-a71db493]{display:flex;justify-content:flex-end;gap:12px;margin-top:24px}.cancel-btn[data-v-a71db493]{background:#f5f7fa;color:#606266;border:none;padding:8px 16px;border-radius:4px;cursor:pointer}.cancel-btn[data-v-a71db493]:hover{background:#e4e7ed}.submit-btn[data-v-a71db493]{background:#409eff;color:#fff;border:none;padding:8px 16px;border-radius:4px;cursor:pointer}.submit-btn[data-v-a71db493]:hover{background:#66b1ff}
|
||||
@ -1 +1 @@
|
||||
import{f as I,c as n,a as e,F as L,z as B,m as T,t as u,L as p,M as b,N as M,y as C,i as r,n as f,E as i,o,_ as R}from"./index-ChTrSGCb.js";const j={class:"setting-container"},z={class:"settings-table"},A={class:"value-cell"},H=["src"],Y={key:1},q={class:"actions"},G=["onClick"],J={class:"modal-content"},K={class:"form-group"},O={class:"form-group"},P={class:"form-group"},Q={key:0,class:"image-upload-area"},W=["src"],X={key:1,class:"no-image"},Z={key:2,class:"upload-hint"},ee={class:"form-group"},te={class:"modal-footer"},le=I({__name:"SettingManage",setup(ae){const D=s=>({string:"字符串",image:"图片",select:"下拉列表",boolean:"布尔值"})[s]||s,_=s=>{const t=new Date(s);if(isNaN(t.getTime()))return"-";const l=t.getFullYear(),c=String(t.getMonth()+1).padStart(2,"0"),y=String(t.getDate()).padStart(2,"0"),k=String(t.getHours()).padStart(2,"0"),F=String(t.getMinutes()).padStart(2,"0"),N=String(t.getSeconds()).padStart(2,"0");return`${l}-${c}-${y} ${k}:${F}:${N}`},h=r([]),v=r(!1),d=r(!1),g=r(!1),S=r(),a=r({id:0,key:"",value:"",type:"string",description:""}),w=async()=>{try{const{data:s}=await f.get("/settings/");h.value=s}catch{i.error("加载失败")}},$=()=>{d.value=!1,a.value={id:0,key:"",value:"",type:"string",description:""},v.value=!0},x=s=>{d.value=!0,a.value={id:s.id,key:s.key,value:s.value,type:s.type||"string",description:s.description},v.value=!0},m=()=>{v.value=!1,g.value=!1},V=()=>{var s;(s=S.value)==null||s.click()},U=async s=>{var c;const t=s.target,l=(c=t.files)==null?void 0:c[0];if(l){g.value=!0;try{const y=new FormData;y.append("file",l);const{data:k}=await f.post("/settings/upload",y);a.value.value=k.url,i.success("图片上传成功")}catch{i.error("图片上传失败")}finally{g.value=!1,t.value=""}}},E=async()=>{if(!a.value.key){i.warning("请输入设置键名");return}try{d.value?(await f.put(`/settings/${a.value.id}`,a.value),i.success("修改成功")):(await f.post("/settings/",a.value),i.success("添加成功")),m(),await w()}catch{i.error("操作失败")}};return w(),(s,t)=>(o(),n("div",j,[e("div",{class:"header"},[t[5]||(t[5]=e("h2",null,"系统设置管理",-1)),e("button",{class:"add-btn",onClick:$},"添加设置")]),e("table",z,[t[6]||(t[6]=e("thead",null,[e("tr",null,[e("th",null,"ID"),e("th",null,"设置键名"),e("th",null,"类型"),e("th",null,"设置值"),e("th",null,"描述"),e("th",null,"更新时间"),e("th",null,"创建时间"),e("th",null,"操作")])],-1)),e("tbody",null,[(o(!0),n(L,null,B(h.value,l=>(o(),n("tr",{key:l.id},[e("td",null,u(l.id),1),e("td",null,u(l.key),1),e("td",null,u(D(l.type)),1),e("td",A,[l.type==="image"&&l.value?(o(),n("img",{key:0,src:l.value,class:"cell-image"},null,8,H)):(o(),n("span",Y,u(l.value),1))]),e("td",null,u(l.description||"-"),1),e("td",null,u(_(l.updatetime)),1),e("td",null,u(_(l.createtime)),1),e("td",q,[e("button",{class:"edit-btn",onClick:c=>x(l)},"编辑",8,G)])]))),128))])]),v.value?(o(),n("div",{key:0,class:"modal-overlay",onClick:T(m,["self"])},[e("div",J,[e("h3",null,u(d.value?"编辑设置":"添加设置"),1),e("div",K,[t[7]||(t[7]=e("label",null,"设置键名 *",-1)),p(e("input",{"onUpdate:modelValue":t[0]||(t[0]=l=>a.value.key=l),type:"text",placeholder:"请输入设置键名"},null,512),[[b,a.value.key]])]),e("div",O,[t[9]||(t[9]=e("label",null,"类型",-1)),p(e("select",{"onUpdate:modelValue":t[1]||(t[1]=l=>a.value.type=l),class:"form-select"},[...t[8]||(t[8]=[e("option",{value:"string"},"字符串",-1),e("option",{value:"image"},"图片",-1),e("option",{value:"select"},"下拉列表",-1),e("option",{value:"boolean"},"布尔值",-1)])],512),[[M,a.value.type]])]),e("div",P,[t[11]||(t[11]=e("label",null,"设置值",-1)),a.value.type==="image"?(o(),n("div",Q,[a.value.value?(o(),n("img",{key:0,src:a.value.value,class:"preview-image"},null,8,W)):(o(),n("div",X,"暂无图片")),e("input",{ref_key:"fileInputRef",ref:S,type:"file",accept:"image/png,image/jpeg,image/gif,image/webp",style:{display:"none"},onChange:U},null,544),e("button",{class:"upload-btn",onClick:V},"选择图片"),g.value?(o(),n("span",Z,"上传中...")):C("",!0)])):a.value.type==="boolean"?p((o(),n("select",{key:1,"onUpdate:modelValue":t[2]||(t[2]=l=>a.value.value=l),class:"form-select"},[...t[10]||(t[10]=[e("option",{value:"true"},"是",-1),e("option",{value:"false"},"否",-1)])],512)),[[M,a.value.value]]):p((o(),n("textarea",{key:2,"onUpdate:modelValue":t[3]||(t[3]=l=>a.value.value=l),rows:"3",placeholder:"请输入设置值"},null,512)),[[b,a.value.value]])]),e("div",ee,[t[12]||(t[12]=e("label",null,"描述",-1)),p(e("textarea",{"onUpdate:modelValue":t[4]||(t[4]=l=>a.value.description=l),rows:"2",placeholder:"请输入描述"},null,512),[[b,a.value.description]])]),e("div",te,[e("button",{class:"cancel-btn",onClick:m},"取消"),e("button",{class:"submit-btn",onClick:E},u(d.value?"保存修改":"确认添加"),1)])])])):C("",!0)]))}}),ne=R(le,[["__scopeId","data-v-a71db493"]]);export{ne as default};
|
||||
import{f as I,c as n,a as e,F as L,z as B,m as T,t as u,L as p,M as b,N as M,y as C,i as r,n as f,E as i,o,_ as R}from"./index-rb7Oshfn.js";const j={class:"setting-container"},z={class:"settings-table"},A={class:"value-cell"},H=["src"],Y={key:1},q={class:"actions"},G=["onClick"],J={class:"modal-content"},K={class:"form-group"},O={class:"form-group"},P={class:"form-group"},Q={key:0,class:"image-upload-area"},W=["src"],X={key:1,class:"no-image"},Z={key:2,class:"upload-hint"},ee={class:"form-group"},te={class:"modal-footer"},le=I({__name:"SettingManage",setup(ae){const D=s=>({string:"字符串",image:"图片",select:"下拉列表",boolean:"布尔值"})[s]||s,_=s=>{const t=new Date(s);if(isNaN(t.getTime()))return"-";const l=t.getFullYear(),c=String(t.getMonth()+1).padStart(2,"0"),y=String(t.getDate()).padStart(2,"0"),k=String(t.getHours()).padStart(2,"0"),F=String(t.getMinutes()).padStart(2,"0"),N=String(t.getSeconds()).padStart(2,"0");return`${l}-${c}-${y} ${k}:${F}:${N}`},h=r([]),v=r(!1),d=r(!1),g=r(!1),S=r(),a=r({id:0,key:"",value:"",type:"string",description:""}),w=async()=>{try{const{data:s}=await f.get("/settings/");h.value=s}catch{i.error("加载失败")}},$=()=>{d.value=!1,a.value={id:0,key:"",value:"",type:"string",description:""},v.value=!0},x=s=>{d.value=!0,a.value={id:s.id,key:s.key,value:s.value,type:s.type||"string",description:s.description},v.value=!0},m=()=>{v.value=!1,g.value=!1},V=()=>{var s;(s=S.value)==null||s.click()},U=async s=>{var c;const t=s.target,l=(c=t.files)==null?void 0:c[0];if(l){g.value=!0;try{const y=new FormData;y.append("file",l);const{data:k}=await f.post("/settings/upload",y);a.value.value=k.url,i.success("图片上传成功")}catch{i.error("图片上传失败")}finally{g.value=!1,t.value=""}}},E=async()=>{if(!a.value.key){i.warning("请输入设置键名");return}try{d.value?(await f.put(`/settings/${a.value.id}`,a.value),i.success("修改成功")):(await f.post("/settings/",a.value),i.success("添加成功")),m(),await w()}catch{i.error("操作失败")}};return w(),(s,t)=>(o(),n("div",j,[e("div",{class:"header"},[t[5]||(t[5]=e("h2",null,"系统设置管理",-1)),e("button",{class:"add-btn",onClick:$},"添加设置")]),e("table",z,[t[6]||(t[6]=e("thead",null,[e("tr",null,[e("th",null,"ID"),e("th",null,"设置键名"),e("th",null,"类型"),e("th",null,"设置值"),e("th",null,"描述"),e("th",null,"更新时间"),e("th",null,"创建时间"),e("th",null,"操作")])],-1)),e("tbody",null,[(o(!0),n(L,null,B(h.value,l=>(o(),n("tr",{key:l.id},[e("td",null,u(l.id),1),e("td",null,u(l.key),1),e("td",null,u(D(l.type)),1),e("td",A,[l.type==="image"&&l.value?(o(),n("img",{key:0,src:l.value,class:"cell-image"},null,8,H)):(o(),n("span",Y,u(l.value),1))]),e("td",null,u(l.description||"-"),1),e("td",null,u(_(l.updatetime)),1),e("td",null,u(_(l.createtime)),1),e("td",q,[e("button",{class:"edit-btn",onClick:c=>x(l)},"编辑",8,G)])]))),128))])]),v.value?(o(),n("div",{key:0,class:"modal-overlay",onClick:T(m,["self"])},[e("div",J,[e("h3",null,u(d.value?"编辑设置":"添加设置"),1),e("div",K,[t[7]||(t[7]=e("label",null,"设置键名 *",-1)),p(e("input",{"onUpdate:modelValue":t[0]||(t[0]=l=>a.value.key=l),type:"text",placeholder:"请输入设置键名"},null,512),[[b,a.value.key]])]),e("div",O,[t[9]||(t[9]=e("label",null,"类型",-1)),p(e("select",{"onUpdate:modelValue":t[1]||(t[1]=l=>a.value.type=l),class:"form-select"},[...t[8]||(t[8]=[e("option",{value:"string"},"字符串",-1),e("option",{value:"image"},"图片",-1),e("option",{value:"select"},"下拉列表",-1),e("option",{value:"boolean"},"布尔值",-1)])],512),[[M,a.value.type]])]),e("div",P,[t[11]||(t[11]=e("label",null,"设置值",-1)),a.value.type==="image"?(o(),n("div",Q,[a.value.value?(o(),n("img",{key:0,src:a.value.value,class:"preview-image"},null,8,W)):(o(),n("div",X,"暂无图片")),e("input",{ref_key:"fileInputRef",ref:S,type:"file",accept:"image/png,image/jpeg,image/gif,image/webp",style:{display:"none"},onChange:U},null,544),e("button",{class:"upload-btn",onClick:V},"选择图片"),g.value?(o(),n("span",Z,"上传中...")):C("",!0)])):a.value.type==="boolean"?p((o(),n("select",{key:1,"onUpdate:modelValue":t[2]||(t[2]=l=>a.value.value=l),class:"form-select"},[...t[10]||(t[10]=[e("option",{value:"true"},"是",-1),e("option",{value:"false"},"否",-1)])],512)),[[M,a.value.value]]):p((o(),n("textarea",{key:2,"onUpdate:modelValue":t[3]||(t[3]=l=>a.value.value=l),rows:"3",placeholder:"请输入设置值"},null,512)),[[b,a.value.value]])]),e("div",ee,[t[12]||(t[12]=e("label",null,"描述",-1)),p(e("textarea",{"onUpdate:modelValue":t[4]||(t[4]=l=>a.value.description=l),rows:"2",placeholder:"请输入描述"},null,512),[[b,a.value.description]])]),e("div",te,[e("button",{class:"cancel-btn",onClick:m},"取消"),e("button",{class:"submit-btn",onClick:E},u(d.value?"保存修改":"确认添加"),1)])])])):C("",!0)]))}}),ne=R(le,[["__scopeId","data-v-1321c610"]]);export{ne as default};
|
||||
1
frontend/dist/assets/SettingManage-DcldOeo6.css
vendored
Normal file
1
frontend/dist/assets/SettingManage-DcldOeo6.css
vendored
Normal file
@ -0,0 +1 @@
|
||||
.setting-container[data-v-1321c610]{padding:20px}.header[data-v-1321c610]{display:flex;justify-content:space-between;align-items:center;margin-bottom:20px}.header h2[data-v-1321c610]{margin:0;font-size:18px}.add-btn[data-v-1321c610]{background:#409eff;color:#fff;border:none;padding:8px 16px;border-radius:4px;cursor:pointer}.add-btn[data-v-1321c610]:hover{background:#66b1ff}.settings-table[data-v-1321c610]{width:100%;border-collapse:collapse}.settings-table th[data-v-1321c610],.settings-table td[data-v-1321c610]{border:1px solid #ebeef5;padding:12px;text-align:left}.settings-table th[data-v-1321c610]{background:#f5f7fa;font-weight:600}.value-cell[data-v-1321c610]{max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.cell-image[data-v-1321c610]{width:60px;height:60px;object-fit:contain;border-radius:4px;background:#f5f7fa}.actions[data-v-1321c610]{display:flex;gap:8px}.edit-btn[data-v-1321c610]{background:#67c23a;color:#fff;border:none;padding:4px 12px;border-radius:4px;cursor:pointer}.edit-btn[data-v-1321c610]:hover{background:#85ce61}.modal-overlay[data-v-1321c610]{position:fixed;top:0;left:0;right:0;bottom:0;background:#00000080;display:flex;justify-content:center;align-items:center;z-index:1000}.modal-content[data-v-1321c610]{background:#fff;padding:24px;border-radius:8px;width:480px}.modal-content h3[data-v-1321c610]{margin:0 0 20px;font-size:16px}.form-group[data-v-1321c610]{margin-bottom:16px}.form-group label[data-v-1321c610]{display:block;margin-bottom:8px;font-weight:500}.form-group input[data-v-1321c610],.form-group textarea[data-v-1321c610],.form-select[data-v-1321c610]{width:100%;padding:8px;border:1px solid #dcdfe6;border-radius:4px;box-sizing:border-box}.form-group textarea[data-v-1321c610]{resize:vertical}.form-select[data-v-1321c610]{height:36px}.image-upload-area[data-v-1321c610]{display:flex;flex-direction:column;align-items:center;gap:12px;padding:16px;border:1px dashed #dcdfe6;border-radius:4px;background:#fafafa}.preview-image[data-v-1321c610]{max-width:200px;max-height:120px;object-fit:contain;border-radius:4px}.no-image[data-v-1321c610]{color:#c0c4cc;font-size:13px;padding:12px}.upload-btn[data-v-1321c610]{background:#409eff;color:#fff;border:none;padding:6px 16px;border-radius:4px;cursor:pointer;font-size:13px}.upload-btn[data-v-1321c610]:hover{background:#66b1ff}.upload-hint[data-v-1321c610]{color:#909399;font-size:13px}.modal-footer[data-v-1321c610]{display:flex;justify-content:flex-end;gap:12px;margin-top:24px}.cancel-btn[data-v-1321c610]{background:#f5f7fa;color:#606266;border:none;padding:8px 16px;border-radius:4px;cursor:pointer}.cancel-btn[data-v-1321c610]:hover{background:#e4e7ed}.submit-btn[data-v-1321c610]{background:#409eff;color:#fff;border:none;padding:8px 16px;border-radius:4px;cursor:pointer}.submit-btn[data-v-1321c610]:hover{background:#66b1ff}
|
||||
@ -1 +1 @@
|
||||
import{G as n,O as c,n as i,E as o,f as d,H as p,g as f,q as h,w as g,r as u,d as y,I as w,B as x,o as _}from"./index-ChTrSGCb.js";import{t as k}from"./list-query-4ZZvVtjn.js";const Y=()=>({crudOptions:{pagination:{show:!1},search:{show:!0},searchFormOptions:{inline:!0,labelWidth:"80px",col:{span:0}},request:{transformQuery:({form:e})=>k(e,["username","nickname","email","phone"]),pageRequest:async e=>{const{data:s}=await i.post("/users/search",e);return s},transformRes:({res:e})=>{const s=Array.isArray(e)?e:[];return{currentPage:1,pageSize:Math.max(s.length,1),total:s.length,records:s}},onError:e=>{var t;let s="操作失败";if((t=e.response)!=null&&t.data){const a=e.response.data;if(a.errors&&Array.isArray(a.errors))s=a.errors.map(r=>r.message).join(";");else if(a.message)s=a.message;else if(a.detail){const r=a.detail;typeof r=="string"&&(r.includes("at least 6 characters")?s="密码长度至少为6个字符":s=r)}}else e.message&&(s=e.message);throw o.error({message:s,duration:5e3}),e},addRequest:async({form:e})=>{await i.post("/users/",e),o.success({message:"添加成功",duration:5e3})},editRequest:async({form:e,row:s})=>{if(!s)return;const t={...e};t.password||delete t.password,await i.put(`/users/${s.id}`,t),o.success({message:"修改成功",duration:5e3})},delRequest:async({row:e})=>{e&&(await i.delete(`/users/${e.id}`),o.success({message:"删除成功",duration:5e3}))},onSuccess:({action:e})=>{e==="add"?o.success({message:"添加成功",duration:5e3}):e==="edit"&&o.success({message:"修改成功",duration:5e3})}},table:{remove:{showSuccessNotification:!1}},rowHandle:{buttons:{edit:{text:"编辑"},remove:{text:"删除",show:!0}}},columns:{id:{title:"ID",key:"id",type:"number",column:{width:70},form:{show:!1}},username:{title:"登录名",key:"username",type:"text",search:{show:!0},form:{rules:[{required:!0,message:"请输入登录名",trigger:"blur"},{min:3,max:50,message:"用户名长度必须在3-50个字符之间",trigger:"blur"}]}},nickname:{title:"昵称",key:"nickname",type:"text",search:{show:!0},form:{rules:[{required:!0,message:"必填"}]}},password:{title:"密码",key:"password",type:"password",column:{show:!1},form:{show:!1},addForm:{show:!0,rules:[{required:!0,message:"请输入密码",trigger:"blur"},{min:6,max:50,message:"密码长度必须在6-50个字符之间",trigger:"blur"}]},editForm:{show:!0,component:{placeholder:"不修改请留空"},rules:[{min:6,max:50,message:"密码长度必须在6-50个字符之间",trigger:"blur"}]}},email:{title:"邮箱",key:"email",type:"text",search:{show:!0}},phone:{title:"手机",key:"phone",type:"text",search:{show:!0}},wx_openid:{title:"微信OpenID",key:"wx_openid",type:"text",column:{show:!1}},avatar:{title:"头像",key:"avatar",type:"text",column:{show:!1}},role_id:{title:"角色",key:"role_id",type:"dict-select",dict:c({getData:async()=>{const{data:e}=await i.post("/roles/search",{});return e},value:"id",label:"name"}),column:{width:120,dict:!0},form:{rules:[{required:!0,message:"必选"}],value:2}},createtime:{title:"创建时间",key:"createtime",type:"text",form:{show:!1},column:{width:170},valueBuilder({value:e,row:s,key:t}){!s||e==null||(s[t]=n(e).format("YYYY-MM-DD HH:mm:ss"))}},updatetime:{title:"更新时间",key:"updatetime",type:"text",form:{show:!1},column:{width:170},valueBuilder({value:e,row:s,key:t}){!s||e==null||(s[t]=n(e).format("YYYY-MM-DD HH:mm:ss"))}},lastlogintime:{title:"最后登录",key:"lastlogintime",type:"text",form:{show:!1},column:{width:170},valueBuilder({value:e,row:s,key:t}){!s||e==null||(s[t]=n(e).format("YYYY-MM-DD HH:mm:ss"))}}}}}),D=d({__name:"UserManage",setup(e){const{crudBinding:s,crudRef:t,crudExpose:a}=p({createCrudOptions:Y});return f(()=>{a.doRefresh()}),(r,b)=>{const l=u("fs-crud"),m=u("fs-page");return _(),h(m,null,{default:g(()=>[y(l,w({ref_key:"crudRef",ref:t},x(s)),null,16)]),_:1})}}});export{D as default};
|
||||
import{G as n,O as c,n as i,E as o,f as d,H as p,g as f,q as h,w as g,r as u,d as y,I as w,B as x,o as _}from"./index-rb7Oshfn.js";import{t as k}from"./list-query-4ZZvVtjn.js";const Y=()=>({crudOptions:{pagination:{show:!1},search:{show:!0},searchFormOptions:{inline:!0,labelWidth:"80px",col:{span:0}},request:{transformQuery:({form:e})=>k(e,["username","nickname","email","phone"]),pageRequest:async e=>{const{data:s}=await i.post("/users/search",e);return s},transformRes:({res:e})=>{const s=Array.isArray(e)?e:[];return{currentPage:1,pageSize:Math.max(s.length,1),total:s.length,records:s}},onError:e=>{var t;let s="操作失败";if((t=e.response)!=null&&t.data){const a=e.response.data;if(a.errors&&Array.isArray(a.errors))s=a.errors.map(r=>r.message).join(";");else if(a.message)s=a.message;else if(a.detail){const r=a.detail;typeof r=="string"&&(r.includes("at least 6 characters")?s="密码长度至少为6个字符":s=r)}}else e.message&&(s=e.message);throw o.error({message:s,duration:5e3}),e},addRequest:async({form:e})=>{await i.post("/users/",e),o.success({message:"添加成功",duration:5e3})},editRequest:async({form:e,row:s})=>{if(!s)return;const t={...e};t.password||delete t.password,await i.put(`/users/${s.id}`,t),o.success({message:"修改成功",duration:5e3})},delRequest:async({row:e})=>{e&&(await i.delete(`/users/${e.id}`),o.success({message:"删除成功",duration:5e3}))},onSuccess:({action:e})=>{e==="add"?o.success({message:"添加成功",duration:5e3}):e==="edit"&&o.success({message:"修改成功",duration:5e3})}},table:{remove:{showSuccessNotification:!1}},rowHandle:{buttons:{edit:{text:"编辑"},remove:{text:"删除",show:!0}}},columns:{id:{title:"ID",key:"id",type:"number",column:{width:70},form:{show:!1}},username:{title:"登录名",key:"username",type:"text",search:{show:!0},form:{rules:[{required:!0,message:"请输入登录名",trigger:"blur"},{min:3,max:50,message:"用户名长度必须在3-50个字符之间",trigger:"blur"}]}},nickname:{title:"昵称",key:"nickname",type:"text",search:{show:!0},form:{rules:[{required:!0,message:"必填"}]}},password:{title:"密码",key:"password",type:"password",column:{show:!1},form:{show:!1},addForm:{show:!0,rules:[{required:!0,message:"请输入密码",trigger:"blur"},{min:6,max:50,message:"密码长度必须在6-50个字符之间",trigger:"blur"}]},editForm:{show:!0,component:{placeholder:"不修改请留空"},rules:[{min:6,max:50,message:"密码长度必须在6-50个字符之间",trigger:"blur"}]}},email:{title:"邮箱",key:"email",type:"text",search:{show:!0}},phone:{title:"手机",key:"phone",type:"text",search:{show:!0}},wx_openid:{title:"微信OpenID",key:"wx_openid",type:"text",column:{show:!1}},avatar:{title:"头像",key:"avatar",type:"text",column:{show:!1}},role_id:{title:"角色",key:"role_id",type:"dict-select",dict:c({getData:async()=>{const{data:e}=await i.post("/roles/search",{});return e},value:"id",label:"name"}),column:{width:120,dict:!0},form:{rules:[{required:!0,message:"必选"}],value:2}},createtime:{title:"创建时间",key:"createtime",type:"text",form:{show:!1},column:{width:170},valueBuilder({value:e,row:s,key:t}){!s||e==null||(s[t]=n(e).format("YYYY-MM-DD HH:mm:ss"))}},updatetime:{title:"更新时间",key:"updatetime",type:"text",form:{show:!1},column:{width:170},valueBuilder({value:e,row:s,key:t}){!s||e==null||(s[t]=n(e).format("YYYY-MM-DD HH:mm:ss"))}},lastlogintime:{title:"最后登录",key:"lastlogintime",type:"text",form:{show:!1},column:{width:170},valueBuilder({value:e,row:s,key:t}){!s||e==null||(s[t]=n(e).format("YYYY-MM-DD HH:mm:ss"))}}}}}),D=d({__name:"UserManage",setup(e){const{crudBinding:s,crudRef:t,crudExpose:a}=p({createCrudOptions:Y});return f(()=>{a.doRefresh()}),(r,b)=>{const l=u("fs-crud"),m=u("fs-page");return _(),h(m,null,{default:g(()=>[y(l,w({ref_key:"crudRef",ref:t},x(s)),null,16)]),_:1})}}});export{D as default};
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
2
frontend/dist/index.html
vendored
2
frontend/dist/index.html
vendored
@ -4,7 +4,7 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>BookSystem</title>
|
||||
<script type="module" crossorigin src="/assets/index-ChTrSGCb.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-rb7Oshfn.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-Cn04Rt7y.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@ -1,176 +1,188 @@
|
||||
import type { CreateCrudOptions } from "@fast-crud/fast-crud";
|
||||
import dayjs from "dayjs";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { http } from "@/modules/admin/api/http";
|
||||
import { toSearchParams } from "@/modules/admin/utils/list-query";
|
||||
|
||||
export type PermissionRow = {
|
||||
id: number;
|
||||
name: string;
|
||||
code: string;
|
||||
parentid: number;
|
||||
route?: string | null;
|
||||
description?: string | null;
|
||||
createtime: string;
|
||||
updatetime: string;
|
||||
children?: PermissionRow[];
|
||||
};
|
||||
|
||||
export const createPermissionCrudOptions: CreateCrudOptions<PermissionRow> = () => {
|
||||
return {
|
||||
crudOptions: {
|
||||
pagination: { show: false },
|
||||
search: {
|
||||
show: true,
|
||||
options: {
|
||||
layout: 'inline',
|
||||
},
|
||||
expand: false,
|
||||
},
|
||||
tree: {
|
||||
enable: true,
|
||||
childrenKey: "children",
|
||||
parentKey: "parentid",
|
||||
hasChildField: "children",
|
||||
},
|
||||
request: {
|
||||
transformQuery: ({ form }) =>
|
||||
toSearchParams(form as Record<string, unknown>, ["name", "code"]),
|
||||
pageRequest: async (_query) => {
|
||||
const { data } = await http.get<PermissionRow[]>("/permissions/tree");
|
||||
return data;
|
||||
},
|
||||
transformRes: ({ res }) => {
|
||||
const records = Array.isArray(res) ? res : [];
|
||||
return {
|
||||
currentPage: 1,
|
||||
pageSize: Math.max(records.length, 1),
|
||||
total: records.length,
|
||||
records,
|
||||
};
|
||||
},
|
||||
addRequest: async ({ form }) => {
|
||||
await http.post("/permissions/", form);
|
||||
ElMessage.success({ message: "添加成功", duration: 5000 });
|
||||
},
|
||||
editRequest: async ({ form, row }) => {
|
||||
if (!row) return;
|
||||
await http.put(`/permissions/${row.id}`, form);
|
||||
ElMessage.success({ message: "修改成功", duration: 5000 });
|
||||
},
|
||||
delRequest: async ({ row }) => {
|
||||
if (!row) return;
|
||||
await http.delete(`/permissions/${row.id}`);
|
||||
ElMessage.success({ message: "删除成功", duration: 5000 });
|
||||
},
|
||||
onSuccess: ({ action }: { action: string }) => {
|
||||
if (action === "add") {
|
||||
ElMessage.success({ message: "添加成功", duration: 5000 });
|
||||
} else if (action === "edit") {
|
||||
ElMessage.success({ message: "修改成功", duration: 5000 });
|
||||
}
|
||||
},
|
||||
onError: (error: any) => {
|
||||
let message = "操作失败";
|
||||
if (error.response?.data) {
|
||||
const data = error.response.data;
|
||||
if (data.message) {
|
||||
message = data.message;
|
||||
} else if (data.detail) {
|
||||
message = typeof data.detail === "string" ? data.detail : "操作失败";
|
||||
}
|
||||
}
|
||||
ElMessage.error({ message, duration: 5000 });
|
||||
},
|
||||
},
|
||||
table: {
|
||||
remove: {
|
||||
showSuccessNotification: false,
|
||||
},
|
||||
},
|
||||
rowHandle: {
|
||||
buttons: {
|
||||
edit: { text: "编辑" },
|
||||
remove: { text: "删除" },
|
||||
},
|
||||
},
|
||||
columns: {
|
||||
id: {
|
||||
title: "ID",
|
||||
key: "id",
|
||||
type: "number",
|
||||
column: { width: 70 },
|
||||
form: { show: false },
|
||||
},
|
||||
name: {
|
||||
title: "名称",
|
||||
key: "name",
|
||||
type: "text",
|
||||
search: { show: true },
|
||||
form: {
|
||||
rules: [{ required: true, message: "必填" }],
|
||||
},
|
||||
},
|
||||
code: {
|
||||
title: "编码",
|
||||
key: "code",
|
||||
type: "text",
|
||||
search: { show: true },
|
||||
form: {
|
||||
rules: [{ required: true, message: "必填" }],
|
||||
},
|
||||
},
|
||||
parentid: {
|
||||
title: "父级ID",
|
||||
key: "parentid",
|
||||
type: "number",
|
||||
form: {
|
||||
value: 0,
|
||||
rules: [{ required: true, message: "必填" }],
|
||||
},
|
||||
},
|
||||
description: {
|
||||
title: "描述",
|
||||
key: "description",
|
||||
type: "textarea",
|
||||
column: { show: false },
|
||||
},
|
||||
route: {
|
||||
title: "路由",
|
||||
key: "route",
|
||||
type: "text",
|
||||
column: { width: 200 },
|
||||
form: {
|
||||
placeholder: "如:/admin/users",
|
||||
},
|
||||
},
|
||||
createtime: {
|
||||
title: "创建时间",
|
||||
key: "createtime",
|
||||
type: "text",
|
||||
form: { show: false },
|
||||
column: { width: 170 },
|
||||
valueBuilder({ value, row, key }) {
|
||||
if (!row || value == null) return;
|
||||
(row as Record<string, unknown>)[key as string] = dayjs(value).format(
|
||||
"YYYY-MM-DD HH:mm:ss",
|
||||
);
|
||||
},
|
||||
},
|
||||
updatetime: {
|
||||
title: "更新时间",
|
||||
key: "updatetime",
|
||||
type: "text",
|
||||
form: { show: false },
|
||||
column: { width: 170 },
|
||||
valueBuilder({ value, row, key }) {
|
||||
if (!row || value == null) return;
|
||||
(row as Record<string, unknown>)[key as string] = dayjs(value).format(
|
||||
"YYYY-MM-DD HH:mm:ss",
|
||||
);
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
import type { CreateCrudOptions } from "@fast-crud/fast-crud";
|
||||
import dayjs from "dayjs";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { http } from "@/modules/admin/api/http";
|
||||
import { toSearchParams } from "@/modules/admin/utils/list-query";
|
||||
|
||||
export type PermissionRow = {
|
||||
id: number;
|
||||
name: string;
|
||||
code: string;
|
||||
parentid: number;
|
||||
route?: string | null;
|
||||
description?: string | null;
|
||||
sort: number;
|
||||
createtime: string;
|
||||
updatetime: string;
|
||||
children?: PermissionRow[];
|
||||
};
|
||||
|
||||
export const createPermissionCrudOptions: CreateCrudOptions<PermissionRow> = () => {
|
||||
return {
|
||||
crudOptions: {
|
||||
pagination: { show: false },
|
||||
search: {
|
||||
show: true,
|
||||
options: {
|
||||
layout: 'inline',
|
||||
},
|
||||
expand: false,
|
||||
},
|
||||
tree: {
|
||||
enable: true,
|
||||
childrenKey: "children",
|
||||
parentKey: "parentid",
|
||||
hasChildField: "children",
|
||||
},
|
||||
request: {
|
||||
transformQuery: ({ form }) =>
|
||||
toSearchParams(form as Record<string, unknown>, ["name", "code"]),
|
||||
pageRequest: async (_query) => {
|
||||
const { data } = await http.get<PermissionRow[]>("/permissions/tree");
|
||||
return data;
|
||||
},
|
||||
transformRes: ({ res }) => {
|
||||
const records = Array.isArray(res) ? res : [];
|
||||
return {
|
||||
currentPage: 1,
|
||||
pageSize: Math.max(records.length, 1),
|
||||
total: records.length,
|
||||
records,
|
||||
};
|
||||
},
|
||||
addRequest: async ({ form }) => {
|
||||
await http.post("/permissions/", form);
|
||||
ElMessage.success({ message: "添加成功", duration: 5000 });
|
||||
},
|
||||
editRequest: async ({ form, row }) => {
|
||||
if (!row) return;
|
||||
await http.put(`/permissions/${row.id}`, form);
|
||||
ElMessage.success({ message: "修改成功", duration: 5000 });
|
||||
},
|
||||
delRequest: async ({ row }) => {
|
||||
if (!row) return;
|
||||
await http.delete(`/permissions/${row.id}`);
|
||||
ElMessage.success({ message: "删除成功", duration: 5000 });
|
||||
},
|
||||
onSuccess: ({ action }: { action: string }) => {
|
||||
if (action === "add") {
|
||||
ElMessage.success({ message: "添加成功", duration: 5000 });
|
||||
} else if (action === "edit") {
|
||||
ElMessage.success({ message: "修改成功", duration: 5000 });
|
||||
}
|
||||
},
|
||||
onError: (error: any) => {
|
||||
let message = "操作失败";
|
||||
if (error.response?.data) {
|
||||
const data = error.response.data;
|
||||
if (data.message) {
|
||||
message = data.message;
|
||||
} else if (data.detail) {
|
||||
message = typeof data.detail === "string" ? data.detail : "操作失败";
|
||||
}
|
||||
}
|
||||
ElMessage.error({ message, duration: 5000 });
|
||||
},
|
||||
},
|
||||
table: {
|
||||
remove: {
|
||||
showSuccessNotification: false,
|
||||
},
|
||||
},
|
||||
rowHandle: {
|
||||
buttons: {
|
||||
edit: { text: "编辑" },
|
||||
remove: { text: "删除" },
|
||||
},
|
||||
},
|
||||
columns: {
|
||||
id: {
|
||||
title: "ID",
|
||||
key: "id",
|
||||
type: "number",
|
||||
column: { width: 70 },
|
||||
form: { show: false },
|
||||
},
|
||||
name: {
|
||||
title: "名称",
|
||||
key: "name",
|
||||
type: "text",
|
||||
search: { show: true },
|
||||
form: {
|
||||
rules: [{ required: true, message: "必填" }],
|
||||
},
|
||||
},
|
||||
code: {
|
||||
title: "编码",
|
||||
key: "code",
|
||||
type: "text",
|
||||
search: { show: true },
|
||||
form: {
|
||||
rules: [{ required: true, message: "必填" }],
|
||||
},
|
||||
},
|
||||
parentid: {
|
||||
title: "父级ID",
|
||||
key: "parentid",
|
||||
type: "number",
|
||||
form: {
|
||||
value: 0,
|
||||
rules: [{ required: true, message: "必填" }],
|
||||
},
|
||||
},
|
||||
description: {
|
||||
title: "描述",
|
||||
key: "description",
|
||||
type: "textarea",
|
||||
column: { show: false },
|
||||
},
|
||||
route: {
|
||||
title: "路由",
|
||||
key: "route",
|
||||
type: "text",
|
||||
column: { width: 200 },
|
||||
form: {
|
||||
placeholder: "如:/admin/users",
|
||||
},
|
||||
},
|
||||
sort: {
|
||||
title: "排序",
|
||||
key: "sort",
|
||||
type: "number",
|
||||
column: { width: 80, align: "center" },
|
||||
form: {
|
||||
value: 0,
|
||||
rules: [{ required: true, message: "必填" }],
|
||||
helper: "数字越小,排序越靠前",
|
||||
},
|
||||
},
|
||||
createtime: {
|
||||
title: "创建时间",
|
||||
key: "createtime",
|
||||
type: "text",
|
||||
form: { show: false },
|
||||
column: { width: 170 },
|
||||
valueBuilder({ value, row, key }) {
|
||||
if (!row || value == null) return;
|
||||
(row as Record<string, unknown>)[key as string] = dayjs(value).format(
|
||||
"YYYY-MM-DD HH:mm:ss",
|
||||
);
|
||||
},
|
||||
},
|
||||
updatetime: {
|
||||
title: "更新时间",
|
||||
key: "updatetime",
|
||||
type: "text",
|
||||
form: { show: false },
|
||||
column: { width: 170 },
|
||||
valueBuilder({ value, row, key }) {
|
||||
if (!row || value == null) return;
|
||||
(row as Record<string, unknown>)[key as string] = dayjs(value).format(
|
||||
"YYYY-MM-DD HH:mm:ss",
|
||||
);
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
Loading…
Reference in New Issue
Block a user