feat(wordcloud): 收口在途开发(布局/存储/前端)+ R4 WCD 生产任务(jobs wcd_file)与生产订单列表

This commit is contained in:
2026-08-13 14:22:48 +08:00
parent 1d17b5e20d
commit e518540235
32 changed files with 3525 additions and 592 deletions
+70 -3
View File
@@ -1,12 +1,33 @@
import { useCallback, useEffect, useRef } from 'react';
import { SSEProgress } from '../types';
import { IconCross, IconCheckmark, IconGear } from './Icons';
import { IconCross, IconCheckmark, IconGear, IconCopy } from './Icons';
interface ProgressPanelProps {
progress: SSEProgress | null;
logLines: string[];
visible: boolean;
}
export default function ProgressPanel({ progress, visible }: ProgressPanelProps) {
export default function ProgressPanel({ progress, logLines, visible }: ProgressPanelProps) {
const logEndRef = useRef<HTMLDivElement>(null);
// Auto-scroll the log view to the bottom whenever new lines arrive.
useEffect(() => {
logEndRef.current?.scrollIntoView({ behavior: 'smooth', block: 'end' });
}, [logLines]);
// All hooks MUST run before any early return, so derive the values the
// callback needs without depending on `progress` being non-null.
const message = progress?.message ?? '';
const handleCopyError = useCallback(() => {
const text = logLines.length > 0 ? logLines.join('\n') : message;
navigator.clipboard.writeText(text).catch(() => {
const el = document.querySelector('.error-detail-textarea') as HTMLTextAreaElement | null;
if (el) { el.select(); document.execCommand('copy'); }
});
}, [logLines, message]);
if (!visible || !progress) return null;
const isFailed = progress.stage === '生成失败' || progress.stage === '错误';
@@ -20,6 +41,13 @@ export default function ProgressPanel({ progress, visible }: ProgressPanelProps)
<div className="progress-stage" style={isFailed ? { color: 'var(--danger)' } : {}}>
{progress.stage}
</div>
{(progress.elapsedSeconds != null || progress.clientElapsedSeconds != null) && (
<div className="progress-timing" style={{ color: isDone ? 'var(--success)' : 'var(--text-muted)' }}>
{progress.elapsedSeconds != null && `后端耗时 ${progress.elapsedSeconds.toFixed(2)}`}
{progress.elapsedSeconds != null && progress.clientElapsedSeconds != null ? ' · ' : ''}
{progress.clientElapsedSeconds != null && `前端显示耗时 ${progress.clientElapsedSeconds.toFixed(2)}`}
</div>
)}
{!isFailed && (
<div className="progress-bar-track">
<div
@@ -31,6 +59,33 @@ export default function ProgressPanel({ progress, visible }: ProgressPanelProps)
/>
</div>
)}
{/* ── 详细日志(实时滚动) ───────────────────────────────── */}
{logLines.length > 0 && (
<div
className="progress-log-view"
style={{
marginTop: 8,
maxHeight: 320,
overflowY: 'auto',
background: 'var(--bg-secondary, #1a1a2e)',
borderRadius: 6,
padding: '8px 10px',
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Consolas, monospace',
fontSize: 12,
lineHeight: 1.6,
color: 'var(--text-muted, #888)',
border: '1px solid var(--border-color, #333)',
}}
>
{logLines.map((line, i) => (
<div key={i} style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-all' }}>
{line}
</div>
))}
<div ref={logEndRef} />
</div>
)}
{/* ── 失败时的错误详情 + 复制按钮 ───────────────────────── */}
<div
className="progress-message"
style={{
@@ -41,7 +96,19 @@ export default function ProgressPanel({ progress, visible }: ProgressPanelProps)
marginTop: isFailed ? 8 : 0,
}}
>
{progress.message}
{isFailed ? (
<div className="error-detail-container">
<button
type="button"
className="btn btn-secondary btn-sm"
onClick={handleCopyError}
title="复制完整日志"
style={{ marginBottom: 6, display: 'inline-flex', alignItems: 'center', gap: 4 }}
>
<IconCopy />
</button>
</div>
) : null}
</div>
</div>
);