build UI
All checks were successful
Build and Deploy Log Center / build-and-deploy (push) Successful in 1m37s
All checks were successful
Build and Deploy Log Center / build-and-deploy (push) Successful in 1m37s
This commit is contained in:
parent
1da10e8136
commit
252876f81a
@ -6,7 +6,7 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<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.googleapis.com" />
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
<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>
|
<title>日志中台 - Log Center</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@ -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 {
|
:root {
|
||||||
--bg-primary: #09090b;
|
--bg-primary: #020617;
|
||||||
--bg-secondary: #0f0f11;
|
--bg-secondary: #0F172A;
|
||||||
--bg-card: #18181b;
|
--bg-card: #1E293B;
|
||||||
--bg-surface: #1f1f23;
|
--bg-surface: #1E293B;
|
||||||
--bg-hover: #27272a;
|
--bg-hover: #334155;
|
||||||
--accent: #3b82f6;
|
--accent: #3b82f6;
|
||||||
--accent-hover: #2563eb;
|
--accent-hover: #2563eb;
|
||||||
--accent-muted: rgba(59, 130, 246, 0.12);
|
--accent-muted: rgba(59, 130, 246, 0.12);
|
||||||
--indigo: #6366f1;
|
--indigo: #6366f1;
|
||||||
--text-primary: #fafafa;
|
--text-primary: #F8FAFC;
|
||||||
--text-secondary: #a1a1aa;
|
--text-secondary: #94A3B8;
|
||||||
--text-tertiary: #71717a;
|
--text-tertiary: #64748B;
|
||||||
--success: #22c55e;
|
--success: #22c55e;
|
||||||
--success-muted: rgba(34, 197, 94, 0.12);
|
--success-muted: rgba(34, 197, 94, 0.12);
|
||||||
--warning: #f59e0b;
|
--warning: #f59e0b;
|
||||||
--warning-muted: rgba(245, 158, 11, 0.12);
|
--warning-muted: rgba(245, 158, 11, 0.12);
|
||||||
--error: #ef4444;
|
--error: #ef4444;
|
||||||
--error-muted: rgba(239, 68, 68, 0.12);
|
--error-muted: rgba(239, 68, 68, 0.12);
|
||||||
--border: #27272a;
|
--border: rgba(148, 163, 184, 0.1);
|
||||||
--border-subtle: #1f1f23;
|
--border-subtle: rgba(148, 163, 184, 0.06);
|
||||||
--radius-sm: 6px;
|
--radius-sm: 6px;
|
||||||
--radius-md: 8px;
|
--radius-md: 8px;
|
||||||
--radius-lg: 12px;
|
--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 {
|
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);
|
background: var(--bg-primary);
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
@ -128,6 +133,7 @@ a:hover {
|
|||||||
.nav-link.active {
|
.nav-link.active {
|
||||||
background: var(--accent-muted);
|
background: var(--accent-muted);
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
|
box-shadow: var(--glow-accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-link svg {
|
.nav-link svg {
|
||||||
@ -185,7 +191,8 @@ a:hover {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.stat-card: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 {
|
.stat-label {
|
||||||
@ -203,10 +210,10 @@ a:hover {
|
|||||||
letter-spacing: -0.02em;
|
letter-spacing: -0.02em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-value.accent { color: var(--accent); }
|
.stat-value.accent { color: var(--accent); text-shadow: 0 0 10px rgba(59, 130, 246, 0.3); }
|
||||||
.stat-value.success { color: var(--success); }
|
.stat-value.success { color: var(--success); text-shadow: 0 0 10px rgba(34, 197, 94, 0.3); }
|
||||||
.stat-value.warning { color: var(--warning); }
|
.stat-value.warning { color: var(--warning); text-shadow: 0 0 10px rgba(245, 158, 11, 0.3); }
|
||||||
.stat-value.error { color: var(--error); }
|
.stat-value.error { color: var(--error); text-shadow: 0 0 10px rgba(239, 68, 68, 0.3); }
|
||||||
|
|
||||||
.stat-icon {
|
.stat-icon {
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -288,7 +295,7 @@ td a:hover {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.cell-mono {
|
.cell-mono {
|
||||||
font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;
|
font-family: 'Fira Code', 'SF Mono', 'Cascadia Code', monospace;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
}
|
}
|
||||||
@ -464,7 +471,7 @@ td a:hover {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.status-tab:hover {
|
.status-tab:hover {
|
||||||
border-color: #3f3f46;
|
border-color: rgba(148, 163, 184, 0.2);
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -642,7 +649,7 @@ td a:hover {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.detail-section-value {
|
.detail-section-value {
|
||||||
font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;
|
font-family: 'Fira Code', 'SF Mono', 'Cascadia Code', monospace;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
}
|
}
|
||||||
@ -654,7 +661,7 @@ td a:hover {
|
|||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius-md);
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;
|
font-family: 'Fira Code', 'SF Mono', 'Cascadia Code', monospace;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
@ -680,7 +687,7 @@ td a:hover {
|
|||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius-md);
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;
|
font-family: 'Fira Code', 'SF Mono', 'Cascadia Code', monospace;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
@ -750,7 +757,7 @@ td a:hover {
|
|||||||
|
|
||||||
.btn-secondary:hover:not(:disabled) {
|
.btn-secondary:hover:not(:disabled) {
|
||||||
background: var(--bg-hover);
|
background: var(--bg-hover);
|
||||||
border-color: #3f3f46;
|
border-color: rgba(148, 163, 184, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-secondary:disabled {
|
.btn-secondary:disabled {
|
||||||
@ -923,7 +930,7 @@ td a:hover {
|
|||||||
|
|
||||||
.pagination button:hover:not(:disabled) {
|
.pagination button:hover:not(:disabled) {
|
||||||
background: var(--bg-hover);
|
background: var(--bg-hover);
|
||||||
border-color: #3f3f46;
|
border-color: rgba(148, 163, 184, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.pagination button:disabled {
|
.pagination button:disabled {
|
||||||
@ -1285,6 +1292,8 @@ td a:hover {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mobile-card-item:last-child {
|
.mobile-card-item:last-child {
|
||||||
@ -1326,7 +1335,7 @@ td a:hover {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.mobile-card-file {
|
.mobile-card-file {
|
||||||
font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;
|
font-family: 'Fira Code', 'SF Mono', 'Cascadia Code', monospace;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
@ -1671,7 +1680,7 @@ td a:hover {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.project-card:hover {
|
.project-card:hover {
|
||||||
border-color: #3f3f46;
|
border-color: rgba(148, 163, 184, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.project-card-disabled {
|
.project-card-disabled {
|
||||||
@ -1809,6 +1818,7 @@ td a:hover {
|
|||||||
|
|
||||||
.repair-toggle-on .repair-toggle-track {
|
.repair-toggle-on .repair-toggle-track {
|
||||||
background: var(--success);
|
background: var(--success);
|
||||||
|
box-shadow: var(--glow-success);
|
||||||
}
|
}
|
||||||
|
|
||||||
.repair-toggle-off .repair-toggle-track {
|
.repair-toggle-off .repair-toggle-track {
|
||||||
@ -2000,3 +2010,335 @@ td a:hover {
|
|||||||
grid-template-columns: 1fr;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -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() {
|
export default function BugDetail() {
|
||||||
const { id } = useParams<{ id: string }>();
|
const { id } = useParams<{ id: string }>();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
@ -37,19 +43,16 @@ export default function BugDetail() {
|
|||||||
const [repairMessage, setRepairMessage] = useState('');
|
const [repairMessage, setRepairMessage] = useState('');
|
||||||
const [repairHistory, setRepairHistory] = useState<RepairReport[]>([]);
|
const [repairHistory, setRepairHistory] = useState<RepairReport[]>([]);
|
||||||
|
|
||||||
// PR 操作相关状态
|
|
||||||
const [mergingPR, setMergingPR] = useState(false);
|
const [mergingPR, setMergingPR] = useState(false);
|
||||||
const [closingPR, setClosingPR] = useState(false);
|
const [closingPR, setClosingPR] = useState(false);
|
||||||
const [prMessage, setPRMessage] = useState('');
|
const [prMessage, setPRMessage] = useState('');
|
||||||
const [showRejectModal, setShowRejectModal] = useState(false);
|
const [showRejectModal, setShowRejectModal] = useState(false);
|
||||||
const [rejectReason, setRejectReason] = useState('');
|
const [rejectReason, setRejectReason] = useState('');
|
||||||
|
|
||||||
// 人工审核操作状态(无 PR 时使用)
|
|
||||||
const [approving, setApproving] = useState(false);
|
const [approving, setApproving] = useState(false);
|
||||||
const [rejecting, setRejecting] = useState(false);
|
const [rejecting, setRejecting] = useState(false);
|
||||||
const [reviewMessage, setReviewMessage] = useState('');
|
const [reviewMessage, setReviewMessage] = useState('');
|
||||||
|
|
||||||
// 重试操作状态
|
|
||||||
const [retrying, setRetrying] = useState(false);
|
const [retrying, setRetrying] = useState(false);
|
||||||
const [retryMessage, setRetryMessage] = useState('');
|
const [retryMessage, setRetryMessage] = useState('');
|
||||||
|
|
||||||
@ -101,10 +104,10 @@ export default function BugDetail() {
|
|||||||
try {
|
try {
|
||||||
await mergePR(bug.id);
|
await mergePR(bug.id);
|
||||||
setBug({ ...bug, status: 'FIXED', merged_at: new Date().toISOString() });
|
setBug({ ...bug, status: 'FIXED', merged_at: new Date().toISOString() });
|
||||||
setPRMessage('✅ PR 已成功合并');
|
setPRMessage('PR 已成功合并');
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('Failed to merge PR:', error);
|
console.error('Failed to merge PR:', error);
|
||||||
setPRMessage(`❌ 合并失败: ${error.response?.data?.detail || error.message}`);
|
setPRMessage(`合并失败: ${error.response?.data?.detail || error.message}`);
|
||||||
} finally {
|
} finally {
|
||||||
setMergingPR(false);
|
setMergingPR(false);
|
||||||
}
|
}
|
||||||
@ -112,7 +115,7 @@ export default function BugDetail() {
|
|||||||
|
|
||||||
const handleClosePR = async () => {
|
const handleClosePR = async () => {
|
||||||
if (!bug || !rejectReason.trim()) {
|
if (!bug || !rejectReason.trim()) {
|
||||||
setPRMessage('❌ 请输入拒绝原因');
|
setPRMessage('请输入拒绝原因');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setClosingPR(true);
|
setClosingPR(true);
|
||||||
@ -125,12 +128,12 @@ export default function BugDetail() {
|
|||||||
rejection_count: (bug.rejection_count || 0) + 1,
|
rejection_count: (bug.rejection_count || 0) + 1,
|
||||||
last_rejected_at: new Date().toISOString()
|
last_rejected_at: new Date().toISOString()
|
||||||
});
|
});
|
||||||
setPRMessage('✅ PR 已拒绝,Bug 将重新修复');
|
setPRMessage('PR 已拒绝,Bug 将重新修复');
|
||||||
setShowRejectModal(false);
|
setShowRejectModal(false);
|
||||||
setRejectReason('');
|
setRejectReason('');
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('Failed to close PR:', error);
|
console.error('Failed to close PR:', error);
|
||||||
setPRMessage(`❌ 关闭失败: ${error.response?.data?.detail || error.message}`);
|
setPRMessage(`关闭失败: ${error.response?.data?.detail || error.message}`);
|
||||||
} finally {
|
} finally {
|
||||||
setClosingPR(false);
|
setClosingPR(false);
|
||||||
}
|
}
|
||||||
@ -143,10 +146,10 @@ export default function BugDetail() {
|
|||||||
try {
|
try {
|
||||||
await retryFix(bug.id);
|
await retryFix(bug.id);
|
||||||
setBug({ ...bug, status: 'NEW', failure_reason: null });
|
setBug({ ...bug, status: 'NEW', failure_reason: null });
|
||||||
setRetryMessage('✅ Bug 已重置,repair agent 将重新扫描');
|
setRetryMessage('Bug 已重置,repair agent 将重新扫描');
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('Failed to retry:', error);
|
console.error('Failed to retry:', error);
|
||||||
setRetryMessage(`❌ 重试失败: ${error.response?.data?.detail || error.message}`);
|
setRetryMessage(`重试失败: ${error.response?.data?.detail || error.message}`);
|
||||||
} finally {
|
} finally {
|
||||||
setRetrying(false);
|
setRetrying(false);
|
||||||
}
|
}
|
||||||
@ -212,10 +215,12 @@ export default function BugDetail() {
|
|||||||
const isPendingReview = bug.status === 'PENDING_FIX';
|
const isPendingReview = bug.status === 'PENDING_FIX';
|
||||||
const canOperatePR = hasPR && isPendingReview;
|
const canOperatePR = hasPR && isPendingReview;
|
||||||
const canManualReview = !hasPR && isPendingReview;
|
const canManualReview = !hasPR && isPendingReview;
|
||||||
// PR 信息只在 PENDING_FIX 或 FIXED 状态时显示
|
|
||||||
const shouldShowPR = hasPR && (isPendingReview || bug.status === 'FIXED');
|
const shouldShowPR = hasPR && (isPendingReview || bug.status === 'FIXED');
|
||||||
const canRetry = bug.status === 'FIX_FAILED';
|
const canRetry = bug.status === 'FIX_FAILED';
|
||||||
|
|
||||||
|
const msgIsSuccess = (msg: string) =>
|
||||||
|
msg.includes('成功') || msg.includes('确认') || msg.includes('驳回') || msg.includes('已重置') || msg.includes('已拒绝');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Link to={`/bugs${backSearch ? `?${backSearch}` : ''}`} className="back-link">
|
<Link to={`/bugs${backSearch ? `?${backSearch}` : ''}`} className="back-link">
|
||||||
@ -226,7 +231,7 @@ export default function BugDetail() {
|
|||||||
<div className="detail-card">
|
<div className="detail-card">
|
||||||
<div className="detail-header">
|
<div className="detail-header">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="detail-title" style={{ color: 'var(--error)' }}>
|
<h2 className="detail-title detail-title--error">
|
||||||
{bug.error_type}: {bug.error_message}
|
{bug.error_type}: {bug.error_message}
|
||||||
</h2>
|
</h2>
|
||||||
<div className="detail-meta">
|
<div className="detail-meta">
|
||||||
@ -236,19 +241,9 @@ export default function BugDetail() {
|
|||||||
<span>级别:{bug.level}</span>
|
<span>级别:{bug.level}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
<div className="detail-header-badges">
|
||||||
{bug.severity != null && bug.status !== 'NEW' && (
|
{bug.severity != null && bug.status !== 'NEW' && (
|
||||||
<span style={{
|
<span className={`severity-badge severity-badge--${severityLevel(bug.severity)}`}>
|
||||||
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)',
|
|
||||||
}}>
|
|
||||||
等级 {bug.severity}/10
|
等级 {bug.severity}/10
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@ -260,35 +255,29 @@ export default function BugDetail() {
|
|||||||
|
|
||||||
{/* 严重等级说明 */}
|
{/* 严重等级说明 */}
|
||||||
{bug.severity != null && bug.severity_reason && bug.status !== 'NEW' && (
|
{bug.severity != null && bug.severity_reason && bug.status !== 'NEW' && (
|
||||||
<div className="detail-section" style={{
|
<div className={`severity-section severity-section--${bug.severity >= 8 ? 'critical' : severityLevel(bug.severity)}`}>
|
||||||
background: bug.severity >= 8 ? 'rgba(239,68,68,0.08)' : 'var(--bg-secondary)',
|
<div className="detail-section-title">
|
||||||
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' }}>
|
|
||||||
严重等级:{bug.severity}/10 {bug.severity >= 8 ? '(需人工审核)' : ''}
|
严重等级:{bug.severity}/10 {bug.severity >= 8 ? '(需人工审核)' : ''}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ fontSize: '13px', color: 'var(--text-secondary)' }}>
|
<div className="severity-section-desc">
|
||||||
{bug.severity_reason}
|
{bug.severity_reason}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* PR 信息显示 - 仅在 PENDING_FIX 或 FIXED 状态时显示 */}
|
{/* PR 信息显示 */}
|
||||||
{shouldShowPR && (
|
{shouldShowPR && (
|
||||||
<div className="detail-section" style={{ background: 'var(--bg-secondary)', padding: '12px', borderRadius: '6px', marginTop: '16px' }}>
|
<div className="detail-section pr-section">
|
||||||
<div className="detail-section-title" style={{ marginBottom: '8px' }}>Pull Request</div>
|
<div className="detail-section-title">Pull Request</div>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', flexWrap: 'wrap' }}>
|
<div className="pr-section-row">
|
||||||
<span style={{ fontSize: '14px' }}>
|
<span className="pr-section-id">
|
||||||
PR #{bug.pr_number} | {bug.branch_name || 'fix branch'}
|
PR #{bug.pr_number} | {bug.branch_name || 'fix branch'}
|
||||||
</span>
|
</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} />
|
查看 PR <ExternalLink size={12} />
|
||||||
</a>
|
</a>
|
||||||
{((bug.rejection_count ?? 0) > 0) && (
|
{((bug.rejection_count ?? 0) > 0) && (
|
||||||
<span style={{ fontSize: '13px', color: 'var(--warning)' }}>
|
<span className="pr-section-rejections">
|
||||||
已拒绝 {bug.rejection_count ?? 0} 次
|
已拒绝 {bug.rejection_count ?? 0} 次
|
||||||
</span>
|
</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 操作按钮 */}
|
{/* PR 操作按钮 */}
|
||||||
{canOperatePR && (
|
{canOperatePR && (
|
||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
className="trigger-repair-btn"
|
className="trigger-repair-btn btn-success"
|
||||||
onClick={handleMergePR}
|
onClick={handleMergePR}
|
||||||
disabled={mergingPR}
|
disabled={mergingPR}
|
||||||
style={{ background: 'var(--success)', borderColor: 'var(--success)' }}
|
|
||||||
>
|
>
|
||||||
{mergingPR ? (
|
{mergingPR ? <Loader2 size={14} className="spinner" /> : <Check size={14} />}
|
||||||
<Loader2 size={14} className="spinner" />
|
|
||||||
) : (
|
|
||||||
<Check size={14} />
|
|
||||||
)}
|
|
||||||
{mergingPR ? '合并中...' : '批准并合并'}
|
{mergingPR ? '合并中...' : '批准并合并'}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
className="trigger-repair-btn"
|
className="trigger-repair-btn btn-error"
|
||||||
onClick={() => setShowRejectModal(true)}
|
onClick={() => setShowRejectModal(true)}
|
||||||
disabled={closingPR}
|
disabled={closingPR}
|
||||||
style={{ background: 'var(--error)', borderColor: 'var(--error)' }}
|
|
||||||
>
|
>
|
||||||
<X size={14} />
|
<X size={14} />
|
||||||
拒绝修复
|
拒绝修复
|
||||||
@ -395,24 +377,17 @@ export default function BugDetail() {
|
|||||||
{canManualReview && (
|
{canManualReview && (
|
||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
className="trigger-repair-btn"
|
className="trigger-repair-btn btn-success"
|
||||||
onClick={handleApproveFix}
|
onClick={handleApproveFix}
|
||||||
disabled={approving}
|
disabled={approving}
|
||||||
style={{ background: 'var(--success)', borderColor: 'var(--success)' }}
|
|
||||||
>
|
>
|
||||||
{approving ? (
|
{approving ? <Loader2 size={14} className="spinner" /> : <Check size={14} />}
|
||||||
<Loader2 size={14} className="spinner" />
|
|
||||||
) : (
|
|
||||||
<Check size={14} />
|
|
||||||
)}
|
|
||||||
{approving ? '确认中...' : '确认修复'}
|
{approving ? '确认中...' : '确认修复'}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
className="trigger-repair-btn"
|
className="trigger-repair-btn btn-error"
|
||||||
onClick={() => setShowRejectModal(true)}
|
onClick={() => setShowRejectModal(true)}
|
||||||
disabled={rejecting}
|
disabled={rejecting}
|
||||||
style={{ background: 'var(--error)', borderColor: 'var(--error)' }}
|
|
||||||
>
|
>
|
||||||
<X size={14} />
|
<X size={14} />
|
||||||
驳回修复
|
驳回修复
|
||||||
@ -420,78 +395,54 @@ export default function BugDetail() {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 原有的触发修复按钮 */}
|
{/* 触发修复按钮 */}
|
||||||
{!hasPR && !isPendingReview && (
|
{!hasPR && !isPendingReview && (
|
||||||
<button
|
<button
|
||||||
className="trigger-repair-btn"
|
className="trigger-repair-btn"
|
||||||
onClick={handleTriggerRepair}
|
onClick={handleTriggerRepair}
|
||||||
disabled={!canTriggerRepair || repairing}
|
disabled={!canTriggerRepair || repairing}
|
||||||
>
|
>
|
||||||
{repairing ? (
|
{repairing ? <Loader2 size={14} className="spinner" /> : <Play size={14} />}
|
||||||
<Loader2 size={14} className="spinner" />
|
|
||||||
) : (
|
|
||||||
<Play size={14} />
|
|
||||||
)}
|
|
||||||
{repairing ? '触发中...' : '触发修复'}
|
{repairing ? '触发中...' : '触发修复'}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 重新尝试按钮 - 仅在修复失败时显示 */}
|
{/* 重新尝试按钮 */}
|
||||||
{canRetry && (
|
{canRetry && (
|
||||||
<button
|
<button
|
||||||
className="trigger-repair-btn"
|
className="trigger-repair-btn btn-warning"
|
||||||
onClick={handleRetry}
|
onClick={handleRetry}
|
||||||
disabled={retrying}
|
disabled={retrying}
|
||||||
style={{ background: 'var(--warning)', borderColor: 'var(--warning)' }}
|
|
||||||
>
|
>
|
||||||
{retrying ? (
|
{retrying ? <Loader2 size={14} className="spinner" /> : <Play size={14} />}
|
||||||
<Loader2 size={14} className="spinner" />
|
|
||||||
) : (
|
|
||||||
<Play size={14} />
|
|
||||||
)}
|
|
||||||
{retrying ? '重置中...' : '重新尝试'}
|
{retrying ? '重置中...' : '重新尝试'}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 消息显示 */}
|
{/* 消息显示 */}
|
||||||
{prMessage && (
|
{prMessage && (
|
||||||
<span style={{
|
<span className={`action-message ${msgIsSuccess(prMessage) ? 'action-message--success' : 'action-message--error'}`}>
|
||||||
fontSize: '13px',
|
|
||||||
color: prMessage.includes('✅') ? 'var(--success)' : 'var(--error)'
|
|
||||||
}}>
|
|
||||||
{prMessage}
|
{prMessage}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{repairMessage && (
|
{repairMessage && (
|
||||||
<span style={{
|
<span className={`action-message ${msgIsSuccess(repairMessage) ? 'action-message--success' : 'action-message--error'}`}>
|
||||||
fontSize: '13px',
|
|
||||||
color: repairMessage.includes('成功') ? 'var(--success)' : 'var(--error)'
|
|
||||||
}}>
|
|
||||||
{repairMessage}
|
{repairMessage}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{retryMessage && (
|
{retryMessage && (
|
||||||
<span style={{
|
<span className={`action-message ${msgIsSuccess(retryMessage) ? 'action-message--success' : 'action-message--error'}`}>
|
||||||
fontSize: '13px',
|
|
||||||
color: retryMessage.includes('✅') ? 'var(--success)' : 'var(--error)'
|
|
||||||
}}>
|
|
||||||
{retryMessage}
|
{retryMessage}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{reviewMessage && (
|
{reviewMessage && (
|
||||||
<span style={{
|
<span className={`action-message ${msgIsSuccess(reviewMessage) ? 'action-message--success' : 'action-message--error'}`}>
|
||||||
fontSize: '13px',
|
|
||||||
color: reviewMessage.includes('确认') || reviewMessage.includes('驳回') ? 'var(--success)' : 'var(--error)'
|
|
||||||
}}>
|
|
||||||
{reviewMessage}
|
{reviewMessage}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!canTriggerRepair && !repairing && !hasPR && !canRetry && !canManualReview && (
|
{!canTriggerRepair && !repairing && !hasPR && !canRetry && !canManualReview && (
|
||||||
<span style={{ fontSize: '13px', color: 'var(--text-tertiary)' }}>
|
<span className="action-hint">
|
||||||
{!isRuntime
|
{!isRuntime
|
||||||
? 'CI/CD 和部署错误暂不支持自动修复'
|
? 'CI/CD 和部署错误暂不支持自动修复'
|
||||||
: '仅"新发现"或"修复失败"状态的缺陷可触发修复'}
|
: '仅"新发现"或"修复失败"状态的缺陷可触发修复'}
|
||||||
@ -501,8 +452,8 @@ export default function BugDetail() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{bug.failure_reason && (
|
{bug.failure_reason && (
|
||||||
<div className="detail-card" style={{ borderLeft: '3px solid var(--error)' }}>
|
<div className="detail-card detail-card--error">
|
||||||
<div className="detail-section-title" style={{ marginBottom: '8px', display: 'flex', alignItems: 'center', gap: '6px', color: 'var(--error)' }}>
|
<div className="alert-card-header alert-card-header--error">
|
||||||
<AlertTriangle size={14} />
|
<AlertTriangle size={14} />
|
||||||
修复失败原因
|
修复失败原因
|
||||||
</div>
|
</div>
|
||||||
@ -511,8 +462,8 @@ export default function BugDetail() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{bug.rejection_reason && (
|
{bug.rejection_reason && (
|
||||||
<div className="detail-card" style={{ borderLeft: '3px solid var(--warning)' }}>
|
<div className="detail-card detail-card--warning">
|
||||||
<div className="detail-section-title" style={{ marginBottom: '8px', display: 'flex', alignItems: 'center', gap: '6px', color: 'var(--warning)' }}>
|
<div className="alert-card-header alert-card-header--warning">
|
||||||
<AlertTriangle size={14} />
|
<AlertTriangle size={14} />
|
||||||
上次拒绝原因
|
上次拒绝原因
|
||||||
</div>
|
</div>
|
||||||
@ -521,7 +472,7 @@ export default function BugDetail() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="detail-card">
|
<div className="detail-card">
|
||||||
<div className="detail-section-title" style={{ marginBottom: '12px' }}>元数据</div>
|
<div className="detail-section-title">元数据</div>
|
||||||
<table className="meta-table">
|
<table className="meta-table">
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
@ -552,7 +503,7 @@ export default function BugDetail() {
|
|||||||
|
|
||||||
{repairHistory.length > 0 && (
|
{repairHistory.length > 0 && (
|
||||||
<div className="detail-card">
|
<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} />
|
<History size={14} />
|
||||||
修复历史 ({repairHistory.length} 次尝试)
|
修复历史 ({repairHistory.length} 次尝试)
|
||||||
</div>
|
</div>
|
||||||
@ -582,7 +533,7 @@ export default function BugDetail() {
|
|||||||
{report.test_passed ? '通过' : '失败'}
|
{report.test_passed ? '通过' : '失败'}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td style={{ maxWidth: '300px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
<td className="failure-cell-wide">
|
||||||
{report.failure_reason || '-'}
|
{report.failure_reason || '-'}
|
||||||
</td>
|
</td>
|
||||||
<td className="cell-secondary">
|
<td className="cell-secondary">
|
||||||
@ -603,48 +554,23 @@ export default function BugDetail() {
|
|||||||
|
|
||||||
{/* 拒绝原因模态框 */}
|
{/* 拒绝原因模态框 */}
|
||||||
{showRejectModal && (
|
{showRejectModal && (
|
||||||
<div style={{
|
<div className="modal-overlay" onClick={() => { setShowRejectModal(false); setRejectReason(''); }}>
|
||||||
position: 'fixed',
|
<div className="modal-content reject-modal-content" onClick={(e) => e.stopPropagation()}>
|
||||||
top: 0,
|
<h3 className="modal-title">{hasPR ? '拒绝修复' : '驳回修复'}</h3>
|
||||||
left: 0,
|
<p className="reject-modal-desc">
|
||||||
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)' }}>
|
|
||||||
请说明{hasPR ? '拒绝' : '驳回'}原因,Agent 将根据您的反馈重新修复:
|
请说明{hasPR ? '拒绝' : '驳回'}原因,Agent 将根据您的反馈重新修复:
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div style={{ marginBottom: '12px' }}>
|
<div className="reject-templates">
|
||||||
<label style={{ fontSize: '13px', color: 'var(--text-tertiary)', marginBottom: '6px', display: 'block' }}>
|
<label className="reject-templates-label">
|
||||||
常用模板(点击填充):
|
常用模板(点击填充):
|
||||||
</label>
|
</label>
|
||||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '6px' }}>
|
<div className="reject-templates-list">
|
||||||
{REJECT_REASON_TEMPLATES.map((template, idx) => (
|
{REJECT_REASON_TEMPLATES.map((template, idx) => (
|
||||||
<button
|
<button
|
||||||
key={idx}
|
key={idx}
|
||||||
|
className="reject-template-btn"
|
||||||
onClick={() => setRejectReason(template)}
|
onClick={() => setRejectReason(template)}
|
||||||
style={{
|
|
||||||
fontSize: '12px',
|
|
||||||
padding: '4px 8px',
|
|
||||||
background: 'var(--bg-secondary)',
|
|
||||||
border: '1px solid var(--border)',
|
|
||||||
borderRadius: '4px',
|
|
||||||
cursor: 'pointer'
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{template}
|
{template}
|
||||||
</button>
|
</button>
|
||||||
@ -653,43 +579,26 @@ export default function BugDetail() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<textarea
|
<textarea
|
||||||
|
className="reject-textarea"
|
||||||
value={rejectReason}
|
value={rejectReason}
|
||||||
onChange={(e) => setRejectReason(e.target.value)}
|
onChange={(e) => setRejectReason(e.target.value)}
|
||||||
placeholder="请输入详细的拒绝原因..."
|
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
|
<button
|
||||||
|
className="btn btn-secondary"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setShowRejectModal(false);
|
setShowRejectModal(false);
|
||||||
setRejectReason('');
|
setRejectReason('');
|
||||||
}}
|
}}
|
||||||
style={{
|
|
||||||
padding: '8px 16px',
|
|
||||||
background: 'var(--bg-secondary)',
|
|
||||||
border: '1px solid var(--border)',
|
|
||||||
borderRadius: '4px',
|
|
||||||
cursor: 'pointer'
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
取消
|
取消
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
|
className="trigger-repair-btn btn-error"
|
||||||
onClick={hasPR ? handleClosePR : handleRejectFix}
|
onClick={hasPR ? handleClosePR : handleRejectFix}
|
||||||
disabled={(hasPR ? closingPR : rejecting) || !rejectReason.trim()}
|
disabled={(hasPR ? closingPR : rejecting) || !rejectReason.trim()}
|
||||||
className="trigger-repair-btn"
|
|
||||||
style={{ background: 'var(--error)', borderColor: 'var(--error)' }}
|
|
||||||
>
|
>
|
||||||
{(hasPR ? closingPR : rejecting) ? (
|
{(hasPR ? closingPR : rejecting) ? (
|
||||||
<Loader2 size={14} className="spinner" />
|
<Loader2 size={14} className="spinner" />
|
||||||
|
|||||||
@ -210,7 +210,7 @@ export default function BugList() {
|
|||||||
{STATUS_LABELS[bug.status] || bug.status}
|
{STATUS_LABELS[bug.status] || bug.status}
|
||||||
</span>
|
</span>
|
||||||
{bug.status === 'FIX_FAILED' && bug.failure_reason && (
|
{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}
|
{bug.failure_reason}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -232,7 +232,6 @@ export default function BugList() {
|
|||||||
to={`/bugs/${bug.id}`}
|
to={`/bugs/${bug.id}`}
|
||||||
state={{ fromSearch: searchParams.toString() }}
|
state={{ fromSearch: searchParams.toString() }}
|
||||||
className="mobile-card-item"
|
className="mobile-card-item"
|
||||||
style={{ textDecoration: 'none', color: 'inherit' }}
|
|
||||||
>
|
>
|
||||||
<div className="mobile-card-top">
|
<div className="mobile-card-top">
|
||||||
<span className="mobile-card-id">#{bug.id}</span>
|
<span className="mobile-card-id">#{bug.id}</span>
|
||||||
|
|||||||
@ -81,6 +81,15 @@ function FixRateRing({ rate }: { rate: number }) {
|
|||||||
return (
|
return (
|
||||||
<div className="fix-rate-ring">
|
<div className="fix-rate-ring">
|
||||||
<svg viewBox="0 0 120 120" className="fix-rate-svg">
|
<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
|
<circle
|
||||||
cx="60" cy="60" r={r}
|
cx="60" cy="60" r={r}
|
||||||
fill="none"
|
fill="none"
|
||||||
@ -95,6 +104,7 @@ function FixRateRing({ rate }: { rate: number }) {
|
|||||||
strokeDasharray={circumference}
|
strokeDasharray={circumference}
|
||||||
strokeDashoffset={offset}
|
strokeDashoffset={offset}
|
||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
|
filter="url(#glow)"
|
||||||
transform="rotate(-90 60 60)"
|
transform="rotate(-90 60 60)"
|
||||||
style={{ transition: 'stroke-dashoffset 0.6s ease' }}
|
style={{ transition: 'stroke-dashoffset 0.6s ease' }}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -27,7 +27,6 @@ export default function RepairDetail() {
|
|||||||
const [report, setReport] = useState<RepairReport | null>(null);
|
const [report, setReport] = useState<RepairReport | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
// 审核操作状态
|
|
||||||
const [approving, setApproving] = useState(false);
|
const [approving, setApproving] = useState(false);
|
||||||
const [rejecting, setRejecting] = useState(false);
|
const [rejecting, setRejecting] = useState(false);
|
||||||
const [actionMessage, setActionMessage] = useState('');
|
const [actionMessage, setActionMessage] = useState('');
|
||||||
@ -102,6 +101,9 @@ export default function RepairDetail() {
|
|||||||
const isPendingReview = report.status === 'PENDING_FIX';
|
const isPendingReview = report.status === 'PENDING_FIX';
|
||||||
const hasPR = !!report.pr_url;
|
const hasPR = !!report.pr_url;
|
||||||
|
|
||||||
|
const msgIsSuccess = (msg: string) =>
|
||||||
|
msg.includes('批准') || msg.includes('驳回');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Link to="/repairs" className="back-link">
|
<Link to="/repairs" className="back-link">
|
||||||
@ -109,7 +111,7 @@ export default function RepairDetail() {
|
|||||||
返回修复报告列表
|
返回修复报告列表
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<div style={{ marginBottom: '20px' }}>
|
<div className="report-header">
|
||||||
<div className="title-row">
|
<div className="title-row">
|
||||||
<h1 className="page-title">修复报告 #{report.id}</h1>
|
<h1 className="page-title">修复报告 #{report.id}</h1>
|
||||||
<span className={`status-badge status-${report.status}`}>
|
<span className={`status-badge status-${report.status}`}>
|
||||||
@ -153,13 +155,13 @@ export default function RepairDetail() {
|
|||||||
|
|
||||||
{/* PR 信息 */}
|
{/* PR 信息 */}
|
||||||
{hasPR && (
|
{hasPR && (
|
||||||
<div className="card" style={{ borderLeft: '3px solid var(--accent)' }}>
|
<div className="card card--accent">
|
||||||
<h2>Pull Request</h2>
|
<h2>Pull Request</h2>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', flexWrap: 'wrap' }}>
|
<div className="pr-section-row">
|
||||||
<span style={{ fontSize: '14px' }}>
|
<span className="pr-section-id">
|
||||||
PR #{report.pr_number} | {report.branch_name || 'fix branch'}
|
PR #{report.pr_number} | {report.branch_name || 'fix branch'}
|
||||||
</span>
|
</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} />
|
查看 PR <ExternalLink size={12} />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@ -168,43 +170,34 @@ export default function RepairDetail() {
|
|||||||
|
|
||||||
{/* 审核操作区 */}
|
{/* 审核操作区 */}
|
||||||
{isPendingReview && (
|
{isPendingReview && (
|
||||||
<div className="card" style={{ borderLeft: '3px solid var(--warning)' }}>
|
<div className="card card--warning">
|
||||||
<h2>审核操作</h2>
|
<h2>审核操作</h2>
|
||||||
<p style={{ fontSize: '14px', color: 'var(--text-secondary)', marginBottom: '12px' }}>
|
<p className="review-section-desc">
|
||||||
{hasPR
|
{hasPR
|
||||||
? '批准将合并 PR 并标记所有关联缺陷为已修复'
|
? '批准将合并 PR 并标记所有关联缺陷为已修复'
|
||||||
: '确认修复将标记所有关联缺陷为已修复'}
|
: '确认修复将标记所有关联缺陷为已修复'}
|
||||||
</p>
|
</p>
|
||||||
<div style={{ display: 'flex', gap: '12px', flexWrap: 'wrap', alignItems: 'center' }}>
|
<div className="review-actions">
|
||||||
<button
|
<button
|
||||||
className="trigger-repair-btn"
|
className="trigger-repair-btn btn-success"
|
||||||
onClick={handleApprove}
|
onClick={handleApprove}
|
||||||
disabled={approving}
|
disabled={approving}
|
||||||
style={{ background: 'var(--success)', borderColor: 'var(--success)' }}
|
|
||||||
>
|
>
|
||||||
{approving ? (
|
{approving ? <Loader2 size={14} className="spinner" /> : <Check size={14} />}
|
||||||
<Loader2 size={14} className="spinner" />
|
|
||||||
) : (
|
|
||||||
<Check size={14} />
|
|
||||||
)}
|
|
||||||
{approving ? '处理中...' : (hasPR ? '批准并合并' : '确认修复')}
|
{approving ? '处理中...' : (hasPR ? '批准并合并' : '确认修复')}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
className="trigger-repair-btn"
|
className="trigger-repair-btn btn-error"
|
||||||
onClick={() => setShowRejectModal(true)}
|
onClick={() => setShowRejectModal(true)}
|
||||||
disabled={rejecting}
|
disabled={rejecting}
|
||||||
style={{ background: 'var(--error)', borderColor: 'var(--error)' }}
|
|
||||||
>
|
>
|
||||||
<X size={14} />
|
<X size={14} />
|
||||||
{hasPR ? '拒绝修复' : '驳回修复'}
|
{hasPR ? '拒绝修复' : '驳回修复'}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{actionMessage && (
|
{actionMessage && (
|
||||||
<span style={{
|
<span className={`action-message ${msgIsSuccess(actionMessage) ? 'action-message--success' : 'action-message--error'}`}>
|
||||||
fontSize: '13px',
|
|
||||||
color: actionMessage.includes('批准') || actionMessage.includes('驳回') ? 'var(--success)' : 'var(--error)'
|
|
||||||
}}>
|
|
||||||
{actionMessage}
|
{actionMessage}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@ -213,7 +206,7 @@ export default function RepairDetail() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{report.failure_reason && (
|
{report.failure_reason && (
|
||||||
<div className="card" style={{ borderLeft: '3px solid var(--error)' }}>
|
<div className="card card--error">
|
||||||
<h2><AlertTriangle size={16} /> 失败原因</h2>
|
<h2><AlertTriangle size={16} /> 失败原因</h2>
|
||||||
<pre className="code-block error">{report.failure_reason}</pre>
|
<pre className="code-block error">{report.failure_reason}</pre>
|
||||||
</div>
|
</div>
|
||||||
@ -236,48 +229,23 @@ export default function RepairDetail() {
|
|||||||
|
|
||||||
{/* 驳回原因模态框 */}
|
{/* 驳回原因模态框 */}
|
||||||
{showRejectModal && (
|
{showRejectModal && (
|
||||||
<div style={{
|
<div className="modal-overlay" onClick={() => { setShowRejectModal(false); setRejectReason(''); }}>
|
||||||
position: 'fixed',
|
<div className="modal-content reject-modal-content" onClick={(e) => e.stopPropagation()}>
|
||||||
top: 0,
|
<h3 className="modal-title">{hasPR ? '拒绝修复' : '驳回修复'}</h3>
|
||||||
left: 0,
|
<p className="reject-modal-desc">
|
||||||
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)' }}>
|
|
||||||
请说明原因,Agent 将根据您的反馈重新修复所有关联缺陷:
|
请说明原因,Agent 将根据您的反馈重新修复所有关联缺陷:
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div style={{ marginBottom: '12px' }}>
|
<div className="reject-templates">
|
||||||
<label style={{ fontSize: '13px', color: 'var(--text-tertiary)', marginBottom: '6px', display: 'block' }}>
|
<label className="reject-templates-label">
|
||||||
常用模板(点击填充):
|
常用模板(点击填充):
|
||||||
</label>
|
</label>
|
||||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '6px' }}>
|
<div className="reject-templates-list">
|
||||||
{REJECT_REASON_TEMPLATES.map((template, idx) => (
|
{REJECT_REASON_TEMPLATES.map((template, idx) => (
|
||||||
<button
|
<button
|
||||||
key={idx}
|
key={idx}
|
||||||
|
className="reject-template-btn"
|
||||||
onClick={() => setRejectReason(template)}
|
onClick={() => setRejectReason(template)}
|
||||||
style={{
|
|
||||||
fontSize: '12px',
|
|
||||||
padding: '4px 8px',
|
|
||||||
background: 'var(--bg-secondary)',
|
|
||||||
border: '1px solid var(--border)',
|
|
||||||
borderRadius: '4px',
|
|
||||||
cursor: 'pointer'
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{template}
|
{template}
|
||||||
</button>
|
</button>
|
||||||
@ -286,49 +254,28 @@ export default function RepairDetail() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<textarea
|
<textarea
|
||||||
|
className="reject-textarea"
|
||||||
value={rejectReason}
|
value={rejectReason}
|
||||||
onChange={(e) => setRejectReason(e.target.value)}
|
onChange={(e) => setRejectReason(e.target.value)}
|
||||||
placeholder="请输入详细原因..."
|
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
|
<button
|
||||||
|
className="btn btn-secondary"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setShowRejectModal(false);
|
setShowRejectModal(false);
|
||||||
setRejectReason('');
|
setRejectReason('');
|
||||||
}}
|
}}
|
||||||
style={{
|
|
||||||
padding: '8px 16px',
|
|
||||||
background: 'var(--bg-secondary)',
|
|
||||||
border: '1px solid var(--border)',
|
|
||||||
borderRadius: '4px',
|
|
||||||
cursor: 'pointer'
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
取消
|
取消
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
|
className="trigger-repair-btn btn-error"
|
||||||
onClick={handleReject}
|
onClick={handleReject}
|
||||||
disabled={rejecting || !rejectReason.trim()}
|
disabled={rejecting || !rejectReason.trim()}
|
||||||
className="trigger-repair-btn"
|
|
||||||
style={{ background: 'var(--error)', borderColor: 'var(--error)' }}
|
|
||||||
>
|
>
|
||||||
{rejecting ? (
|
{rejecting ? <Loader2 size={14} className="spinner" /> : <X size={14} />}
|
||||||
<Loader2 size={14} className="spinner" />
|
|
||||||
) : (
|
|
||||||
<X size={14} />
|
|
||||||
)}
|
|
||||||
{rejecting ? '提交中...' : '确认驳回'}
|
{rejecting ? '提交中...' : '确认驳回'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -135,7 +135,7 @@ export default function RepairList() {
|
|||||||
{STATUS_LABELS[report.status] || report.status}
|
{STATUS_LABELS[report.status] || report.status}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td style={{ maxWidth: '200px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', fontSize: '12px', color: 'var(--error)' }}>
|
<td className="failure-cell">
|
||||||
{report.failure_reason || '-'}
|
{report.failure_reason || '-'}
|
||||||
</td>
|
</td>
|
||||||
<td className="cell-secondary">
|
<td className="cell-secondary">
|
||||||
@ -159,7 +159,6 @@ export default function RepairList() {
|
|||||||
key={report.id}
|
key={report.id}
|
||||||
to={`/repairs/${report.id}`}
|
to={`/repairs/${report.id}`}
|
||||||
className="mobile-card-item"
|
className="mobile-card-item"
|
||||||
style={{ textDecoration: 'none', color: 'inherit' }}
|
|
||||||
>
|
>
|
||||||
<div className="mobile-card-top">
|
<div className="mobile-card-top">
|
||||||
<span className="mobile-card-id">#{report.id}</span>
|
<span className="mobile-card-id">#{report.id}</span>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user