fix(roles): 修复角色创建时 model_dump 错误

修复角色创建时出现的 AttributeError: 'dict' object has no attribute 'model_dump' 错误,将 role_data.model_dump() 改为 dict(role_data) 以兼容 Pydantic v1 和 v2。

同时优化了搜索布局和样式,确保查询条件和按钮在同一行显示:

1. 修改 admin-crud.css 添加搜索区域不换行样式
2. 调整 role-crud.ts 中的 search 配置
3. 将 GET 查询接口改为 POST /search 方式
4. 修复角色权限分配时的权限加载问题
This commit is contained in:
jayhgq 2026-05-09 17:59:10 +08:00
parent 83d84254f5
commit 6f4ace886f
29 changed files with 404 additions and 56 deletions

View File

@ -0,0 +1,121 @@
# Bug 修复计划
## 问题描述
### 问题1查询条件布局优化
前端页面中用户、角色、权限的页面中查询条件和查询、重置按钮需要放到一行,并且去掉展开按钮。
### 问题2查询方法修改
查询目前使用 GET 方法进行的,需要改为 POST 方法。
## 项目结构分析
```
frontend/
└── src/
└── modules/
└── admin/
├── views/
│ ├── users/
│ │ ├── UserManage.vue # 用户管理页面
│ │ └── user-crud.ts # 用户 CRUD 配置
│ ├── roles/
│ │ ├── RoleManage.vue # 角色管理页面
│ │ └── role-crud.ts # 角色 CRUD 配置
│ └── permissions/
│ ├── PermissionManage.vue # 权限管理页面
│ └── permission-crud.ts # 权限 CRUD 配置
└── api/
└── http.ts # HTTP 请求封装
```
## 修复方案
### 问题1查询条件布局优化
在 fast-crud 中,可以通过配置 `search` 属性来控制搜索表单的布局:
| 配置项 | 值 | 说明 |
|--------|-----|------|
| `show` | `true` | 显示搜索表单 |
| `inline` | `true` | 查询条件和按钮在一行 |
| `expand` | `false` | 隐藏展开按钮 |
需要修改的文件:
- `frontend/src/modules/admin/views/users/user-crud.ts`
- `frontend/src/modules/admin/views/roles/role-crud.ts`
- `frontend/src/modules/admin/views/permissions/permission-crud.ts`
### 问题2查询方法修改
需要同时修改前端和后端:
**前端修改**
- 将 `pageRequest` 中的 `http.get` 改为 `http.post`
- 将查询参数从 URL 参数改为请求体
**后端修改**
- 将 `/users/`、`/roles/`、`/permissions/` 的 GET 请求改为 POST 请求
- 修改参数接收方式,从 Query 参数改为请求体
需要修改的文件:
- 前端:`user-crud.ts`、`role-crud.ts`、`permission-crud.ts`
- 后端:`users/urls.py`、`roles/urls.py`、`permissions/urls.py`
## 修改步骤
### 步骤1修改前端 CRUD 配置问题1+问题2
修改三个 CRUD 配置文件:
1. `user-crud.ts`:
- 添加 `search: { inline: true, expand: false }` 配置
- 将 `pageRequest` 中的 `http.get` 改为 `http.post`
2. `role-crud.ts`:
- 添加 `search: { inline: true, expand: false }` 配置
- 将 `pageRequest` 中的 `http.get` 改为 `http.post`
3. `permission-crud.ts`:
- 添加 `search: { inline: true, expand: false }` 配置
- 将 `pageRequest` 中的 `http.get` 改为 `http.post`
### 步骤2修改后端 API问题2
修改三个后端路由文件:
1. `users/urls.py`:
- 将 `@router.get("/")` 改为 `@router.post("/search")`
- 修改参数接收方式
2. `roles/urls.py`:
- 将 `@router.get("/")` 改为 `@router.post("/search")`
- 修改参数接收方式
3. `permissions/urls.py`:
- 将 `@router.get("/")` 改为 `@router.post("/search")`
- 修改参数接收方式
### 步骤3验证修改
启动项目,验证:
1. 查询条件和按钮是否在一行显示
2. 展开按钮是否已隐藏
3. 查询请求是否使用 POST 方法
4. 查询功能是否正常工作
## 风险评估
| 风险项 | 风险等级 | 应对措施 |
|--------|----------|----------|
| 后端 API 变更可能影响其他功能 | 中 | 保留原 GET 接口,新增 POST 接口 |
| fast-crud 配置不生效 | 低 | 检查 fast-crud 文档确认配置项 |
| 查询参数传递方式变更 | 低 | 修改前端传递方式,确保与后端一致 |
## 注意事项
1. 后端修改时,建议保留原 GET 接口,新增 POST `/search` 接口,避免影响其他功能
2. 修改完成后需要测试所有查询功能是否正常工作
3. 确保前后端参数传递方式一致

