From 8771d91a306b873e4dc1ae65d8c647173da35058 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Mon, 22 Jun 2026 17:07:29 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E6=9D=83=E9=99=90=E6=8E=A7?= =?UTF-8?q?=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + .../__pycache__/database.cpython-312.pyc | Bin 3073 -> 3463 bytes web/backend/__pycache__/deps.cpython-312.pyc | Bin 2459 -> 2427 bytes .../__pycache__/models.cpython-312.pyc | Bin 4801 -> 4943 bytes .../__pycache__/permissions.cpython-312.pyc | Bin 2212 -> 2662 bytes .../__pycache__/schemas.cpython-312.pyc | Bin 6004 -> 6136 bytes web/backend/database.py | 7 + web/backend/deps.py | 4 +- web/backend/models.py | 1 + web/backend/permissions.py | 11 +- .../routers/__pycache__/auth.cpython-312.pyc | Bin 3405 -> 3355 bytes .../routers/__pycache__/users.cpython-312.pyc | Bin 6074 -> 6698 bytes web/backend/routers/auth.py | 6 +- web/backend/routers/users.py | 23 ++- web/backend/schemas.py | 2 + web/frontend/src/api/modules.ts | 4 +- web/frontend/src/layouts/MainLayout.tsx | 2 +- web/frontend/src/pages/UsersPage.tsx | 177 +++++++++++++++++- 18 files changed, 222 insertions(+), 16 deletions(-) diff --git a/.gitignore b/.gitignore index 9368ade..19ca86c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ .venv/ __pycache__/ +*/__pycache__ *.pyc # 运行时数据 diff --git a/web/backend/__pycache__/database.cpython-312.pyc b/web/backend/__pycache__/database.cpython-312.pyc index 537e6ee318519f52302d28cd5a94bcddad2f617a..562d44a902497556fbe68289f6618e52f82d00ba 100644 GIT binary patch delta 487 zcmZpaXqVm!R%u4|$+4{F>>M>LWsF7YU=hyA z+gXjJxl({|H4|7L10zEvS27bLLo!n>b2-E0->lM{Toufb4CRcP+>_M63H9|;z|^4N73}Y);Nt4$80r(E;1}xStUWCov~EBQ_gl<&O&U5#C3t}!@OJ|Qk>OUVe@(R|BM29KpnR@ic^zIi!w_pi}WYo zYBkdO?FFrRG1wU|+L@-?nTMwiJE U+;eyZ7^Oa|2{8&6$pO^>0KIpI6951J delta 246 zcmZpdZj|6V&CAQh00gh@T4eoT+sOBwg;8WO8|!1C6m}3?%>VcqZS5ZXMZlS_*-ODc=>fZQTMkQi%WX=+jB zE!MQmqT-TU%!w%}Md~0)At0g2QUuZmQ7sH&=}(@+EzM{!c?b7mMwQ7iJdKRblkf4& S;pJqM`Ya*DC|o25R0ROxZa_%@ diff --git a/web/backend/__pycache__/deps.cpython-312.pyc b/web/backend/__pycache__/deps.cpython-312.pyc index 2563ff6e48f1bad2c3cb85ede803c4de4b1968e8..fae3d62a90d9a47f5c827201a734ba1b211d8a71 100644 GIT binary patch delta 133 zcmbO&{9B0kG%qg~0}zybv&bsj$lJomQd*o^w0SDy0T#xJ$zg1+lh?E53osYS1Jx9< zfCye7G1-JYn9*%=2fGsUWdWnfo7tThwI_dLS7FqhEX`res59B0W2=lVP~l33A{~$> a6(Dho!zMRBr8FniuE=V#0%ti3NCp5;P$78$ delta 151 zcmew@G+UVWG%qg~0}%Y&Z;@5Kk++4Br6@lqb@Nol11yX^lf&3tnX=d>e`FG#Jclh$ zjHO5(sI-U$MDPNMTVTbLrPzZReJ1CzD{)>|HM^*4c3HrD@?>@=MxDvm*i{(yCjVwP pX4IXm&#_fT52$G+Ly<1X02Pn{95%W6DWy57c16~cILlc;5&+M9DE|Ne diff --git a/web/backend/__pycache__/models.cpython-312.pyc b/web/backend/__pycache__/models.cpython-312.pyc index 90d7e0b7837572601a1e378868c1dbcb0e6f1982..1f10992f8896721988d2dbe2945a5e0fd9f3362a 100644 GIT binary patch delta 931 zcmZvZUr19?9LGKTACZ}!-2!|@VV#w{(R5(ckbt=@6F8D ziV9N+UA}+riEic{6GP|%Su%Ra7c^VSYk5n;N^Y83$b`mpsY+8K=ml1=O*BKx!F6~v z!M@h!>n_`Aw}ZFwm3&pgUWiu599%Wo)Xbezx@ci;@>OQ_$@Yioou#|Giw`nWk8;cL zKcDW2N;ph+*_Dsm>l@jJ@!X5~-1ORR>Tzy$C11*Ea5WW38-meDL=a_iRXd7m$cA>Z z#td;=P?6+l#D7D;k&q;XqM}63>h7a8@>3^ToORE2ae1pZ_QQE=OXna1`cC9d-qxoO zswZv(-*Sp-;6@NK4o9zr#F2n>J#7dCgHc74Un1N@Q(mr?ytUeETcK75&;yPGN&zRx zU&H87J8X>rcpB^ibO6c$4HRi}81C@P^b!f5hOisZ38>%4QS!9xSE-t)CwKJD}^3Ix-9ebpR&- zY~daXFUKD4!pHcXMVI)X2{1r5nVI~H_>xyF{ip(@232V<{t# zf9bciwV^O+@koBS4RD^cvoS`U365T_3Lhiy*|9eDDf^&Ib*45Pw45&Mi9~rNEO_uZ Z8Md8dEJ#LE9eXA4-D@N>HkS^B{{sf$;Y1RIFZncTpU;AsG2a)Jnu%Zdy^gb|3~0TD_-;ud#KetKqJd`V() zc5#s@NEqw@a}Y}rM#xOolABosG#G_I#<_uw_tO-H8!Ct7DWHN|?4`x2 zMe&&_u=pty)K-T36Wv3xAR9qMz~udcm3-hR0df5_MT#^hy9uS~fddlcWw67*YS9e2 o#bJ}1pHiBWYFCst`Ie9YA0J~0V~5EX1`z#4X|jZ{F$>6G04af+yZ`_I diff --git a/web/backend/__pycache__/permissions.cpython-312.pyc b/web/backend/__pycache__/permissions.cpython-312.pyc index c92afda39b08614eea23331f899df04fae724882..5375917d04d7bf17ecf47be8a56b17666daaaa3c 100644 GIT binary patch delta 626 zcmZ1?_)LWFG%qg~0}vSevdB{8oX97^#K1UFV*^t<sce!!=`^Mk{T7xe zjuf3#wiE*()=lM1V@ojvig8sMX&P^qVLZdaXgB#Kt5SV3BgjA~U#e*42KzhLdsDcH?M$oIFb_FwNGM3H(iofWq-~P0J+Vd$Zo^@=0KC9*F zx_K{WwmzHN{Bq`mrybK@EL`!pp@~ZY2%hYj@wBz$$=>}ywGibm+E+bYH|xpHPKfgN zdpeR!i%arz;|o%Yax;sIGxPI`-|y*szo!Rmz>DssPj>85@XOCjRe0Jl^Xcq)FB=-3 zcGPcpv3$~t&P^|O&v>zJ3B)Lvotn(IgfPu45(7HCNB~G^GTve-Elw>e26;=NNEArk z5=l=ji3bTHJEKS%DCG>q#o9okf#Cxe0|(D_cDak}atasNwQleWU*}i5$gg&RL+uW? z`~sHcoC`TGb8B5-(JB(3%)wUAC^NZ(O@UPosM&Y&S~ev{?#UO}>KUa0w*0m%FyGP#{Y768cE!EgWo delta 153 zcmaDRvP6*YG%qg~0}#C2Z;|zeeIlO((+7r$8XH(rB-5ED-qIA;No7mX2V&h+jx@Fu zgBF%3&Pqc~qs=;uXIL2RC;wzsn!KC6hEZa&1&0Ew6wn}-$*~+tj9il&IP5rXu_Wc^ z=M+gzKFuM;r3{p01mfbP&F?vc7=>lzvH;sj BDF6Tf diff --git a/web/backend/__pycache__/schemas.cpython-312.pyc b/web/backend/__pycache__/schemas.cpython-312.pyc index 1dcce471e44a0f27700a45da27d3fcb4b11fbf86..0239c89cd7706d2aaaa9c93eac9a04c8aac220af 100644 GIT binary patch delta 1202 zcmaizO-vI}5Xbv)`@wFzU0~Z#5=%i4iU?W)Rzhh5QNR#_1R^O0P+AcpA8s|VVj>4j z^q?`$MMyMwG#B6)-Z4XEnylEw+w#l);)@4g+OSY8zRe_w! zC#h$hVGq4i;^x=liJO@lb2DS}X=`RGlbM>E%{;MZLsil(B;Q@9#FA~REcn1{6-nEw zkqCQf+1}}>#cAZ6q-FO>(Xqbat`rWAjM1*=vZ#jJT^y@* zPGSf~EwF#wMF$h_>*mDhT3*LlSY1Fq1FUW!1XKWr08VGX;w;59ZYt^-%z~*uu%>9ye^|`WbN{I-O_bly=4^7v1r_0Y V9LU+^kkjp=3AP+^#q`U+@CSz)=QRKT delta 1189 zcmZ{j%TE(g6vpS)p|mrd=~QTG8!Rml>_`exB)pm;f>eS?Kps9KqCtWP!3+^oeBc6& zE;PpTXSgz5xN+~otqatJt~D5gUAQsvey1_u7@PDr-^}^$IrrSh&(>0l`PMKrMgB5h zI~RAfmu5y4_vv#uMk67eX4RpnN69O5+NLt6zwp4QDCdnrr5&J;YA>}^6p{_{Q=R<( zsFc`Lw&ID$K_?`&(^i98+g9xFcdt_!Uh2B48N-?(U|4Lv4i=$pTW zp88Iz1z~%s^jP%E?~`)~JlEUwEQYB-nsRzn>wyG1X;x2&9yw-NWNBNE zXVM4-f!#r_x#kcBvUD_%7CElC7ML9GmEUa5(Gd>-25=nU;wBX5hSP}yT+daY4*{T$ z4j6|;Uvbo^7hcYjrCa8e{ze#FgRo1^ki;2?gTU_Yh1`=$Q*AIhcml3Lpqv~k4pTp8HVut!|%uuH4h8W`}UsZjN z&mh33nTuS)D_5GO*VWOOtJ8}J11q#sJ*h&;I~SqL;e28qF`yv-s}&OR@~OV^=FM}# zPvM-HrbOgW!!?+>tgFHT0>C}G9;x#~owg%R7g3~Z3U{h}!}$L2>E}bt2aPw9_kz3S zx$>kq=ihZc__E6@b2pc+=A0#}ujy#iMBSr8Ns*6Itt!ILVkJdBN~S7OWt}QnbhqaA EZ~k@9d;kCd diff --git a/web/backend/database.py b/web/backend/database.py index 2cdaa23..8bd0a3a 100644 --- a/web/backend/database.py +++ b/web/backend/database.py @@ -44,6 +44,13 @@ def _migrate(): conn.execute(text("ALTER TABLE accounts ADD COLUMN tag VARCHAR(64) DEFAULT ''")) conn.commit() + # 检查 users.custom_permissions 列是否存在 + result = conn.execute(text("PRAGMA table_info(users)")) + columns = [row[1] for row in result] + if 'custom_permissions' not in columns: + conn.execute(text("ALTER TABLE users ADD COLUMN custom_permissions JSON DEFAULT NULL")) + conn.commit() + def _seed(): """写入默认超管账号和角色。""" diff --git a/web/backend/deps.py b/web/backend/deps.py index 91f856a..6498b9d 100644 --- a/web/backend/deps.py +++ b/web/backend/deps.py @@ -8,7 +8,7 @@ from jose import JWTError from .database import get_db from .security import decode_access_token from .models import User -from .permissions import get_role_permissions +from .permissions import get_user_permissions oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/login") @@ -43,7 +43,7 @@ def require_permission(permission: str): def checker(current_user: User = Depends(get_current_user)) -> User: if not current_user.is_active: raise HTTPException(status_code=403, detail="账号已禁用") - perms = get_role_permissions(current_user.role) + perms = get_user_permissions(current_user) if permission not in perms: raise HTTPException(status_code=403, detail=f"无权限: {permission}") return current_user diff --git a/web/backend/models.py b/web/backend/models.py index 95ee8ef..188a53c 100644 --- a/web/backend/models.py +++ b/web/backend/models.py @@ -18,6 +18,7 @@ class User(Base): role = Column(String(32), nullable=False, default="support") # super_admin / operation / support is_active = Column(Boolean, default=True) remark = Column(String(256), default="") + custom_permissions = Column(JSON, nullable=True, comment="自定义权限列表,null表示使用角色默认权限") created_at = Column(DateTime, default=datetime.utcnow) updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) diff --git a/web/backend/permissions.py b/web/backend/permissions.py index 686cac1..0c44b91 100644 --- a/web/backend/permissions.py +++ b/web/backend/permissions.py @@ -48,7 +48,6 @@ ROLE_PERMISSIONS = { ], "support": [ "account:view_assigned", - "login:view_assigned", ], } @@ -65,6 +64,16 @@ def get_role_permissions(role: str) -> list[str]: return ROLE_PERMISSIONS.get(role, []) +def get_user_permissions(user) -> list[str]: + """获取用户的实际权限列表。 + 优先使用用户自定义权限(custom_permissions), + 若为 None 则回退到角色默认权限。 + """ + if user.custom_permissions is not None: + return user.custom_permissions + return get_role_permissions(user.role) + + def has_permission(role: str, permission: str) -> bool: """检查角色是否拥有某权限。""" return permission in get_role_permissions(role) diff --git a/web/backend/routers/__pycache__/auth.cpython-312.pyc b/web/backend/routers/__pycache__/auth.cpython-312.pyc index c7b98e27558e41aa2d23ebaca45809a007b12f42..63b38643fff4c8cf8d4664d17992e62f51464bde 100644 GIT binary patch delta 87 zcmV-d0I2`X8k-so%MA?*00000?DjZn-?0r!0t9t)Wpa~S0;>XI0+Ub*8?z$=bpf-i t1epQ>+LJ~Hj04jQCX?9*d;#2(RtV9PI|+&bTa(5KQ3KN#IkO-NEd%9I9G?IH delta 108 zcmV-y0F(cl8qFFF%MA?*000000l+wF>9Gw;0t9kzY-N*M0;>Xc0+SvJ8?z$=bpZlu z8k0^08nd7TkpcnUlQIX42d5dK8Pg0alfegk0q~PP2+@uxcs9N@yWOR*C5C4Pp6x8P;lVvDd`a0bxglxwdrccQ7aR-fBJhWf<^u@wQvqHS;|K%Ft zhOLj6m}Nfb6__mF#wdI%DThPFN{@=W`i;`|jGpdEMp7C{#&kW_)2%B`Lp+wzNH4L0 zp<(SIszxG5dx;-^^3g=XkfN!S)~ymZXh=1Z8YC-_$Vh6DIFST_C?csqxe|yW;ze~s zBAT9|Rw-;>4O$m$eUK(3QZrEznv!cL<;HQj@uA!1vp-^NHb;(UoSs}c{X^Qp-R0;% z?ziZl7VqdaODh*#s0JUegE-69h;7(H%j|nvF;Am771j0QaHU9BRPV~CZDwrdz7|Zb z3x)+#jKoy)YH->1G}OhA(oq79GRt?}LdV4_IRN(g8+yn}l+B7s9kE1OBcz(%vdfZL z8JYfTmycip^EETSwpbs5tivO-NFk66!7P#hVl6DP3U!WlqD9l1IVfZgXzF3>5=}ZD zY@G0}oAS$({te^)4G(Q%k(6UZQ6g)=;)Ff)_WXVN{2s<_5y^8%97qu5md`R~PY{*JE+kDz5M=X%Z$1I+q_SHi zTTuOYE6Hxq>G&D!WTBOrl)BK$m*A`!jdsu*u1i%@7W<^7X53OUW>Kbs^^?J^7bf3tw;Jp0UJ6d_^M4)?fni{F&HbPjgo}*Is$7CevVAPeq%jY3Wm%QK%mMNV znQfq$`|iTbN1x1|9hkj*cII6FSGO;GKKt?MJC{DZed?Q;bMMVvJ3n{!#+`46=Dxg| z*Ut`Jnfdz6uLGxu1>6!rTrFdX;6)_p(*m`4&LhJNz}r>fW76M1~#{QLuNRqE-^vd(@JjPPxmpEA+c zu;}ewS~9AW64WEgn)qV~s+Q78DgO2V0W|K(Kwk|W0L~CF7lN>C2&oj3`P$~;n8cP4w(OhRiSM?r@j(?{v%a#?5@XIkiM<;}92EuFHK zBP(06Eis7h2W6x2xNmQyP=ryiOrmuS@kz2#DA^V1%3>dYD_AfB|}%+7o> zJ2N}GKMdUO3;z}hX#$+s&qI|+csZORmzGZ!dx&7lW}wflFL6PyHYe&>Bso}#|+aBs;9xoCrOSZxI%g}8Q`AX&%-<5>c|>QnEEK0^A>VnvLfVNJe>P#Jcj9&=#COmfNCFps!NQ>~ekHQS1_f!FsK)DPDJn_L7GrMvQfVXiY?!Emv(SF_z(_ zU{3K`@8q8cb8q8>T0`+3?+rj^XVB_R;T6yv8LP2mQ1fuuDYOM=7%kbfh%Pn?mL9k) z!e0iT_uWgcr61}2FI4%6;tH})VW+|QiPrb&c3U@J22eXk&9cn~c~>Y+UfmoHtrAJB zr1^Jp2Y(nz@u^6J4@csfjqk=gd?k?JuWJd#lo#bno^M19;_+o7*vI%^au><-r?6sY ze7*eLXpS7``DiDulf0qAOIV2>UwREPv5N=;2)IUU2*8N{KQM+LWoy3YQ1c-SBLorf z^FFvg=h1cnVFUsHAgy=fZQS-^4GO}DaA3OZ4WL^11RUc-OUf*jhfw4DS_IZ$eFu1mA`uSId^o@UwN~Mjg~qRHa7K zcBJyW&2X6PO$4lFP4HH;wbHtOq*gbZReFKF2R;IqwaDwK92w?crY3Sx5_{~AxDv=M zHTcX2^0_34eXasS9tYyC1ab?wK9H^=gk#TC(2#xF711`rucVFsPGE api.get('/users'), create: (data: { username: string; password: string; role: string; remark?: string }) => api.post('/users', data), - update: (id: number, data: { password?: string; role?: string; is_active?: boolean; remark?: string }) => + update: (id: number, data: { password?: string; role?: string; is_active?: boolean; remark?: string; custom_permissions?: string[] | null }) => api.put(`/users/${id}`, data), delete: (id: number) => api.delete(`/users/${id}`), + listPermissions: () => api.get('/users/permissions/list'), }; export const accountApi = { diff --git a/web/frontend/src/layouts/MainLayout.tsx b/web/frontend/src/layouts/MainLayout.tsx index c9f23c1..5d4bc3c 100644 --- a/web/frontend/src/layouts/MainLayout.tsx +++ b/web/frontend/src/layouts/MainLayout.tsx @@ -46,7 +46,7 @@ export default function MainLayout({ onLogout }: { onLogout?: () => void }) { } // 登录任务 - if (hasPerm(user, 'login:batch') || hasPerm(user, 'login:view_assigned')) { + if (hasPerm(user, 'login:batch') || hasPerm(user, 'login:view_all')) { menuItems.push({ key: '/login-tasks', label: '登录任务', icon: }); } diff --git a/web/frontend/src/pages/UsersPage.tsx b/web/frontend/src/pages/UsersPage.tsx index a0f2dea..6dff2a6 100644 --- a/web/frontend/src/pages/UsersPage.tsx +++ b/web/frontend/src/pages/UsersPage.tsx @@ -1,9 +1,11 @@ import { useEffect, useState } from 'react'; import { Table, Button, Modal, Form, Input, Select, Tag, Popconfirm, message, Space, + Checkbox, Divider, Alert, Tooltip, } from 'antd'; -import { PlusOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons'; +import { PlusOutlined, EditOutlined, DeleteOutlined, SafetyOutlined } from '@ant-design/icons'; import { userApi, type UserInfo } from '../api/modules'; +import { getUser, hasPerm } from '../store/auth'; const ROLE_OPTIONS = [ { value: 'super_admin', label: '超级管理员' }, @@ -17,6 +19,21 @@ const ROLE_COLORS: Record = { support: 'green', }; +// 权限分组定义 +const PERMISSION_GROUPS = [ + { label: '用户管理', prefix: 'user:' }, + { label: '账号管理', prefix: 'account:' }, + { label: '登录任务', prefix: 'login:' }, + { label: 'Cookie', prefix: 'cookie:' }, + { label: '代理 & 白名单', prefix: ['proxy:', 'whitelist:'] }, + { label: '系统', prefix: ['system:', 'audit:'] }, +]; + +function isPrefixMatch(key: string, prefix: string | string[]): boolean { + if (Array.isArray(prefix)) return prefix.some(p => key.startsWith(p)); + return key.startsWith(prefix); +} + export default function UsersPage() { const [users, setUsers] = useState([]); const [loading, setLoading] = useState(false); @@ -24,6 +41,17 @@ export default function UsersPage() { const [editing, setEditing] = useState(null); const [form] = Form.useForm(); + // 权限管理 + const [permModalOpen, setPermModalOpen] = useState(false); + const [permUser, setPermUser] = useState(null); + const [permList, setPermList] = useState>({}); + const [rolePermMap, setRolePermMap] = useState>({}); + const [selectedPerms, setSelectedPerms] = useState([]); + const [useCustom, setUseCustom] = useState(false); + const [permLoading, setPermLoading] = useState(false); + + const canAssignPerm = hasPerm(getUser(), 'user:assign_permissions'); + const loadUsers = async () => { setLoading(true); try { @@ -86,6 +114,52 @@ export default function UsersPage() { } }; + // ---- 权限管理 ---- + const handlePermClick = async (user: UserInfo) => { + setPermUser(user); + setPermModalOpen(true); + setPermLoading(true); + try { + const data = await userApi.listPermissions(); + setPermList(data.permissions || {}); + setRolePermMap(data.role_permissions || {}); + // 初始化选中权限:如果用户有 custom_permissions 则使用,否则使用角色默认 + if (user.custom_permissions !== null && user.custom_permissions !== undefined) { + setUseCustom(true); + setSelectedPerms(user.custom_permissions); + } else { + setUseCustom(false); + setSelectedPerms(data.role_permissions?.[user.role] || []); + } + } catch (e: any) { + message.error(e.message); + } finally { + setPermLoading(false); + } + }; + + const handlePermSave = async () => { + if (!permUser) return; + setPermLoading(true); + try { + await userApi.update(permUser.id, { + custom_permissions: useCustom ? selectedPerms : null, + }); + message.success('权限已更新'); + setPermModalOpen(false); + loadUsers(); + } catch (e: any) { + message.error(e.message); + } finally { + setPermLoading(false); + } + }; + + const handleRoleChangeForPerm = (role: string) => { + const rolePerms = rolePermMap[role] || []; + setSelectedPerms(rolePerms); + }; + const columns = [ { title: 'ID', dataIndex: 'id', width: 60 }, { title: '用户名', dataIndex: 'username' }, @@ -97,18 +171,33 @@ export default function UsersPage() { return {label}; }, }, + { + title: '权限模式', + dataIndex: 'custom_permissions', + width: 100, + render: (_: any, record: UserInfo) => + record.custom_permissions !== null && record.custom_permissions !== undefined + ? 自定义 + : 角色默认, + }, { title: '状态', dataIndex: 'is_active', + width: 80, render: (active: boolean) => active ? 启用 : 禁用, }, - { title: '备注', dataIndex: 'remark' }, + { title: '备注', dataIndex: 'remark', ellipsis: true }, { title: '操作', - width: 160, + width: 200, render: (_: any, record: UserInfo) => ( + {canAssignPerm && ( + + + + )} {record.role !== 'super_admin' && ( handleDelete(record.id)}> @@ -133,6 +222,8 @@ export default function UsersPage() { size="small" pagination={{ pageSize: 20 }} /> + + {/* 编辑/新建用户 Modal */} + + {/* 权限管理 Modal */} + setPermModalOpen(false)} + onOk={handlePermSave} + okText="保存" + width={640} + confirmLoading={permLoading} + > + {permUser && ( + <> + +
+ 权限模式: +