feat(workflow): Phase 6 前端项目多工作流切换器、前置依赖横幅与文档血缘失效提醒

This commit is contained in:
WeiCHEN
2026-09-13 02:50:21 +00:00
committed by weichen
parent abeab90388
commit 716aa6f49a
9 changed files with 658 additions and 2 deletions
@@ -0,0 +1,148 @@
import React, { useState } from 'react';
import { Workflow, Lock, CheckCircle2, AlertCircle } from 'lucide-react';
import { activateWorkflow } from '../../api/projectWorkflow';
interface ProjectWorkflowSwitcherProps {
projectId: number;
activeWorkflowKey: string;
workflowVersion: number;
isCreator: boolean;
onWorkflowChanged?: (newKey: string, newVersion: number) => void;
}
const WORKFLOW_OPTIONS = [
{ key: 'main', label: '主协同流程', icon: '⚡' },
{ key: 'product_manager', label: '智能产品经理', icon: '📋' },
{ key: 'test_manager', label: '智能测试经理', icon: '🧪' },
];
export const ProjectWorkflowSwitcher: React.FC<ProjectWorkflowSwitcherProps> = ({
projectId,
activeWorkflowKey,
workflowVersion,
isCreator,
onWorkflowChanged,
}) => {
const [switching, setSwitching] = useState(false);
const [errorMsg, setErrorMsg] = useState<string | null>(null);
const handleSelect = async (targetKey: string) => {
if (targetKey === activeWorkflowKey) return;
if (!isCreator) {
setErrorMsg('权限不足:仅项目创建人可切换激活工作流');
setTimeout(() => setErrorMsg(null), 3000);
return;
}
setSwitching(true);
setErrorMsg(null);
try {
const res = await activateWorkflow(projectId, {
workflow_key: targetKey,
expected_version: workflowVersion,
reason: '前端控制台用户切换工作流',
});
if (onWorkflowChanged) {
onWorkflowChanged(res.active_workflow_key, res.version);
}
} catch (err: any) {
setErrorMsg(err.message || '切换工作流失败');
setTimeout(() => setErrorMsg(null), 4000);
} finally {
setSwitching(false);
}
};
return (
<div style={{ display: 'flex', alignItems: 'center', gap: '6px', position: 'relative' }}>
<div
style={{
display: 'flex',
alignItems: 'center',
gap: '4px',
padding: '2px',
background: 'var(--bg-subtle, #f1f5f9)',
borderRadius: 'var(--r-md, 8px)',
border: '1px solid var(--border, #e2e8f0)',
}}
>
<span
style={{
fontSize: '11px',
fontWeight: 600,
color: 'var(--text-muted, #64748b)',
padding: '0 6px',
display: 'flex',
alignItems: 'center',
gap: '3px',
}}
>
<Workflow size={13} />
工作流:
</span>
{WORKFLOW_OPTIONS.map((wf) => {
const isActive = wf.key === activeWorkflowKey;
return (
<button
key={wf.key}
onClick={() => handleSelect(wf.key)}
disabled={switching || (!isCreator && !isActive)}
title={
!isCreator
? `仅创建人可切换工作流 (当前激活: ${wf.label})`
: `点击切换至 ${wf.label}`
}
style={{
display: 'flex',
alignItems: 'center',
gap: '4px',
padding: '4px 8px',
fontSize: '11px',
fontWeight: isActive ? 600 : 500,
borderRadius: '6px',
border: 'none',
cursor: switching || (!isCreator && !isActive) ? 'not-allowed' : 'pointer',
background: isActive ? 'var(--bg-card, #ffffff)' : 'transparent',
color: isActive ? 'var(--brand-600, #4f46e5)' : 'var(--text-muted, #64748b)',
boxShadow: isActive ? '0 1px 3px rgba(0,0,0,0.1)' : 'none',
opacity: !isCreator && !isActive ? 0.6 : 1,
transition: 'all 0.15s ease',
}}
>
<span>{wf.icon}</span>
<span>{wf.label}</span>
{isActive && <CheckCircle2 size={12} style={{ color: 'var(--brand-600, #4f46e5)' }} />}
{!isCreator && !isActive && <Lock size={10} style={{ opacity: 0.5 }} />}
</button>
);
})}
</div>
{errorMsg && (
<div
style={{
position: 'absolute',
top: '36px',
left: '0',
zIndex: 100,
background: 'var(--danger-50, #fef2f2)',
border: '1px solid var(--danger-200, #fecaca)',
color: 'var(--danger-700, #b91c1c)',
padding: '4px 8px',
borderRadius: '6px',
fontSize: '11px',
display: 'flex',
alignItems: 'center',
gap: '4px',
boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
whiteSpace: 'nowrap',
}}
>
<AlertCircle size={12} />
{errorMsg}
</div>
)}
</div>
);
};
@@ -0,0 +1,63 @@
import React from 'react';
import { BellRing, RefreshCw } from 'lucide-react';
import { DocumentLineage } from '../../types/projectWorkflow';
interface StaleDocumentNoticeProps {
lineages: DocumentLineage[];
onTriggerRecheck?: () => void;
}
export const StaleDocumentNotice: React.FC<StaleDocumentNoticeProps> = ({
lineages,
onTriggerRecheck,
}) => {
const staleItem = lineages.find((l) => l.is_stale);
if (!staleItem) return null;
return (
<div
style={{
margin: '8px 16px',
padding: '8px 12px',
borderRadius: '8px',
background: 'rgba(239, 68, 68, 0.08)',
border: '1px solid rgba(239, 68, 68, 0.25)',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
gap: '12px',
}}
>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', fontSize: '12px', color: '#b91c1c' }}>
<BellRing size={16} style={{ color: '#ef4444', flexShrink: 0 }} />
<span>
<strong>上游输入已变更(血缘失效):</strong>
{staleItem.stale_reason || '上游系统需求规格书已重新生成或更新,当前验证方案可能存在覆盖缺口。'}
</span>
</div>
{onTriggerRecheck && (
<button
onClick={onTriggerRecheck}
style={{
display: 'flex',
alignItems: 'center',
gap: '4px',
padding: '4px 10px',
borderRadius: '6px',
background: '#dc2626',
color: '#ffffff',
border: 'none',
fontSize: '11px',
fontWeight: 600,
cursor: 'pointer',
whiteSpace: 'nowrap',
flexShrink: 0,
}}
>
<RefreshCw size={12} />
<span>重新执行验证方案评审</span>
</button>
)}
</div>
);
};
@@ -0,0 +1,63 @@
import React from 'react';
import { AlertTriangle, ArrowRight } from 'lucide-react';
interface WorkflowPrerequisiteBannerProps {
workflowKey: string;
hasApprovedSystemReqs: boolean;
onSwitchToProductManager: () => void;
}
export const WorkflowPrerequisiteBanner: React.FC<WorkflowPrerequisiteBannerProps> = ({
workflowKey,
hasApprovedSystemReqs,
onSwitchToProductManager,
}) => {
if (workflowKey !== 'test_manager' || hasApprovedSystemReqs) {
return null;
}
return (
<div
style={{
margin: '8px 16px',
padding: '8px 12px',
borderRadius: '8px',
background: 'rgba(245, 158, 11, 0.1)',
border: '1px solid rgba(245, 158, 11, 0.3)',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
gap: '12px',
}}
>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', fontSize: '12px', color: '#b45309' }}>
<AlertTriangle size={16} style={{ color: '#d97706', flexShrink: 0 }} />
<span>
<strong>前置依赖未就绪:</strong>
智能测试经理工作流强依赖同项目已批准的《系统需求规格书 (RD-SR)》。请先由产品经理完成编制并经三级审批批准。
</span>
</div>
<button
onClick={onSwitchToProductManager}
style={{
display: 'flex',
alignItems: 'center',
gap: '4px',
padding: '4px 10px',
borderRadius: '6px',
background: '#d97706',
color: '#ffffff',
border: 'none',
fontSize: '11px',
fontWeight: 600,
cursor: 'pointer',
whiteSpace: 'nowrap',
flexShrink: 0,
}}
>
<span>前往产品经理编制需求</span>
<ArrowRight size={12} />
</button>
</div>
);
};