build UI
All checks were successful
Build and Deploy Log Center / build-and-deploy (push) Successful in 1m37s

This commit is contained in:
repair-agent 2026-03-17 18:21:59 +08:00
parent 1da10e8136
commit 252876f81a
7 changed files with 475 additions and 269 deletions

View File

@ -6,7 +6,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
<link href="https://fonts.googleapis.com/css2?family=Fira+Code:wght@400;500;600;700&family=Fira+Sans:wght@300;400;500;600;700&display=swap" rel="stylesheet" />
<title>日志中台 - Log Center</title>
</head>
<body>

View File

@ -1,31 +1,36 @@
/* ============================================
Log Center - Modern Minimalist Design System
Log Center - OLED Dark Design System
Style: Dark Mode (OLED) per ui-ux-pro-max
Colors: #020617 bg / #0F172A primary / #22C55E CTA
Fonts: Fira Sans + Fira Code
============================================ */
:root {
--bg-primary: #09090b;
--bg-secondary: #0f0f11;
--bg-card: #18181b;
--bg-surface: #1f1f23;
--bg-hover: #27272a;
--bg-primary: #020617;
--bg-secondary: #0F172A;
--bg-card: #1E293B;
--bg-surface: #1E293B;
--bg-hover: #334155;
--accent: #3b82f6;
--accent-hover: #2563eb;
--accent-muted: rgba(59, 130, 246, 0.12);
--indigo: #6366f1;
--text-primary: #fafafa;
--text-secondary: #a1a1aa;
--text-tertiary: #71717a;
--text-primary: #F8FAFC;
--text-secondary: #94A3B8;
--text-tertiary: #64748B;
--success: #22c55e;
--success-muted: rgba(34, 197, 94, 0.12);
--warning: #f59e0b;
--warning-muted: rgba(245, 158, 11, 0.12);
--error: #ef4444;
--error-muted: rgba(239, 68, 68, 0.12);
--border: #27272a;
--border-subtle: #1f1f23;
--border: rgba(148, 163, 184, 0.1);
--border-subtle: rgba(148, 163, 184, 0.06);
--radius-sm: 6px;
--radius-md: 8px;
--radius-lg: 12px;
--glow-accent: 0 0 12px rgba(59, 130, 246, 0.15);
--glow-success: 0 0 12px rgba(34, 197, 94, 0.15);
}
* {
@ -35,7 +40,7 @@
}
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
font-family: 'Fira Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: var(--bg-primary);
color: var(--text-primary);
line-height: 1.5;
@ -128,6 +133,7 @@ a:hover {
.nav-link.active {
background: var(--accent-muted);
color: var(--accent);
box-shadow: var(--glow-accent);
}
.nav-link svg {
@ -185,7 +191,8 @@ a:hover {
}
.stat-card:hover {
border-color: #3f3f46;
border-color: rgba(148, 163, 184, 0.2);
box-shadow: 0 2px 16px rgba(0, 0, 0, 0.3);
}
.stat-label {
@ -203,10 +210,10 @@ a:hover {
letter-spacing: -0.02em;
}
.stat-value.accent { color: var(--accent); }
.stat-value.success { color: var(--success); }
.stat-value.warning { color: var(--warning); }
.stat-value.error { color: var(--error); }
.stat-value.accent { color: var(--accent); text-shadow: 0 0 10px rgba(59, 130, 246, 0.3); }
.stat-value.success { color: var(--success); text-shadow: 0 0 10px rgba(34, 197, 94, 0.3); }
.stat-value.warning { color: var(--warning); text-shadow: 0 0 10px rgba(245, 158, 11, 0.3); }
.stat-value.error { color: var(--error); text-shadow: 0 0 10px rgba(239, 68, 68, 0.3); }
.stat-icon {
display: flex;
@ -288,7 +295,7 @@ td a:hover {
}
.cell-mono {
font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;
font-family: 'Fira Code', 'SF Mono', 'Cascadia Code', monospace;
font-size: 12px;
color: var(--text-secondary);
}
@ -464,7 +471,7 @@ td a:hover {
}
.status-tab:hover {
border-color: #3f3f46;
border-color: rgba(148, 163, 184, 0.2);
color: var(--text-secondary);
}
@ -642,7 +649,7 @@ td a:hover {
}
.detail-section-value {
font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;
font-family: 'Fira Code', 'SF Mono', 'Cascadia Code', monospace;
font-size: 13px;
color: var(--text-secondary);
}
@ -654,7 +661,7 @@ td a:hover {
border: 1px solid var(--border);
border-radius: var(--radius-md);
padding: 16px;
font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;
font-family: 'Fira Code', 'SF Mono', 'Cascadia Code', monospace;
font-size: 12px;
line-height: 1.6;
overflow-x: auto;
@ -680,7 +687,7 @@ td a:hover {
border: 1px solid var(--border);
border-radius: var(--radius-md);
padding: 16px;
font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;
font-family: 'Fira Code', 'SF Mono', 'Cascadia Code', monospace;
font-size: 12px;
line-height: 1.6;
overflow-x: auto;
@ -750,7 +757,7 @@ td a:hover {
.btn-secondary:hover:not(:disabled) {
background: var(--bg-hover);
border-color: #3f3f46;
border-color: rgba(148, 163, 184, 0.2);
}
.btn-secondary:disabled {
@ -923,7 +930,7 @@ td a:hover {
.pagination button:hover:not(:disabled) {
background: var(--bg-hover);
border-color: #3f3f46;
border-color: rgba(148, 163, 184, 0.2);
}
.pagination button:disabled {
@ -1285,6 +1292,8 @@ td a:hover {
display: flex;
flex-direction: column;
gap: 8px;
text-decoration: none;
color: inherit;
}
.mobile-card-item:last-child {
@ -1326,7 +1335,7 @@ td a:hover {
}
.mobile-card-file {
font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;
font-family: 'Fira Code', 'SF Mono', 'Cascadia Code', monospace;
font-size: 11px;
color: var(--text-secondary);
word-break: break-all;
@ -1671,7 +1680,7 @@ td a:hover {
}
.project-card:hover {
border-color: #3f3f46;
border-color: rgba(148, 163, 184, 0.2);
}
.project-card-disabled {
@ -1809,6 +1818,7 @@ td a:hover {
.repair-toggle-on .repair-toggle-track {
background: var(--success);
box-shadow: var(--glow-success);
}
.repair-toggle-off .repair-toggle-track {
@ -2000,3 +2010,335 @@ td a:hover {
grid-template-columns: 1fr;
}
}
/* ============ Button Color Variants ============ */
.trigger-repair-btn.btn-success {
background: var(--success);
}
.trigger-repair-btn.btn-success:hover:not(:disabled) {
background: #16a34a;
}
.trigger-repair-btn.btn-error {
background: var(--error);
}
.trigger-repair-btn.btn-error:hover:not(:disabled) {
background: #dc2626;
}
.trigger-repair-btn.btn-warning {
background: var(--warning);
}
.trigger-repair-btn.btn-warning:hover:not(:disabled) {
background: #d97706;
}
/* ============ Detail Card Variants ============ */
.detail-card--error {
border-left: 3px solid var(--error);
}
.detail-card--warning {
border-left: 3px solid var(--warning);
}
.detail-card--accent {
border-left: 3px solid var(--accent);
}
/* ============ Card Variants ============ */
.card--error {
border-left: 3px solid var(--error);
}
.card--warning {
border-left: 3px solid var(--warning);
}
.card--accent {
border-left: 3px solid var(--accent);
}
/* ============ Severity Badge ============ */
.severity-badge {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 4px 10px;
border-radius: 12px;
font-size: 13px;
font-weight: 600;
color: #fff;
}
.severity-badge--high {
background: var(--error);
}
.severity-badge--mid {
background: var(--warning);
}
.severity-badge--low {
background: var(--success);
}
/* ============ Severity Section ============ */
.severity-section {
background: var(--bg-secondary);
padding: 12px;
border-radius: 6px;
margin-top: 12px;
}
.severity-section--critical {
background: rgba(239, 68, 68, 0.08);
border-left: 3px solid var(--error);
}
.severity-section--mid {
border-left: 3px solid var(--warning);
}
.severity-section--low {
border-left: 3px solid var(--success);
}
.severity-section .detail-section-title {
margin-bottom: 4px;
}
.severity-section-desc {
font-size: 13px;
color: var(--text-secondary);
}
/* ============ PR Info Section ============ */
.pr-section {
background: var(--bg-secondary);
padding: 12px;
border-radius: 6px;
margin-top: 16px;
}
.pr-section .detail-section-title {
margin-bottom: 8px;
}
.pr-section-row {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.pr-section-id {
font-size: 14px;
}
.pr-section-link {
display: inline-flex;
align-items: center;
gap: 4px;
}
.pr-section-rejections {
font-size: 13px;
color: var(--warning);
}
/* ============ Action Messages ============ */
.action-message {
font-size: 13px;
}
.action-message--success {
color: var(--success);
}
.action-message--error {
color: var(--error);
}
/* ============ Action Hint ============ */
.action-hint {
font-size: 13px;
color: var(--text-tertiary);
}
/* ============ Detail Header Right ============ */
.detail-header-badges {
display: flex;
align-items: center;
gap: 8px;
}
/* ============ Reject Modal ============ */
.reject-modal-content {
max-width: 500px;
}
.reject-modal-desc {
margin: 0 0 12px 0;
font-size: 14px;
color: var(--text-secondary);
}
.reject-templates {
margin-bottom: 12px;
}
.reject-templates-label {
font-size: 13px;
color: var(--text-tertiary);
margin-bottom: 6px;
display: block;
}
.reject-templates-list {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.reject-template-btn {
font-size: 12px;
padding: 4px 8px;
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: 4px;
cursor: pointer;
color: var(--text-secondary);
transition: all 0.15s ease;
}
.reject-template-btn:hover {
background: var(--bg-hover);
color: var(--text-primary);
border-color: rgba(148, 163, 184, 0.2);
}
.reject-textarea {
width: 100%;
min-height: 120px;
padding: 8px;
font-size: 14px;
border-radius: 4px;
border: 1px solid var(--border);
background: var(--bg-secondary);
color: var(--text-primary);
resize: vertical;
font-family: inherit;
outline: none;
transition: border-color 0.15s ease;
box-sizing: border-box;
}
.reject-textarea:focus {
border-color: var(--accent);
}
/* ============ Failure Hint (in table) ============ */
.failure-hint {
font-size: 11px;
color: var(--error);
margin-top: 4px;
max-width: 200px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* ============ Failure Cell (in repair table) ============ */
.failure-cell {
max-width: 200px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 12px;
color: var(--error);
}
/* ============ Failure Cell Wide (in repair history) ============ */
.failure-cell-wide {
max-width: 300px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* ============ Detail Error Title ============ */
.detail-title--error {
color: var(--error);
}
/* ============ Error Alert Card ============ */
.alert-card-header {
margin-bottom: 8px;
display: flex;
align-items: center;
gap: 6px;
}
.alert-card-header--error {
color: var(--error);
}
.alert-card-header--warning {
color: var(--warning);
}
/* ============ Review Section ============ */
.review-section-desc {
font-size: 14px;
color: var(--text-secondary);
margin-bottom: 12px;
}
.review-actions {
display: flex;
gap: 12px;
flex-wrap: wrap;
align-items: center;
}
/* ============ Report Header ============ */
.report-header {
margin-bottom: 20px;
}
/* ============ Reduced Motion ============ */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
.spinner {
animation: none;
border-top-color: var(--accent);
opacity: 0.7;
}
}

View File

@ -28,6 +28,12 @@ const REJECT_REASON_TEMPLATES = [
'需要补充异常处理',
];
function severityLevel(s: number) {
if (s >= 8) return 'high';
if (s >= 5) return 'mid';
return 'low';
}
export default function BugDetail() {
const { id } = useParams<{ id: string }>();
const location = useLocation();
@ -37,19 +43,16 @@ export default function BugDetail() {
const [repairMessage, setRepairMessage] = useState('');
const [repairHistory, setRepairHistory] = useState<RepairReport[]>([]);
// PR 操作相关状态
const [mergingPR, setMergingPR] = useState(false);
const [closingPR, setClosingPR] = useState(false);
const [prMessage, setPRMessage] = useState('');
const [showRejectModal, setShowRejectModal] = useState(false);
const [rejectReason, setRejectReason] = useState('');
// 人工审核操作状态(无 PR 时使用)
const [approving, setApproving] = useState(false);
const [rejecting, setRejecting] = useState(false);
const [reviewMessage, setReviewMessage] = useState('');
// 重试操作状态
const [retrying, setRetrying] = useState(false);
const [retryMessage, setRetryMessage] = useState('');
@ -101,10 +104,10 @@ export default function BugDetail() {
try {
await mergePR(bug.id);
setBug({ ...bug, status: 'FIXED', merged_at: new Date().toISOString() });
setPRMessage('PR 已成功合并');
setPRMessage('PR 已成功合并');
} catch (error: any) {
console.error('Failed to merge PR:', error);
setPRMessage(`合并失败: ${error.response?.data?.detail || error.message}`);
setPRMessage(`合并失败: ${error.response?.data?.detail || error.message}`);
} finally {
setMergingPR(false);
}
@ -112,7 +115,7 @@ export default function BugDetail() {
const handleClosePR = async () => {
if (!bug || !rejectReason.trim()) {
setPRMessage('请输入拒绝原因');
setPRMessage('请输入拒绝原因');
return;
}
setClosingPR(true);
@ -125,12 +128,12 @@ export default function BugDetail() {
rejection_count: (bug.rejection_count || 0) + 1,
last_rejected_at: new Date().toISOString()
});
setPRMessage('PR 已拒绝Bug 将重新修复');
setPRMessage('PR 已拒绝Bug 将重新修复');
setShowRejectModal(false);
setRejectReason('');
} catch (error: any) {
console.error('Failed to close PR:', error);
setPRMessage(`关闭失败: ${error.response?.data?.detail || error.message}`);
setPRMessage(`关闭失败: ${error.response?.data?.detail || error.message}`);
} finally {
setClosingPR(false);
}
@ -143,10 +146,10 @@ export default function BugDetail() {
try {
await retryFix(bug.id);
setBug({ ...bug, status: 'NEW', failure_reason: null });
setRetryMessage('Bug 已重置repair agent 将重新扫描');
setRetryMessage('Bug 已重置repair agent 将重新扫描');
} catch (error: any) {
console.error('Failed to retry:', error);
setRetryMessage(`重试失败: ${error.response?.data?.detail || error.message}`);
setRetryMessage(`重试失败: ${error.response?.data?.detail || error.message}`);
} finally {
setRetrying(false);
}
@ -212,10 +215,12 @@ export default function BugDetail() {
const isPendingReview = bug.status === 'PENDING_FIX';
const canOperatePR = hasPR && isPendingReview;
const canManualReview = !hasPR && isPendingReview;
// PR 信息只在 PENDING_FIX 或 FIXED 状态时显示
const shouldShowPR = hasPR && (isPendingReview || bug.status === 'FIXED');
const canRetry = bug.status === 'FIX_FAILED';
const msgIsSuccess = (msg: string) =>
msg.includes('成功') || msg.includes('确认') || msg.includes('驳回') || msg.includes('已重置') || msg.includes('已拒绝');
return (
<div>
<Link to={`/bugs${backSearch ? `?${backSearch}` : ''}`} className="back-link">
@ -226,7 +231,7 @@ export default function BugDetail() {
<div className="detail-card">
<div className="detail-header">
<div>
<h2 className="detail-title" style={{ color: 'var(--error)' }}>
<h2 className="detail-title detail-title--error">
{bug.error_type}: {bug.error_message}
</h2>
<div className="detail-meta">
@ -236,19 +241,9 @@ export default function BugDetail() {
<span>{bug.level}</span>
</div>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<div className="detail-header-badges">
{bug.severity != null && bug.status !== 'NEW' && (
<span style={{
display: 'inline-flex',
alignItems: 'center',
gap: '4px',
padding: '4px 10px',
borderRadius: '12px',
fontSize: '13px',
fontWeight: 600,
color: '#fff',
background: bug.severity >= 8 ? 'var(--error)' : bug.severity >= 5 ? 'var(--warning)' : 'var(--success)',
}}>
<span className={`severity-badge severity-badge--${severityLevel(bug.severity)}`}>
{bug.severity}/10
</span>
)}
@ -260,35 +255,29 @@ export default function BugDetail() {
{/* 严重等级说明 */}
{bug.severity != null && bug.severity_reason && bug.status !== 'NEW' && (
<div className="detail-section" style={{
background: bug.severity >= 8 ? 'rgba(239,68,68,0.08)' : 'var(--bg-secondary)',
padding: '12px',
borderRadius: '6px',
marginTop: '12px',
borderLeft: `3px solid ${bug.severity >= 8 ? 'var(--error)' : bug.severity >= 5 ? 'var(--warning)' : 'var(--success)'}`,
}}>
<div className="detail-section-title" style={{ marginBottom: '4px' }}>
<div className={`severity-section severity-section--${bug.severity >= 8 ? 'critical' : severityLevel(bug.severity)}`}>
<div className="detail-section-title">
{bug.severity}/10 {bug.severity >= 8 ? '(需人工审核)' : ''}
</div>
<div style={{ fontSize: '13px', color: 'var(--text-secondary)' }}>
<div className="severity-section-desc">
{bug.severity_reason}
</div>
</div>
)}
{/* PR 信息显示 - 仅在 PENDING_FIX 或 FIXED 状态时显示 */}
{/* PR 信息显示 */}
{shouldShowPR && (
<div className="detail-section" style={{ background: 'var(--bg-secondary)', padding: '12px', borderRadius: '6px', marginTop: '16px' }}>
<div className="detail-section-title" style={{ marginBottom: '8px' }}>Pull Request</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', flexWrap: 'wrap' }}>
<span style={{ fontSize: '14px' }}>
<div className="detail-section pr-section">
<div className="detail-section-title">Pull Request</div>
<div className="pr-section-row">
<span className="pr-section-id">
PR #{bug.pr_number} | {bug.branch_name || 'fix branch'}
</span>
<a href={bug.pr_url || undefined} target="_blank" rel="noopener noreferrer" className="btn-link" style={{ display: 'inline-flex', alignItems: 'center', gap: '4px' }}>
<a href={bug.pr_url || undefined} target="_blank" rel="noopener noreferrer" className="btn-link pr-section-link">
PR <ExternalLink size={12} />
</a>
{((bug.rejection_count ?? 0) > 0) && (
<span style={{ fontSize: '13px', color: 'var(--warning)' }}>
<span className="pr-section-rejections">
{bug.rejection_count ?? 0}
</span>
)}
@ -361,29 +350,22 @@ export default function BugDetail() {
)}
{/* 操作按钮区 */}
<div className="actions-bar" style={{ display: 'flex', gap: '12px', flexWrap: 'wrap', alignItems: 'center' }}>
<div className="actions-bar">
{/* PR 操作按钮 */}
{canOperatePR && (
<>
<button
className="trigger-repair-btn"
className="trigger-repair-btn btn-success"
onClick={handleMergePR}
disabled={mergingPR}
style={{ background: 'var(--success)', borderColor: 'var(--success)' }}
>
{mergingPR ? (
<Loader2 size={14} className="spinner" />
) : (
<Check size={14} />
)}
{mergingPR ? <Loader2 size={14} className="spinner" /> : <Check size={14} />}
{mergingPR ? '合并中...' : '批准并合并'}
</button>
<button
className="trigger-repair-btn"
className="trigger-repair-btn btn-error"
onClick={() => setShowRejectModal(true)}
disabled={closingPR}
style={{ background: 'var(--error)', borderColor: 'var(--error)' }}
>
<X size={14} />
@ -395,24 +377,17 @@ export default function BugDetail() {
{canManualReview && (
<>
<button
className="trigger-repair-btn"
className="trigger-repair-btn btn-success"
onClick={handleApproveFix}
disabled={approving}
style={{ background: 'var(--success)', borderColor: 'var(--success)' }}
>
{approving ? (
<Loader2 size={14} className="spinner" />
) : (
<Check size={14} />
)}
{approving ? <Loader2 size={14} className="spinner" /> : <Check size={14} />}
{approving ? '确认中...' : '确认修复'}
</button>
<button
className="trigger-repair-btn"
className="trigger-repair-btn btn-error"
onClick={() => setShowRejectModal(true)}
disabled={rejecting}
style={{ background: 'var(--error)', borderColor: 'var(--error)' }}
>
<X size={14} />
@ -420,78 +395,54 @@ export default function BugDetail() {
</>
)}
{/* 原有的触发修复按钮 */}
{/* 触发修复按钮 */}
{!hasPR && !isPendingReview && (
<button
className="trigger-repair-btn"
onClick={handleTriggerRepair}
disabled={!canTriggerRepair || repairing}
>
{repairing ? (
<Loader2 size={14} className="spinner" />
) : (
<Play size={14} />
)}
{repairing ? <Loader2 size={14} className="spinner" /> : <Play size={14} />}
{repairing ? '触发中...' : '触发修复'}
</button>
)}
{/* 重新尝试按钮 - 仅在修复失败时显示 */}
{/* 重新尝试按钮 */}
{canRetry && (
<button
className="trigger-repair-btn"
className="trigger-repair-btn btn-warning"
onClick={handleRetry}
disabled={retrying}
style={{ background: 'var(--warning)', borderColor: 'var(--warning)' }}
>
{retrying ? (
<Loader2 size={14} className="spinner" />
) : (
<Play size={14} />
)}
{retrying ? <Loader2 size={14} className="spinner" /> : <Play size={14} />}
{retrying ? '重置中...' : '重新尝试'}
</button>
)}
{/* 消息显示 */}
{prMessage && (
<span style={{
fontSize: '13px',
color: prMessage.includes('✅') ? 'var(--success)' : 'var(--error)'
}}>
<span className={`action-message ${msgIsSuccess(prMessage) ? 'action-message--success' : 'action-message--error'}`}>
{prMessage}
</span>
)}
{repairMessage && (
<span style={{
fontSize: '13px',
color: repairMessage.includes('成功') ? 'var(--success)' : 'var(--error)'
}}>
<span className={`action-message ${msgIsSuccess(repairMessage) ? 'action-message--success' : 'action-message--error'}`}>
{repairMessage}
</span>
)}
{retryMessage && (
<span style={{
fontSize: '13px',
color: retryMessage.includes('✅') ? 'var(--success)' : 'var(--error)'
}}>
<span className={`action-message ${msgIsSuccess(retryMessage) ? 'action-message--success' : 'action-message--error'}`}>
{retryMessage}
</span>
)}
{reviewMessage && (
<span style={{
fontSize: '13px',
color: reviewMessage.includes('确认') || reviewMessage.includes('驳回') ? 'var(--success)' : 'var(--error)'
}}>
<span className={`action-message ${msgIsSuccess(reviewMessage) ? 'action-message--success' : 'action-message--error'}`}>
{reviewMessage}
</span>
)}
{!canTriggerRepair && !repairing && !hasPR && !canRetry && !canManualReview && (
<span style={{ fontSize: '13px', color: 'var(--text-tertiary)' }}>
<span className="action-hint">
{!isRuntime
? 'CI/CD 和部署错误暂不支持自动修复'
: '仅"新发现"或"修复失败"状态的缺陷可触发修复'}
@ -501,8 +452,8 @@ export default function BugDetail() {
</div>
{bug.failure_reason && (
<div className="detail-card" style={{ borderLeft: '3px solid var(--error)' }}>
<div className="detail-section-title" style={{ marginBottom: '8px', display: 'flex', alignItems: 'center', gap: '6px', color: 'var(--error)' }}>
<div className="detail-card detail-card--error">
<div className="alert-card-header alert-card-header--error">
<AlertTriangle size={14} />
</div>
@ -511,8 +462,8 @@ export default function BugDetail() {
)}
{bug.rejection_reason && (
<div className="detail-card" style={{ borderLeft: '3px solid var(--warning)' }}>
<div className="detail-section-title" style={{ marginBottom: '8px', display: 'flex', alignItems: 'center', gap: '6px', color: 'var(--warning)' }}>
<div className="detail-card detail-card--warning">
<div className="alert-card-header alert-card-header--warning">
<AlertTriangle size={14} />
</div>
@ -521,7 +472,7 @@ export default function BugDetail() {
)}
<div className="detail-card">
<div className="detail-section-title" style={{ marginBottom: '12px' }}></div>
<div className="detail-section-title"></div>
<table className="meta-table">
<tbody>
<tr>
@ -552,7 +503,7 @@ export default function BugDetail() {
{repairHistory.length > 0 && (
<div className="detail-card">
<div className="detail-section-title" style={{ marginBottom: '12px', display: 'flex', alignItems: 'center', gap: '6px' }}>
<div className="alert-card-header">
<History size={14} />
({repairHistory.length} )
</div>
@ -582,7 +533,7 @@ export default function BugDetail() {
{report.test_passed ? '通过' : '失败'}
</span>
</td>
<td style={{ maxWidth: '300px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
<td className="failure-cell-wide">
{report.failure_reason || '-'}
</td>
<td className="cell-secondary">
@ -603,48 +554,23 @@ export default function BugDetail() {
{/* 拒绝原因模态框 */}
{showRejectModal && (
<div style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
background: 'rgba(0,0,0,0.5)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 1000
}}>
<div style={{
background: 'var(--bg-primary)',
padding: '24px',
borderRadius: '8px',
maxWidth: '500px',
width: '90%',
boxShadow: '0 4px 12px rgba(0,0,0,0.15)'
}}>
<h3 style={{ marginBottom: '16px' }}>{hasPR ? '拒绝修复' : '驳回修复'}</h3>
<p style={{ marginBottom: '12px', fontSize: '14px', color: 'var(--text-secondary)' }}>
<div className="modal-overlay" onClick={() => { setShowRejectModal(false); setRejectReason(''); }}>
<div className="modal-content reject-modal-content" onClick={(e) => e.stopPropagation()}>
<h3 className="modal-title">{hasPR ? '拒绝修复' : '驳回修复'}</h3>
<p className="reject-modal-desc">
{hasPR ? '拒绝' : '驳回'}Agent
</p>
<div style={{ marginBottom: '12px' }}>
<label style={{ fontSize: '13px', color: 'var(--text-tertiary)', marginBottom: '6px', display: 'block' }}>
<div className="reject-templates">
<label className="reject-templates-label">
</label>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '6px' }}>
<div className="reject-templates-list">
{REJECT_REASON_TEMPLATES.map((template, idx) => (
<button
key={idx}
className="reject-template-btn"
onClick={() => setRejectReason(template)}
style={{
fontSize: '12px',
padding: '4px 8px',
background: 'var(--bg-secondary)',
border: '1px solid var(--border)',
borderRadius: '4px',
cursor: 'pointer'
}}
>
{template}
</button>
@ -653,43 +579,26 @@ export default function BugDetail() {
</div>
<textarea
className="reject-textarea"
value={rejectReason}
onChange={(e) => setRejectReason(e.target.value)}
placeholder="请输入详细的拒绝原因..."
style={{
width: '100%',
minHeight: '120px',
padding: '8px',
fontSize: '14px',
borderRadius: '4px',
border: '1px solid var(--border)',
background: 'var(--bg-secondary)',
color: 'var(--text-primary)',
resize: 'vertical'
}}
/>
<div style={{ display: 'flex', gap: '12px', marginTop: '16px', justifyContent: 'flex-end' }}>
<div className="modal-actions">
<button
className="btn btn-secondary"
onClick={() => {
setShowRejectModal(false);
setRejectReason('');
}}
style={{
padding: '8px 16px',
background: 'var(--bg-secondary)',
border: '1px solid var(--border)',
borderRadius: '4px',
cursor: 'pointer'
}}
>
</button>
<button
className="trigger-repair-btn btn-error"
onClick={hasPR ? handleClosePR : handleRejectFix}
disabled={(hasPR ? closingPR : rejecting) || !rejectReason.trim()}
className="trigger-repair-btn"
style={{ background: 'var(--error)', borderColor: 'var(--error)' }}
>
{(hasPR ? closingPR : rejecting) ? (
<Loader2 size={14} className="spinner" />

View File

@ -210,7 +210,7 @@ export default function BugList() {
{STATUS_LABELS[bug.status] || bug.status}
</span>
{bug.status === 'FIX_FAILED' && bug.failure_reason && (
<div style={{ fontSize: '11px', color: 'var(--error)', marginTop: '4px', maxWidth: '200px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
<div className="failure-hint">
{bug.failure_reason}
</div>
)}
@ -232,7 +232,6 @@ export default function BugList() {
to={`/bugs/${bug.id}`}
state={{ fromSearch: searchParams.toString() }}
className="mobile-card-item"
style={{ textDecoration: 'none', color: 'inherit' }}
>
<div className="mobile-card-top">
<span className="mobile-card-id">#{bug.id}</span>

View File

@ -81,6 +81,15 @@ function FixRateRing({ rate }: { rate: number }) {
return (
<div className="fix-rate-ring">
<svg viewBox="0 0 120 120" className="fix-rate-svg">
<defs>
<filter id="glow">
<feGaussianBlur stdDeviation="3" result="blur" />
<feMerge>
<feMergeNode in="blur" />
<feMergeNode in="SourceGraphic" />
</feMerge>
</filter>
</defs>
<circle
cx="60" cy="60" r={r}
fill="none"
@ -95,6 +104,7 @@ function FixRateRing({ rate }: { rate: number }) {
strokeDasharray={circumference}
strokeDashoffset={offset}
strokeLinecap="round"
filter="url(#glow)"
transform="rotate(-90 60 60)"
style={{ transition: 'stroke-dashoffset 0.6s ease' }}
/>

View File

@ -27,7 +27,6 @@ export default function RepairDetail() {
const [report, setReport] = useState<RepairReport | null>(null);
const [loading, setLoading] = useState(true);
// 审核操作状态
const [approving, setApproving] = useState(false);
const [rejecting, setRejecting] = useState(false);
const [actionMessage, setActionMessage] = useState('');
@ -102,6 +101,9 @@ export default function RepairDetail() {
const isPendingReview = report.status === 'PENDING_FIX';
const hasPR = !!report.pr_url;
const msgIsSuccess = (msg: string) =>
msg.includes('批准') || msg.includes('驳回');
return (
<div>
<Link to="/repairs" className="back-link">
@ -109,7 +111,7 @@ export default function RepairDetail() {
</Link>
<div style={{ marginBottom: '20px' }}>
<div className="report-header">
<div className="title-row">
<h1 className="page-title"> #{report.id}</h1>
<span className={`status-badge status-${report.status}`}>
@ -153,13 +155,13 @@ export default function RepairDetail() {
{/* PR 信息 */}
{hasPR && (
<div className="card" style={{ borderLeft: '3px solid var(--accent)' }}>
<div className="card card--accent">
<h2>Pull Request</h2>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', flexWrap: 'wrap' }}>
<span style={{ fontSize: '14px' }}>
<div className="pr-section-row">
<span className="pr-section-id">
PR #{report.pr_number} | {report.branch_name || 'fix branch'}
</span>
<a href={report.pr_url!} target="_blank" rel="noopener noreferrer" className="btn-link" style={{ display: 'inline-flex', alignItems: 'center', gap: '4px' }}>
<a href={report.pr_url!} target="_blank" rel="noopener noreferrer" className="btn-link pr-section-link">
PR <ExternalLink size={12} />
</a>
</div>
@ -168,43 +170,34 @@ export default function RepairDetail() {
{/* 审核操作区 */}
{isPendingReview && (
<div className="card" style={{ borderLeft: '3px solid var(--warning)' }}>
<div className="card card--warning">
<h2></h2>
<p style={{ fontSize: '14px', color: 'var(--text-secondary)', marginBottom: '12px' }}>
<p className="review-section-desc">
{hasPR
? '批准将合并 PR 并标记所有关联缺陷为已修复'
: '确认修复将标记所有关联缺陷为已修复'}
</p>
<div style={{ display: 'flex', gap: '12px', flexWrap: 'wrap', alignItems: 'center' }}>
<div className="review-actions">
<button
className="trigger-repair-btn"
className="trigger-repair-btn btn-success"
onClick={handleApprove}
disabled={approving}
style={{ background: 'var(--success)', borderColor: 'var(--success)' }}
>
{approving ? (
<Loader2 size={14} className="spinner" />
) : (
<Check size={14} />
)}
{approving ? <Loader2 size={14} className="spinner" /> : <Check size={14} />}
{approving ? '处理中...' : (hasPR ? '批准并合并' : '确认修复')}
</button>
<button
className="trigger-repair-btn"
className="trigger-repair-btn btn-error"
onClick={() => setShowRejectModal(true)}
disabled={rejecting}
style={{ background: 'var(--error)', borderColor: 'var(--error)' }}
>
<X size={14} />
{hasPR ? '拒绝修复' : '驳回修复'}
</button>
{actionMessage && (
<span style={{
fontSize: '13px',
color: actionMessage.includes('批准') || actionMessage.includes('驳回') ? 'var(--success)' : 'var(--error)'
}}>
<span className={`action-message ${msgIsSuccess(actionMessage) ? 'action-message--success' : 'action-message--error'}`}>
{actionMessage}
</span>
)}
@ -213,7 +206,7 @@ export default function RepairDetail() {
)}
{report.failure_reason && (
<div className="card" style={{ borderLeft: '3px solid var(--error)' }}>
<div className="card card--error">
<h2><AlertTriangle size={16} /> </h2>
<pre className="code-block error">{report.failure_reason}</pre>
</div>
@ -236,48 +229,23 @@ export default function RepairDetail() {
{/* 驳回原因模态框 */}
{showRejectModal && (
<div style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
background: 'rgba(0,0,0,0.5)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 1000
}}>
<div style={{
background: 'var(--bg-primary)',
padding: '24px',
borderRadius: '8px',
maxWidth: '500px',
width: '90%',
boxShadow: '0 4px 12px rgba(0,0,0,0.15)'
}}>
<h3 style={{ marginBottom: '16px' }}>{hasPR ? '拒绝修复' : '驳回修复'}</h3>
<p style={{ marginBottom: '12px', fontSize: '14px', color: 'var(--text-secondary)' }}>
<div className="modal-overlay" onClick={() => { setShowRejectModal(false); setRejectReason(''); }}>
<div className="modal-content reject-modal-content" onClick={(e) => e.stopPropagation()}>
<h3 className="modal-title">{hasPR ? '拒绝修复' : '驳回修复'}</h3>
<p className="reject-modal-desc">
Agent
</p>
<div style={{ marginBottom: '12px' }}>
<label style={{ fontSize: '13px', color: 'var(--text-tertiary)', marginBottom: '6px', display: 'block' }}>
<div className="reject-templates">
<label className="reject-templates-label">
</label>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '6px' }}>
<div className="reject-templates-list">
{REJECT_REASON_TEMPLATES.map((template, idx) => (
<button
key={idx}
className="reject-template-btn"
onClick={() => setRejectReason(template)}
style={{
fontSize: '12px',
padding: '4px 8px',
background: 'var(--bg-secondary)',
border: '1px solid var(--border)',
borderRadius: '4px',
cursor: 'pointer'
}}
>
{template}
</button>
@ -286,49 +254,28 @@ export default function RepairDetail() {
</div>
<textarea
className="reject-textarea"
value={rejectReason}
onChange={(e) => setRejectReason(e.target.value)}
placeholder="请输入详细原因..."
style={{
width: '100%',
minHeight: '120px',
padding: '8px',
fontSize: '14px',
borderRadius: '4px',
border: '1px solid var(--border)',
background: 'var(--bg-secondary)',
color: 'var(--text-primary)',
resize: 'vertical'
}}
/>
<div style={{ display: 'flex', gap: '12px', marginTop: '16px', justifyContent: 'flex-end' }}>
<div className="modal-actions">
<button
className="btn btn-secondary"
onClick={() => {
setShowRejectModal(false);
setRejectReason('');
}}
style={{
padding: '8px 16px',
background: 'var(--bg-secondary)',
border: '1px solid var(--border)',
borderRadius: '4px',
cursor: 'pointer'
}}
>
</button>
<button
className="trigger-repair-btn btn-error"
onClick={handleReject}
disabled={rejecting || !rejectReason.trim()}
className="trigger-repair-btn"
style={{ background: 'var(--error)', borderColor: 'var(--error)' }}
>
{rejecting ? (
<Loader2 size={14} className="spinner" />
) : (
<X size={14} />
)}
{rejecting ? <Loader2 size={14} className="spinner" /> : <X size={14} />}
{rejecting ? '提交中...' : '确认驳回'}
</button>
</div>

View File

@ -135,7 +135,7 @@ export default function RepairList() {
{STATUS_LABELS[report.status] || report.status}
</span>
</td>
<td style={{ maxWidth: '200px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', fontSize: '12px', color: 'var(--error)' }}>
<td className="failure-cell">
{report.failure_reason || '-'}
</td>
<td className="cell-secondary">
@ -159,7 +159,6 @@ export default function RepairList() {
key={report.id}
to={`/repairs/${report.id}`}
className="mobile-card-item"
style={{ textDecoration: 'none', color: 'inherit' }}
>
<div className="mobile-card-top">
<span className="mobile-card-id">#{report.id}</span>