Stop tracking pyc files and dist directories

This commit is contained in:
jayhgq 2026-05-27 13:53:16 +08:00
parent c4aac6979a
commit 66096e7a04
48 changed files with 586 additions and 288 deletions

20
.gitignore vendored
View File

@ -1,13 +1,13 @@
.trae/documents/add_traefik_config_plan.md .trae/documents/add_traefik_config_plan.md
redis/data/dump.rdb ./redis/data/dump.rdb
redis/logs/redis-server.log ./redis/logs/redis-server.log
backend/auth/__pycache__/ ./backend/auth/__pycache__/
backend/auth/apps/__pycache__/ ./backend/auth/apps/__pycache__/
backend/auth/apps/permissions/__pycache__/ ./backend/auth/apps/permissions/__pycache__/
backend/auth/apps/roles/__pycache__/ ./backend/auth/apps/roles/__pycache__/
backend/auth/utils/__pycache__/ ./backend/auth/utils/__pycache__/
postgres_data/ ./postgres_data/
frontend/node_modules/ ./frontend/node_modules/
frontend/dist/ ./frontend/dist/
*.pyc *.pyc

View File

@ -0,0 +1,15 @@
# BookSystem - 菜单重构与系统设置功能验证清单
- [x] 后端数据库模型创建:`SystemSetting` 模型已添加到 `models.py`
- [x] 后端API接口系统设置的CRUD接口已实现
- [x] 后端路由注册:系统设置路由已注册到主应用
- [x] 前端CRUD配置`setting-crud.ts` 已创建
- [x] 前端视图组件:`SettingManage.vue` 已创建
- [x] 前端路由配置:系统设置路由已添加
- [x] 菜单结构重构:权限管理一级菜单包含三个二级菜单
- [x] 系统设置菜单:已添加到左侧导航栏
- [x] 权限树状展示:权限列表已改为树形结构
- [x] 权限数据更新:系统设置相关权限已初始化
- [x] 权限Store更新系统设置权限检查方法已添加
- [ ] 系统设置功能测试所有CRUD操作正常
- [ ] 菜单权限控制测试:各角色权限控制正常

View File

@ -0,0 +1,70 @@
# BookSystem - 菜单重构与系统设置功能 PRD
## Overview
- **Summary**: 重构后台管理菜单结构,添加系统设置功能,并优化权限管理的树形展示。
- **Purpose**: 提升系统管理的组织结构清晰度,添加系统级配置管理能力,优化权限管理的用户体验。
- **Target Users**: 系统管理员、运维人员
## Goals
- 重构菜单结构,将用户、角色、权限管理整合到"权限管理"一级菜单下
- 添加"系统设置"一级菜单支持系统标题、logo、脚注等配置
- 权限管理列表改为树状显示,支持三级菜单结构
## Non-Goals (Out of Scope)
- 用户认证逻辑的修改
- 现有数据模型的破坏性变更
- 前端UI框架的更换
## Background & Context
- 当前系统已实现基础的用户、角色、权限管理功能
- 菜单结构较为扁平,随着功能扩展需要更好的组织
- 系统缺少统一的配置管理入口
## Functional Requirements
- **FR-1**: 菜单结构重构,创建"权限管理"一级菜单,包含用户管理、角色管理、权限管理三个二级菜单
- **FR-2**: 添加"系统设置"一级菜单包含系统配置CRUD功能
- **FR-3**: 权限管理列表改为树状结构展示,支持展开/折叠
- **FR-4**: 系统设置支持配置系统标题、系统logo、系统脚注内容
## Non-Functional Requirements
- **NFR-1**: 菜单权限控制保持原有逻辑
- **NFR-2**: 树状结构支持无限层级展示
- **NFR-3**: 系统设置数据持久化存储
## Constraints
- **Technical**: Python 3.13 + FastAPI + SQLAlchemy + Vue 3 + Element Plus
- **Dependencies**: 现有代码结构和数据库模型
## Assumptions
- 权限表已存在 `parentid` 字段支持树形结构
- 前端使用 fast-crud 组件库
## Acceptance Criteria
### AC-1: 菜单结构重构完成
- **Given**: 用户登录后台管理系统
- **When**: 查看左侧菜单
- **Then**: 看到"权限管理"一级菜单,展开后包含"用户管理"、"角色管理"、"权限管理"二级菜单
- **Verification**: `human-judgment`
### AC-2: 系统设置菜单添加完成
- **Given**: 用户登录后台管理系统且有系统设置权限
- **When**: 查看左侧菜单
- **Then**: 看到"系统设置"一级菜单
- **Verification**: `human-judgment`
### AC-3: 系统设置CRUD功能正常
- **Given**: 系统设置模块已实现
- **When**: 点击系统设置菜单,进行增删改查操作
- **Then**: 所有操作正常,数据正确持久化
- **Verification**: `programmatic`
### AC-4: 权限树状展示正常
- **Given**: 权限列表页面
- **When**: 查看权限列表
- **Then**: 权限以树形结构展示,支持展开/折叠,正确显示层级关系
- **Verification**: `human-judgment`
## Open Questions
- [ ] 系统设置是否需要多语言支持?
- [ ] 系统logo是否需要支持上传功能

