Stop shipping hard-coded demo docs/projects into production builds, drop local mock items on authoritative empty server lists, and purge stale demo seed residue from localStorage on boot. Co-authored-by: Cursor <cursoragent@cursor.com>
791 lines
30 KiB
TypeScript
791 lines
30 KiB
TypeScript
import React, { useEffect, useState } from 'react';
|
|
import { DeliverableItem, DeliverableCategory } from '../../types/workbench';
|
|
import { DocLifecycleState } from '../../types/docgov';
|
|
import { DocVisibilityRule } from '../../types/rbac';
|
|
import { isDemoSeedDocId } from '../../data/demoSeedIds';
|
|
import {
|
|
X,
|
|
CheckCircle2,
|
|
Clock,
|
|
ShieldCheck,
|
|
UserCheck,
|
|
AlertTriangle,
|
|
Lock,
|
|
Globe,
|
|
Share2,
|
|
Undo2,
|
|
Trash2,
|
|
Shield,
|
|
Layers,
|
|
FileText,
|
|
} from 'lucide-react';
|
|
|
|
interface DeckDrawerProps {
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
deliverables: DeliverableItem[];
|
|
activeDocId: string;
|
|
currentUserId: number;
|
|
onSelectDoc: (docId: string) => void;
|
|
onShareToProject?: (docId: string) => void;
|
|
onRevokeShare?: (docId: string) => void;
|
|
onDeleteDoc?: (docId: string) => void;
|
|
onOpenDocVisibility?: (doc: DeliverableItem) => void;
|
|
}
|
|
|
|
export const DeckDrawer: React.FC<DeckDrawerProps> = ({
|
|
isOpen,
|
|
onClose,
|
|
deliverables,
|
|
activeDocId,
|
|
currentUserId,
|
|
onSelectDoc,
|
|
onShareToProject,
|
|
onRevokeShare,
|
|
onDeleteDoc,
|
|
onOpenDocVisibility,
|
|
}) => {
|
|
const [filterCategory, setFilterCategory] = useState<'all' | DeliverableCategory>('all');
|
|
|
|
useEffect(() => {
|
|
const handleKeyDown = (e: KeyboardEvent) => {
|
|
if (e.key === 'Escape' && isOpen) {
|
|
onClose();
|
|
}
|
|
};
|
|
window.addEventListener('keydown', handleKeyDown);
|
|
return () => window.removeEventListener('keydown', handleKeyDown);
|
|
}, [isOpen, onClose]);
|
|
|
|
if (!isOpen) {
|
|
return null;
|
|
}
|
|
|
|
const renderBadge = (
|
|
state: DocLifecycleState,
|
|
isClaimed: boolean,
|
|
isTemplateOnly?: boolean,
|
|
docId?: string,
|
|
) => {
|
|
if (docId && isDemoSeedDocId(docId)) {
|
|
return (
|
|
<span
|
|
style={{
|
|
fontSize: '11px',
|
|
fontWeight: 600,
|
|
color: '#64748b',
|
|
background: '#f1f5f9',
|
|
border: '1px solid #e2e8f0',
|
|
padding: '2px 8px',
|
|
borderRadius: 'var(--r-pill)',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: '3px',
|
|
}}
|
|
title="本地演示数据,不进入服务端文档账本"
|
|
>
|
|
<FileText size={11} />
|
|
<span>本地示例(不入账本)</span>
|
|
</span>
|
|
);
|
|
}
|
|
if (isTemplateOnly || state === 'template') {
|
|
return (
|
|
<span
|
|
style={{
|
|
fontSize: '11px',
|
|
fontWeight: 600,
|
|
color: '#0284c7',
|
|
background: '#e0f2fe',
|
|
border: '1px solid #bae6fd',
|
|
padding: '2px 8px',
|
|
borderRadius: 'var(--r-pill)',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: '3px',
|
|
}}
|
|
title="该文档为项目标准预置模板副本,点击可全景查阅大纲与章节规范"
|
|
>
|
|
<FileText size={11} />
|
|
<span>📄 初始模板</span>
|
|
</span>
|
|
);
|
|
}
|
|
switch (state) {
|
|
case 'approved':
|
|
return (
|
|
<span
|
|
style={{
|
|
fontSize: '11px',
|
|
fontWeight: 600,
|
|
color: 'var(--success)',
|
|
background: 'var(--success-bg)',
|
|
padding: '2px 8px',
|
|
borderRadius: 'var(--r-pill)',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: '3px',
|
|
}}
|
|
>
|
|
<CheckCircle2 size={11} />
|
|
<span>终审已生效</span>
|
|
</span>
|
|
);
|
|
case 'reviewing':
|
|
return (
|
|
<span
|
|
style={{
|
|
fontSize: '11px',
|
|
fontWeight: 600,
|
|
color: 'var(--amber-600)',
|
|
background: 'var(--amber-50)',
|
|
padding: '2px 8px',
|
|
borderRadius: 'var(--r-pill)',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: '3px',
|
|
}}
|
|
>
|
|
<Clock size={11} />
|
|
<span>审核中 (已锁定)</span>
|
|
</span>
|
|
);
|
|
case 'approving':
|
|
return (
|
|
<span
|
|
style={{
|
|
fontSize: '11px',
|
|
fontWeight: 600,
|
|
color: '#9333EA',
|
|
background: '#F3E8FF',
|
|
padding: '2px 8px',
|
|
borderRadius: 'var(--r-pill)',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: '3px',
|
|
}}
|
|
>
|
|
<ShieldCheck size={11} />
|
|
<span>批准中</span>
|
|
</span>
|
|
);
|
|
case 'rejected':
|
|
return (
|
|
<span
|
|
style={{
|
|
fontSize: '11px',
|
|
fontWeight: 600,
|
|
color: 'var(--danger)',
|
|
background: 'var(--danger-bg)',
|
|
padding: '2px 8px',
|
|
borderRadius: 'var(--r-pill)',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: '3px',
|
|
}}
|
|
>
|
|
<AlertTriangle size={11} />
|
|
<span>被驳回需修改</span>
|
|
</span>
|
|
);
|
|
case 'drafting':
|
|
default:
|
|
return (
|
|
<span
|
|
style={{
|
|
fontSize: '11px',
|
|
fontWeight: 600,
|
|
color: 'var(--brand-600)',
|
|
background: 'var(--brand-50)',
|
|
padding: '2px 8px',
|
|
borderRadius: 'var(--r-pill)',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: '3px',
|
|
}}
|
|
>
|
|
<UserCheck size={11} />
|
|
<span>编制中 {isClaimed ? '(已认领)' : '(未认领)'}</span>
|
|
</span>
|
|
);
|
|
}
|
|
};
|
|
|
|
const renderVisibilityBadge = (rule?: DocVisibilityRule, onClick?: (e: React.MouseEvent) => void) => {
|
|
if (!rule || rule.mode === 'inherit') return null;
|
|
const isDesignated = rule.mode === 'designated_only';
|
|
const isWhitelist = rule.mode === 'role_whitelist';
|
|
|
|
return (
|
|
<span
|
|
onClick={onClick}
|
|
style={{
|
|
fontSize: '10.5px',
|
|
fontWeight: 600,
|
|
padding: '1px 6px',
|
|
borderRadius: 'var(--r-pill)',
|
|
background: isDesignated
|
|
? 'rgba(239, 68, 68, 0.12)'
|
|
: isWhitelist
|
|
? 'rgba(245, 158, 11, 0.12)'
|
|
: 'rgba(34, 197, 94, 0.12)',
|
|
color: isDesignated
|
|
? 'var(--danger)'
|
|
: isWhitelist
|
|
? 'var(--amber-600)'
|
|
: 'var(--success)',
|
|
display: 'inline-flex',
|
|
alignItems: 'center',
|
|
gap: '3px',
|
|
cursor: onClick ? 'pointer' : 'default',
|
|
}}
|
|
title={`覆盖原因: ${rule.overrideReason || '权限覆盖模式'}`}
|
|
>
|
|
<Lock size={10} />
|
|
<span>{isDesignated ? '仅责任人' : isWhitelist ? '指定角色' : '公开'}</span>
|
|
</span>
|
|
);
|
|
};
|
|
|
|
const stages: { stage: string; title: string }[] = [
|
|
{ stage: 'V0.1', title: 'V0.1 需求摸底与产品定义' },
|
|
{ stage: 'V0.5', title: 'V0.5 方案成型与法规对齐' },
|
|
{ stage: 'V1.0', title: 'V1.0 商务报价与WBS工时测算' },
|
|
];
|
|
|
|
const workflowDocs = deliverables.filter((d) => d.category === 'workflow');
|
|
const sharedDocs = deliverables.filter((d) => d.category === 'shared');
|
|
const privateDocs = deliverables.filter((d) => d.category === 'private');
|
|
|
|
return (
|
|
<>
|
|
{/* 外部遮罩 */}
|
|
<div
|
|
onClick={onClose}
|
|
style={{
|
|
position: 'fixed',
|
|
inset: 0,
|
|
zIndex: 45,
|
|
background: 'rgba(15, 23, 42, 0.35)',
|
|
backdropFilter: 'blur(3px)',
|
|
}}
|
|
/>
|
|
|
|
<div
|
|
style={{
|
|
position: 'absolute',
|
|
top: '36px',
|
|
left: 0,
|
|
right: 0,
|
|
maxHeight: '560px',
|
|
background: 'var(--bg-panel)',
|
|
borderBottom: '2px solid var(--brand-500)',
|
|
boxShadow: 'var(--shadow-xl)',
|
|
zIndex: 50,
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
overflow: 'hidden',
|
|
animation: 'fadeIn 0.15s ease-out',
|
|
}}
|
|
>
|
|
{/* 抽屉顶栏 */}
|
|
<div
|
|
style={{
|
|
padding: '10px 16px',
|
|
borderBottom: '1px solid var(--border)',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'space-between',
|
|
background: 'var(--bg-subtle)',
|
|
}}
|
|
>
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
|
|
<Layers size={16} color="var(--brand-500)" />
|
|
<span style={{ fontSize: '13px', fontWeight: 700, color: 'var(--text-strong)' }}>
|
|
交付物分类台账全景大盘
|
|
</span>
|
|
<span style={{ fontSize: '11px', color: 'var(--text-muted)' }}>
|
|
(共 {deliverables.length} 份可用交付物)
|
|
</span>
|
|
</div>
|
|
|
|
{/* 大盘内分类筛选切换 */}
|
|
<div
|
|
style={{
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: '2px',
|
|
background: 'var(--bg-panel)',
|
|
padding: '2px',
|
|
borderRadius: '6px',
|
|
border: '1px solid var(--border)',
|
|
}}
|
|
>
|
|
<button
|
|
onClick={() => setFilterCategory('all')}
|
|
style={{
|
|
height: '22px',
|
|
padding: '0 8px',
|
|
borderRadius: '4px',
|
|
border: 'none',
|
|
fontSize: '11px',
|
|
cursor: 'pointer',
|
|
fontWeight: filterCategory === 'all' ? 600 : 500,
|
|
background: filterCategory === 'all' ? 'var(--brand-500)' : 'transparent',
|
|
color: filterCategory === 'all' ? '#ffffff' : 'var(--text-muted)',
|
|
}}
|
|
>
|
|
全部全景 ({deliverables.length})
|
|
</button>
|
|
<button
|
|
onClick={() => setFilterCategory('workflow')}
|
|
style={{
|
|
height: '22px',
|
|
padding: '0 8px',
|
|
borderRadius: '4px',
|
|
border: 'none',
|
|
fontSize: '11px',
|
|
cursor: 'pointer',
|
|
fontWeight: filterCategory === 'workflow' ? 600 : 500,
|
|
background: filterCategory === 'workflow' ? 'var(--brand-500)' : 'transparent',
|
|
color: filterCategory === 'workflow' ? '#ffffff' : 'var(--text-muted)',
|
|
}}
|
|
>
|
|
流程受控 ({workflowDocs.length})
|
|
</button>
|
|
<button
|
|
onClick={() => setFilterCategory('shared')}
|
|
style={{
|
|
height: '22px',
|
|
padding: '0 8px',
|
|
borderRadius: '4px',
|
|
border: 'none',
|
|
fontSize: '11px',
|
|
cursor: 'pointer',
|
|
fontWeight: filterCategory === 'shared' ? 600 : 500,
|
|
background: filterCategory === 'shared' ? 'var(--brand-500)' : 'transparent',
|
|
color: filterCategory === 'shared' ? '#ffffff' : 'var(--text-muted)',
|
|
}}
|
|
>
|
|
项目共享 ({sharedDocs.length})
|
|
</button>
|
|
<button
|
|
onClick={() => setFilterCategory('private')}
|
|
style={{
|
|
height: '22px',
|
|
padding: '0 8px',
|
|
borderRadius: '4px',
|
|
border: 'none',
|
|
fontSize: '11px',
|
|
cursor: 'pointer',
|
|
fontWeight: filterCategory === 'private' ? 600 : 500,
|
|
background: filterCategory === 'private' ? '#D97706' : 'transparent',
|
|
color: filterCategory === 'private' ? '#ffffff' : 'var(--text-muted)',
|
|
}}
|
|
>
|
|
我的私密 ({privateDocs.length})
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<button
|
|
onClick={onClose}
|
|
style={{
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
width: '26px',
|
|
height: '26px',
|
|
borderRadius: 'var(--r-sm)',
|
|
border: 'none',
|
|
background: 'transparent',
|
|
color: 'var(--text-muted)',
|
|
cursor: 'pointer',
|
|
}}
|
|
title="关闭大盘"
|
|
>
|
|
<X size={15} />
|
|
</button>
|
|
</div>
|
|
|
|
{/* 交付物大盘内容区 */}
|
|
<div
|
|
style={{
|
|
flex: 1,
|
|
overflowY: 'auto',
|
|
padding: '16px',
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
gap: '20px',
|
|
}}
|
|
>
|
|
{/* 板块一:🏛️ 研发体系预设受控交付物 */}
|
|
{(filterCategory === 'all' || filterCategory === 'workflow') && (
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}>
|
|
<div
|
|
style={{
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: '8px',
|
|
paddingBottom: '6px',
|
|
borderBottom: '1px dashed var(--border)',
|
|
}}
|
|
>
|
|
<Shield size={14} color="var(--brand-500)" />
|
|
<span style={{ fontSize: '12px', fontWeight: 700, color: 'var(--text-strong)' }}>
|
|
研发体系预设受控交付物 (固定流程 · 严格审签)
|
|
</span>
|
|
<span style={{ fontSize: '11px', color: 'var(--text-muted)' }}>
|
|
共 {workflowDocs.length} 份受控文档
|
|
</span>
|
|
</div>
|
|
|
|
{stages.map((stg) => {
|
|
const docsInStage = workflowDocs.filter((d) => d.stage === stg.stage);
|
|
if (docsInStage.length === 0) return null;
|
|
|
|
return (
|
|
<div key={stg.stage} style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
|
<div style={{ fontSize: '11px', fontWeight: 600, color: 'var(--brand-600)', paddingLeft: '4px' }}>
|
|
{stg.title}
|
|
</div>
|
|
|
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(290px, 1fr))', gap: '10px' }}>
|
|
{docsInStage.map((doc) => {
|
|
const isActive = doc.docId === activeDocId;
|
|
return (
|
|
<div
|
|
key={doc.docId}
|
|
onClick={() => {
|
|
onSelectDoc(doc.docId);
|
|
onClose();
|
|
}}
|
|
style={{
|
|
padding: '10px 12px',
|
|
borderRadius: 'var(--r-md)',
|
|
background: isActive ? 'var(--bg-active)' : 'var(--bg-card)',
|
|
border: isActive ? '1.5px solid var(--brand-500)' : '1px solid var(--border)',
|
|
boxShadow: 'var(--shadow-xs)',
|
|
cursor: 'pointer',
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
gap: '6px',
|
|
transition: 'all 0.15s ease',
|
|
}}
|
|
>
|
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
|
<span style={{ fontSize: '12.5px', fontWeight: 600, color: 'var(--text-strong)' }}>
|
|
{doc.title}
|
|
</span>
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
|
|
{renderVisibilityBadge(doc.visibilityRule, (e) => {
|
|
e.stopPropagation();
|
|
onOpenDocVisibility?.(doc);
|
|
})}
|
|
{doc.editLease?.holderType &&
|
|
(!doc.editLease.expiresAt || new Date(doc.editLease.expiresAt).getTime() > Date.now()) && (
|
|
<span
|
|
style={{
|
|
fontSize: '10.5px',
|
|
fontWeight: 600,
|
|
color: 'var(--amber-600)',
|
|
background: 'var(--amber-50)',
|
|
padding: '2px 6px',
|
|
borderRadius: 'var(--r-pill)',
|
|
}}
|
|
title={`编辑占用:${doc.editLease.holderType === 'squad' ? '项目助手' : '其他用户'}`}
|
|
>
|
|
编辑中
|
|
</span>
|
|
)}
|
|
{renderBadge(doc.lifecycleState, doc.isClaimed, doc.isTemplateOnly, doc.docId)}
|
|
</div>
|
|
</div>
|
|
|
|
<div style={{ fontSize: '11px', color: 'var(--text-muted)', display: 'flex', gap: '12px' }}>
|
|
<span>编制: <b>{doc.author}</b></span>
|
|
<span>审核: <b>{doc.reviewer}</b></span>
|
|
<span>批准: <b>{doc.approver}</b></span>
|
|
</div>
|
|
|
|
{doc.summary && (
|
|
<div style={{ fontSize: '11px', color: 'var(--text-faint)', lineHeight: 1.4 }}>
|
|
{doc.summary}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
|
|
{/* 板块二:🌐 项目协作参考资料库 */}
|
|
{(filterCategory === 'all' || filterCategory === 'shared') && (
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}>
|
|
<div
|
|
style={{
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: '8px',
|
|
paddingBottom: '6px',
|
|
borderBottom: '1px dashed var(--border)',
|
|
}}
|
|
>
|
|
<Globe size={14} color="var(--brand-500)" />
|
|
<span style={{ fontSize: '12px', fontWeight: 700, color: 'var(--text-strong)' }}>
|
|
项目协作参考资料库 (团队共享 · 方案推演)
|
|
</span>
|
|
<span style={{ fontSize: '11px', color: 'var(--text-muted)' }}>
|
|
共 {sharedDocs.length} 份共享参考材料
|
|
</span>
|
|
</div>
|
|
|
|
{sharedDocs.length === 0 ? (
|
|
<div style={{ fontSize: '12px', color: 'var(--text-faint)', padding: '12px', textAlign: 'center' }}>
|
|
当前暂无团队成员共享的参考资料。个人私密生成物成熟后可一键共享至此处。
|
|
</div>
|
|
) : (
|
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(290px, 1fr))', gap: '10px' }}>
|
|
{sharedDocs.map((doc) => {
|
|
const isActive = doc.docId === activeDocId;
|
|
const isCreator = doc.creatorId === currentUserId;
|
|
|
|
return (
|
|
<div
|
|
key={doc.docId}
|
|
onClick={() => {
|
|
onSelectDoc(doc.docId);
|
|
onClose();
|
|
}}
|
|
style={{
|
|
padding: '10px 12px',
|
|
borderRadius: 'var(--r-md)',
|
|
background: isActive ? 'var(--bg-active)' : 'var(--bg-card)',
|
|
border: isActive ? '1.5px solid var(--brand-500)' : '1px solid var(--border)',
|
|
boxShadow: 'var(--shadow-xs)',
|
|
cursor: 'pointer',
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
gap: '6px',
|
|
transition: 'all 0.15s ease',
|
|
}}
|
|
>
|
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
|
<span style={{ fontSize: '12.5px', fontWeight: 600, color: 'var(--text-strong)' }}>
|
|
{doc.title}
|
|
</span>
|
|
<span
|
|
style={{
|
|
fontSize: '10.5px',
|
|
fontWeight: 600,
|
|
color: 'var(--brand-600)',
|
|
background: 'var(--brand-50)',
|
|
padding: '2px 6px',
|
|
borderRadius: 'var(--r-pill)',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: '3px',
|
|
}}
|
|
>
|
|
<Globe size={10} />
|
|
<span>项目共享</span>
|
|
</span>
|
|
</div>
|
|
|
|
<div style={{ fontSize: '11px', color: 'var(--text-muted)', display: 'flex', gap: '12px' }}>
|
|
<span>共享人: <b>{doc.creatorName}</b></span>
|
|
<span>共享时间: <b>{doc.sharedAt || doc.updatedAt}</b></span>
|
|
</div>
|
|
|
|
{doc.summary && (
|
|
<div style={{ fontSize: '11px', color: 'var(--text-faint)', lineHeight: 1.4 }}>
|
|
{doc.summary}
|
|
</div>
|
|
)}
|
|
|
|
{/* 共享人专属撤回操作 */}
|
|
{isCreator && onRevokeShare && (
|
|
<div style={{ display: 'flex', justifyContent: 'flex-end', marginTop: '4px' }}>
|
|
<button
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
onRevokeShare(doc.docId);
|
|
}}
|
|
style={{
|
|
fontSize: '11px',
|
|
padding: '3px 8px',
|
|
borderRadius: '4px',
|
|
border: '1px solid var(--border)',
|
|
background: 'var(--bg-subtle)',
|
|
color: 'var(--text)',
|
|
cursor: 'pointer',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: '3px',
|
|
}}
|
|
title="将该共享资料撤回为个人私密草稿 (仅自己可见)"
|
|
>
|
|
<Undo2 size={11} />
|
|
<span>取消共享 (恢复私有)</span>
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* 板块三:🔒 我的私密生成档案 */}
|
|
{(filterCategory === 'all' || filterCategory === 'private') && (
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}>
|
|
<div
|
|
style={{
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: '8px',
|
|
paddingBottom: '6px',
|
|
borderBottom: '1px dashed var(--border)',
|
|
}}
|
|
>
|
|
<Lock size={14} color="#D97706" />
|
|
<span style={{ fontSize: '12px', fontWeight: 700, color: 'var(--text-strong)' }}>
|
|
我的私密生成档案 (仅您本人可见 · 商业与推演隐私)
|
|
</span>
|
|
<span style={{ fontSize: '11px', color: '#D97706', fontWeight: 600 }}>
|
|
共 {privateDocs.length} 份私有草稿
|
|
</span>
|
|
</div>
|
|
|
|
{privateDocs.length === 0 ? (
|
|
<div style={{ fontSize: '12px', color: 'var(--text-faint)', padding: '12px', textAlign: 'center' }}>
|
|
您当前未生成私密文档。在中间栏向智能体提出测算、草拟需求时,生成的文档将自动归档于此处。
|
|
</div>
|
|
) : (
|
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(290px, 1fr))', gap: '10px' }}>
|
|
{privateDocs.map((doc) => {
|
|
const isActive = doc.docId === activeDocId;
|
|
|
|
return (
|
|
<div
|
|
key={doc.docId}
|
|
onClick={() => {
|
|
onSelectDoc(doc.docId);
|
|
onClose();
|
|
}}
|
|
style={{
|
|
padding: '10px 12px',
|
|
borderRadius: 'var(--r-md)',
|
|
background: isActive ? 'var(--bg-active)' : 'var(--bg-card)',
|
|
border: isActive ? '1.5px solid #D97706' : '1px solid var(--border)',
|
|
boxShadow: 'var(--shadow-xs)',
|
|
cursor: 'pointer',
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
gap: '6px',
|
|
transition: 'all 0.15s ease',
|
|
}}
|
|
>
|
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
|
<span style={{ fontSize: '12.5px', fontWeight: 600, color: 'var(--text-strong)' }}>
|
|
{doc.title}
|
|
</span>
|
|
<span
|
|
style={{
|
|
fontSize: '10.5px',
|
|
fontWeight: 600,
|
|
color: '#D97706',
|
|
background: '#FEF3C7',
|
|
padding: '2px 6px',
|
|
borderRadius: 'var(--r-pill)',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: '3px',
|
|
}}
|
|
>
|
|
<Lock size={10} />
|
|
<span>仅我可见</span>
|
|
</span>
|
|
</div>
|
|
|
|
<div style={{ fontSize: '11px', color: 'var(--text-muted)', display: 'flex', gap: '12px' }}>
|
|
<span>创建人: <b>本人 ({doc.creatorName})</b></span>
|
|
<span>生成时间: <b>{doc.updatedAt}</b></span>
|
|
</div>
|
|
|
|
{doc.summary && (
|
|
<div style={{ fontSize: '11px', color: 'var(--text-faint)', lineHeight: 1.4 }}>
|
|
{doc.summary}
|
|
</div>
|
|
)}
|
|
|
|
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: '6px', marginTop: '4px' }}>
|
|
{onShareToProject && (
|
|
<button
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
onShareToProject(doc.docId);
|
|
}}
|
|
style={{
|
|
fontSize: '11px',
|
|
padding: '3px 8px',
|
|
borderRadius: '4px',
|
|
border: '1px solid var(--brand-500)',
|
|
background: 'var(--brand-500)',
|
|
color: '#ffffff',
|
|
cursor: 'pointer',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: '3px',
|
|
}}
|
|
title="将该私密文档一键共享给项目组成员作为协作参考资料"
|
|
>
|
|
<Share2 size={11} />
|
|
<span>一键共享给项目组</span>
|
|
</button>
|
|
)}
|
|
|
|
{onDeleteDoc && (
|
|
<button
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
onDeleteDoc(doc.docId);
|
|
}}
|
|
style={{
|
|
fontSize: '11px',
|
|
padding: '3px 6px',
|
|
borderRadius: '4px',
|
|
border: '1px solid var(--border)',
|
|
background: 'var(--bg-subtle)',
|
|
color: 'var(--danger)',
|
|
cursor: 'pointer',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: '2px',
|
|
}}
|
|
title="删除此私密草稿"
|
|
>
|
|
<Trash2 size={11} />
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</>
|
|
);
|
|
};
|