Compare commits

..

3 Commits

Author SHA1 Message Date
6751a5c2b2 Merge pull request 'feat: 菜单管理添加排序功能' (#1) from feat/menu-sort into main
Reviewed-on: https://gitea.jayhgq.cn/jayhgq/BookSystem/pulls/1
2026-07-20 17:17:48 +08:00
jayhgq
677d8a53b5 build(frontend): update frontend build artifacts for new deploy
update the hashed asset filenames in index.html, replace old deleted assets with new built ones, add all latest dist assets to the project bundle
2026-07-20 17:16:38 +08:00
jayhgq
9586c7467a feat: 菜单管理添加排序功能
- Permission 模型新增 sort 字段
- 后端 API 按 sort 升序排列树形菜单
- 前端菜单管理页面增加排序列
- 初始数据添加排序号
- 修复 GET /permissions/{id} 详情接口 bug

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 08:33:51 +00:00
20 changed files with 1349 additions and 1290 deletions

View File

@ -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",

View File

@ -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("数据库数据初始化完成")

View File

@ -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=["_"])

View File

@ -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="父权限ID0表示顶级模块")
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="父权限ID0表示顶级模块")
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)

View File

@ -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="父权限ID0表示顶级模块")
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="父权限ID0表示顶级模块")
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 = "注册成功"

View File

@ -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};

View File

@ -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};

View File

@ -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};

View File

@ -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};

View File

@ -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};

View File

@ -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

View File

@ -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}

View File

@ -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};

View 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}

View File

@ -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

View File

@ -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>

View File

@ -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",
);
},
},
},
},
};
};