384 lines
13 KiB
TypeScript
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>
|
|
);
|
|
};
|