View File

@ -0,0 +1,102 @@
# BookSystem - 菜单重构与系统设置功能实现计划
## [ ] Task 1: 后端 - 添加系统设置数据库模型
- **Priority**: P0
- **Depends On**: None
- **Description**:
- 在 `backend/auth/models.py` 中添加 `SystemSetting` 模型
- 字段包括id、key、value、description、createtime、updatetime
- **Acceptance Criteria Addressed**: AC-3
- **Test Requirements**:
- `programmatic` TR-1.1: 数据库表自动创建成功
- `programmatic` TR-1.2: 模型字段正确映射到数据库列
- **Notes**: 使用 SQLAlchemy 声明式基类
## [ ] Task 2: 后端 - 添加系统设置API接口
- **Priority**: P0
- **Depends On**: Task 1
- **Description**:
- 在 `backend/auth/apps/` 下创建 `settings/` 目录
- 创建 `urls.py` 实现 CRUD 接口
- 在 `schemas.py` 中添加系统设置相关的 Pydantic 模型
- **Acceptance Criteria Addressed**: AC-3
- **Test Requirements**:
- `programmatic` TR-2.1: POST /settings/ 成功创建设置项
- `programmatic` TR-2.2: GET /settings/ 成功获取所有设置项
- `programmatic` TR-2.3: PUT /settings/{id} 成功更新设置项
- `programmatic` TR-2.4: DELETE /settings/{id} 成功删除设置项
- **Notes**: 参考现有 users/roles/permissions 的实现模式
## [ ] Task 3: 后端 - 更新应用路由配置
- **Priority**: P0
- **Depends On**: Task 2
- **Description**:
- 在 `backend/auth/apps/urls.py` 中注册系统设置路由
- **Acceptance Criteria Addressed**: AC-3
- **Test Requirements**:
- `programmatic` TR-3.1: /settings/* 接口可访问
## [ ] Task 4: 前端 - 创建系统设置CRUD配置
- **Priority**: P0
- **Depends On**: Task 3
- **Description**:
- 在 `frontend/src/modules/admin/views/` 下创建 `settings/` 目录
- 创建 `setting-crud.ts` CRUD配置文件
- 创建 `SettingManage.vue` 视图组件
- **Acceptance Criteria Addressed**: AC-3
- **Test Requirements**:
- `human-judgment` TR-4.1: 设置管理页面正常显示
- `human-judgment` TR-4.2: CRUD操作按钮正常工作
## [ ] Task 5: 前端 - 更新路由配置
- **Priority**: P0
- **Depends On**: Task 4
- **Description**:
- 在 `frontend/src/router/index.ts` 中添加系统设置路由
- **Acceptance Criteria Addressed**: AC-2, AC-3
- **Test Requirements**:
- `human-judgment` TR-5.1: 系统设置路由可访问
## [ ] Task 6: 前端 - 重构菜单结构
- **Priority**: P0
- **Depends On**: Task 5
- **Description**:
- 修改 `frontend/src/modules/admin/layout/AdminLayout.vue`
- 将用户、角色、权限菜单改为"权限管理"一级菜单下的二级菜单
- 添加"系统设置"一级菜单
- **Acceptance Criteria Addressed**: AC-1, AC-2
- **Test Requirements**:
- `human-judgment` TR-6.1: 菜单结构正确显示
- `human-judgment` TR-6.2: 菜单权限控制正常
## [ ] Task 7: 前端 - 权限管理改为树状展示
- **Priority**: P1
- **Depends On**: None
- **Description**:
- 修改 `frontend/src/modules/admin/views/permissions/permission-crud.ts`
- 将列表展示改为树形展示
- 使用 fast-crud 的树形组件
- **Acceptance Criteria Addressed**: AC-4
- **Test Requirements**:
- `human-judgment` TR-7.1: 权限以树形结构展示
- `human-judgment` TR-7.2: 支持展开/折叠操作
- `human-judgment` TR-7.3: 层级关系正确显示
## [ ] Task 8: 更新权限配置数据
- **Priority**: P1
- **Depends On**: Task 1
- **Description**:
- 更新 `backend/auth/init_data.py` 添加系统设置相关权限
- 添加系统设置的一级/二级权限
- **Acceptance Criteria Addressed**: AC-2
- **Test Requirements**:
- `programmatic` TR-8.1: 系统启动时权限数据正确初始化
## [x] Task 9: 前端 - 更新权限Store
- **Priority**: P1
- **Depends On**: Task 8
- **Description**:
- 更新 `frontend/src/modules/admin/stores/auth.ts` 添加系统设置权限检查方法
- **Acceptance Criteria Addressed**: AC-2
- **Test Requirements**:
- `human-judgment` TR-9.1: 系统设置菜单权限控制正常

View File

@ -0,0 +1,131 @@
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, and_
from fastcrud import FastCRUD
from database import get_db
from models import SystemSetting, User
from schemas import (
SystemSettingCreate,
SystemSettingUpdate,
SystemSettingResponse,
)
from middleware import get_current_user
from utils.like_filter import ilike_contains
router = APIRouter(prefix="/settings", tags=["settings"])
# 创建FastCRUD实例
setting_crud = FastCRUD(SystemSetting, SystemSettingResponse)
@router.post("/", response_model=SystemSettingResponse)
async def create_setting(
setting_data: SystemSettingCreate,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""创建设置项"""
# 检查设置键名是否已存在
result = await db.execute(select(SystemSetting).filter(SystemSetting.key == setting_data.key))
existing_setting = result.scalar_one_or_none()
if existing_setting:
raise HTTPException(status_code=400, detail="设置键名已存在")
# 创建设置实例
db_setting = SystemSetting(
key=setting_data.key,
value=setting_data.value,
description=setting_data.description,
)
db.add(db_setting)
await db.commit()
await db.refresh(db_setting)
return db_setting
@router.get("/", response_model=list[SystemSettingResponse])
async def get_settings(
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""获取所有设置项"""
result = await db.execute(select(SystemSetting))
settings = result.scalars().all()
return [SystemSettingResponse.model_validate(s) for s in settings]
@router.get("/{setting_id}", response_model=SystemSettingResponse)
async def get_setting(
setting_id: int,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""获取单个设置项"""
setting = await setting_crud.get(db, id=setting_id)
if not setting:
raise HTTPException(status_code=404, detail="设置项不存在")
return setting
@router.get("/key/{key}", response_model=SystemSettingResponse)
async def get_setting_by_key(
key: str,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""根据键名获取设置项"""
result = await db.execute(select(SystemSetting).filter(SystemSetting.key == key))
setting = result.scalar_one_or_none()
if not setting:
raise HTTPException(status_code=404, detail="设置项不存在")
return setting
@router.put("/{setting_id}", response_model=SystemSettingResponse)
async def update_setting(
setting_id: int,
setting_data: SystemSettingUpdate,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""更新设置项"""
# 检查设置键名是否被其他设置使用
if setting_data.key:
result = await db.execute(
select(SystemSetting).filter(SystemSetting.key == setting_data.key, SystemSetting.id != setting_id)
)
existing_setting = result.scalar_one_or_none()
if existing_setting:
raise HTTPException(status_code=400, detail="设置键名已存在")
# 准备更新数据
update_data = setting_data.model_dump(exclude_unset=True)
# 使用FastCRUD更新设置
updated_setting = await setting_crud.update(
db, update_data, id=setting_id, schema_to_select=SystemSettingResponse, return_as_model=True
)
if not updated_setting:
raise HTTPException(status_code=404, detail="设置项不存在")
return updated_setting
@router.delete("/{setting_id}")
async def delete_setting(
setting_id: int,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""删除设置项"""
# 检查设置是否存在
result = await db.execute(select(SystemSetting).filter(SystemSetting.id == setting_id))
existing_setting = result.scalar_one_or_none()
if not existing_setting:
raise HTTPException(status_code=404, detail="设置项不存在")
# 使用FastCRUD删除设置
deleted = await setting_crud.delete(db, id=setting_id)
return {"message": "设置项删除成功"}

View File

@ -2,6 +2,7 @@ from fastapi import APIRouter
from .users.urls import router as users_router from .users.urls import router as users_router
from .roles.urls import router as roles_router from .roles.urls import router as roles_router
from .permissions.urls import router as permissions_router from .permissions.urls import router as permissions_router
from .settings.urls import router as settings_router
# 创建主路由 # 创建主路由
api_router = APIRouter() api_router = APIRouter()
@ -10,3 +11,4 @@ api_router = APIRouter()
api_router.include_router(users_router) api_router.include_router(users_router)
api_router.include_router(roles_router) api_router.include_router(roles_router)
api_router.include_router(permissions_router) api_router.include_router(permissions_router)
api_router.include_router(settings_router)

View File

@ -57,6 +57,7 @@ async def init_permissions(db: AsyncSession):
{"name": "用户管理", "code": "perm:user", "parentid": 1, "description": "用户管理子模块"}, {"name": "用户管理", "code": "perm:user", "parentid": 1, "description": "用户管理子模块"},
{"name": "角色管理", "code": "perm:role", "parentid": 1, "description": "角色管理子模块"}, {"name": "角色管理", "code": "perm:role", "parentid": 1, "description": "角色管理子模块"},
{"name": "权限配置", "code": "perm:config", "parentid": 1, "description": "权限配置子模块"}, {"name": "权限配置", "code": "perm:config", "parentid": 1, "description": "权限配置子模块"},
{"name": "系统设置", "code": "perm:system", "parentid": 1, "description": "系统设置子模块"},
# 用户管理操作 # 用户管理操作
{"name": "查看列表", "code": "perm:user:list", "parentid": 2, "description": "查看用户列表"}, {"name": "查看列表", "code": "perm:user:list", "parentid": 2, "description": "查看用户列表"},
@ -85,6 +86,13 @@ async def init_permissions(db: AsyncSession):
{"name": "编辑", "code": "perm:config:edit", "parentid": 4, "description": "编辑权限信息"}, {"name": "编辑", "code": "perm:config:edit", "parentid": 4, "description": "编辑权限信息"},
{"name": "删除", "code": "perm:config:delete", "parentid": 4, "description": "删除权限"}, {"name": "删除", "code": "perm:config:delete", "parentid": 4, "description": "删除权限"},
{"name": "导出", "code": "perm:config:export", "parentid": 4, "description": "导岀权限数据"}, {"name": "导出", "code": "perm:config:export", "parentid": 4, "description": "导岀权限数据"},
# 系统设置操作parentid 为 5对应系统设置子模块
{"name": "查看列表", "code": "perm:system:list", "parentid": 5, "description": "查看系统设置列表"},
{"name": "查看详情", "code": "perm:system:detail", "parentid": 5, "description": "查看系统设置详情"},
{"name": "创建", "code": "perm:system:create", "parentid": 5, "description": "创建设置项"},
{"name": "编辑", "code": "perm:system:edit", "parentid": 5, "description": "编辑设置项"},
{"name": "删除", "code": "perm:system:delete", "parentid": 5, "description": "删除设置项"},
] ]
# 检查现有权限 # 检查现有权限

View File

@ -80,3 +80,17 @@ role_permission = Table(
Column("role_id", Integer, ForeignKey("role.id"), primary_key=True, comment="角色ID"), Column("role_id", Integer, ForeignKey("role.id"), primary_key=True, comment="角色ID"),
Column("permission_id", Integer, ForeignKey("permission.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(1000), nullable=False, 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="更新时间"
)

View File

@ -183,3 +183,38 @@ class RolePermissionResponse(BaseModel):
role_id: int = Field(..., description="角色ID") role_id: int = Field(..., description="角色ID")
role_name: str = Field(..., description="角色名称") role_name: str = Field(..., description="角色名称")
permissions: list[PermissionResponse] = Field(..., description="权限列表") permissions: list[PermissionResponse] = Field(..., description="权限列表")
# 系统设置相关模型
class SystemSettingBase(BaseModel):
"""系统设置基础模型"""
key: str = Field(..., max_length=100, description="设置键名")
value: str = Field(..., max_length=1000, 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="设置值")
description: Optional[str] = Field(None, description="设置描述")
createtime: datetime = Field(..., description="创建时间")
updatetime: datetime = Field(..., description="更新时间")
class Config:
from_attributes = True

View File

@ -1 +0,0 @@
import{f as C,u as A,g as L,E as N,p as S,w as t,k as V,l as q,r as a,d as e,a as o,q as l,s as E,t as M,v as P,x as p,b as R,y as f,o as T,_ as D}from"./index-BtuGmbJn.js";const I={class:"right"},j={class:"nick"},z={class:"page-title-bar"},F={class:"page-title"},G={class:"page-content"},H=C({__name:"AdminLayout",setup(J){const u=V(),d=q(),s=A(),v=f(()=>u.path),g=f(()=>u.meta.title||"");L(async()=>{try{s.user||await s.fetchProfile()}catch{N.error("获取用户信息失败,请重新登录"),await s.logout(),d.replace({name:"admin-login",query:{redirect:u.fullPath}})}});async function x(){await s.logout(),d.push({name:"admin-login"})}return(K,n)=>{const i=a("el-icon"),r=a("el-menu-item"),y=a("el-menu"),h=a("el-aside"),k=a("el-button"),b=a("el-header"),w=a("router-view"),B=a("el-main"),c=a("el-container");return T(),S(c,{class:"admin-root"},{default:t(()=>[e(h,{width:"220px",class:"aside"},{default:t(()=>[n[3]||(n[3]=o("div",{class:"brand"},"BookSystem 管理",-1)),e(y,{"default-active":v.value,router:"","background-color":"#0f172a","text-color":"#e2e8f0","active-text-color":"#38bdf8"},{default:t(()=>[e(r,{index:"/admin/users",route:{name:"admin-users"}},{default:t(()=>[e(i,null,{default:t(()=>[e(l(E))]),_:1}),n[0]||(n[0]=o("span",null,"用户",-1))]),_:1}),e(r,{index:"/admin/roles",route:{name:"admin-roles"}},{default:t(()=>[e(i,null,{default:t(()=>[e(l(M))]),_:1}),n[1]||(n[1]=o("span",null,"角色",-1))]),_:1}),e(r,{index:"/admin/permissions",route:{name:"admin-permissions"}},{default:t(()=>[e(i,null,{default:t(()=>[e(l(P))]),_:1}),n[2]||(n[2]=o("span",null,"权限",-1))]),_:1})]),_:1},8,["default-active"])]),_:1}),e(c,null,{default:t(()=>[e(b,{class:"header"},{default:t(()=>{var _,m;return[o("div",I,[o("span",j,p(((_=l(s).user)==null?void 0:_.nickname)||((m=l(s).user)==null?void 0:m.username)),1),e(k,{type:"primary",link:"",onClick:x},{default:t(()=>[...n[4]||(n[4]=[R("退出",-1)])]),_:1})])]}),_:1}),e(B,{class:"main"},{default:t(()=>[o("div",z,[o("h1",F,p(g.value),1)]),o("div",G,[e(w)])]),_:1})]),_:1})]),_:1})}}}),Q=D(H,[["__scopeId","data-v-c0bd569a"]]);export{Q as default};

View File

@ -1 +0,0 @@
.admin-root[data-v-c0bd569a]{height:100vh}.aside[data-v-c0bd569a]{background:#0f172a;color:#e2e8f0}.brand[data-v-c0bd569a]{padding:18px 16px;font-weight:600;letter-spacing:.02em;border-bottom:1px solid rgba(148,163,184,.2)}.header[data-v-c0bd569a]{display:flex;align-items:center;justify-content:flex-end;border-bottom:1px solid var(--el-border-color-lighter);background:#fff}.right[data-v-c0bd569a]{display:flex;align-items:center;gap:12px}.page-title-bar[data-v-c0bd569a]{flex-shrink:0;margin-bottom:16px}.page-title[data-v-c0bd569a]{margin:0;font-size:20px;font-weight:600;color:var(--el-text-color-primary);line-height:1.4}.nick[data-v-c0bd569a]{color:var(--el-text-color-secondary);font-size:14px}.main[data-v-c0bd569a]{background:#f8fafc;padding:16px;display:flex;flex-direction:column;min-width:0}

View File

@ -1 +0,0 @@
import{f as L,u as q,g as I,c as f,d as o,w as s,h as S,E as h,r as u,i,j,k as B,l as E,o as w,m as M,a as g,b as N,n as O,_ as T}from"./index-BtuGmbJn.js";const D={class:"login-wrap"},A={class:"captcha-row"},P=["src"],W={key:1,class:"captcha-placeholder"},X=L({__name:"LoginView",setup($){const y=B(),V=E(),v=q(),b=i(),m=i(!1),l=i(""),p=i(""),k="/api",a=j({username:"",password:"",captcha_code:""}),x={username:[{required:!0,message:"请输入用户名",trigger:"blur"}],password:[{required:!0,message:"请输入密码",trigger:"blur"}],captcha_code:[{required:!0,message:"请输入验证码",trigger:"blur"}]};async function _(){l.value.startsWith("blob:")&&URL.revokeObjectURL(l.value),l.value="",p.value="";const r=await S.get(`${k}/users/captcha`,{responseType:"blob",headers:{}}),e=r.headers["x-captcha-id"]||r.headers["X-Captcha-ID"];if(!e){h.error("未获取到验证码 ID请检查后端 CORS expose_headers");return}p.value=e,l.value=URL.createObjectURL(r.data),a.captcha_code=""}async function R(){var e,n;if(!p.value){h.warning("请先加载验证码");return}const r=b.value;if(r){try{await r.validate()}catch{return}m.value=!0;try{const{data:t}=await O.post("/users/login",{username:a.username,password:a.password,captcha_id:p.value,captcha_code:a.captcha_code});v.setToken(t.access_token),await v.fetchProfile();const c=y.query.redirect||"/admin";await V.replace(c)}catch(t){const c=(n=(e=t==null?void 0:t.response)==null?void 0:e.data)==null?void 0:n.detail;h.error(typeof c=="string"?c:"登录失败"),await _()}finally{m.value=!1}}}return I(()=>{_()}),(r,e)=>{const n=u("el-input"),t=u("el-form-item"),c=u("el-button"),U=u("el-form"),C=u("el-card");return w(),f("div",D,[o(C,{class:"card",shadow:"hover"},{header:s(()=>[...e[4]||(e[4]=[g("span",{class:"card-title"},"管理后台登录",-1)])]),default:s(()=>[o(U,{ref_key:"formRef",ref:b,model:a,rules:x,"label-width":"88px",onSubmit:e[3]||(e[3]=M(()=>{},["prevent"]))},{default:s(()=>[o(t,{label:"用户名",prop:"username"},{default:s(()=>[o(n,{modelValue:a.username,"onUpdate:modelValue":e[0]||(e[0]=d=>a.username=d),autocomplete:"username"},null,8,["modelValue"])]),_:1}),o(t,{label:"密码",prop:"password"},{default:s(()=>[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(t,{label:"验证码",prop:"captcha_code"},{default:s(()=>[g("div",A,[o(n,{modelValue:a.captcha_code,"onUpdate:modelValue":e[2]||(e[2]=d=>a.captcha_code=d),maxlength:"8",placeholder:"请输入验证码"},null,8,["modelValue"]),g("div",{class:"captcha-img",onClick:_},[l.value?(w(),f("img",{key:0,src:l.value,alt:"captcha"},null,8,P)):(w(),f("span",W,"点击获取"))])])]),_:1}),o(t,null,{default:s(()=>[o(c,{type:"primary",loading:m.value,style:{width:"100%"},onClick:R},{default:s(()=>[...e[5]||(e[5]=[N(" 登录 ",-1)])]),_:1},8,["loading"])]),_:1})]),_:1},8,["model"])]),_:1})])}}}),F=T(X,[["__scopeId","data-v-24c39918"]]);export{F as default};

View File

@ -1 +0,0 @@
.login-wrap[data-v-24c39918]{min-height:100vh;display:flex;align-items:center;justify-content:center;background:linear-gradient(160deg,#0f172a,#1e3a5f,#0f172a);padding:24px}.card[data-v-24c39918]{width:100%;max-width:420px;border-radius:12px}.card-title[data-v-24c39918]{font-weight:600;font-size:16px}.captcha-row[data-v-24c39918]{display:flex;gap:12px;align-items:center;width:100%}.captcha-img[data-v-24c39918]{flex-shrink:0;width:120px;height:40px;border:1px solid var(--el-border-color);border-radius:4px;cursor:pointer;overflow:hidden;display:flex;align-items:center;justify-content:center;background:#f8fafc}.captcha-img img[data-v-24c39918]{width:100%;height:100%;object-fit:cover}.captcha-placeholder[data-v-24c39918]{font-size:12px;color:var(--el-text-color-secondary)}

View File

@ -1 +0,0 @@
import{z as a,n as r,f as m,A as c,g as d,p,w as l,r as n,d as f,B as y,q as h,o as _}from"./index-BtuGmbJn.js";import{t as g}from"./list-query-4ZZvVtjn.js";const w=()=>({crudOptions:{pagination:{show:!1},search:{show:!0,options:{layout:"inline"},expand:!1},request:{transformQuery:({form:e})=>g(e,["name","code"]),pageRequest:async e=>{const{data:t}=await r.post("/permissions/search",e);return t},transformRes:({res:e})=>{const t=Array.isArray(e)?e:[];return{currentPage:1,pageSize:Math.max(t.length,1),total:t.length,records:t}},addRequest:async({form:e})=>{await r.post("/permissions/",e)},editRequest:async({form:e,row:t})=>{t&&await r.put(`/permissions/${t.id}`,e)},delRequest:async({row:e})=>{e&&await r.delete(`/permissions/${e.id}`)}},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}},createtime:{title:"创建时间",key:"createtime",type:"text",form:{show:!1},column:{width:170},valueBuilder({value:e,row:t,key:s}){!t||e==null||(t[s]=a(e).format("YYYY-MM-DD HH:mm:ss"))}},updatetime:{title:"更新时间",key:"updatetime",type:"text",form:{show:!1},column:{width:170},valueBuilder({value:e,row:t,key:s}){!t||e==null||(t[s]=a(e).format("YYYY-MM-DD HH:mm:ss"))}}}}}),Y=m({__name:"PermissionManage",setup(e){const{crudBinding:t,crudRef:s,crudExpose:o}=c({createCrudOptions:w});return d(()=>{o.doRefresh()}),(x,k)=>{const i=n("fs-crud"),u=n("fs-page");return _(),p(u,null,{default:l(()=>[f(i,y({ref_key:"crudRef",ref:s},h(t)),null,16)]),_:1})}}});export{Y as default};

View File

@ -1 +0,0 @@
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-BtuGmbJn.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};

View File

@ -1 +0,0 @@
.public-wrap[data-v-f5876933]{max-width:640px;margin:10vh auto;padding:0 24px;font-family:system-ui,-apple-system,sans-serif}.hint[data-v-f5876933]{line-height:1.6;color:#555}

View File

@ -1 +0,0 @@
.dlg-role[data-v-0e80fdd4]{margin:0 0 12px;font-size:14px}.perm-tags[data-v-0e80fdd4]{display:flex;flex-wrap:wrap;gap:8px}.perm-tag[data-v-0e80fdd4]{margin:0}.perm-tree[data-v-0e80fdd4]{max-height:440px;overflow:auto;padding:8px 4px;border:1px solid var(--el-border-color-lighter);border-radius:8px;background:var(--el-fill-color-blank)}

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
import{z as n,O as d,n as a,E as m,f as c,A as p,g as f,p as h,w as y,r as i,d as w,B as g,q as _,o as x}from"./index-BtuGmbJn.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:t}=await a.post("/users/search",e);return t},transformRes:({res:e})=>{const t=Array.isArray(e)?e:[];return{currentPage:1,pageSize:Math.max(t.length,1),total:t.length,records:t}},onError:e=>{var s,r;let t="操作失败";if((r=(s=e.response)==null?void 0:s.data)!=null&&r.detail){const o=e.response.data.detail;typeof o=="string"&&(o.includes("at least 6 characters")?t="密码长度至少为6个字符":t=o)}else e.message&&(t=e.message);throw m.error(t),e},addRequest:async({form:e})=>{await a.post("/users/",e)},editRequest:async({form:e,row:t})=>{if(!t)return;const s={...e};s.password||delete s.password,await a.put(`/users/${t.id}`,s)},delRequest:async({row:e})=>{e&&await a.delete(`/users/${e.id}`)}},rowHandle:{buttons:{edit:{text:"编辑"},remove:{text:"删除"}}},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:"必填"}]}},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:"新建用户需设置密码"}]},editForm:{show:!0,component:{placeholder:"不修改请留空"}}},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:d({getData:async()=>{const{data:e}=await a.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:t,key:s}){!t||e==null||(t[s]=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:t,key:s}){!t||e==null||(t[s]=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:t,key:s}){!t||e==null||(t[s]=n(e).format("YYYY-MM-DD HH:mm:ss"))}}}}}),R=c({__name:"UserManage",setup(e){const{crudBinding:t,crudRef:s,crudExpose:r}=p({createCrudOptions:Y});return f(()=>{r.doRefresh()}),(o,q)=>{const l=i("fs-crud"),u=i("fs-page");return x(),h(u,null,{default:y(()=>[w(l,g({ref_key:"crudRef",ref:s},_(t)),null,16)]),_:1})}}});export{R as default};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
function c(o,i){const r={};for(const n of i){const t=o[n];t!=null&&String(t).trim()!==""&&(r[n]=String(t).trim())}return r}export{c as t};

View File

@ -1,13 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<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-DXkIJgFR.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-Cn04Rt7y.css">
</head>
<body>
<div id="app"></div>
</body>
</html>

View File

@ -9,29 +9,46 @@
text-color="#e2e8f0" text-color="#e2e8f0"
active-text-color="#38bdf8" active-text-color="#38bdf8"
> >
<!-- 权限管理一级菜单 -->
<el-sub-menu
v-if="hasAnyPermission"
index="/admin/permissions"
>
<template #title>
<el-icon><Key /></el-icon>
<span>权限管理</span>
</template>
<el-menu-item <el-menu-item
v-if="hasUserPermission" v-if="hasUserPermission"
index="/admin/users" index="/admin/users"
:route="{ name: 'admin-users' }" :route="{ name: 'admin-users' }"
> >
<el-icon><User /></el-icon> <span>用户管理</span>
<span>用户</span>
</el-menu-item> </el-menu-item>
<el-menu-item <el-menu-item
v-if="hasRolePermission" v-if="hasRolePermission"
index="/admin/roles" index="/admin/roles"
:route="{ name: 'admin-roles' }" :route="{ name: 'admin-roles' }"
> >
<el-icon><UserFilled /></el-icon> <span>角色管理</span>
<span>角色</span>
</el-menu-item> </el-menu-item>
<el-menu-item <el-menu-item
v-if="hasPermissionPermission" v-if="hasPermissionPermission"
index="/admin/permissions" index="/admin/permissions"
:route="{ name: 'admin-permissions' }" :route="{ name: 'admin-permissions' }"
> >
<el-icon><Key /></el-icon> <span>权限管理</span>
<span>权限</span> </el-menu-item>
</el-sub-menu>
<!-- 系统设置一级菜单 -->
<el-menu-item
v-if="hasSystemPermission"
index="/admin/settings"
:route="{ name: 'admin-settings' }"
>
<el-icon><Setting /></el-icon>
<span>系统设置</span>
</el-menu-item> </el-menu-item>
</el-menu> </el-menu>
</el-aside> </el-aside>
@ -57,7 +74,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, onMounted } from "vue"; import { computed, onMounted } from "vue";
import { useRoute, useRouter } from "vue-router"; import { useRoute, useRouter } from "vue-router";
import { User, UserFilled, Key } from "@element-plus/icons-vue"; import { Key, Setting } from "@element-plus/icons-vue";
import { ElMessage } from "element-plus"; import { ElMessage } from "element-plus";
import { useAuthStore } from "@/modules/admin/stores/auth"; import { useAuthStore } from "@/modules/admin/stores/auth";
@ -80,6 +97,14 @@ const hasPermissionPermission = computed(() => {
return auth.permissions.some(p => p.code.startsWith("perm:config")); return auth.permissions.some(p => p.code.startsWith("perm:config"));
}); });
const hasSystemPermission = computed(() => {
return auth.permissions.some(p => p.code.startsWith("perm:system"));
});
const hasAnyPermission = computed(() => {
return hasUserPermission.value || hasRolePermission.value || hasPermissionPermission.value;
});
onMounted(async () => { onMounted(async () => {
try { try {
if (!auth.user) { if (!auth.user) {

View File

@ -11,6 +11,7 @@ export type PermissionRow = {
description?: string | null; description?: string | null;
createtime: string; createtime: string;
updatetime: string; updatetime: string;
children?: PermissionRow[];
}; };
export const createPermissionCrudOptions: CreateCrudOptions<PermissionRow> = () => { export const createPermissionCrudOptions: CreateCrudOptions<PermissionRow> = () => {
@ -24,11 +25,17 @@ export const createPermissionCrudOptions: CreateCrudOptions<PermissionRow> = ()
}, },
expand: false, expand: false,
}, },
tree: {
enable: true,
childrenKey: "children",
parentKey: "parentid",
hasChildField: "children",
},
request: { request: {
transformQuery: ({ form }) => transformQuery: ({ form }) =>
toSearchParams(form as Record<string, unknown>, ["name", "code"]), toSearchParams(form as Record<string, unknown>, ["name", "code"]),
pageRequest: async (query) => { pageRequest: async (_query) => {
const { data } = await http.post<PermissionRow[]>("/permissions/search", query); const { data } = await http.get<PermissionRow[]>("/permissions/tree");
return data; return data;
}, },
transformRes: ({ res }) => { transformRes: ({ res }) => {

View File

@ -0,0 +1,25 @@
<template>
<fs-page>
<fs-crud ref="crudRef" v-bind="crudBinding" />
</fs-page>
</template>
<script setup lang="ts">
import { onMounted } from "vue";
import { useFs } from "@fast-crud/fast-crud";
import { createSettingCrudOptions } from "./setting-crud";
const { crudBinding, crudRef, crudExpose } = useFs({
createCrudOptions: createSettingCrudOptions,
});
onMounted(() => {
crudExpose.doRefresh();
});
</script>
<style scoped>
.setting-manage {
height: 100%;
}
</style>

View File

@ -0,0 +1,117 @@
import type { CreateCrudOptions } from "@fast-crud/fast-crud";
import dayjs from "dayjs";
import { http } from "@/modules/admin/api/http";
export type SettingRow = {
id: number;
key: string;
value: string;
description?: string | null;
createtime: string;
updatetime: string;
};
export const createSettingCrudOptions: CreateCrudOptions<SettingRow> = () => {
return {
crudOptions: {
pagination: { show: false },
search: {
show: true,
options: {
layout: 'inline',
},
expand: false,
},
request: {
pageRequest: async () => {
const { data } = await http.get<SettingRow[]>("/settings/");
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("/settings/", form);
},
editRequest: async ({ form, row }) => {
if (!row) return;
await http.put(`/settings/${row.id}`, form);
},
delRequest: async ({ row }) => {
if (!row) return;
await http.delete(`/settings/${row.id}`);
},
},
rowHandle: {
buttons: {
edit: { text: "编辑" },
remove: { text: "删除" },
},
},
columns: {
id: {
title: "ID",
key: "id",
type: "number",
column: { width: 70 },
form: { show: false },
},
key: {
title: "设置键名",
key: "key",
type: "text",
search: { show: true },
form: {
rules: [{ required: true, message: "请输入设置键名", trigger: "blur" }],
},
},
value: {
title: "设置值",
key: "value",
type: "textarea",
form: {
rules: [{ required: true, message: "请输入设置值", trigger: "blur" }],
},
},
description: {
title: "描述",
key: "description",
type: "textarea",
column: { show: false },
},
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",
);
},
},
},
},
};
};