View File

@ -0,0 +1,63 @@
# Bug 修复计划版本2
## 问题描述
### 问题1查询条件布局仍未生效
前端页面中用户、角色、权限的查询条件和查询、重置按钮没有在同一行显示,需要重新检查配置。
### 问题2删除 GET 查询接口
用户要求删除原有的 GET 方式查询接口,只保留 POST 方式。
## 分析
### 问题1原因分析
fast-crud 的搜索布局配置可能需要调整。当前配置:
```javascript
search: {
show: true,
inline: true,
expand: false,
}
```
可能需要调整为:
```javascript
search: {
show: true,
form: {
inline: true,
},
expand: false,
}
```
### 问题2修改方案
删除后端三个路由文件中的 GET 查询接口(`@router.get("/")`),只保留 POST `/search` 接口。
## 修改步骤
### 步骤1修复前端搜索布局配置
修改三个 CRUD 配置文件,调整搜索布局配置:
1. `user-crud.ts`: 调整 search 配置
2. `role-crud.ts`: 调整 search 配置
3. `permission-crud.ts`: 调整 search 配置
### 步骤2删除后端 GET 查询接口
删除三个后端路由文件中的 GET 查询接口:
1. `users/urls.py`: 删除 `@router.get("/")` 接口
2. `roles/urls.py`: 删除 `@router.get("/")` 接口
3. `permissions/urls.py`: 删除 `@router.get("/")` 接口
## 修改文件清单
| 文件 | 修改内容 |
|------|----------|
| `frontend/src/modules/admin/views/users/user-crud.ts` | 调整 search 布局配置 |
| `frontend/src/modules/admin/views/roles/role-crud.ts` | 调整 search 布局配置 |
| `frontend/src/modules/admin/views/permissions/permission-crud.ts` | 调整 search 布局配置 |
| `backend/auth/apps/users/urls.py` | 删除 GET `/users/` 查询接口 |
| `backend/auth/apps/roles/urls.py` | 删除 GET `/roles/` 查询接口 |
| `backend/auth/apps/permissions/urls.py` | 删除 GET `/permissions/` 查询接口 |

View File

@ -0,0 +1,43 @@
# Bug 修复计划版本3
## 问题描述
搜索组件的查询按钮和条件无法显示在同一行,需要调整 fast-crud 的搜索布局配置。
## 分析
根据 fast-crud 的官方文档和 GitHub issues正确的行内布局配置应该是
```javascript
search: {
show: true,
options: {
layout: 'inline' // 使用 options.layout 而非 form.inline
},
expand: false,
}
```
之前使用的 `search.form.inline` 配置不正确,导致布局没有生效。
## 修改步骤
修改三个 CRUD 配置文件,调整搜索布局配置:
1. `user-crud.ts`: 调整 search.options.layout 配置
2. `role-crud.ts`: 调整 search.options.layout 配置
3. `permission-crud.ts`: 调整 search.options.layout 配置
## 修改文件清单
| 文件 | 修改内容 |
|------|----------|
| `frontend/src/modules/admin/views/users/user-crud.ts` | 调整 search 配置为 options.layout: 'inline' |
| `frontend/src/modules/admin/views/roles/role-crud.ts` | 调整 search 配置为 options.layout: 'inline' |
| `frontend/src/modules/admin/views/permissions/permission-crud.ts` | 调整 search 配置为 options.layout: 'inline' |
## 预期结果
修改后,查询条件和查询、重置按钮将显示在同一行,且展开按钮被隐藏。

View File

@ -0,0 +1,34 @@
# Bug 修复计划版本4
## 问题描述
搜索组件的查询按钮和条件分成了两行显示,需要通过 CSS 样式强制它们在同一行显示。
## 分析
当前 `admin-crud.css` 文件中有以下样式:
```css
.page-content .fs-crud .el-row {
flex-wrap: wrap;
max-width: 100%;
}
```
这个 `flex-wrap: wrap` 导致搜索条件和按钮换行显示。需要修改或添加 CSS 样式来强制搜索区域的元素不换行。
## 修改步骤
修改 `admin-crud.css` 文件,添加搜索区域的样式,强制搜索条件和按钮在同一行显示。
## 修改文件清单
| 文件 | 修改内容 |
|------|----------|
| `frontend/src/styles/admin-crud.css` | 添加搜索区域不换行样式 |
## 预期结果
修改后,查询条件和查询、重置按钮将显示在同一行。

