feat(workflow): Phase 6 前端项目多工作流切换器、前置依赖横幅与文档血缘失效提醒
This commit is contained in:
@@ -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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user