'use client' import { useState, useEffect, useCallback } from 'react' import HeaderUI from '@/components/HeaderUI' import RoleManager from './role-manager' interface Role { name: string; display_name: string } interface LdapUser { username: string; email: string; displayName: string; createdAt: string } const s = { wrap: { minHeight: '100vh', background: 'var(--bg)' } as React.CSSProperties, main: { padding: 'clamp(16px, 3vw, 48px)' } as React.CSSProperties, tabBar: { display: 'flex', gap: 0, borderBottom: '1px solid var(--border)', marginBottom: 32, overflowX: 'auto' as any, whiteSpace: 'nowrap' as any } as React.CSSProperties, content: {} as React.CSSProperties, grid2: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(280px, 1fr))', gap: 20 } as React.CSSProperties, tableWrap: { overflowX: 'auto' as any } as React.CSSProperties, field: { marginBottom: 20 } as React.CSSProperties, label: { display: 'block', fontSize: 13, fontWeight: 500, color: 'var(--text-secondary)', marginBottom: 6 } as React.CSSProperties, input: { width: '100%', height: 44, padding: '0 14px', border: '1px solid var(--border)', borderRadius: 8, background: 'var(--bg-card)', color: 'var(--text)', fontSize: 14, outline: 'none', boxSizing: 'border-box' as any }, select: { width: '100%', height: 44, padding: '0 14px', border: '1px solid var(--border)', borderRadius: 8, background: 'var(--bg-card)', color: 'var(--text)', fontSize: 14, outline: 'none', cursor: 'pointer', boxSizing: 'border-box' as any }, btn: { width: '100%', height: 46, background: '#2563eb', color: '#fff', border: 'none', borderRadius: 8, fontSize: 15, fontWeight: 600, cursor: 'pointer' }, 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: '14px 12px', borderBottom: '1px solid var(--border)', fontSize: 13, color: 'var(--text)' } as React.CSSProperties, toast: { position: 'fixed', bottom: 24, right: 24, zIndex: 100, padding: '12px 20px', borderRadius: 10, fontSize: 13, fontWeight: 500, boxShadow: '0 4px 24px rgba(0,0,0,0.12)', animation: 'slideUp 0.3s ease', maxWidth: 400 } as React.CSSProperties, badge: { display: 'inline-block', padding: '2px 10px', borderRadius: 10, fontSize: 11, fontWeight: 500 } as React.CSSProperties, } export default function AdminUsersPage() { const [tab, setTab] = useState<'create' | 'manage' | 'roles'>('create') const [username, setUsername] = useState('') const [displayName, setDisplayName] = useState('') const [email, setEmail] = useState('') const [assetsRole, setAssetsRole] = useState('viewer') const [issueRole, setIssueRole] = useState('viewer') const [monitorRole, setMonitorRole] = useState('viewer') const [assetsRoles, setAssetsRoles] = useState([]) const [issueRoles, setIssueRoles] = useState([]) const [monitorRoles, setMonitorRoles] = useState([]) const [users, setUsers] = useState([]) const [loading, setLoading] = useState(false) const [result, setResult] = useState<{ ok: boolean; msg: string } | null>(null) const [deleting, setDeleting] = useState(null) const [loginUser, setLoginUser] = useState('') const [loginDisplayName, setLoginDisplayName] = useState('') const [isAdmin, setIsAdmin] = useState(false) const [syncingEmails, setSyncingEmails] = useState(false) const [showPwd, setShowPwd] = useState(false) const [generatedPwd, setGeneratedPwd] = useState('') const [pwdUser, setPwdUser] = useState('') const [pwdName, setPwdName] = useState('') const [copied, setCopied] = useState(false) const fetchRoles = useCallback(async () => { try { const [rolesR, usersR] = await Promise.all([ fetch('/api/admin/roles').then(r => r.json()), fetch('/api/admin/users').then(r => r.json()), ]) if (rolesR.assets?.length) setAssetsRoles(rolesR.assets) if (rolesR.issue?.length) setIssueRoles(rolesR.issue) if (rolesR.monitor?.length) setMonitorRoles(rolesR.monitor) if (usersR.users?.length) setUsers(usersR.users) } catch {} }, []) const fetchLoginUser = useCallback(async () => { try { const res = await fetch('/api/auth/me') const d = await res.json() if (d.user) { setLoginUser(d.user.username || ''); setLoginDisplayName(d.user.displayName || d.user.username || ''); setIsAdmin(d.user.isAdmin || false) } } catch {} }, []) useEffect(() => { fetchRoles(); fetchLoginUser() }, [fetchRoles, fetchLoginUser]) async function refreshUsers() { try { const u = await fetch('/api/admin/users').then(r => r.json()); if (u.users?.length) setUsers(u.users) } catch {} } function showResult(ok: boolean, msg: string) { setResult({ ok, msg }); setTimeout(() => setResult(null), 4000) } async function handleCreate(e: React.FormEvent) { e.preventDefault() setLoading(true) try { const res = await fetch('/api/admin/create-user', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username, displayName, assetsRole, issueRole, monitorRole, email: email || undefined }) }) const d = await res.json() if (res.ok) { if (d.password) { setGeneratedPwd(d.password); setPwdUser(username); setPwdName(displayName || username); setShowPwd(true); setCopied(false) } setUsername(''); setDisplayName(''); setEmail('') refreshUsers() } showResult(res.ok, d.message || d.error || '操作完成') } catch { showResult(false, '网络错误') } finally { setLoading(false) } } async function handleCopy() { const text = `您好,${pwdName}:\n\n您的 OA 统一门户账号已创建,请使用以下信息登录:\n\n 用户名:${pwdUser}\n 密 码:${generatedPwd}\n\n登录地址:https://oa.tlyq.ai\n\n请在首次登录后及时修改密码。` try { await navigator.clipboard.writeText(text); setCopied(true); setTimeout(() => setCopied(false), 2000) } catch {} } async function handleDelete(target: string) { if (!confirm(`确定删除用户「${target}」?\n\n将从 LLDAP 及所有站点中永久删除,不可撤销。`)) return setDeleting(target) try { const res = await fetch('/api/admin/users', { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: target }) }) const d = await res.json() if (res.ok) setUsers(users.filter(u => u.username !== target)) showResult(res.ok, res.ok ? `已删除 ${target}` : (d.error || '删除失败')) } catch { showResult(false, '网络错误') } finally { setDeleting(null) } } async function handleSyncEmails() { setSyncingEmails(true) try { const res = await fetch('/api/admin/sync-emails', { method: 'POST' }) const d = await res.json() showResult(res.ok, res.ok ? `已同步 ${d.synced} 个用户邮箱至各站点` : (d.error || '同步失败')) } catch { showResult(false, '网络错误') } finally { setSyncingEmails(false) } } const tabItem = (t: string, label: string) => ( ) return (
{showPwd && (
setShowPwd(false)}>
e.stopPropagation()}>
🔑

