Files
Agent-System/web/src/components/layout/LeftRail.tsx
T

384 lines
13 KiB
TypeScript

import React from 'react';
import { Plus, Settings, User, ShieldCheck, MessageSquare, Shield, Users } from 'lucide-react';
import { ProjectSession } from '../../types/workbench';
import { UserRole, UserProfile } from '../../types/docgov';
interface LeftRailProps {
width: number;
collapsed: boolean;
sessions: ProjectSession[];
activeSessionId: string;
onSelectSession: (sessionId: string) => void;
onNewSession: () => void;
canCreateProject?: boolean;
currentUser: UserProfile;
currentRole: UserRole;
onOpenRightsModal: () => void;
onOpenUserSettings: () => void;
onOpenAdminSettings?: () => void;
onOpenProjectMembers?: () => void;
isAdmin?: boolean;
}
export const LeftRail: React.FC<LeftRailProps> = ({
width,
collapsed,
sessions,
activeSessionId,
onSelectSession,
onNewSession,
canCreateProject = true,
currentUser,
currentRole,
onOpenRightsModal,
onOpenUserSettings,
onOpenAdminSettings,
onOpenProjectMembers,
isAdmin = false,
}) => {
if (collapsed) {
return null;
}
const getRoleBadgeInfo = (role: UserRole) => {
switch (role) {
case 'author':
return { title: '编制人', color: 'var(--brand-500)', bg: 'rgba(79, 107, 255, 0.12)' };
case 'reviewer':
return { title: '审核人', color: 'var(--amber-500)', bg: 'rgba(217, 119, 6, 0.12)' };
case 'approver':
return { title: '批准人', color: 'var(--success)', bg: 'rgba(16, 185, 129, 0.12)' };
default:
return { title: '查阅者', color: 'var(--text-muted)', bg: 'rgba(100, 116, 139, 0.12)' };
}
};
const roleInfo = getRoleBadgeInfo(currentRole);
return (
<aside
style={{
width: `${width}px`,
minWidth: `${width}px`,
height: '100%',
background: 'var(--bg-panel)',
borderRight: '1px solid var(--border)',
display: 'flex',
flexDirection: 'column',
overflow: 'hidden',
flexShrink: 0,
position: 'relative',
}}
>
{/* 顶部操作区 */}
<div style={{ padding: '14px 12px 10px', borderBottom: '1px solid var(--border-light)' }}>
<button
onClick={canCreateProject ? onNewSession : undefined}
disabled={!canCreateProject}
style={{
width: '100%',
height: '36px',
borderRadius: 'var(--r-md)',
background: canCreateProject ? 'var(--brand-grad)' : 'var(--bg-subtle)',
color: canCreateProject ? '#fff' : 'var(--text-faint)',
border: canCreateProject ? 'none' : '1px solid var(--border)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: '6px',
fontWeight: 600,
fontSize: '13px',
boxShadow: canCreateProject ? '0 2px 8px rgba(79, 107, 255, 0.28)' : 'none',
cursor: canCreateProject ? 'pointer' : 'not-allowed',
opacity: canCreateProject ? 1 : 0.6,
transition: 'all 0.15s ease',
}}
title={
canCreateProject
? '发起新研发项目与立项'
: `当前角色 [${currentUser.position || '方案工程师'}] 无权发起新研发项目与立项`
}
>
<Plus size={16} />
<span>新建立项会话</span>
</button>
</div>
{/* 会话列表 */}
<div
style={{
flex: 1,
overflowY: 'auto',
padding: '8px 10px',
display: 'flex',
flexDirection: 'column',
gap: '6px',
}}
>
<div
style={{
padding: '4px 6px',
fontSize: '11px',
fontWeight: 600,
color: 'var(--text-faint)',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
}}
>
<span>项目会话 (一项目一会话)</span>
{onOpenProjectMembers && (
<button
onClick={(e) => {
e.stopPropagation();
onOpenProjectMembers();
}}
style={{
border: 'none',
background: 'transparent',
cursor: 'pointer',
color: 'var(--brand-600, #4f6bff)',
fontSize: '11px',
display: 'flex',
alignItems: 'center',
gap: '3px',
padding: '2px 6px',
borderRadius: '4px',
fontWeight: 600,
}}
title="管理当前项目成员与职责分配"
>
<Users size={12} />
<span>项目成员</span>
</button>
)}
</div>
{sessions.map((sess) => {
const isActive = sess.sessionId === activeSessionId;
const isExited = sess.isExited;
return (
<div
key={sess.sessionId}
onClick={() => onSelectSession(sess.sessionId)}
style={{
padding: '10px 12px',
borderRadius: 'var(--r-md)',
background: isActive ? 'var(--bg-active)' : 'transparent',
border: isActive ? '1px solid var(--brand-100)' : '1px solid transparent',
cursor: 'pointer',
display: 'flex',
flexDirection: 'column',
gap: '4px',
transition: 'all 0.15s var(--ease)',
opacity: isExited ? 0.75 : 1,
}}
onMouseEnter={(e) => {
if (!isActive) e.currentTarget.style.background = 'var(--bg-hover)';
}}
onMouseLeave={(e) => {
if (!isActive) e.currentTarget.style.background = 'transparent';
}}
>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '6px' }}>
<span
style={{
fontSize: '13px',
fontWeight: isActive ? 600 : 500,
color: isExited ? 'var(--text-muted)' : isActive ? 'var(--text-strong)' : 'var(--text)',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
flex: 1,
}}
title={isExited ? `${sess.title} (您已退出该项目)` : sess.title}
>
{sess.title}
</span>
<div style={{ display: 'flex', alignItems: 'center', gap: '4px', flexShrink: 0 }}>
{isExited && (
<span
style={{
fontSize: '9px',
fontWeight: 600,
padding: '1px 5px',
borderRadius: '4px',
background: 'rgba(239, 68, 68, 0.12)',
color: '#dc2626',
}}
>
已退出
</span>
)}
<span
style={{
fontSize: '10px',
fontWeight: 600,
padding: '1px 6px',
borderRadius: 'var(--r-pill)',
background: isActive ? 'var(--brand-50)' : 'var(--bg-subtle)',
color: isActive ? 'var(--brand-600)' : 'var(--text-muted)',
}}
>
{sess.stage}
</span>
</div>
</div>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', fontSize: '11px', color: 'var(--text-muted)' }}>
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{sess.deviceType}
</span>
<span style={{ display: 'flex', alignItems: 'center', gap: '3px' }}>
<MessageSquare size={11} />
{sess.messageCount}
</span>
</div>
</div>
);
})}
</div>
{/* 底部用户账号信息与设置入口 */}
<div
style={{
padding: '10px 12px',
borderTop: '1px solid var(--border)',
background: 'var(--bg-subtle)',
display: 'flex',
alignItems: 'center',
gap: '8px',
}}
>
{/* 用户信息卡片 (点击打开权限矩阵) */}
<div
onClick={onOpenRightsModal}
style={{
flex: 1,
display: 'flex',
alignItems: 'center',
gap: '8px',
padding: '6px 8px',
borderRadius: 'var(--r-md)',
cursor: 'pointer',
transition: 'all 0.15s ease',
overflow: 'hidden',
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = 'var(--bg-hover)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = 'transparent';
}}
title="点击查看账号信息与当前项目各文档法定权限矩阵"
>
<div
style={{
width: '32px',
height: '32px',
borderRadius: '50%',
background: roleInfo.color,
color: '#fff',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontWeight: 700,
fontSize: '13px',
flexShrink: 0,
boxShadow: '0 2px 5px rgba(0,0,0,0.15)',
}}
>
{currentUser.name ? currentUser.name[0] : <User size={16} />}
</div>
<div style={{ display: 'flex', flexDirection: 'column', overflow: 'hidden', minWidth: 0 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
<span
style={{
fontSize: '13px',
fontWeight: 600,
color: 'var(--text-strong)',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
>
{currentUser.name}
</span>
<ShieldCheck size={12} style={{ color: roleInfo.color, flexShrink: 0 }} />
</div>
<span
style={{
fontSize: '11px',
color: 'var(--text-muted)',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
>
{currentUser.position || '资深方案工程师'}
</span>
</div>
</div>
{/* 设置操作区:普通设置所有人可用,系统管理仅管理员可见 */}
<div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
{isAdmin && (
<button
onClick={onOpenAdminSettings}
style={{
width: '32px',
height: '32px',
borderRadius: 'var(--r-sm)',
border: '1px solid rgba(245, 158, 11, 0.4)',
background: 'rgba(245, 158, 11, 0.12)',
color: 'var(--amber-500)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
cursor: 'pointer',
flexShrink: 0,
transition: 'all 0.15s ease',
}}
title="系统管理与准入治理控制台 (管理员独占)"
>
<Shield size={16} />
</button>
)}
<button
onClick={onOpenUserSettings}
style={{
width: '32px',
height: '32px',
borderRadius: 'var(--r-sm)',
border: '1px solid var(--border)',
background: 'var(--bg-panel)',
color: 'var(--text-muted)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
cursor: 'pointer',
flexShrink: 0,
transition: 'all 0.15s ease',
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = 'var(--bg-hover)';
e.currentTarget.style.color = 'var(--text-strong)';
e.currentTarget.style.borderColor = 'var(--brand-300)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = 'var(--bg-panel)';
e.currentTarget.style.color = 'var(--text-muted)';
e.currentTarget.style.borderColor = 'var(--border)';
}}
title="个人资料与系统偏好设置"
>
<Settings size={16} />
</button>
</div>
</div>
</aside>
);
};