View File

@ -0,0 +1,28 @@
# Bug 修复计划版本5
## 问题描述
添加角色时,后台报错误:`AttributeError: 'dict' object has no attribute 'model_dump'`
## 分析
这个错误说明 `role_data` 已经是一个字典dict而不是 Pydantic 模型对象。这可能是因为:
1. **Pydantic 版本问题**:在 Pydantic v1 中使用 `dict()` 方法,而在 Pydantic v2 中使用 `model_dump()` 方法
2. FastAPI 解析问题:请求格式不正确导致无法正确解析为 Pydantic 模型
## 修改步骤
修改 `backend/auth/apps/roles/urls.py` 文件,将 `role_data.model_dump()` 改为 `dict(role_data)`,这是兼容 Pydantic v1 和 v2 的写法。
## 修改文件清单
| 文件 | 修改内容 |
|------|----------|
| `backend/auth/apps/roles/urls.py` | 将 `role_data.model_dump()` 改为 `dict(role_data)` |
## 预期结果
修改后,创建角色功能应该正常工作。

View File

@ -1,6 +1,7 @@
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, and_
from sqlalchemy.orm import selectinload
from fastcrud import FastCRUD
from typing import Dict, Any
@ -51,17 +52,17 @@ async def create_permission(
return db_permission
@router.get("/", response_model=list[PermissionResponse])
async def get_permissions(
@router.post("/search", response_model=list[PermissionResponse])
async def search_permissions(
query: dict = {},
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
name: str | None = Query(None, description="权限名称,模糊匹配"),
code: str | None = Query(None, description="权限编码,模糊匹配"),
):
"""获取权限列表支持按名称、编码的模糊查询与组合查询AND"""
"""获取权限列表POST方式支持按名称、编码的模糊查询与组合查询AND"""
stmt = select(Permission)
filters = []
for col, raw in ((Permission.name, name), (Permission.code, code)):
for col, key in ((Permission.name, "name"), (Permission.code, "code")):
raw = query.get(key)
cond = ilike_contains(col, raw)
if cond is not None:
filters.append(cond)
@ -173,8 +174,8 @@ async def assign_permissions_to_role(
current_user: User = Depends(get_current_user),
):
"""为角色分配权限"""
# 查找角色
result = await db.execute(select(Role).filter(Role.id == data.role_id))
# 查找角色,同时加载权限关系
result = await db.execute(select(Role).options(selectinload(Role.permissions)).filter(Role.id == data.role_id))
role = result.scalar_one_or_none()
if not role:
raise HTTPException(status_code=404, detail="角色不存在")
@ -196,7 +197,7 @@ async def assign_permissions_to_role(
return RolePermissionResponse(
role_id=role.id,
role_name=role.name,
permissions=[PermissionResponse.from_orm(p) for p in role.permissions]
permissions=[PermissionResponse.from_orm(p) for p in permissions]
)
@ -207,8 +208,8 @@ async def get_role_permissions(
current_user: User = Depends(get_current_user),
):
"""获取角色的权限列表"""
# 查找角色
result = await db.execute(select(Role).filter(Role.id == role_id))
# 查找角色,同时加载权限关系
result = await db.execute(select(Role).options(selectinload(Role.permissions)).filter(Role.id == role_id))
role = result.scalar_one_or_none()
if not role:
raise HTTPException(status_code=404, detail="角色不存在")

View File

@ -30,22 +30,25 @@ async def create_role(
if existing_role:
raise HTTPException(status_code=400, detail="角色名已存在")
# 准备创建数据
create_data = role_data.model_dump()
# 设置创建人为当前登录用户
role_data.creator = current_user.nickname or current_user.username
# 使用FastCRUD创建角色
created_role = await role_crud.create(db, create_data)
# 使用FastCRUD创建角色指定返回模型
created_role = await role_crud.create(
db, role_data, schema_to_select=RoleResponse, return_as_model=True
)
return created_role
@router.get("/", response_model=list[RoleResponse])
async def get_roles(
@router.post("/search", response_model=list[RoleResponse])
async def search_roles(
query: dict = {},
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
name: str | None = Query(None, description="角色名称,模糊匹配"),
):
"""获取角色列表;支持按名称模糊查询"""
"""获取角色列表POST方式;支持按名称模糊查询"""
stmt = select(Role)
name = query.get("name")
cond = ilike_contains(Role.name, name)
if cond is not None:
stmt = stmt.where(cond)

View File

@ -117,24 +117,22 @@ async def create_user(user_data: UserCreateRequest, db: AsyncSession = Depends(g
return created_user
@router.get("/", response_model=list[UserResponse])
async def get_users(
@router.post("/search", response_model=list[UserResponse])
async def search_users(
query: dict = {},
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
username: str | None = Query(None, description="登录名,模糊匹配"),
nickname: str | None = Query(None, description="昵称,模糊匹配"),
email: str | None = Query(None, description="邮箱,模糊匹配"),
phone: str | None = Query(None, description="手机,模糊匹配"),
):
"""分页列表用户支持登录名、昵称、邮箱、手机的模糊查询与组合查询AND"""
"""分页列表用户POST方式支持登录名、昵称、邮箱、手机的模糊查询与组合查询AND"""
stmt = select(User)
filters = []
for col, raw in (
(User.username, username),
(User.nickname, nickname),
(User.email, email),
(User.phone, phone),
for col, key in (
(User.username, "username"),
(User.nickname, "nickname"),
(User.email, "email"),
(User.phone, "phone"),
):
raw = query.get(key)
cond = ilike_contains(col, raw)
if cond is not None:
filters.append(cond)

View File

@ -1 +1 @@
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-B340d_U7.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};
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-CRc7EmAb.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 +1 @@
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-B340d_U7.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};
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-CRc7EmAb.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 @@
import{z as a,n as s,f as m,A as d,g as c,p,w as l,r as n,d as f,B as y,q as h,o as g}from"./index-B340d_U7.js";import{t as _}from"./list-query-4ZZvVtjn.js";const w=()=>({crudOptions:{pagination:{show:!1},request:{transformQuery:({form:e})=>_(e,["name","code"]),pageRequest:async e=>{const{data:t}=await s.get("/permissions/",{params: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 s.post("/permissions/",e)},editRequest:async({form:e,row:t})=>{t&&await s.put(`/permissions/${t.id}`,e)},delRequest:async({row:e})=>{e&&await s.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:r}){!t||e==null||(t[r]=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:r}){!t||e==null||(t[r]=a(e).format("YYYY-MM-DD HH:mm:ss"))}}}}}),Y=m({__name:"PermissionManage",setup(e){const{crudBinding:t,crudRef:r,crudExpose:o}=d({createCrudOptions:w});return c(()=>{o.doRefresh()}),(x,k)=>{const i=n("fs-crud"),u=n("fs-page");return g(),p(u,null,{default:l(()=>[f(i,y({ref_key:"crudRef",ref:r},h(t)),null,16)]),_:1})}}});export{Y as default};

View File

@ -0,0 +1 @@
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-CRc7EmAb.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 +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-B340d_U7.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-CRc7EmAb.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 @@
import{z as A,n as u,f as F,A as I,g as U,p as v,w as m,r as p,i,d as _,B as j,q as G,c as h,b as k,a as B,x,C as Y,F as L,D as Q,E as R,o as l,G as J,_ as W}from"./index-B340d_U7.js";import{t as X}from"./list-query-4ZZvVtjn.js";function Z(C){return()=>({crudOptions:{pagination:{show:!1},request:{transformQuery:({form:t})=>X(t,["name"]),pageRequest:async t=>{const{data:s}=await u.get("/roles/",{params:t});return s},transformRes:({res:t})=>{const s=Array.isArray(t)?t:[];return{currentPage:1,pageSize:Math.max(s.length,1),total:s.length,records:s}},addRequest:async({form:t})=>{await u.post("/roles/",t)},editRequest:async({form:t,row:s})=>{s&&await u.put(`/roles/${s.id}`,t)},delRequest:async({row:t})=>{t&&await u.delete(`/roles/${t.id}`)}},rowHandle:{width:300,buttons:{viewPerm:{text:"查看",order:1,click:({row:t})=>{t&&C.onView(t)}},assignPerm:{text:"分配权限",order:2,click:({row:t})=>{t&&C.onAssign(t)}},edit:{text:"编辑",order:10},remove:{text:"删除",order:20}}},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:"必填"}]}},creator:{title:"创建人",key:"creator",type:"text"},createtime:{title:"创建时间",key:"createtime",type:"text",form:{show:!1},column:{width:170},valueBuilder({value:t,row:s,key:d}){!s||t==null||(s[d]=A(t).format("YYYY-MM-DD HH:mm:ss"))}},updatetime:{title:"更新时间",key:"updatetime",type:"text",form:{show:!1},column:{width:170},valueBuilder({value:t,row:s,key:d}){!s||t==null||(s[d]=A(t).format("YYYY-MM-DD HH:mm:ss"))}}}}})}const ee={key:0,class:"dlg-role"},te={key:1,class:"perm-tags"},se={key:0,class:"dlg-role"},ae=F({__name:"RoleManage",setup(C){const t=i(!1),s=i(!1),d=i(null),g=i([]),y=i(null),V=i([]),w=i(),b=i(!1),$={label:"label",children:"children"};function D(a){return a.map(e=>{var n;return{id:e.id,label:`${e.name}${e.code}`,children:(n=e.children)!=null&&n.length?D(e.children):void 0}})}async function q(){const{data:a}=await u.get("/permissions/tree");V.value=D(a??[])}async function T(a){d.value=a,t.value=!0;try{const{data:e}=await u.get(`/permissions/role/${a.id}`);g.value=e.permissions??[]}catch{R.error("加载权限失败"),g.value=[]}}function H(a){y.value=a,s.value=!0}async function O(){var e,n;const a=y.value;if(a)try{const{data:f}=await u.get(`/permissions/role/${a.id}`),o=(f.permissions??[]).map(c=>c.id);await J(),(e=w.value)==null||e.setCheckedKeys(o)}catch{R.error("加载角色权限失败"),(n=w.value)==null||n.setCheckedKeys([])}}async function z(){var e,n,f;const a=y.value;if(a){b.value=!0;try{const o=((e=w.value)==null?void 0:e.getCheckedKeys(!1))??[];await u.post("/permissions/assign",{role_id:a.id,permission_ids:o}),R.success("保存成功"),s.value=!1,M.doRefresh()}catch(o){const c=(f=(n=o==null?void 0:o.response)==null?void 0:n.data)==null?void 0:f.detail;R.error(typeof c=="string"?c:"保存失败")}finally{b.value=!1}}}const{crudBinding:E,crudRef:N,crudExpose:M}=I({createCrudOptions:Z({onView:T,onAssign:H})});return U(async()=>{await q(),M.doRefresh()}),(a,e)=>{const n=p("fs-crud"),f=p("el-tag"),o=p("el-empty"),c=p("el-dialog"),S=p("el-tree"),P=p("el-button"),K=p("fs-page");return l(),v(K,null,{default:m(()=>[_(n,j({ref_key:"crudRef",ref:N},G(E)),null,16),_(c,{modelValue:t.value,"onUpdate:modelValue":e[0]||(e[0]=r=>t.value=r),title:"查看角色权限",width:"560px","destroy-on-close":"",onClosed:e[1]||(e[1]=r=>g.value=[])},{default:m(()=>[d.value?(l(),h("p",ee,[e[4]||(e[4]=k(" 角色:",-1)),B("strong",null,x(d.value.name),1)])):Y("",!0),g.value.length?(l(),h("div",te,[(l(!0),h(L,null,Q(g.value,r=>(l(),v(f,{key:r.id,class:"perm-tag",type:"info"},{default:m(()=>[k(x(r.name)+""+x(r.code)+" ",1)]),_:2},1024))),128))])):(l(),v(o,{key:2,description:"暂未分配权限","image-size":72}))]),_:1},8,["modelValue"]),_(c,{modelValue:s.value,"onUpdate:modelValue":e[3]||(e[3]=r=>s.value=r),title:"分配权限",width:"640px","destroy-on-close":"",onOpen:O},{footer:m(()=>[_(P,{onClick:e[2]||(e[2]=r=>s.value=!1)},{default:m(()=>[...e[6]||(e[6]=[k("取消",-1)])]),_:1}),_(P,{type:"primary",loading:b.value,onClick:z},{default:m(()=>[...e[7]||(e[7]=[k(" 保存 ",-1)])]),_:1},8,["loading"])]),default:m(()=>[y.value?(l(),h("p",se,[e[5]||(e[5]=k(" 角色:",-1)),B("strong",null,x(y.value.name),1)])):Y("",!0),V.value.length?(l(),v(S,{key:1,ref_key:"permTreeRef",ref:w,class:"perm-tree",data:V.value,"show-checkbox":"","node-key":"id",props:$,"default-expand-all":"","check-strictly":!1},null,8,["data"])):(l(),v(o,{key:2,description:"暂无权限数据","image-size":64}))]),_:1},8,["modelValue"])]),_:1})}}}),re=W(ae,[["__scopeId","data-v-714ba56c"]]);export{re as default};

View File

@ -0,0 +1 @@
import{z as A,n as u,f as F,A as I,g as U,p as v,w as m,r as p,i,d as _,B as j,q as G,c as w,b as k,a as B,x,C as Y,F as L,D as Q,E as R,o as l,G as J,_ as W}from"./index-CRc7EmAb.js";import{t as X}from"./list-query-4ZZvVtjn.js";function Z(C){return()=>({crudOptions:{pagination:{show:!1},search:{show:!0,options:{layout:"inline"},expand:!1},request:{transformQuery:({form:t})=>X(t,["name"]),pageRequest:async t=>{const{data:s}=await u.post("/roles/search",t);return s},transformRes:({res:t})=>{const s=Array.isArray(t)?t:[];return{currentPage:1,pageSize:Math.max(s.length,1),total:s.length,records:s}},addRequest:async({form:t})=>{await u.post("/roles/",t)},editRequest:async({form:t,row:s})=>{s&&await u.put(`/roles/${s.id}`,t)},delRequest:async({row:t})=>{t&&await u.delete(`/roles/${t.id}`)}},rowHandle:{width:300,buttons:{viewPerm:{text:"查看",order:1,click:({row:t})=>{t&&C.onView(t)}},assignPerm:{text:"分配权限",order:2,click:({row:t})=>{t&&C.onAssign(t)}},edit:{text:"编辑",order:10},remove:{text:"删除",order:20}}},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:"必填"}]}},creator:{title:"创建人",key:"creator",type:"text"},createtime:{title:"创建时间",key:"createtime",type:"text",form:{show:!1},column:{width:170},valueBuilder({value:t,row:s,key:d}){!s||t==null||(s[d]=A(t).format("YYYY-MM-DD HH:mm:ss"))}},updatetime:{title:"更新时间",key:"updatetime",type:"text",form:{show:!1},column:{width:170},valueBuilder({value:t,row:s,key:d}){!s||t==null||(s[d]=A(t).format("YYYY-MM-DD HH:mm:ss"))}}}}})}const ee={key:0,class:"dlg-role"},te={key:1,class:"perm-tags"},se={key:0,class:"dlg-role"},ae=F({__name:"RoleManage",setup(C){const t=i(!1),s=i(!1),d=i(null),g=i([]),y=i(null),V=i([]),h=i(),b=i(!1),$={label:"label",children:"children"};function D(a){return a.map(e=>{var n;return{id:e.id,label:`${e.name}${e.code}`,children:(n=e.children)!=null&&n.length?D(e.children):void 0}})}async function q(){const{data:a}=await u.get("/permissions/tree");V.value=D(a??[])}async function T(a){d.value=a,t.value=!0;try{const{data:e}=await u.get(`/permissions/role/${a.id}`);g.value=e.permissions??[]}catch{R.error("加载权限失败"),g.value=[]}}function H(a){y.value=a,s.value=!0}async function O(){var e,n;const a=y.value;if(a)try{const{data:f}=await u.get(`/permissions/role/${a.id}`),o=(f.permissions??[]).map(c=>c.id);await J(),(e=h.value)==null||e.setCheckedKeys(o)}catch{R.error("加载角色权限失败"),(n=h.value)==null||n.setCheckedKeys([])}}async function z(){var e,n,f;const a=y.value;if(a){b.value=!0;try{const o=((e=h.value)==null?void 0:e.getCheckedKeys(!1))??[];await u.post("/permissions/assign",{role_id:a.id,permission_ids:o}),R.success("保存成功"),s.value=!1,M.doRefresh()}catch(o){const c=(f=(n=o==null?void 0:o.response)==null?void 0:n.data)==null?void 0:f.detail;R.error(typeof c=="string"?c:"保存失败")}finally{b.value=!1}}}const{crudBinding:E,crudRef:N,crudExpose:M}=I({createCrudOptions:Z({onView:T,onAssign:H})});return U(async()=>{await q(),M.doRefresh()}),(a,e)=>{const n=p("fs-crud"),f=p("el-tag"),o=p("el-empty"),c=p("el-dialog"),S=p("el-tree"),P=p("el-button"),K=p("fs-page");return l(),v(K,null,{default:m(()=>[_(n,j({ref_key:"crudRef",ref:N},G(E)),null,16),_(c,{modelValue:t.value,"onUpdate:modelValue":e[0]||(e[0]=r=>t.value=r),title:"查看角色权限",width:"560px","destroy-on-close":"",onClosed:e[1]||(e[1]=r=>g.value=[])},{default:m(()=>[d.value?(l(),w("p",ee,[e[4]||(e[4]=k(" 角色:",-1)),B("strong",null,x(d.value.name),1)])):Y("",!0),g.value.length?(l(),w("div",te,[(l(!0),w(L,null,Q(g.value,r=>(l(),v(f,{key:r.id,class:"perm-tag",type:"info"},{default:m(()=>[k(x(r.name)+""+x(r.code)+" ",1)]),_:2},1024))),128))])):(l(),v(o,{key:2,description:"暂未分配权限","image-size":72}))]),_:1},8,["modelValue"]),_(c,{modelValue:s.value,"onUpdate:modelValue":e[3]||(e[3]=r=>s.value=r),title:"分配权限",width:"640px","destroy-on-close":"",onOpen:O},{footer:m(()=>[_(P,{onClick:e[2]||(e[2]=r=>s.value=!1)},{default:m(()=>[...e[6]||(e[6]=[k("取消",-1)])]),_:1}),_(P,{type:"primary",loading:b.value,onClick:z},{default:m(()=>[...e[7]||(e[7]=[k(" 保存 ",-1)])]),_:1},8,["loading"])]),default:m(()=>[y.value?(l(),w("p",se,[e[5]||(e[5]=k(" 角色:",-1)),B("strong",null,x(y.value.name),1)])):Y("",!0),V.value.length?(l(),v(S,{key:1,ref_key:"permTreeRef",ref:h,class:"perm-tree",data:V.value,"show-checkbox":"","node-key":"id",props:$,"default-expand-all":"","check-strictly":!1},null,8,["data"])):(l(),v(o,{key:2,description:"暂无权限数据","image-size":64}))]),_:1},8,["modelValue"])]),_:1})}}}),re=W(ae,[["__scopeId","data-v-714ba56c"]]);export{re as default};

