2025-10-30 08:52:48 +08:00
|
|
|
|
<!DOCTYPE html>
|
|
|
|
|
|
<html lang="zh-CN">
|
|
|
|
|
|
<head>
|
|
|
|
|
|
<meta charset="UTF-8">
|
|
|
|
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
|
|
|
|
<title>数据库维护页面 - LawRisk</title>
|
|
|
|
|
|
<style>
|
|
|
|
|
|
* {
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
padding: 0;
|
|
|
|
|
|
box-sizing: border-box;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
body {
|
|
|
|
|
|
font-family: 'Microsoft YaHei', Arial, sans-serif;
|
|
|
|
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
|
|
|
|
min-height: 100vh;
|
|
|
|
|
|
padding: 20px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.container {
|
|
|
|
|
|
max-width: 1400px;
|
|
|
|
|
|
margin: 0 auto;
|
|
|
|
|
|
background: white;
|
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
|
box-shadow: 0 10px 40px rgba(0,0,0,0.2);
|
|
|
|
|
|
padding: 30px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.header {
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
margin-bottom: 30px;
|
|
|
|
|
|
padding-bottom: 20px;
|
|
|
|
|
|
border-bottom: 3px solid #667eea;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.header h1 {
|
|
|
|
|
|
color: #333;
|
|
|
|
|
|
font-size: 28px;
|
|
|
|
|
|
margin-bottom: 10px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.header p {
|
|
|
|
|
|
color: #666;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.step-indicator {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
margin-bottom: 30px;
|
|
|
|
|
|
gap: 10px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.step {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 10px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.step-number {
|
|
|
|
|
|
width: 36px;
|
|
|
|
|
|
height: 36px;
|
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
|
background: #e0e7ff;
|
|
|
|
|
|
color: #667eea;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.step.active .step-number {
|
|
|
|
|
|
background: #667eea;
|
|
|
|
|
|
color: white;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.step-label {
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
color: #666;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.step.active .step-label {
|
|
|
|
|
|
color: #667eea;
|
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.arrow {
|
|
|
|
|
|
color: #ccc;
|
|
|
|
|
|
font-size: 24px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.content-area {
|
|
|
|
|
|
display: grid;
|
2025-10-30 09:54:36 +08:00
|
|
|
|
grid-template-columns: 350px 1fr;
|
2025-10-30 08:52:48 +08:00
|
|
|
|
gap: 30px;
|
|
|
|
|
|
min-height: 600px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.panel {
|
|
|
|
|
|
background: #f8f9fa;
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
padding: 20px;
|
|
|
|
|
|
border: 1px solid #e0e0e0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.panel h2 {
|
|
|
|
|
|
color: #333;
|
|
|
|
|
|
font-size: 18px;
|
|
|
|
|
|
margin-bottom: 15px;
|
|
|
|
|
|
padding-bottom: 10px;
|
|
|
|
|
|
border-bottom: 2px solid #667eea;
|
2025-10-30 09:54:36 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.nav-controls {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.back-button {
|
|
|
|
|
|
padding: 4px 12px;
|
|
|
|
|
|
background: #f0f0f0;
|
|
|
|
|
|
border: 1px solid #ddd;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
color: #666;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
transition: all 0.3s;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.back-button:hover:not(:disabled) {
|
|
|
|
|
|
background: #e0e0e0;
|
|
|
|
|
|
border-color: #999;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.back-button:disabled {
|
|
|
|
|
|
opacity: 0.5;
|
|
|
|
|
|
cursor: not-allowed;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.breadcrumb {
|
|
|
|
|
|
background: white;
|
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
|
padding: 12px 16px;
|
|
|
|
|
|
margin-bottom: 15px;
|
|
|
|
|
|
border: 1px solid #e0e0e0;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
color: #666;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.breadcrumb-item {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 6px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.breadcrumb-item a {
|
|
|
|
|
|
color: #667eea;
|
|
|
|
|
|
text-decoration: none;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
transition: color 0.3s;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.breadcrumb-item a:hover {
|
|
|
|
|
|
color: #5568d3;
|
|
|
|
|
|
text-decoration: underline;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.breadcrumb-separator {
|
|
|
|
|
|
color: #ccc;
|
|
|
|
|
|
margin: 0 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.breadcrumb-current {
|
|
|
|
|
|
color: #333;
|
|
|
|
|
|
font-weight: 500;
|
2025-10-30 08:52:48 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.selection-area {
|
|
|
|
|
|
background: white;
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
padding: 20px;
|
2025-10-30 09:54:36 +08:00
|
|
|
|
max-height: 600px;
|
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
|
overflow-x: hidden;
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 自定义滚动条样式 */
|
|
|
|
|
|
.selection-area::-webkit-scrollbar {
|
|
|
|
|
|
width: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.selection-area::-webkit-scrollbar-track {
|
|
|
|
|
|
background: #f1f1f1;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.selection-area::-webkit-scrollbar-thumb {
|
|
|
|
|
|
background: #c0c0c0;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
transition: background 0.3s;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.selection-area::-webkit-scrollbar-thumb:hover {
|
|
|
|
|
|
background: #a0a0a0;
|
2025-10-30 08:52:48 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.item-list {
|
|
|
|
|
|
list-style: none;
|
|
|
|
|
|
margin-top: 10px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.item-list li {
|
|
|
|
|
|
padding: 12px 16px;
|
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
|
background: #f8f9fa;
|
|
|
|
|
|
border: 1px solid #e0e0e0;
|
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
transition: all 0.3s;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.item-list li:hover {
|
|
|
|
|
|
background: #e0e7ff;
|
|
|
|
|
|
border-color: #667eea;
|
|
|
|
|
|
transform: translateX(5px);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.item-list li.active {
|
|
|
|
|
|
background: #667eea;
|
|
|
|
|
|
color: white;
|
|
|
|
|
|
border-color: #667eea;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.item-list li.active:hover {
|
|
|
|
|
|
background: #5568d3;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.item-name {
|
|
|
|
|
|
font-size: 15px;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.item-count {
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
background: rgba(102, 126, 234, 0.1);
|
|
|
|
|
|
padding: 4px 10px;
|
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
|
color: #667eea;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.item-list li.active .item-count {
|
|
|
|
|
|
background: rgba(255, 255, 255, 0.2);
|
|
|
|
|
|
color: white;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-30 10:33:35 +08:00
|
|
|
|
.checkpoint-nav-item {
|
|
|
|
|
|
background: #fff3cd;
|
|
|
|
|
|
border: 2px solid #ffc107;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.checkpoint-nav-item:hover {
|
|
|
|
|
|
background: #ffe69c;
|
|
|
|
|
|
border-color: #ff9800;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.checkpoint-section {
|
|
|
|
|
|
background: white;
|
|
|
|
|
|
padding: 20px;
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
margin-bottom: 20px;
|
|
|
|
|
|
border: 1px solid #e0e0e0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.checkpoint-section h3 {
|
|
|
|
|
|
color: #667eea;
|
|
|
|
|
|
font-size: 18px;
|
|
|
|
|
|
margin-bottom: 15px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 10px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.checkpoint-section h3::before {
|
|
|
|
|
|
content: '🔒';
|
|
|
|
|
|
font-size: 20px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-03 11:30:38 +08:00
|
|
|
|
.checkpoint-section.snapshot-section h3::before {
|
|
|
|
|
|
content: '🕘';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.snapshot-description {
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
color: #555;
|
|
|
|
|
|
margin-bottom: 12px;
|
|
|
|
|
|
line-height: 1.6;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.timeline-list {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 12px;
|
|
|
|
|
|
margin-top: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.timeline-item {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 14px;
|
|
|
|
|
|
background: white;
|
|
|
|
|
|
border: 1px solid #dde1ff;
|
|
|
|
|
|
border-radius: 10px;
|
|
|
|
|
|
padding: 14px 18px;
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
box-shadow: 0 4px 18px rgba(102, 126, 234, 0.08);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.timeline-item-snapshot {
|
|
|
|
|
|
border-left: 4px solid #667eea;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.timeline-item-checkpoint {
|
|
|
|
|
|
border-left: 4px solid #ff9800;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.timeline-icon {
|
|
|
|
|
|
width: 40px;
|
|
|
|
|
|
height: 40px;
|
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
|
background: #eef2ff;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
font-size: 20px;
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.timeline-item-checkpoint .timeline-icon {
|
|
|
|
|
|
background: #fff4e5;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.timeline-body {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.timeline-header {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.timeline-title {
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: #2f3e9e;
|
|
|
|
|
|
font-size: 15px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.timeline-item-checkpoint .timeline-title {
|
|
|
|
|
|
color: #e65100;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.timeline-subtitle {
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
color: #444;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.timeline-time {
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
color: #888;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.timeline-content {
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
color: #333;
|
|
|
|
|
|
line-height: 1.6;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.timeline-meta {
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
color: #666;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
gap: 10px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.timeline-note {
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
color: #555;
|
|
|
|
|
|
background: #f7f9ff;
|
|
|
|
|
|
padding: 8px 10px;
|
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.timeline-stats {
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
color: #333;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 16px;
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.timeline-actions {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
margin-top: 6px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.timeline-footer {
|
|
|
|
|
|
margin-top: 14px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
gap: 10px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.snapshot-filters {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
gap: 10px;
|
|
|
|
|
|
margin-bottom: 15px;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.snapshot-filters input,
|
|
|
|
|
|
.snapshot-filters select {
|
|
|
|
|
|
flex: 1 1 200px;
|
|
|
|
|
|
padding: 8px 10px;
|
|
|
|
|
|
border: 1px solid #d0d5ff;
|
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.snapshot-filters select:disabled {
|
|
|
|
|
|
background: #f0f2ff;
|
|
|
|
|
|
color: #aaa;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.snapshot-filters .filter-actions {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.snapshot-table {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
border-collapse: collapse;
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.snapshot-table th,
|
|
|
|
|
|
.snapshot-table td {
|
|
|
|
|
|
border-bottom: 1px solid #eceffb;
|
|
|
|
|
|
padding: 10px;
|
|
|
|
|
|
text-align: left;
|
|
|
|
|
|
vertical-align: top;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.snapshot-table th {
|
|
|
|
|
|
background: #f4f6ff;
|
|
|
|
|
|
color: #334;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.snapshot-permit-name {
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: #333;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.snapshot-permit-region {
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
color: #666;
|
|
|
|
|
|
margin-top: 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.snapshot-risk-text {
|
|
|
|
|
|
color: #333;
|
|
|
|
|
|
line-height: 1.5;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.snapshot-risk-meta {
|
|
|
|
|
|
margin-top: 6px;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
color: #777;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.snapshot-version-badge {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 4px;
|
|
|
|
|
|
background: rgba(102, 126, 234, 0.12);
|
|
|
|
|
|
color: #4c51bf;
|
|
|
|
|
|
padding: 4px 8px;
|
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
margin-bottom: 6px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.snapshot-status-tag {
|
|
|
|
|
|
display: inline-block;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
padding: 4px 8px;
|
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
|
background: #e8f5e9;
|
|
|
|
|
|
color: #2e7d32;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.snapshot-editor {
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: #333;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.snapshot-note {
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
color: #555;
|
|
|
|
|
|
margin-top: 6px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.snapshot-time {
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
color: #888;
|
|
|
|
|
|
margin-top: 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.snapshot-footer {
|
|
|
|
|
|
margin-top: 12px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
gap: 10px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.snapshot-pagination button {
|
|
|
|
|
|
min-width: 90px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-30 10:33:35 +08:00
|
|
|
|
.checkpoint-form {
|
|
|
|
|
|
margin-bottom: 20px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.form-group {
|
|
|
|
|
|
margin-bottom: 15px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.form-group label {
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
|
color: #333;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.form-group input[type="text"] {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
padding: 10px 12px;
|
|
|
|
|
|
border: 1px solid #ddd;
|
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
transition: border-color 0.3s;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.form-group input[type="text"]:focus {
|
|
|
|
|
|
outline: none;
|
|
|
|
|
|
border-color: #667eea;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.btn {
|
|
|
|
|
|
padding: 10px 20px;
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
transition: all 0.3s;
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.btn-primary {
|
|
|
|
|
|
background: #667eea;
|
|
|
|
|
|
color: white;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.btn-primary:hover:not(:disabled) {
|
|
|
|
|
|
background: #5568d3;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.btn-danger {
|
|
|
|
|
|
background: #dc3545;
|
|
|
|
|
|
color: white;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.btn-danger:hover:not(:disabled) {
|
|
|
|
|
|
background: #c82333;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.btn-warning {
|
|
|
|
|
|
background: #ffc107;
|
|
|
|
|
|
color: #333;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.btn-warning:hover:not(:disabled) {
|
|
|
|
|
|
background: #e0a800;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.btn:disabled {
|
|
|
|
|
|
opacity: 0.6;
|
|
|
|
|
|
cursor: not-allowed;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.checkpoint-list {
|
|
|
|
|
|
margin-top: 20px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.checkpoint-item {
|
|
|
|
|
|
background: #f8f9fa;
|
|
|
|
|
|
padding: 15px;
|
|
|
|
|
|
margin-bottom: 12px;
|
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
|
border: 1px solid #e0e0e0;
|
|
|
|
|
|
transition: all 0.3s;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.checkpoint-item:hover {
|
|
|
|
|
|
background: #e9ecef;
|
|
|
|
|
|
border-color: #667eea;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.checkpoint-header {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
margin-bottom: 10px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.checkpoint-id {
|
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
|
color: #333;
|
|
|
|
|
|
font-size: 15px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.checkpoint-timestamp {
|
|
|
|
|
|
color: #666;
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.checkpoint-description {
|
|
|
|
|
|
color: #555;
|
|
|
|
|
|
margin-bottom: 10px;
|
|
|
|
|
|
line-height: 1.6;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.checkpoint-stats {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 15px;
|
|
|
|
|
|
margin-bottom: 10px;
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
color: #666;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.checkpoint-stats span {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 5px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.checkpoint-actions {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 10px;
|
|
|
|
|
|
margin-top: 10px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.btn-sm {
|
|
|
|
|
|
padding: 6px 12px;
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.modal {
|
|
|
|
|
|
display: none;
|
|
|
|
|
|
position: fixed;
|
|
|
|
|
|
top: 0;
|
|
|
|
|
|
left: 0;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
background: rgba(0, 0, 0, 0.5);
|
2025-10-30 12:00:57 +08:00
|
|
|
|
z-index: 9999;
|
2025-10-30 10:33:35 +08:00
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.modal.show {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.modal-content {
|
|
|
|
|
|
background: white;
|
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
|
padding: 30px;
|
|
|
|
|
|
max-width: 500px;
|
|
|
|
|
|
width: 90%;
|
|
|
|
|
|
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
|
|
|
|
|
|
animation: modalFadeIn 0.3s;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@keyframes modalFadeIn {
|
|
|
|
|
|
from {
|
|
|
|
|
|
opacity: 0;
|
|
|
|
|
|
transform: translateY(-20px);
|
|
|
|
|
|
}
|
|
|
|
|
|
to {
|
|
|
|
|
|
opacity: 1;
|
|
|
|
|
|
transform: translateY(0);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.modal-header {
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
margin-bottom: 20px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.modal-header h3 {
|
|
|
|
|
|
color: #dc3545;
|
|
|
|
|
|
font-size: 20px;
|
|
|
|
|
|
margin-bottom: 10px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.modal-header .warning-icon {
|
|
|
|
|
|
font-size: 48px;
|
|
|
|
|
|
margin-bottom: 15px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.modal-body {
|
|
|
|
|
|
margin-bottom: 20px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.modal-body p {
|
|
|
|
|
|
color: #555;
|
|
|
|
|
|
line-height: 1.6;
|
|
|
|
|
|
margin-bottom: 10px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.modal-footer {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 10px;
|
|
|
|
|
|
justify-content: flex-end;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.danger-text {
|
|
|
|
|
|
color: #dc3545;
|
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.success-text {
|
|
|
|
|
|
color: #28a745;
|
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-30 11:48:15 +08:00
|
|
|
|
.checkpoint-toolbar {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: flex-end;
|
|
|
|
|
|
margin-bottom: 20px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.checkpoint-toolbar .btn {
|
|
|
|
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.checkpoint-modal {
|
|
|
|
|
|
display: none;
|
|
|
|
|
|
position: fixed;
|
|
|
|
|
|
top: 0;
|
|
|
|
|
|
left: 0;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
background: rgba(0, 0, 0, 0.5);
|
|
|
|
|
|
z-index: 1000;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
padding: 20px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.checkpoint-modal.show {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.checkpoint-modal-content {
|
|
|
|
|
|
background: white;
|
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
|
padding: 0;
|
|
|
|
|
|
max-width: 900px;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
max-height: 90vh;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
|
|
|
|
|
|
animation: modalFadeIn 0.3s;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.checkpoint-modal-header {
|
|
|
|
|
|
padding: 20px 30px;
|
|
|
|
|
|
border-bottom: 2px solid #667eea;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.checkpoint-modal-header h2 {
|
|
|
|
|
|
color: #333;
|
|
|
|
|
|
font-size: 20px;
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.checkpoint-modal-close {
|
|
|
|
|
|
background: none;
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
font-size: 28px;
|
|
|
|
|
|
color: #999;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
padding: 0;
|
|
|
|
|
|
width: 32px;
|
|
|
|
|
|
height: 32px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
|
transition: all 0.3s;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.checkpoint-modal-close:hover {
|
|
|
|
|
|
background: #f0f0f0;
|
|
|
|
|
|
color: #333;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.checkpoint-modal-body {
|
|
|
|
|
|
padding: 30px;
|
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-30 08:52:48 +08:00
|
|
|
|
.details-area {
|
|
|
|
|
|
background: white;
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
padding: 20px;
|
|
|
|
|
|
min-height: 500px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.empty-state {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
color: #999;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.empty-state svg {
|
|
|
|
|
|
width: 80px;
|
|
|
|
|
|
height: 80px;
|
|
|
|
|
|
margin-bottom: 15px;
|
|
|
|
|
|
opacity: 0.3;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.empty-state p {
|
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.details-content {
|
|
|
|
|
|
animation: fadeIn 0.3s;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@keyframes fadeIn {
|
|
|
|
|
|
from {
|
|
|
|
|
|
opacity: 0;
|
|
|
|
|
|
transform: translateY(10px);
|
|
|
|
|
|
}
|
|
|
|
|
|
to {
|
|
|
|
|
|
opacity: 1;
|
|
|
|
|
|
transform: translateY(0);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.detail-section {
|
|
|
|
|
|
margin-bottom: 25px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.detail-section h3 {
|
|
|
|
|
|
color: #667eea;
|
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
|
margin-bottom: 10px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.detail-section h3::before {
|
|
|
|
|
|
content: '';
|
|
|
|
|
|
width: 4px;
|
|
|
|
|
|
height: 16px;
|
|
|
|
|
|
background: #667eea;
|
|
|
|
|
|
border-radius: 2px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.detail-content {
|
|
|
|
|
|
background: #f8f9fa;
|
|
|
|
|
|
padding: 15px;
|
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
|
border-left: 3px solid #667eea;
|
|
|
|
|
|
line-height: 1.8;
|
|
|
|
|
|
color: #444;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.risk-item {
|
|
|
|
|
|
background: white;
|
|
|
|
|
|
padding: 15px;
|
|
|
|
|
|
margin-bottom: 12px;
|
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
|
border: 1px solid #e0e0e0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.risk-item h4 {
|
|
|
|
|
|
color: #d32f2f;
|
|
|
|
|
|
font-size: 15px;
|
|
|
|
|
|
margin-bottom: 10px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.risk-field {
|
|
|
|
|
|
margin-bottom: 10px;
|
|
|
|
|
|
line-height: 1.6;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.risk-field strong {
|
|
|
|
|
|
color: #333;
|
|
|
|
|
|
display: inline-block;
|
|
|
|
|
|
min-width: 80px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.risk-field p {
|
|
|
|
|
|
color: #555;
|
|
|
|
|
|
display: inline;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.scope-item {
|
|
|
|
|
|
background: white;
|
|
|
|
|
|
padding: 10px 15px;
|
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
border-left: 3px solid #667eea;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.permit-status {
|
|
|
|
|
|
display: inline-block;
|
|
|
|
|
|
padding: 4px 12px;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
margin-right: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.status-active {
|
|
|
|
|
|
background: #e8f5e9;
|
|
|
|
|
|
color: #2e7d32;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.status-inactive {
|
|
|
|
|
|
background: #ffebee;
|
|
|
|
|
|
color: #c62828;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.loading {
|
|
|
|
|
|
display: inline-block;
|
|
|
|
|
|
width: 20px;
|
|
|
|
|
|
height: 20px;
|
|
|
|
|
|
border: 3px solid #f3f3f3;
|
|
|
|
|
|
border-top: 3px solid #667eea;
|
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
|
animation: spin 1s linear infinite;
|
|
|
|
|
|
margin-right: 10px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@keyframes spin {
|
|
|
|
|
|
0% { transform: rotate(0deg); }
|
|
|
|
|
|
100% { transform: rotate(360deg); }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.error {
|
|
|
|
|
|
background: #ffebee;
|
|
|
|
|
|
color: #c62828;
|
|
|
|
|
|
padding: 15px;
|
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
|
margin: 15px 0;
|
|
|
|
|
|
border-left: 4px solid #c62828;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@media (max-width: 1024px) {
|
|
|
|
|
|
.content-area {
|
|
|
|
|
|
grid-template-columns: 1fr;
|
|
|
|
|
|
}
|
2025-10-30 09:54:36 +08:00
|
|
|
|
|
|
|
|
|
|
.panel:first-child {
|
|
|
|
|
|
max-height: 300px;
|
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
|
}
|
2025-10-30 08:52:48 +08:00
|
|
|
|
}
|
|
|
|
|
|
</style>
|
|
|
|
|
|
</head>
|
|
|
|
|
|
<body>
|
|
|
|
|
|
<div class="container">
|
|
|
|
|
|
<div class="header">
|
|
|
|
|
|
<h1>🗃️ 数据库维护系统</h1>
|
|
|
|
|
|
<p>LawRisk 法律风险提示系统 - 数据库维护与查询工具</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="step-indicator">
|
|
|
|
|
|
<div class="step active" id="step1">
|
|
|
|
|
|
<div class="step-number">1</div>
|
|
|
|
|
|
<div class="step-label">选择地区</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="arrow">→</div>
|
|
|
|
|
|
<div class="step" id="step2">
|
|
|
|
|
|
<div class="step-number">2</div>
|
|
|
|
|
|
<div class="step-label">选择主题</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="arrow">→</div>
|
|
|
|
|
|
<div class="step" id="step3">
|
|
|
|
|
|
<div class="step-number">3</div>
|
|
|
|
|
|
<div class="step-label">选择许可</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="arrow">→</div>
|
|
|
|
|
|
<div class="step" id="step4">
|
|
|
|
|
|
<div class="step-number">4</div>
|
|
|
|
|
|
<div class="step-label">查看详情</div>
|
|
|
|
|
|
</div>
|
2025-10-30 11:48:15 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 检查点管理按钮 -->
|
|
|
|
|
|
<div class="checkpoint-toolbar">
|
|
|
|
|
|
<button class="btn btn-warning" onclick="openCheckpointModal()">
|
|
|
|
|
|
<span>🔒</span> 检查点管理
|
|
|
|
|
|
</button>
|
2025-10-30 08:52:48 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="content-area">
|
|
|
|
|
|
<div class="panel">
|
2025-10-30 09:54:36 +08:00
|
|
|
|
<h2 id="navTitle">
|
|
|
|
|
|
<span>选择区域</span>
|
|
|
|
|
|
<div class="nav-controls">
|
|
|
|
|
|
<button class="back-button" id="backButton" onclick="goBack()" disabled>← 上一步</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</h2>
|
|
|
|
|
|
<div class="breadcrumb" id="breadcrumb"></div>
|
2025-10-30 08:52:48 +08:00
|
|
|
|
<div class="selection-area">
|
2025-10-30 11:48:15 +08:00
|
|
|
|
<div id="navList" class="item-list"></div>
|
2025-10-30 08:52:48 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="panel">
|
2025-10-30 09:54:36 +08:00
|
|
|
|
<h2 id="detailsTitle">详情内容</h2>
|
|
|
|
|
|
<div class="details-area" id="detailsArea">
|
2025-10-30 08:52:48 +08:00
|
|
|
|
<div class="empty-state">
|
|
|
|
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
|
|
|
|
<path d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"/>
|
|
|
|
|
|
</svg>
|
2025-10-30 09:54:36 +08:00
|
|
|
|
<p id="emptyMessage">请选择区域开始导航</p>
|
2025-10-30 08:52:48 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-10-30 10:33:35 +08:00
|
|
|
|
<!-- 危险操作确认模态框 -->
|
|
|
|
|
|
<div class="modal" id="dangerModal">
|
|
|
|
|
|
<div class="modal-content">
|
|
|
|
|
|
<div class="modal-header">
|
|
|
|
|
|
<div class="warning-icon">⚠️</div>
|
|
|
|
|
|
<h3 id="dangerModalTitle">危险操作确认</h3>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="modal-body">
|
|
|
|
|
|
<p id="dangerModalMessage"></p>
|
|
|
|
|
|
<p class="danger-text" id="dangerModalWarning"></p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="modal-footer">
|
|
|
|
|
|
<button class="btn" onclick="closeDangerModal()">取消</button>
|
|
|
|
|
|
<button class="btn btn-danger" id="dangerModalConfirmBtn" onclick="confirmDangerOperation()">确认执行</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-10-30 11:48:15 +08:00
|
|
|
|
<!-- 检查点管理模态窗口 -->
|
|
|
|
|
|
<div class="checkpoint-modal" id="checkpointModal">
|
|
|
|
|
|
<div class="checkpoint-modal-content">
|
|
|
|
|
|
<div class="checkpoint-modal-header">
|
|
|
|
|
|
<h2>🔒 数据库检查点管理</h2>
|
|
|
|
|
|
<button class="checkpoint-modal-close" onclick="closeCheckpointModal()">×</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="checkpoint-modal-body" id="checkpointModalBody">
|
|
|
|
|
|
<!-- 检查点管理内容将在这里动态加载 -->
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-10-30 08:52:48 +08:00
|
|
|
|
<script>
|
2025-10-30 09:54:36 +08:00
|
|
|
|
// 导航状态管理
|
2025-10-30 11:48:15 +08:00
|
|
|
|
let currentStep = 1; // 1=区域, 2=主题, 3=许可, 4=详情
|
2025-10-30 09:54:36 +08:00
|
|
|
|
let historyStack = []; // 历史记录栈
|
2025-10-30 08:52:48 +08:00
|
|
|
|
let currentRegion = null;
|
|
|
|
|
|
let currentTheme = null;
|
2025-11-03 11:30:38 +08:00
|
|
|
|
let currentPermit = null;
|
|
|
|
|
|
let pendingDangerOperation = null; // 待执行的危险操作
|
|
|
|
|
|
|
|
|
|
|
|
const permitRiskSnapshotState = {
|
|
|
|
|
|
limit: 10,
|
|
|
|
|
|
offset: 0,
|
|
|
|
|
|
total: 0,
|
|
|
|
|
|
regionFilter: '',
|
|
|
|
|
|
permitFilter: '',
|
|
|
|
|
|
editorFilter: '',
|
|
|
|
|
|
snapshots: [],
|
|
|
|
|
|
loading: false,
|
|
|
|
|
|
error: '',
|
|
|
|
|
|
regionOptions: [],
|
|
|
|
|
|
regionLoading: false,
|
|
|
|
|
|
regionError: '',
|
|
|
|
|
|
permitOptions: [],
|
|
|
|
|
|
permitLoading: false,
|
|
|
|
|
|
permitOptionsError: ''
|
|
|
|
|
|
};
|
|
|
|
|
|
let checkpointListCache = [];
|
|
|
|
|
|
let checkpointListLoading = false;
|
|
|
|
|
|
let checkpointListError = '';
|
2025-10-30 08:52:48 +08:00
|
|
|
|
|
2025-10-30 09:54:36 +08:00
|
|
|
|
// 步骤配置
|
|
|
|
|
|
const steps = {
|
|
|
|
|
|
1: { title: '选择区域', loadData: loadRegions },
|
|
|
|
|
|
2: { title: '选择主题', loadData: (region) => loadThemes(region.id, region.name) },
|
|
|
|
|
|
3: { title: '选择许可', loadData: (theme) => loadPermits(theme.id, theme.name) },
|
2025-10-30 11:48:15 +08:00
|
|
|
|
4: { title: '许可详情', loadData: null }
|
2025-10-30 09:54:36 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
2025-10-30 08:52:48 +08:00
|
|
|
|
// 加载地区列表
|
|
|
|
|
|
async function loadRegions() {
|
2025-10-30 09:54:36 +08:00
|
|
|
|
const navList = document.getElementById('navList');
|
|
|
|
|
|
navList.innerHTML = '<div class="loading"></div>加载地区列表...';
|
|
|
|
|
|
|
2025-10-30 08:52:48 +08:00
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch('/fs-ai-asistant/api/workflow/lawrisk/admin/regions');
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
|
|
|
|
|
|
if (data.success) {
|
2025-10-30 09:54:36 +08:00
|
|
|
|
navList.innerHTML = '';
|
2025-10-30 08:52:48 +08:00
|
|
|
|
|
|
|
|
|
|
if (data.data.regions.length === 0) {
|
2025-10-30 09:54:36 +08:00
|
|
|
|
navList.innerHTML = '<div class="error">未找到地区数据</div>';
|
2025-10-30 08:52:48 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
data.data.regions.forEach(region => {
|
|
|
|
|
|
const li = document.createElement('li');
|
|
|
|
|
|
li.innerHTML = `
|
|
|
|
|
|
<span class="item-name">${region.name}</span>
|
2025-10-30 09:54:36 +08:00
|
|
|
|
<span class="item-count">点击选择</span>
|
2025-10-30 08:52:48 +08:00
|
|
|
|
`;
|
2025-10-30 09:54:36 +08:00
|
|
|
|
li.onclick = () => selectRegion(region.id, region.name);
|
|
|
|
|
|
navList.appendChild(li);
|
2025-10-30 08:52:48 +08:00
|
|
|
|
});
|
|
|
|
|
|
} else {
|
2025-10-30 09:54:36 +08:00
|
|
|
|
navList.innerHTML = `<div class="error">加载地区失败:${data.message}</div>`;
|
2025-10-30 08:52:48 +08:00
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
2025-10-30 09:54:36 +08:00
|
|
|
|
navList.innerHTML = `<div class="error">网络错误:${error.message}</div>`;
|
2025-10-30 08:52:48 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-30 09:54:36 +08:00
|
|
|
|
// 加载主题列表
|
|
|
|
|
|
async function loadThemes(regionId, regionName) {
|
|
|
|
|
|
const navList = document.getElementById('navList');
|
|
|
|
|
|
navList.innerHTML = '<div class="loading"></div>加载主题列表...';
|
2025-10-30 08:52:48 +08:00
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch(`/fs-ai-asistant/api/workflow/lawrisk/admin/themes?region=${regionId}`);
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
|
|
|
|
|
|
if (data.success) {
|
|
|
|
|
|
const themes = data.data.themes;
|
|
|
|
|
|
|
|
|
|
|
|
if (themes.length === 0) {
|
2025-10-30 09:54:36 +08:00
|
|
|
|
navList.innerHTML = `<div class="error">地区 "${regionName}" 下没有可用的主题</div>`;
|
2025-10-30 08:52:48 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let html = '<div class="item-list">';
|
|
|
|
|
|
themes.forEach(theme => {
|
|
|
|
|
|
html += `
|
2025-10-30 09:54:36 +08:00
|
|
|
|
<li onclick="selectTheme('${theme.id}', '${theme.name.replace(/'/g, "\\'")}')">
|
2025-10-30 08:52:48 +08:00
|
|
|
|
<span class="item-name">${theme.name}</span>
|
2025-10-30 09:54:36 +08:00
|
|
|
|
<span class="item-count">点击选择</span>
|
2025-10-30 08:52:48 +08:00
|
|
|
|
</li>
|
|
|
|
|
|
`;
|
|
|
|
|
|
});
|
|
|
|
|
|
html += '</div>';
|
2025-10-30 09:54:36 +08:00
|
|
|
|
navList.innerHTML = html;
|
2025-10-30 08:52:48 +08:00
|
|
|
|
} else {
|
2025-10-30 09:54:36 +08:00
|
|
|
|
navList.innerHTML = `<div class="error">加载主题失败:${data.message}</div>`;
|
2025-10-30 08:52:48 +08:00
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
2025-10-30 09:54:36 +08:00
|
|
|
|
navList.innerHTML = `<div class="error">网络错误:${error.message}</div>`;
|
2025-10-30 08:52:48 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-30 09:54:36 +08:00
|
|
|
|
// 加载许可列表
|
|
|
|
|
|
async function loadPermits(themeId, themeName) {
|
|
|
|
|
|
const navList = document.getElementById('navList');
|
|
|
|
|
|
navList.innerHTML = '<div class="loading"></div>加载许可列表...';
|
2025-10-30 08:52:48 +08:00
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch(`/fs-ai-asistant/api/workflow/lawrisk/admin/permits?region=${currentRegion.id}&theme=${themeId}`);
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
|
|
|
|
|
|
if (data.success) {
|
|
|
|
|
|
const permits = data.data.permits;
|
|
|
|
|
|
|
|
|
|
|
|
if (permits.length === 0) {
|
2025-10-30 09:54:36 +08:00
|
|
|
|
navList.innerHTML = `<div class="error">主题 "${themeName}" 下没有可用的许可</div>`;
|
2025-10-30 08:52:48 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let html = '<div class="item-list">';
|
|
|
|
|
|
permits.forEach(permit => {
|
|
|
|
|
|
const riskCount = permit.risks ? permit.risks.length : 0;
|
|
|
|
|
|
html += `
|
2025-10-30 09:54:36 +08:00
|
|
|
|
<li onclick="selectPermit('${permit.id}', '${permit.name.replace(/'/g, "\\'")}', '${themeId}')">
|
2025-10-30 08:52:48 +08:00
|
|
|
|
<span class="item-name">${permit.name}</span>
|
|
|
|
|
|
<span class="item-count">${riskCount} 个风险</span>
|
|
|
|
|
|
</li>
|
|
|
|
|
|
`;
|
|
|
|
|
|
});
|
|
|
|
|
|
html += '</div>';
|
2025-10-30 09:54:36 +08:00
|
|
|
|
navList.innerHTML = html;
|
2025-10-30 08:52:48 +08:00
|
|
|
|
} else {
|
2025-10-30 09:54:36 +08:00
|
|
|
|
navList.innerHTML = `<div class="error">加载许可失败:${data.message}</div>`;
|
2025-10-30 08:52:48 +08:00
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
2025-10-30 09:54:36 +08:00
|
|
|
|
navList.innerHTML = `<div class="error">网络错误:${error.message}</div>`;
|
2025-10-30 08:52:48 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-30 09:54:36 +08:00
|
|
|
|
// 选择地区
|
|
|
|
|
|
async function selectRegion(regionId, regionName) {
|
|
|
|
|
|
// 保存到历史栈
|
|
|
|
|
|
historyStack.push({ step: currentStep, region: currentRegion });
|
|
|
|
|
|
|
|
|
|
|
|
currentRegion = { id: regionId, name: regionName };
|
|
|
|
|
|
currentTheme = null;
|
|
|
|
|
|
currentPermit = null;
|
|
|
|
|
|
|
|
|
|
|
|
// 更新步骤
|
|
|
|
|
|
goToStep(2);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 选择主题
|
|
|
|
|
|
async function selectTheme(themeId, themeName) {
|
|
|
|
|
|
// 保存到历史栈
|
|
|
|
|
|
historyStack.push({ step: currentStep, theme: currentTheme });
|
|
|
|
|
|
|
|
|
|
|
|
currentTheme = { id: themeId, name: themeName };
|
|
|
|
|
|
currentPermit = null;
|
|
|
|
|
|
|
|
|
|
|
|
// 更新步骤
|
|
|
|
|
|
goToStep(3);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-30 08:52:48 +08:00
|
|
|
|
// 选择许可
|
2025-10-30 09:54:36 +08:00
|
|
|
|
async function selectPermit(permitId, permitName, themeId) {
|
|
|
|
|
|
// 保存到历史栈
|
|
|
|
|
|
historyStack.push({ step: currentStep, permit: currentPermit });
|
|
|
|
|
|
|
|
|
|
|
|
currentPermit = { id: permitId, name: permitName, themeId: themeId };
|
|
|
|
|
|
|
|
|
|
|
|
// 更新步骤
|
|
|
|
|
|
goToStep(4);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 跳转到指定步骤
|
|
|
|
|
|
async function goToStep(step) {
|
|
|
|
|
|
currentStep = step;
|
|
|
|
|
|
|
|
|
|
|
|
// 更新导航标题
|
|
|
|
|
|
document.getElementById('navTitle').querySelector('span').textContent = steps[step].title;
|
|
|
|
|
|
|
|
|
|
|
|
// 更新上一步按钮
|
|
|
|
|
|
const backButton = document.getElementById('backButton');
|
|
|
|
|
|
backButton.disabled = historyStack.length === 0;
|
2025-10-30 08:52:48 +08:00
|
|
|
|
|
|
|
|
|
|
// 更新步骤指示器
|
2025-10-30 09:54:36 +08:00
|
|
|
|
updateStepIndicator(step);
|
2025-10-30 08:52:48 +08:00
|
|
|
|
|
2025-10-30 09:54:36 +08:00
|
|
|
|
// 更新面包屑导航
|
|
|
|
|
|
updateBreadcrumb();
|
2025-10-30 08:52:48 +08:00
|
|
|
|
|
2025-10-30 09:54:36 +08:00
|
|
|
|
// 清空详情区域
|
|
|
|
|
|
const detailsArea = document.getElementById('detailsArea');
|
|
|
|
|
|
if (step === 1) {
|
|
|
|
|
|
detailsArea.innerHTML = `
|
|
|
|
|
|
<div class="empty-state">
|
|
|
|
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
|
|
|
|
<path d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"/>
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
<p id="emptyMessage">请选择区域开始导航</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 加载数据
|
|
|
|
|
|
if (step === 1) {
|
|
|
|
|
|
await loadRegions();
|
|
|
|
|
|
} else if (step === 2) {
|
|
|
|
|
|
await loadThemes(currentRegion.id, currentRegion.name);
|
|
|
|
|
|
} else if (step === 3) {
|
|
|
|
|
|
await loadPermits(currentTheme.id, currentTheme.name);
|
|
|
|
|
|
} else if (step === 4) {
|
|
|
|
|
|
await showPermitDetails();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 更新面包屑导航
|
|
|
|
|
|
function updateBreadcrumb() {
|
|
|
|
|
|
const breadcrumb = document.getElementById('breadcrumb');
|
|
|
|
|
|
let html = '';
|
|
|
|
|
|
|
|
|
|
|
|
// 总是显示"首页"
|
|
|
|
|
|
html += `
|
|
|
|
|
|
<span class="breadcrumb-item">
|
|
|
|
|
|
<a onclick="goHome()">首页</a>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
|
|
// 显示当前选择的路径
|
|
|
|
|
|
if (currentRegion) {
|
|
|
|
|
|
html += '<span class="breadcrumb-separator">›</span>';
|
|
|
|
|
|
if (currentStep > 2) {
|
|
|
|
|
|
html += `
|
|
|
|
|
|
<span class="breadcrumb-item">
|
|
|
|
|
|
<a onclick="quickJump(2)">${currentRegion.name}</a>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
`;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
html += `
|
|
|
|
|
|
<span class="breadcrumb-item">
|
|
|
|
|
|
<span class="breadcrumb-current">${currentRegion.name}</span>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
`;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (currentTheme) {
|
|
|
|
|
|
html += '<span class="breadcrumb-separator">›</span>';
|
|
|
|
|
|
if (currentStep > 3) {
|
|
|
|
|
|
html += `
|
|
|
|
|
|
<span class="breadcrumb-item">
|
|
|
|
|
|
<a onclick="quickJump(3)">${currentTheme.name}</a>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
`;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
html += `
|
|
|
|
|
|
<span class="breadcrumb-item">
|
|
|
|
|
|
<span class="breadcrumb-current">${currentTheme.name}</span>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
`;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (currentPermit) {
|
|
|
|
|
|
html += '<span class="breadcrumb-separator">›</span>';
|
|
|
|
|
|
if (currentStep > 4) {
|
|
|
|
|
|
html += `
|
|
|
|
|
|
<span class="breadcrumb-item">
|
|
|
|
|
|
<a onclick="quickJump(4)">${currentPermit.name}</a>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
`;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
html += `
|
|
|
|
|
|
<span class="breadcrumb-item">
|
|
|
|
|
|
<span class="breadcrumb-current">${currentPermit.name}</span>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
`;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
breadcrumb.innerHTML = html;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 快速跳转到指定步骤
|
|
|
|
|
|
function quickJump(targetStep) {
|
|
|
|
|
|
// 清空历史栈中比目标步骤更晚的记录
|
|
|
|
|
|
while (historyStack.length > 0 && historyStack[historyStack.length - 1].step >= targetStep) {
|
|
|
|
|
|
historyStack.pop();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 清理后续状态
|
|
|
|
|
|
if (targetStep <= 2) {
|
|
|
|
|
|
currentTheme = null;
|
|
|
|
|
|
currentPermit = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (targetStep <= 3) {
|
|
|
|
|
|
currentPermit = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
goToStep(targetStep);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 返回首页
|
|
|
|
|
|
function goHome() {
|
|
|
|
|
|
currentRegion = null;
|
|
|
|
|
|
currentTheme = null;
|
|
|
|
|
|
currentPermit = null;
|
|
|
|
|
|
historyStack = [];
|
|
|
|
|
|
goToStep(1);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 显示许可详情
|
|
|
|
|
|
async function showPermitDetails() {
|
|
|
|
|
|
const detailsArea = document.getElementById('detailsArea');
|
2025-10-30 08:52:48 +08:00
|
|
|
|
detailsArea.innerHTML = '<div class="loading"></div>加载许可详情...';
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
2025-10-30 09:54:36 +08:00
|
|
|
|
const response = await fetch(`/fs-ai-asistant/api/workflow/lawrisk/admin/permit-details?region=${currentRegion.id}&theme=${currentPermit.themeId}&permit=${currentPermit.id}`);
|
2025-10-30 08:52:48 +08:00
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
|
|
|
|
|
|
if (data.success) {
|
|
|
|
|
|
renderPermitDetails(data.data.permit);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
detailsArea.innerHTML = `<div class="error">加载详情失败:${data.message}</div>`;
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
detailsArea.innerHTML = `<div class="error">网络错误:${error.message}</div>`;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-30 09:54:36 +08:00
|
|
|
|
// 回退到上一步
|
|
|
|
|
|
function goBack() {
|
|
|
|
|
|
if (historyStack.length === 0) return;
|
|
|
|
|
|
|
|
|
|
|
|
const prev = historyStack.pop();
|
|
|
|
|
|
|
|
|
|
|
|
// 恢复状态
|
|
|
|
|
|
if (prev.region) currentRegion = prev.region;
|
|
|
|
|
|
if (prev.theme) currentTheme = prev.theme;
|
|
|
|
|
|
if (prev.permit) currentPermit = prev.permit;
|
|
|
|
|
|
|
|
|
|
|
|
// 跳转到上一步
|
|
|
|
|
|
goToStep(prev.step);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-30 08:52:48 +08:00
|
|
|
|
// 渲染许可详情
|
|
|
|
|
|
function renderPermitDetails(permit) {
|
|
|
|
|
|
const detailsArea = document.querySelector('.details-area');
|
|
|
|
|
|
let html = '<div class="details-content">';
|
|
|
|
|
|
|
|
|
|
|
|
// 许可基本信息
|
|
|
|
|
|
html += `
|
|
|
|
|
|
<div class="detail-section">
|
|
|
|
|
|
<h3>许可信息</h3>
|
|
|
|
|
|
<div class="detail-content">
|
|
|
|
|
|
<p><strong>许可名称:</strong>${permit.name}</p>
|
|
|
|
|
|
${permit.permit_status ? `<p style="margin-top: 10px;"><strong>许可状态:</strong><span class="permit-status ${permit.permit_status === 'active' ? 'status-active' : 'status-inactive'}">${permit.permit_status}</span></p>` : ''}
|
|
|
|
|
|
${permit.subitem_summary ? `<p style="margin-top: 10px;"><strong>子项说明:</strong>${permit.subitem_summary}</p>` : ''}
|
|
|
|
|
|
${permit.responsible_contact ? `<p style="margin-top: 10px;"><strong>负责部门:</strong>${permit.responsible_contact}</p>` : ''}
|
2025-11-01 10:08:11 +08:00
|
|
|
|
${permit.jurisdiction_scope ? `<p style="margin-top: 10px;"><strong>权限划分:</strong>${permit.jurisdiction_scope}</p>` : ''}
|
2025-10-30 08:52:48 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
|
|
// 经营范围
|
|
|
|
|
|
if (permit.business_scopes && permit.business_scopes.length > 0) {
|
|
|
|
|
|
html += '<div class="detail-section"><h3>经营范围</h3><div class="detail-content">';
|
|
|
|
|
|
permit.business_scopes.forEach(scope => {
|
|
|
|
|
|
html += `<div class="scope-item">${scope.description}</div>`;
|
|
|
|
|
|
});
|
|
|
|
|
|
html += '</div></div>';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 法律风险
|
|
|
|
|
|
if (permit.risks && permit.risks.length > 0) {
|
|
|
|
|
|
html += '<div class="detail-section"><h3>法律风险</h3><div class="detail-content">';
|
|
|
|
|
|
permit.risks.forEach(risk => {
|
|
|
|
|
|
html += `
|
|
|
|
|
|
<div class="risk-item">
|
|
|
|
|
|
<h4>风险 ${risk.id}</h4>
|
|
|
|
|
|
${risk.risk_content ? `<div class="risk-field"><strong>风险内容:</strong><p>${risk.risk_content}</p></div>` : ''}
|
|
|
|
|
|
${risk.legal_basis ? `<div class="risk-field"><strong>法律依据:</strong><p>${risk.legal_basis}</p></div>` : ''}
|
|
|
|
|
|
${risk.document_no ? `<div class="risk-field"><strong>文件编号:</strong><p>${risk.document_no}</p></div>` : ''}
|
|
|
|
|
|
${risk.summary ? `<div class="risk-field"><strong>摘要:</strong><div style="margin-top: 5px;">${risk.summary}</div></div>` : ''}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
`;
|
|
|
|
|
|
});
|
|
|
|
|
|
html += '</div></div>';
|
|
|
|
|
|
} else {
|
|
|
|
|
|
html += `
|
|
|
|
|
|
<div class="detail-section">
|
|
|
|
|
|
<h3>法律风险</h3>
|
|
|
|
|
|
<div class="detail-content">
|
|
|
|
|
|
<p style="color: #999;">暂无法律风险信息</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
html += '</div>';
|
|
|
|
|
|
detailsArea.innerHTML = html;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 更新步骤指示器
|
|
|
|
|
|
function updateStepIndicator(step) {
|
2025-10-30 11:48:15 +08:00
|
|
|
|
for (let i = 1; i <= 4; i++) {
|
2025-10-30 08:52:48 +08:00
|
|
|
|
const stepElement = document.getElementById(`step${i}`);
|
|
|
|
|
|
if (i <= step) {
|
|
|
|
|
|
stepElement.classList.add('active');
|
|
|
|
|
|
} else {
|
|
|
|
|
|
stepElement.classList.remove('active');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-30 10:33:35 +08:00
|
|
|
|
// ================ 检查点管理功能 ================
|
|
|
|
|
|
|
2025-10-30 11:48:15 +08:00
|
|
|
|
// 打开检查点模态窗口
|
|
|
|
|
|
async function openCheckpointModal() {
|
|
|
|
|
|
const modal = document.getElementById('checkpointModal');
|
|
|
|
|
|
modal.classList.add('show');
|
2025-10-30 10:33:35 +08:00
|
|
|
|
|
2025-11-03 11:30:38 +08:00
|
|
|
|
// 重置并显示加载状态
|
|
|
|
|
|
permitRiskSnapshotState.limit = 10;
|
|
|
|
|
|
permitRiskSnapshotState.offset = 0;
|
|
|
|
|
|
permitRiskSnapshotState.total = 0;
|
|
|
|
|
|
permitRiskSnapshotState.snapshots = [];
|
|
|
|
|
|
permitRiskSnapshotState.regionFilter = '';
|
|
|
|
|
|
permitRiskSnapshotState.permitFilter = '';
|
|
|
|
|
|
permitRiskSnapshotState.editorFilter = '';
|
|
|
|
|
|
permitRiskSnapshotState.loading = true;
|
|
|
|
|
|
permitRiskSnapshotState.error = '';
|
|
|
|
|
|
permitRiskSnapshotState.regionOptions = [];
|
|
|
|
|
|
permitRiskSnapshotState.permitOptions = [];
|
|
|
|
|
|
permitRiskSnapshotState.regionLoading = true;
|
|
|
|
|
|
permitRiskSnapshotState.regionError = '';
|
|
|
|
|
|
permitRiskSnapshotState.permitLoading = false;
|
|
|
|
|
|
permitRiskSnapshotState.permitOptionsError = '';
|
|
|
|
|
|
|
|
|
|
|
|
checkpointListCache = [];
|
|
|
|
|
|
checkpointListLoading = true;
|
|
|
|
|
|
checkpointListError = '';
|
|
|
|
|
|
|
|
|
|
|
|
renderCheckpointManager(checkpointListCache);
|
|
|
|
|
|
|
|
|
|
|
|
await Promise.all([
|
|
|
|
|
|
ensureRegionFilterOptions(false),
|
|
|
|
|
|
refreshPermitRiskSnapshots(false),
|
|
|
|
|
|
refreshCheckpointList(false)
|
|
|
|
|
|
]);
|
2025-10-30 10:33:35 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-30 11:48:15 +08:00
|
|
|
|
// 关闭检查点模态窗口
|
|
|
|
|
|
function closeCheckpointModal() {
|
|
|
|
|
|
const modal = document.getElementById('checkpointModal');
|
|
|
|
|
|
modal.classList.remove('show');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 点击模态窗口外部关闭
|
|
|
|
|
|
document.getElementById('checkpointModal').addEventListener('click', function(e) {
|
|
|
|
|
|
if (e.target === this) {
|
|
|
|
|
|
closeCheckpointModal();
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 加载检查点列表(保持兼容性)
|
|
|
|
|
|
async function loadCheckpoints() {
|
|
|
|
|
|
await openCheckpointModal();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-30 10:33:35 +08:00
|
|
|
|
// 渲染检查点管理界面
|
|
|
|
|
|
function renderCheckpointManager(checkpoints) {
|
2025-11-03 11:30:38 +08:00
|
|
|
|
if (Array.isArray(checkpoints)) {
|
|
|
|
|
|
checkpointListCache = checkpoints;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-30 11:48:15 +08:00
|
|
|
|
const modalBody = document.getElementById('checkpointModalBody');
|
2025-11-03 11:30:38 +08:00
|
|
|
|
if (!modalBody) return;
|
|
|
|
|
|
|
|
|
|
|
|
const snapshotState = permitRiskSnapshotState;
|
|
|
|
|
|
const checkpointList = checkpointListCache || [];
|
|
|
|
|
|
|
2025-10-30 11:48:15 +08:00
|
|
|
|
let html = '<div class="checkpoint-content">';
|
2025-10-30 10:33:35 +08:00
|
|
|
|
|
2025-11-03 11:30:38 +08:00
|
|
|
|
html += `
|
|
|
|
|
|
<div class="checkpoint-section snapshot-section">
|
|
|
|
|
|
<h3>许可风险快照</h3>
|
|
|
|
|
|
<div class="snapshot-description">
|
|
|
|
|
|
每当编辑<strong>地区 + 许可事项 + 风险提示</strong>组合时,系统都会自动记录一条快照,方便追踪历史变更、责任人及备注。
|
|
|
|
|
|
可通过下方筛选快速定位特定许可的修改记录。
|
|
|
|
|
|
</div>
|
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
|
|
const regionSelectDisabled = snapshotState.regionLoading ? 'disabled' : '';
|
|
|
|
|
|
const permitSelectDisabled = snapshotState.permitLoading || !snapshotState.regionFilter ? 'disabled' : '';
|
|
|
|
|
|
let regionOptionsHtml = '<option value="">全部地区</option>';
|
|
|
|
|
|
|
|
|
|
|
|
if (snapshotState.regionLoading) {
|
|
|
|
|
|
regionOptionsHtml += '<option value="__loading" disabled>正在加载地区...</option>';
|
|
|
|
|
|
} else {
|
|
|
|
|
|
snapshotState.regionOptions.forEach(option => {
|
|
|
|
|
|
const selected = option.id === snapshotState.regionFilter ? 'selected' : '';
|
|
|
|
|
|
regionOptionsHtml += `<option value="${escapeHtml(option.id)}" ${selected}>${escapeHtml(option.name)}</option>`;
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let permitOptionsHtml = '<option value="">全部许可</option>';
|
|
|
|
|
|
if (!snapshotState.regionFilter) {
|
|
|
|
|
|
permitOptionsHtml += '<option value="__placeholder" disabled>请选择地区后再选择许可</option>';
|
|
|
|
|
|
} else if (snapshotState.permitLoading) {
|
|
|
|
|
|
permitOptionsHtml += '<option value="__loading" disabled>正在加载许可...</option>';
|
|
|
|
|
|
} else {
|
|
|
|
|
|
snapshotState.permitOptions.forEach(option => {
|
|
|
|
|
|
const selected = option.id === snapshotState.permitFilter ? 'selected' : '';
|
|
|
|
|
|
permitOptionsHtml += `<option value="${escapeHtml(option.id)}" ${selected}>${escapeHtml(option.name)}</option>`;
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
html += `
|
|
|
|
|
|
<form class="snapshot-filters" onsubmit="return applySnapshotFilters(event)">
|
|
|
|
|
|
<select id="snapshotFilterRegion" ${regionSelectDisabled} onchange="handleSnapshotRegionChange(this.value)">
|
|
|
|
|
|
${regionOptionsHtml}
|
|
|
|
|
|
</select>
|
|
|
|
|
|
<select id="snapshotFilterPermit" ${permitSelectDisabled} onchange="handleSnapshotPermitChange(this.value)">
|
|
|
|
|
|
${permitOptionsHtml}
|
|
|
|
|
|
</select>
|
|
|
|
|
|
<input type="text" id="snapshotFilterEditor" placeholder="编辑人(可选)" value="${escapeHtml(snapshotState.editorFilter)}">
|
|
|
|
|
|
<div class="filter-actions">
|
|
|
|
|
|
<button type="submit" class="btn btn-primary btn-sm"><span>🔍</span> 筛选</button>
|
|
|
|
|
|
<button type="button" class="btn btn-warning btn-sm" onclick="resetSnapshotFilters()">重置</button>
|
|
|
|
|
|
<button type="button" class="btn btn-primary btn-sm" onclick="refreshPermitRiskSnapshots()"><span>🔄</span> 刷新</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</form>
|
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
|
|
if (snapshotState.regionError) {
|
|
|
|
|
|
html += `<div class="error" style="margin-top: 8px;">地区列表加载失败:${escapeHtml(snapshotState.regionError)}</div>`;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (snapshotState.permitOptionsError) {
|
|
|
|
|
|
html += `<div class="error" style="margin-top: 8px;">许可列表加载失败:${escapeHtml(snapshotState.permitOptionsError)}</div>`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const timelineItems = buildTimelineItems(snapshotState.snapshots, checkpointList);
|
|
|
|
|
|
const totalSnapshots = snapshotState.total || snapshotState.snapshots.length;
|
|
|
|
|
|
const hasTimelineData = timelineItems.length > 0;
|
|
|
|
|
|
const loadingInProgress = (snapshotState.loading && snapshotState.snapshots.length === 0) && (checkpointListLoading && checkpointList.length === 0);
|
|
|
|
|
|
|
|
|
|
|
|
if (loadingInProgress) {
|
|
|
|
|
|
html += '<div class="loading"></div>加载许可风险快照与检查点...';
|
|
|
|
|
|
} else if (!hasTimelineData && (snapshotState.error || checkpointListError)) {
|
|
|
|
|
|
const errorMsg = snapshotState.error || checkpointListError || '暂无数据';
|
|
|
|
|
|
html += `<div class="error">${escapeHtml(errorMsg)}</div>`;
|
|
|
|
|
|
} else if (!hasTimelineData) {
|
|
|
|
|
|
html += '<p style="color: #999; text-align: center; padding: 20px;">暂无快照或检查点记录</p>';
|
|
|
|
|
|
} else {
|
|
|
|
|
|
const snapshotStart = totalSnapshots ? snapshotState.offset + 1 : 0;
|
|
|
|
|
|
const snapshotEnd = totalSnapshots ? Math.min(snapshotState.offset + snapshotState.limit, totalSnapshots) : 0;
|
|
|
|
|
|
const disablePrev = snapshotState.offset === 0 ? 'disabled' : '';
|
|
|
|
|
|
const disableNext = snapshotState.offset + snapshotState.limit >= totalSnapshots ? 'disabled' : '';
|
|
|
|
|
|
|
|
|
|
|
|
html += '<div class="timeline-list">';
|
|
|
|
|
|
|
|
|
|
|
|
timelineItems.forEach(entry => {
|
|
|
|
|
|
if (entry.type === 'snapshot') {
|
|
|
|
|
|
const item = entry.raw;
|
|
|
|
|
|
const permitName = escapeHtml(item.permit_name || '-');
|
|
|
|
|
|
const regionName = escapeHtml(item.region_name || '-');
|
|
|
|
|
|
const riskFull = item.risk_content || '';
|
|
|
|
|
|
const riskPreview = escapeHtml(truncateText(riskFull, 160));
|
|
|
|
|
|
const legalSegments = [];
|
|
|
|
|
|
if (item.legal_basis) {
|
|
|
|
|
|
legalSegments.push(`📕 ${escapeHtml(item.legal_basis)}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (item.document_no) {
|
|
|
|
|
|
legalSegments.push(`📄 ${escapeHtml(item.document_no)}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
const legalHtml = legalSegments.length ? `<div class="timeline-meta" style="gap:6px;">${legalSegments.join('<span>|</span>')}</div>` : '';
|
|
|
|
|
|
const statusHtml = item.permit_status ? `<span class="snapshot-status-tag">${escapeHtml(item.permit_status)}</span>` : '';
|
|
|
|
|
|
const changeSummary = item.change_summary ? `<div class="timeline-note">备注:${escapeHtml(item.change_summary)}</div>` : '';
|
|
|
|
|
|
const editorName = escapeHtml(item.edited_by || '—');
|
|
|
|
|
|
const timeDisplay = escapeHtml(entry.timeText || '');
|
|
|
|
|
|
|
|
|
|
|
|
html += `
|
|
|
|
|
|
<div class="timeline-item timeline-item-snapshot">
|
|
|
|
|
|
<div class="timeline-icon">📝</div>
|
|
|
|
|
|
<div class="timeline-body">
|
|
|
|
|
|
<div class="timeline-header">
|
|
|
|
|
|
<div class="timeline-title">风险快照 · 版本 ${escapeHtml(String(item.version || 0))}</div>
|
|
|
|
|
|
<div class="timeline-time">${timeDisplay}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="timeline-subtitle">${permitName}<span style="margin-left: 6px; color: #666;">(${regionName})</span></div>
|
|
|
|
|
|
<div class="timeline-content" title="${escapeHtml(riskFull)}">${riskPreview || '—'}</div>
|
|
|
|
|
|
${legalHtml}
|
|
|
|
|
|
<div class="timeline-meta">
|
|
|
|
|
|
${statusHtml}
|
|
|
|
|
|
<span>编辑人:${editorName}</span>
|
|
|
|
|
|
<span>风险ID:${escapeHtml(item.risk_id || '-')}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
${changeSummary}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
`;
|
|
|
|
|
|
} else if (entry.type === 'checkpoint') {
|
|
|
|
|
|
const item = entry.raw;
|
|
|
|
|
|
const timeDisplay = escapeHtml(entry.timeText || '');
|
|
|
|
|
|
const description = item.description ? escapeHtml(item.description) : '无描述';
|
|
|
|
|
|
const tableCount = Object.keys(item.table_counts || {}).length;
|
|
|
|
|
|
const totalRows = item.total_rows || 0;
|
|
|
|
|
|
const checkpointIdRaw = item.checkpoint_id || '';
|
|
|
|
|
|
const checkpointIdDisplay = escapeHtml(checkpointIdRaw);
|
|
|
|
|
|
const checkpointIdJs = checkpointIdRaw.replace(/'/g, "\\'");
|
|
|
|
|
|
|
|
|
|
|
|
html += `
|
|
|
|
|
|
<div class="timeline-item timeline-item-checkpoint">
|
|
|
|
|
|
<div class="timeline-icon">🗂️</div>
|
|
|
|
|
|
<div class="timeline-body">
|
|
|
|
|
|
<div class="timeline-header">
|
|
|
|
|
|
<div class="timeline-title">数据库检查点</div>
|
|
|
|
|
|
<div class="timeline-time">${timeDisplay}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="timeline-subtitle">${checkpointIdDisplay}</div>
|
|
|
|
|
|
<div class="timeline-content">
|
|
|
|
|
|
<div class="timeline-note">${description}</div>
|
|
|
|
|
|
<div class="timeline-stats">
|
|
|
|
|
|
<span>📊 行数:<strong>${totalRows}</strong></span>
|
|
|
|
|
|
<span>📋 表数:<strong>${tableCount}</strong></span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="timeline-actions">
|
|
|
|
|
|
<button class="btn btn-danger btn-sm" onclick="confirmRestoreCheckpoint('${checkpointIdJs}')">
|
|
|
|
|
|
<span>🔄</span> 恢复
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button class="btn btn-warning btn-sm" onclick="deleteCheckpoint('${checkpointIdJs}')">
|
|
|
|
|
|
<span>🗑️</span> 删除
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
`;
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
html += '</div>';
|
|
|
|
|
|
|
|
|
|
|
|
const warningMessages = [];
|
|
|
|
|
|
if (snapshotState.error) {
|
|
|
|
|
|
warningMessages.push(`快照加载提示:${escapeHtml(snapshotState.error)}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (checkpointListError) {
|
|
|
|
|
|
warningMessages.push(`检查点加载提示:${escapeHtml(checkpointListError)}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (warningMessages.length) {
|
|
|
|
|
|
html += `<div class="error" style="margin-top: 10px;">${warningMessages.join('<br>')}</div>`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const snapshotSummaryText = totalSnapshots
|
|
|
|
|
|
? `快照 ${snapshotStart}-${snapshotEnd} / ${totalSnapshots}`
|
|
|
|
|
|
: '快照 0 / 0';
|
|
|
|
|
|
const checkpointSummaryText = `检查点 ${checkpointList.length} 个`;
|
|
|
|
|
|
|
|
|
|
|
|
html += `
|
|
|
|
|
|
<div class="timeline-footer">
|
|
|
|
|
|
<div class="snapshot-count">${snapshotSummaryText},${checkpointSummaryText}</div>
|
|
|
|
|
|
<div class="snapshot-pagination">
|
|
|
|
|
|
<button class="btn btn-warning btn-sm" onclick="changeSnapshotPage(-1)" ${disablePrev}>上一页</button>
|
|
|
|
|
|
<button class="btn btn-warning btn-sm" onclick="changeSnapshotPage(1)" ${disableNext}>下一页</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
html += '</div>';
|
|
|
|
|
|
|
2025-10-30 13:47:13 +08:00
|
|
|
|
html += `
|
|
|
|
|
|
<div class="checkpoint-info" style="background: #e3f2fd; padding: 15px; border-radius: 6px; margin-bottom: 20px; border-left: 4px solid #2196f3;">
|
|
|
|
|
|
<div style="color: #1976d2; font-size: 13px; line-height: 1.6;">
|
2025-11-03 11:30:38 +08:00
|
|
|
|
<strong>💡 说明:</strong>数据库检查点文件保存在服务器的 data/checkpoints/ 目录中,重启应用后不会丢失。
|
|
|
|
|
|
每个检查点包含完整的数据库备份,可用于快速回滚。
|
2025-10-30 13:47:13 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
`;
|
|
|
|
|
|
|
2025-10-30 10:33:35 +08:00
|
|
|
|
html += `
|
|
|
|
|
|
<div class="checkpoint-section">
|
|
|
|
|
|
<h3>创建新检查点</h3>
|
|
|
|
|
|
<div class="checkpoint-form">
|
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
|
<label for="checkpointDescription">检查点描述(可选):</label>
|
|
|
|
|
|
<input type="text" id="checkpointDescription" placeholder="例如:修改风险提示前的备份">
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<button class="btn btn-primary" onclick="createCheckpoint()">
|
|
|
|
|
|
<span>📸</span> 创建检查点
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
`;
|
|
|
|
|
|
|
2025-11-03 11:30:38 +08:00
|
|
|
|
html += '</div>';
|
|
|
|
|
|
|
|
|
|
|
|
modalBody.innerHTML = html;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function ensureRegionFilterOptions(showSpinner = true) {
|
|
|
|
|
|
permitRiskSnapshotState.regionLoading = true;
|
|
|
|
|
|
permitRiskSnapshotState.regionError = '';
|
|
|
|
|
|
if (showSpinner) {
|
|
|
|
|
|
renderCheckpointManager(checkpointListCache);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch(`/fs-ai-asistant/api/workflow/lawrisk/admin/regions?t=${Date.now()}`);
|
|
|
|
|
|
const data = await parseJsonResponse(response);
|
|
|
|
|
|
|
|
|
|
|
|
if (data && data.success) {
|
|
|
|
|
|
permitRiskSnapshotState.regionOptions = (data.data.regions || []).map(region => ({
|
|
|
|
|
|
id: region.id,
|
|
|
|
|
|
name: region.name
|
|
|
|
|
|
}));
|
|
|
|
|
|
permitRiskSnapshotState.regionError = '';
|
|
|
|
|
|
} else {
|
|
|
|
|
|
permitRiskSnapshotState.regionOptions = [];
|
|
|
|
|
|
permitRiskSnapshotState.regionError = data && data.message ? data.message : `地区列表加载失败(HTTP ${response.status})`;
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
permitRiskSnapshotState.regionOptions = [];
|
|
|
|
|
|
permitRiskSnapshotState.regionError = error.message || '地区列表加载失败';
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
permitRiskSnapshotState.regionLoading = false;
|
|
|
|
|
|
renderCheckpointManager(checkpointListCache);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function fetchPermitOptionsForRegion(regionId, showSpinner = true) {
|
|
|
|
|
|
permitRiskSnapshotState.permitLoading = true;
|
|
|
|
|
|
permitRiskSnapshotState.permitOptions = [];
|
|
|
|
|
|
permitRiskSnapshotState.permitOptionsError = '';
|
|
|
|
|
|
if (showSpinner) {
|
|
|
|
|
|
renderCheckpointManager(checkpointListCache);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!regionId) {
|
|
|
|
|
|
permitRiskSnapshotState.permitLoading = false;
|
|
|
|
|
|
renderCheckpointManager(checkpointListCache);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch(`/fs-ai-asistant/api/workflow/lawrisk/getPermits?region=${regionId}&t=${Date.now()}`);
|
|
|
|
|
|
const data = await parseJsonResponse(response);
|
|
|
|
|
|
|
|
|
|
|
|
if (data && data.success) {
|
|
|
|
|
|
const permits = data.data && data.data.permits ? data.data.permits : [];
|
|
|
|
|
|
permitRiskSnapshotState.permitOptions = permits.map(permit => ({
|
|
|
|
|
|
id: permit.id,
|
|
|
|
|
|
name: permit.name
|
|
|
|
|
|
}));
|
|
|
|
|
|
permitRiskSnapshotState.permitOptionsError = '';
|
|
|
|
|
|
} else {
|
|
|
|
|
|
permitRiskSnapshotState.permitOptions = [];
|
|
|
|
|
|
permitRiskSnapshotState.permitOptionsError = data && data.message ? data.message : `许可列表加载失败(HTTP ${response.status})`;
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
permitRiskSnapshotState.permitOptions = [];
|
|
|
|
|
|
permitRiskSnapshotState.permitOptionsError = error.message || '许可列表加载失败';
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
permitRiskSnapshotState.permitLoading = false;
|
|
|
|
|
|
renderCheckpointManager(checkpointListCache);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function handleSnapshotRegionChange(value) {
|
|
|
|
|
|
let normalizedValue = (value || '').trim();
|
|
|
|
|
|
if (normalizedValue === '__loading') {
|
|
|
|
|
|
normalizedValue = '';
|
|
|
|
|
|
}
|
|
|
|
|
|
permitRiskSnapshotState.regionFilter = normalizedValue;
|
|
|
|
|
|
permitRiskSnapshotState.permitFilter = '';
|
|
|
|
|
|
permitRiskSnapshotState.offset = 0;
|
|
|
|
|
|
permitRiskSnapshotState.permitOptions = [];
|
|
|
|
|
|
permitRiskSnapshotState.permitOptionsError = '';
|
|
|
|
|
|
|
|
|
|
|
|
renderCheckpointManager(checkpointListCache);
|
|
|
|
|
|
|
|
|
|
|
|
if (normalizedValue) {
|
|
|
|
|
|
await fetchPermitOptionsForRegion(normalizedValue, true);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-10-30 10:33:35 +08:00
|
|
|
|
|
2025-11-03 11:30:38 +08:00
|
|
|
|
function handleSnapshotPermitChange(value) {
|
|
|
|
|
|
let normalizedValue = (value || '').trim();
|
|
|
|
|
|
if (normalizedValue === '__placeholder' || normalizedValue === '__loading') {
|
|
|
|
|
|
normalizedValue = '';
|
|
|
|
|
|
}
|
|
|
|
|
|
permitRiskSnapshotState.permitFilter = normalizedValue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function refreshPermitRiskSnapshots(showSpinner = true) {
|
|
|
|
|
|
if (showSpinner) {
|
|
|
|
|
|
permitRiskSnapshotState.loading = true;
|
|
|
|
|
|
permitRiskSnapshotState.error = '';
|
|
|
|
|
|
renderCheckpointManager(checkpointListCache);
|
2025-10-30 10:33:35 +08:00
|
|
|
|
} else {
|
2025-11-03 11:30:38 +08:00
|
|
|
|
permitRiskSnapshotState.loading = true;
|
|
|
|
|
|
permitRiskSnapshotState.error = '';
|
2025-10-30 10:33:35 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-03 11:30:38 +08:00
|
|
|
|
const params = new URLSearchParams({
|
|
|
|
|
|
limit: permitRiskSnapshotState.limit,
|
|
|
|
|
|
offset: permitRiskSnapshotState.offset
|
|
|
|
|
|
});
|
|
|
|
|
|
if (permitRiskSnapshotState.regionFilter) {
|
|
|
|
|
|
params.append('region_id', permitRiskSnapshotState.regionFilter);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (permitRiskSnapshotState.permitFilter) {
|
|
|
|
|
|
params.append('permit_id', permitRiskSnapshotState.permitFilter);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (permitRiskSnapshotState.editorFilter) {
|
|
|
|
|
|
params.append('edited_by', permitRiskSnapshotState.editorFilter);
|
|
|
|
|
|
}
|
|
|
|
|
|
params.append('t', Date.now());
|
2025-10-30 10:33:35 +08:00
|
|
|
|
|
2025-11-03 11:30:38 +08:00
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch(`/fs-ai-asistant/api/workflow/lawrisk/admin/permit-risk-snapshots?${params.toString()}`);
|
|
|
|
|
|
const data = await parseJsonResponse(response);
|
|
|
|
|
|
|
|
|
|
|
|
if (data && data.success) {
|
|
|
|
|
|
permitRiskSnapshotState.snapshots = data.data.snapshots || [];
|
|
|
|
|
|
const pagination = data.data.pagination || {};
|
|
|
|
|
|
permitRiskSnapshotState.total = pagination.total ?? permitRiskSnapshotState.snapshots.length;
|
|
|
|
|
|
permitRiskSnapshotState.limit = pagination.limit ?? permitRiskSnapshotState.limit;
|
|
|
|
|
|
permitRiskSnapshotState.offset = pagination.offset ?? permitRiskSnapshotState.offset;
|
|
|
|
|
|
permitRiskSnapshotState.error = '';
|
|
|
|
|
|
} else {
|
|
|
|
|
|
permitRiskSnapshotState.snapshots = [];
|
|
|
|
|
|
permitRiskSnapshotState.total = 0;
|
|
|
|
|
|
permitRiskSnapshotState.error = data && data.message ? data.message : `加载失败(HTTP ${response.status})`;
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
permitRiskSnapshotState.snapshots = [];
|
|
|
|
|
|
permitRiskSnapshotState.total = 0;
|
|
|
|
|
|
permitRiskSnapshotState.error = error.message || '网络错误';
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
permitRiskSnapshotState.loading = false;
|
|
|
|
|
|
renderCheckpointManager(checkpointListCache);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function refreshCheckpointList(showSpinner = true) {
|
|
|
|
|
|
if (showSpinner) {
|
|
|
|
|
|
checkpointListLoading = true;
|
|
|
|
|
|
checkpointListError = '';
|
|
|
|
|
|
renderCheckpointManager(checkpointListCache);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
checkpointListLoading = true;
|
|
|
|
|
|
checkpointListError = '';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch(`/fs-ai-asistant/api/workflow/lawrisk/admin/checkpoints?t=${Date.now()}`);
|
|
|
|
|
|
const data = await parseJsonResponse(response);
|
|
|
|
|
|
|
|
|
|
|
|
if (data && data.success) {
|
|
|
|
|
|
checkpointListCache = data.data.checkpoints || [];
|
|
|
|
|
|
checkpointListError = '';
|
|
|
|
|
|
} else {
|
|
|
|
|
|
checkpointListError = data && data.message ? data.message : `加载失败(HTTP ${response.status})`;
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
checkpointListError = error.message || '网络错误';
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
checkpointListLoading = false;
|
|
|
|
|
|
renderCheckpointManager(checkpointListCache);
|
|
|
|
|
|
}
|
2025-10-30 10:33:35 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-03 11:30:38 +08:00
|
|
|
|
function applySnapshotFilters(event) {
|
|
|
|
|
|
if (event) {
|
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
|
}
|
|
|
|
|
|
const regionInput = document.getElementById('snapshotFilterRegion');
|
|
|
|
|
|
const permitInput = document.getElementById('snapshotFilterPermit');
|
|
|
|
|
|
const editorInput = document.getElementById('snapshotFilterEditor');
|
|
|
|
|
|
|
|
|
|
|
|
let regionValue = regionInput ? regionInput.value.trim() : '';
|
|
|
|
|
|
if (regionValue === '__loading') {
|
|
|
|
|
|
regionValue = '';
|
|
|
|
|
|
}
|
|
|
|
|
|
let permitValue = permitInput ? permitInput.value.trim() : '';
|
|
|
|
|
|
if (permitValue === '__placeholder' || permitValue === '__loading') {
|
|
|
|
|
|
permitValue = '';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
permitRiskSnapshotState.regionFilter = regionValue;
|
|
|
|
|
|
permitRiskSnapshotState.permitFilter = permitValue;
|
|
|
|
|
|
permitRiskSnapshotState.editorFilter = editorInput ? editorInput.value.trim() : '';
|
|
|
|
|
|
permitRiskSnapshotState.offset = 0;
|
|
|
|
|
|
|
|
|
|
|
|
refreshPermitRiskSnapshots();
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function resetSnapshotFilters() {
|
|
|
|
|
|
permitRiskSnapshotState.regionFilter = '';
|
|
|
|
|
|
permitRiskSnapshotState.permitFilter = '';
|
|
|
|
|
|
permitRiskSnapshotState.editorFilter = '';
|
|
|
|
|
|
permitRiskSnapshotState.offset = 0;
|
|
|
|
|
|
permitRiskSnapshotState.permitOptions = [];
|
|
|
|
|
|
permitRiskSnapshotState.permitOptionsError = '';
|
|
|
|
|
|
|
|
|
|
|
|
const regionInput = document.getElementById('snapshotFilterRegion');
|
|
|
|
|
|
const permitInput = document.getElementById('snapshotFilterPermit');
|
|
|
|
|
|
const editorInput = document.getElementById('snapshotFilterEditor');
|
|
|
|
|
|
if (regionInput) regionInput.value = '';
|
|
|
|
|
|
if (permitInput) permitInput.value = '';
|
|
|
|
|
|
if (editorInput) editorInput.value = '';
|
|
|
|
|
|
|
|
|
|
|
|
refreshPermitRiskSnapshots();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function changeSnapshotPage(direction) {
|
|
|
|
|
|
if (permitRiskSnapshotState.loading) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const step = direction > 0 ? permitRiskSnapshotState.limit : -permitRiskSnapshotState.limit;
|
|
|
|
|
|
let nextOffset = permitRiskSnapshotState.offset + step;
|
|
|
|
|
|
|
|
|
|
|
|
if (direction < 0) {
|
|
|
|
|
|
nextOffset = Math.max(0, nextOffset);
|
|
|
|
|
|
} else if (permitRiskSnapshotState.offset + permitRiskSnapshotState.limit >= permitRiskSnapshotState.total) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
permitRiskSnapshotState.offset = Math.max(0, nextOffset);
|
|
|
|
|
|
await refreshPermitRiskSnapshots();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-10-30 10:33:35 +08:00
|
|
|
|
// 创建检查点
|
|
|
|
|
|
async function createCheckpoint() {
|
|
|
|
|
|
const description = document.getElementById('checkpointDescription').value;
|
|
|
|
|
|
const btn = event.target;
|
|
|
|
|
|
const originalText = btn.innerHTML;
|
|
|
|
|
|
btn.disabled = true;
|
|
|
|
|
|
btn.innerHTML = '<div class="loading"></div>创建中...';
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch('/fs-ai-asistant/api/workflow/lawrisk/admin/checkpoints', {
|
|
|
|
|
|
method: 'POST',
|
|
|
|
|
|
headers: {
|
|
|
|
|
|
'Content-Type': 'application/json'
|
|
|
|
|
|
},
|
|
|
|
|
|
body: JSON.stringify({ description })
|
|
|
|
|
|
});
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
|
|
|
|
|
|
if (data.success) {
|
|
|
|
|
|
document.getElementById('checkpointDescription').value = '';
|
2025-11-03 11:30:38 +08:00
|
|
|
|
await refreshCheckpointList();
|
2025-10-30 13:47:13 +08:00
|
|
|
|
// 显示成功消息
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
alert(`✅ 检查点创建成功!\n\nID: ${data.data.checkpoint_id}\n备份了 ${data.data.total_rows} 行数据`);
|
|
|
|
|
|
}, 100);
|
2025-10-30 10:33:35 +08:00
|
|
|
|
} else {
|
2025-10-30 13:47:13 +08:00
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
alert(`❌ 创建失败:${data.message}`);
|
|
|
|
|
|
}, 100);
|
2025-10-30 10:33:35 +08:00
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
2025-10-30 13:47:13 +08:00
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
alert(`❌ 网络错误:${error.message}`);
|
|
|
|
|
|
}, 100);
|
2025-10-30 10:33:35 +08:00
|
|
|
|
} finally {
|
|
|
|
|
|
btn.disabled = false;
|
|
|
|
|
|
btn.innerHTML = originalText;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 确认恢复检查点
|
|
|
|
|
|
function confirmRestoreCheckpoint(checkpointId) {
|
|
|
|
|
|
showDangerModal(
|
|
|
|
|
|
'恢复检查点',
|
|
|
|
|
|
`您确定要恢复检查点 "${checkpointId}" 吗?`,
|
|
|
|
|
|
'此操作将覆盖当前数据库中的所有数据,且无法撤销!请确保您已经创建了新的检查点作为备份。',
|
|
|
|
|
|
() => restoreCheckpoint(checkpointId)
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 恢复检查点
|
|
|
|
|
|
async function restoreCheckpoint(checkpointId) {
|
|
|
|
|
|
closeDangerModal();
|
|
|
|
|
|
|
2025-10-30 13:52:19 +08:00
|
|
|
|
// 显示恢复进度模态框
|
|
|
|
|
|
showRestoreProgressModal(checkpointId);
|
|
|
|
|
|
|
2025-10-30 10:33:35 +08:00
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch(`/fs-ai-asistant/api/workflow/lawrisk/admin/checkpoints/${checkpointId}/restore`, {
|
|
|
|
|
|
method: 'POST'
|
|
|
|
|
|
});
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
|
2025-10-30 13:52:19 +08:00
|
|
|
|
// 关闭进度模态框
|
|
|
|
|
|
closeRestoreProgressModal();
|
|
|
|
|
|
|
2025-10-30 10:33:35 +08:00
|
|
|
|
if (data.success) {
|
2025-11-03 11:30:38 +08:00
|
|
|
|
await Promise.all([
|
|
|
|
|
|
refreshCheckpointList(),
|
|
|
|
|
|
refreshPermitRiskSnapshots()
|
|
|
|
|
|
]);
|
2025-10-30 13:47:13 +08:00
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
alert(`✅ 检查点恢复成功!\n\n恢复了 ${data.data.total_rows_restored} 行数据,覆盖了 ${data.data.tables_restored} 个表。`);
|
|
|
|
|
|
}, 100);
|
2025-10-30 10:33:35 +08:00
|
|
|
|
} else {
|
2025-10-30 13:47:13 +08:00
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
alert(`❌ 恢复失败:${data.message}`);
|
|
|
|
|
|
}, 100);
|
2025-10-30 10:33:35 +08:00
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
2025-10-30 13:52:19 +08:00
|
|
|
|
closeRestoreProgressModal();
|
2025-10-30 13:47:13 +08:00
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
alert(`❌ 网络错误:${error.message}`);
|
|
|
|
|
|
}, 100);
|
2025-10-30 10:33:35 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-30 13:52:19 +08:00
|
|
|
|
// 显示恢复进度模态框
|
|
|
|
|
|
function showRestoreProgressModal(checkpointId) {
|
|
|
|
|
|
const modal = document.createElement('div');
|
|
|
|
|
|
modal.id = 'restoreProgressModal';
|
|
|
|
|
|
modal.className = 'modal show';
|
|
|
|
|
|
modal.innerHTML = `
|
|
|
|
|
|
<div class="modal-content" style="max-width: 450px; text-align: center; padding: 40px;">
|
|
|
|
|
|
<div style="margin-bottom: 20px;">
|
|
|
|
|
|
<div class="loading" style="width: 50px; height: 50px; border-width: 4px; margin: 0 auto;"></div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<h3 style="color: #333; margin-bottom: 15px;">正在恢复检查点...</h3>
|
|
|
|
|
|
<p style="color: #666; font-size: 14px; line-height: 1.6;">
|
|
|
|
|
|
正在从备份恢复数据库<br>
|
|
|
|
|
|
<strong>${checkpointId}</strong><br>
|
|
|
|
|
|
<span style="color: #999; font-size: 12px; margin-top: 10px; display: block;">
|
|
|
|
|
|
此操作可能需要几分钟时间,请耐心等待...
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
`;
|
|
|
|
|
|
document.body.appendChild(modal);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 关闭恢复进度模态框
|
|
|
|
|
|
function closeRestoreProgressModal() {
|
|
|
|
|
|
const modal = document.getElementById('restoreProgressModal');
|
|
|
|
|
|
if (modal) {
|
|
|
|
|
|
modal.remove();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-30 10:33:35 +08:00
|
|
|
|
// 删除检查点
|
|
|
|
|
|
async function deleteCheckpoint(checkpointId) {
|
|
|
|
|
|
if (!confirm(`确定要删除检查点 "${checkpointId}" 吗?此操作无法撤销。`)) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch(`/fs-ai-asistant/api/workflow/lawrisk/admin/checkpoints/${checkpointId}`, {
|
|
|
|
|
|
method: 'DELETE'
|
|
|
|
|
|
});
|
2025-11-03 11:30:38 +08:00
|
|
|
|
const data = await parseJsonResponse(response);
|
2025-10-30 10:33:35 +08:00
|
|
|
|
|
|
|
|
|
|
if (data.success) {
|
2025-11-03 11:30:38 +08:00
|
|
|
|
await refreshCheckpointList();
|
2025-10-30 13:47:13 +08:00
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
alert(`✅ 检查点已删除`);
|
|
|
|
|
|
}, 100);
|
2025-10-30 10:33:35 +08:00
|
|
|
|
} else {
|
2025-10-30 13:47:13 +08:00
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
alert(`❌ 删除失败:${data.message}`);
|
|
|
|
|
|
}, 100);
|
2025-10-30 10:33:35 +08:00
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
2025-10-30 13:47:13 +08:00
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
alert(`❌ 网络错误:${error.message}`);
|
|
|
|
|
|
}, 100);
|
2025-10-30 10:33:35 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-03 11:30:38 +08:00
|
|
|
|
function truncateText(text, maxLength = 120) {
|
|
|
|
|
|
if (text === null || text === undefined) return '';
|
|
|
|
|
|
const str = String(text);
|
|
|
|
|
|
return str.length > maxLength ? `${str.slice(0, maxLength)}…` : str;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function formatIsoDatetime(value) {
|
|
|
|
|
|
if (!value) return '';
|
|
|
|
|
|
const date = new Date(value);
|
|
|
|
|
|
if (Number.isNaN(date.getTime())) {
|
|
|
|
|
|
return value;
|
|
|
|
|
|
}
|
|
|
|
|
|
const pad = (num) => String(num).padStart(2, '0');
|
|
|
|
|
|
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function escapeHtml(str) {
|
|
|
|
|
|
if (str === null || str === undefined) return '';
|
|
|
|
|
|
return String(str)
|
|
|
|
|
|
.replace(/&/g, '&')
|
|
|
|
|
|
.replace(/</g, '<')
|
|
|
|
|
|
.replace(/>/g, '>')
|
|
|
|
|
|
.replace(/"/g, '"')
|
|
|
|
|
|
.replace(/'/g, ''');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function parseJsonResponse(response) {
|
|
|
|
|
|
const contentType = response.headers.get('content-type') || '';
|
|
|
|
|
|
if (!contentType.includes('application/json')) {
|
|
|
|
|
|
await response.text();
|
|
|
|
|
|
return {
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: `服务器返回非 JSON 格式(HTTP ${response.status})`
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
|
|
|
return await response.json();
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
return {
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: err.message || '响应解析失败'
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function checkpointTimestampToIso(timestamp) {
|
|
|
|
|
|
if (!timestamp) return '';
|
|
|
|
|
|
const match = String(timestamp).match(/(\d{4})(\d{2})(\d{2})_(\d{2})(\d{2})(\d{2})/);
|
|
|
|
|
|
if (!match) return '';
|
|
|
|
|
|
return `${match[1]}-${match[2]}-${match[3]}T${match[4]}:${match[5]}:${match[6]}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function buildTimelineItems(snapshotItems, checkpointItems) {
|
|
|
|
|
|
const items = [];
|
|
|
|
|
|
|
|
|
|
|
|
(snapshotItems || []).forEach(item => {
|
|
|
|
|
|
const iso = item.created_at || '';
|
|
|
|
|
|
const timeValue = Date.parse(iso) || 0;
|
|
|
|
|
|
items.push({
|
|
|
|
|
|
type: 'snapshot',
|
|
|
|
|
|
timeValue,
|
|
|
|
|
|
timeText: iso ? formatIsoDatetime(iso) : '',
|
|
|
|
|
|
raw: item
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
(checkpointItems || []).forEach(item => {
|
|
|
|
|
|
const iso = checkpointTimestampToIso(item.timestamp);
|
|
|
|
|
|
const timeValue = iso ? Date.parse(iso) : 0;
|
|
|
|
|
|
items.push({
|
|
|
|
|
|
type: 'checkpoint',
|
|
|
|
|
|
timeValue,
|
|
|
|
|
|
timeText: formatTimestamp(item.timestamp),
|
|
|
|
|
|
raw: item,
|
|
|
|
|
|
iso
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
items.sort((a, b) => b.timeValue - a.timeValue);
|
|
|
|
|
|
return items;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-30 10:33:35 +08:00
|
|
|
|
// 格式化时间戳
|
|
|
|
|
|
function formatTimestamp(timestamp) {
|
2025-11-03 11:30:38 +08:00
|
|
|
|
if (!timestamp || timestamp.length < 15) {
|
|
|
|
|
|
return timestamp || '';
|
|
|
|
|
|
}
|
2025-10-30 10:33:35 +08:00
|
|
|
|
const year = timestamp.substring(0, 4);
|
|
|
|
|
|
const month = timestamp.substring(4, 6);
|
|
|
|
|
|
const day = timestamp.substring(6, 8);
|
|
|
|
|
|
const hour = timestamp.substring(9, 11);
|
|
|
|
|
|
const minute = timestamp.substring(11, 13);
|
|
|
|
|
|
const second = timestamp.substring(13, 15);
|
|
|
|
|
|
return `${year}-${month}-${day} ${hour}:${minute}:${second}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ================ 危险操作确认模态框 ================
|
|
|
|
|
|
|
|
|
|
|
|
// 显示危险操作确认模态框
|
|
|
|
|
|
function showDangerModal(title, message, warning, callback) {
|
|
|
|
|
|
pendingDangerOperation = callback;
|
|
|
|
|
|
document.getElementById('dangerModalTitle').textContent = title;
|
|
|
|
|
|
document.getElementById('dangerModalMessage').textContent = message;
|
|
|
|
|
|
document.getElementById('dangerModalWarning').textContent = warning;
|
|
|
|
|
|
document.getElementById('dangerModal').classList.add('show');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 关闭危险操作确认模态框
|
|
|
|
|
|
function closeDangerModal() {
|
|
|
|
|
|
document.getElementById('dangerModal').classList.remove('show');
|
|
|
|
|
|
pendingDangerOperation = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 确认执行危险操作
|
|
|
|
|
|
function confirmDangerOperation() {
|
|
|
|
|
|
if (pendingDangerOperation && typeof pendingDangerOperation === 'function') {
|
|
|
|
|
|
pendingDangerOperation();
|
|
|
|
|
|
}
|
|
|
|
|
|
closeDangerModal();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-30 08:52:48 +08:00
|
|
|
|
// 页面加载时初始化
|
|
|
|
|
|
window.addEventListener('DOMContentLoaded', () => {
|
2025-10-30 09:54:36 +08:00
|
|
|
|
goToStep(1);
|
2025-10-30 08:52:48 +08:00
|
|
|
|
});
|
|
|
|
|
|
</script>
|
|
|
|
|
|
</body>
|
|
|
|
|
|
</html>
|