'use client' import { useState, useEffect, useCallback } from 'react' interface SiteUser { username: string; display_name: string; role: string } interface RoleData { assetsUsers: SiteUser[]; issueUsers: SiteUser[] assetsRoles: string[]; issueRoles: string[] emails: Record } interface Props { setResult: (ok: boolean, msg: string) => void } const ss = { bar: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 20 } as React.CSSProperties, hint: { fontSize: 13, color: 'var(--text-muted)', margin: 0 } as React.CSSProperties, saveBtn: (active: boolean) => ({ padding: '10px 24px', borderRadius: 8, border: 'none', cursor: active ? 'pointer' : 'not-allowed', background: active ? '#d97706' : 'var(--bg-hover)', color: active ? '#fff' : 'var(--text-muted)', fontSize: 13, fontWeight: 600, transition: 'all 0.2s' } as React.CSSProperties), table: { width: '100%', borderCollapse: 'collapse' as any } as React.CSSProperties, th: { textAlign: 'left' as any, fontSize: 11, fontWeight: 600, color: 'var(--text-muted)', padding: '10px 12px', borderBottom: '2px solid var(--border)', textTransform: 'uppercase' as any, letterSpacing: '0.05em' } as React.CSSProperties, td: { padding: '12px 12px', borderBottom: '1px solid var(--border)', fontSize: 13, color: 'var(--text)' } as React.CSSProperties, roleSelect: (changed: boolean) => ({ height: 34, padding: '0 10px', borderRadius: 6, fontSize: 12, cursor: 'pointer', border: changed ? '1px solid #d97706' : '1px solid var(--border)', background: changed ? 'rgba(217,119,6,0.06)' : 'var(--bg-card)', color: 'var(--text)', outline: 'none', minWidth: 110 } as React.CSSProperties), emailInput: { height: 30, padding: '0 8px', borderRadius: 5, border: '1px solid var(--border)', background: 'var(--bg-card)', color: 'var(--text)', fontSize: 12, width: 160, outline: 'none' } as React.CSSProperties, miniBtn: (primary: boolean) => ({ padding: '4px 12px', borderRadius: 5, border: primary ? 'none' : '1px solid var(--border)', background: primary ? '#2563eb' : 'var(--bg-card)', color: primary ? '#fff' : 'var(--text-secondary)', fontSize: 11, cursor: 'pointer', fontWeight: 500 } as React.CSSProperties), } export default function RoleManager({ setResult }: Props) { const [roleData, setRoleData] = useState(null) const [pending, setPending] = useState>({}) const [saving, setSaving] = useState(false) const [editingEmail, setEditingEmail] = useState(null) const [editEmailValue, setEditEmailValue] = useState('') const [savingEmail, setSavingEmail] = useState(false) const fetchRoleData = useCallback(async () => { try { const res = await fetch('/api/admin/user-roles'); const d = await res.json() if (d.users) setRoleData({ assetsUsers: d.users.assets || [], issueUsers: d.users.issue || [], assetsRoles: d.assetsRoles || [], issueRoles: d.issueRoles || [], emails: d.emails || {} }) } catch {} }, []) useEffect(() => { fetchRoleData() }, [fetchRoleData]) async function handleEditEmail(target: string) { if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(editEmailValue)) { setResult(false, '邮箱格式不合法'); return } setSavingEmail(true) try { const res = await fetch('/api/admin/users', { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: target, email: editEmailValue }) }) const d = await res.json() if (res.ok) { setRoleData(prev => prev ? { ...prev, emails: { ...prev.emails, [target]: d.email } } : null) setEditingEmail(null); setResult(true, `${target} 邮箱已更新`) } else { setResult(false, d.error || '修改失败') } } catch { setResult(false, '网络错误') } finally { setSavingEmail(false) } } function handleSelect(site: string, username: string, newRole: string, originalRole: string) { if (newRole === originalRole) { const next = { ...pending }; delete next[`${site}:${username}`]; setPending(next) } else { setPending({ ...pending, [`${site}:${username}`]: { site, newRole } }) } } function getCurrentRole(site: string, username: string, originalRole: string): string { return pending[`${site}:${username}`]?.newRole || originalRole } async function handleSave() { if (Object.keys(pending).length === 0) return if (!confirm(`确定保存 ${Object.keys(pending).length} 项角色修改?`)) return setSaving(true); let ok = 0; let fail = 0 for (const [key, { site, newRole }] of Object.entries(pending)) { const username = key.split(':')[1] try { const res = await fetch('/api/admin/user-roles', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username, site, role: newRole }) }) if (res.ok) ok++; else fail++ } catch { fail++ } } setResult(fail === 0, `已保存:${ok} 项成功${fail > 0 ? `,${fail} 项失败` : ''}`) setPending({}); if (fail === 0) fetchRoleData(); setSaving(false) } if (!roleData) return

加载中...

const userMap = new Map() roleData.assetsUsers.forEach(u => userMap.set(u.username, { displayName: u.display_name, email: roleData.emails[u.username] || '', assetsRole: u.role, issueRole: '—' })) roleData.issueUsers.forEach(u => { const e = userMap.get(u.username); if (e) e.issueRole = u.role; else userMap.set(u.username, { displayName: u.display_name, email: roleData.emails[u.username] || '', assetsRole: '—', issueRole: u.role }) }) const users = Array.from(userMap.entries()) const changed = Object.keys(pending).length return (

修改角色后点击「保存修改」统一提交{changed > 0 && ({changed} 项待保存)}

{users.map(([uname, info]) => ( (e.currentTarget.style.background = 'var(--bg-hover)')} onMouseLeave={e => (e.currentTarget.style.background = 'transparent')}> ))}
用户名显示名邮箱资产管理工单跟踪
{uname} {info.displayName} {editingEmail === uname ? ( setEditEmailValue(e.target.value)} placeholder="user@example.com" style={ss.emailInput} /> ) : ( {info.email || '未设置'} {(uname !== 'admin' && uname !== 'localadmin') && ( )} )}
) } function RoleCell({ site, username, originalRole, roles, pending, onSelect, getCurrentRole }: { site: string; username: string; originalRole: string; roles: string[] pending: Record onSelect: (site: string, username: string, newRole: string, originalRole: string) => void getCurrentRole: (site: string, username: string, originalRole: string) => string }) { const isReserved = username === 'admin' || username === 'localadmin' const changed = !!pending[`${site}:${username}`] const currentRole = getCurrentRole(site, username, originalRole) if (isReserved) return {currentRole} if (originalRole === '—' && !changed) return 未同步 return ( ) }