View File

@ -1 +0,0 @@
import{z as a,O as l,n as r,f as m,A as d,g as c,p,w as f,r as o,d as h,B as y,q as w,o as g}from"./index-B340d_U7.js";import{t as _}from"./list-query-4ZZvVtjn.js";const k=()=>({crudOptions:{pagination:{show:!1},request:{transformQuery:({form:e})=>_(e,["username","nickname","email","phone"]),pageRequest:async e=>{const{data:t}=await r.get("/users/",{params: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("/users/",e)},editRequest:async({form:e,row:t})=>{if(!t)return;const s={...e};s.password||delete s.password,await r.put(`/users/${t.id}`,s)},delRequest:async({row:e})=>{e&&await r.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:l({getData:async()=>{const{data:e}=await r.get("/roles/");return e},value:"id",label:"name"}),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]=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"))}},lastlogintime:{title:"最后登录",key:"lastlogintime",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"))}}}}}),M=m({__name:"UserManage",setup(e){const{crudBinding:t,crudRef:s,crudExpose:n}=d({createCrudOptions:k});return c(()=>{n.doRefresh()}),(x,Y)=>{const u=o("fs-crud"),i=o("fs-page");return g(),p(i,null,{default:f(()=>[h(u,y({ref_key:"crudRef",ref:s},w(t)),null,16)]),_:1})}}});export{M as default};