用户创建成功

{pwdName !== pwdUser ? `${pwdName}(${pwdUser})` : pwdUser}

{generatedPwd}

初始密码,请妥善保存

)} {/* Toast */} {result && (
{result.msg}
)}
{/* Tab Bar */}
{tabItem('create', '创建用户')} {tabItem('manage', '删除用户')} {tabItem('roles', '权限管理')}
{/* ====== 创建用户 ====== */} {tab === 'create' && (
setUsername(e.target.value)} placeholder="英文用户名" required style={s.input} />
setDisplayName(e.target.value)} placeholder="可选,留空同用户名" style={s.input} />
setEmail(e.target.value)} placeholder="user@example.com" style={s.input} />
各站点角色
)} {/* ====== 删除用户 ====== */} {tab === 'manage' && (
{users.length === 0 ? (

加载中...

) : (
{users.map(u => ( (e.currentTarget.style.background = 'var(--bg-hover)')} onMouseLeave={e => (e.currentTarget.style.background = 'transparent')}> ))}
用户名 显示名 邮箱 创建时间 操作
{u.username} {(u.displayName && u.displayName !== u.username) ? u.displayName : '—'} {u.email || '未设置'} {u.createdAt?.substring(0, 19)} {(u.username === 'admin' || u.username === 'localadmin') ? ( 系统保留 ) : ( )}
)}
)} {/* ====== 权限管理 ====== */} {tab === 'roles' && }
) }