View File

@ -0,0 +1 @@
import{z as a,O as l,n as r,f as m,A as d,g as c,p,w as f,r as o,d as h,B as y,q as w,o as g}from"./index-CRc7EmAb.js";import{t as _}from"./list-query-4ZZvVtjn.js";const x=()=>({crudOptions:{pagination:{show:!1},search:{show:!0},searchFormOptions:{inline:!0,labelWidth:"80px",col:{span:0}},request:{transformQuery:({form:e})=>_(e,["username","nickname","email","phone"]),pageRequest:async e=>{const{data:t}=await r.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}},addRequest:async({form:e})=>{await r.post("/users/",e)},editRequest:async({form:e,row:t})=>{if(!t)return;const s={...e};s.password||delete s.password,await r.put(`/users/${t.id}`,s)},delRequest:async({row:e})=>{e&&await r.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:l({getData:async()=>{const{data:e}=await r.get("/roles/");return e},value:"id",label:"name"}),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]=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"))}},lastlogintime:{title:"最后登录",key:"lastlogintime",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"))}}}}}),M=m({__name:"UserManage",setup(e){const{crudBinding:t,crudRef:s,crudExpose:n}=d({createCrudOptions:x});return c(()=>{n.doRefresh()}),(k,Y)=>{const u=o("fs-crud"),i=o("fs-page");return g(),p(i,null,{default:f(()=>[h(u,y({ref_key:"crudRef",ref:s},w(t)),null,16)]),_:1})}}});export{M 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

@ -4,8 +4,8 @@
<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-B340d_U7.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-ciY4YJQ7.css">
<script type="module" crossorigin src="/assets/index-CRc7EmAb.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-Cn04Rt7y.css">
</head>
<body>
<div id="app"></div>

View File

@ -17,13 +17,18 @@ export const createPermissionCrudOptions: CreateCrudOptions<PermissionRow> = ()
return {
crudOptions: {
pagination: { show: false },
search: {
show: true,
options: {
layout: 'inline',
},
expand: false,
},
request: {
transformQuery: ({ form }) =>
toSearchParams(form as Record<string, unknown>, ["name", "code"]),
pageRequest: async (query) => {
const { data } = await http.get<PermissionRow[]>("/permissions/", {
params: query,
});
const { data } = await http.post<PermissionRow[]>("/permissions/search", query);
return data;
},
transformRes: ({ res }) => {

View File

@ -57,6 +57,7 @@ import { nextTick, onMounted, ref } from "vue";
import { useFs } from "@fast-crud/fast-crud";
import { ElMessage } from "element-plus";
import type { ElTree } from "element-plus";
import axios, { isAxiosError } from "axios";
import { http } from "@/modules/admin/api/http";
import { buildRoleCrudOptions, type RoleRow } from "./role-crud";
@ -131,13 +132,22 @@ async function onAssignDialogOpen() {
const role = assignRole.value;
if (!role) return;
try {
const { data } = await http.get<{ permissions: Permission[] }>(
console.log(`Loading permissions for role id: ${role.id}`);
const response = await http.get<{ permissions: Permission[] }>(
`/permissions/role/${role.id}`,
);
const ids = (data.permissions ?? []).map((p) => p.id);
console.log("Response data:", response.data);
const ids = (response.data.permissions ?? []).map((p) => p.id);
console.log("Permission IDs:", ids);
await nextTick();
permTreeRef.value?.setCheckedKeys(ids);
} catch {
} catch (error) {
console.error("Error loading role permissions:", error);
if (isAxiosError(error)) {
console.error("Response status:", error.response?.status);
console.error("Response data:", error.response?.data);
console.error("Request URL:", error.request?.url);
}
ElMessage.error("加载角色权限失败");
permTreeRef.value?.setCheckedKeys([]);
}

View File

@ -20,13 +20,18 @@ export function buildRoleCrudOptions(h: RoleCrudHandlers): CreateCrudOptions<Rol
return () => ({
crudOptions: {
pagination: { show: false },
search: {
show: true,
options: {
layout: 'inline',
},
expand: false,
},
request: {
transformQuery: ({ form }) =>
toSearchParams(form as Record<string, unknown>, ["name"]),
pageRequest: async (query) => {
const { data } = await http.get<RoleRow[]>("/roles/", {
params: query,
});
const { data } = await http.post<RoleRow[]>("/roles/search", query);
return data;
},
transformRes: ({ res }) => {

View File

@ -22,6 +22,12 @@ export const createUserCrudOptions: CreateCrudOptions<UserRow> = () => {
return {
crudOptions: {
pagination: { show: false },
search: { show: true},
searchFormOptions: {
inline: true,
labelWidth: "80px",
col: { span: 0 },
},
request: {
transformQuery: ({ form }) =>
toSearchParams(form as Record<string, unknown>, [
@ -31,9 +37,7 @@ export const createUserCrudOptions: CreateCrudOptions<UserRow> = () => {
"phone",
]),
pageRequest: async (query) => {
const { data } = await http.get<UserRow[]>("/users/", {
params: query,
});
const { data } = await http.post<UserRow[]>("/users/search", query);
return data;
},
transformRes: ({ res }) => {

View File

@ -40,3 +40,36 @@
flex-wrap: wrap;
max-width: 100%;
}
/* 搜索区域不换行,强制查询条件和按钮在同一行 */
.page-content .fs-crud .fs-search {
display: flex;
flex-wrap: nowrap;
align-items: center;
gap: 12px;
}
.page-content .fs-crud .fs-search .el-form {
display: flex;
flex-wrap: nowrap;
align-items: center;
gap: 12px;
margin-bottom: 0;
}
.page-content .fs-crud .fs-search .el-form-item {
margin-bottom: 0;
flex-shrink: 0;
}
.page-content .fs-crud .fs-search .el-form-item__content {
display: flex;
align-items: center;
}
.page-content .fs-crud .fs-search .fs-search-buttons {
display: flex;
align-items: center;
gap: 8px;
flex-shrink: 0;
}