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;
}
2025-11-03 16:41:35 +08:00
.snapshot-detail-list {
margin-top: 12px;
display: none;
border-left: 2px solid #e0e0e0;
padding-left: 16px;
}
.snapshot-detail-list.expanded {
display: block;
}
.snapshot-detail-item {
background: #ffffff;
border: 1px solid #e5e7eb;
border-radius: 6px;
padding: 10px 12px;
margin-bottom: 10px;
box-shadow: 0 1px 2px rgba(0,0,0,0.05);
}
.snapshot-detail-item:last-child {
margin-bottom: 0;
}
.snapshot-detail-header {
font-size: 13px;
font-weight: 600;
color: #333;
margin-bottom: 6px;
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.snapshot-detail-meta {
font-size: 12px;
color: #666;
display: flex;
gap: 10px;
flex-wrap: wrap;
}
2025-11-03 11:30:38 +08:00
.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;
}
2025-11-04 13:38:21 +08:00
.import-modal-content {
width: 760px;
max-height: 90vh;
overflow-y: auto;
}
2025-10-30 10:33:35 +08:00
@keyframes modalFadeIn {
from {
opacity: 0;
transform: translateY(-20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
2025-11-04 13:38:21 +08:00
.import-section {
margin-bottom: 18px;
}
.import-section h3 {
font-size: 16px;
margin-bottom: 8px;
color: #333;
display: flex;
align-items: center;
gap: 6px;
}
.import-upload-area {
border: 1px dashed #9fa8da;
padding: 16px;
border-radius: 8px;
background: #f5f7ff;
display: flex;
flex-direction: column;
gap: 8px;
}
.import-upload-area input[type="file"] {
background: #fff;
padding: 10px;
border-radius: 6px;
border: 1px solid #c5cae9;
}
.import-meta {
font-size: 13px;
color: #555;
line-height: 1.5;
}
.import-sheet-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.import-sheet-card {
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 12px 14px;
background: #fafafa;
}
.import-sheet-card.selected {
border-color: #667eea;
background: #eef2ff;
}
.import-sheet-header {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
margin-bottom: 10px;
}
.import-sheet-title {
display: flex;
align-items: center;
gap: 8px;
font-weight: 600;
color: #3949ab;
}
.import-sheet-meta {
font-size: 12px;
color: #666;
}
.import-duplicate-panel {
background: #fff;
border-radius: 6px;
border: 1px solid #e0e0e0;
padding: 10px;
display: flex;
flex-direction: column;
gap: 6px;
}
.import-duplicate-title {
font-size: 12px;
color: #d84315;
font-weight: 600;
}
.import-checkbox {
display: flex;
align-items: center;
gap: 6px;
font-size: 13px;
color: #333;
}
.import-checkbox input[type="checkbox"] {
transform: scale(1.05);
}
.import-messages {
font-size: 13px;
line-height: 1.6;
}
.import-success {
color: #256029;
background: #e3f2e1;
padding: 8px 10px;
border-radius: 6px;
margin-bottom: 10px;
}
.import-error {
color: #c62828;
background: #ffebee;
padding: 8px 10px;
border-radius: 6px;
margin-bottom: 10px;
}
.import-form-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px;
margin-top: 10px;
}
.import-form-grid textarea {
grid-column: span 2;
resize: vertical;
min-height: 68px;
}
.import-form-grid input,
.import-form-grid textarea {
width: 100%;
padding: 8px 10px;
border: 1px solid #c5cae9;
border-radius: 6px;
font-size: 13px;
font-family: inherit;
}
.import-actions {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 16px;
gap: 16px;
}
.import-hint {
font-size: 12px;
color: #666;
line-height: 1.4;
}
2025-10-30 10:33:35 +08:00
.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;
}
2025-11-03 16:41:35 +08:00
.detail-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
margin-bottom: 18px;
border-radius: 8px;
background: #eef2ff;
border: 1px solid #d7dbff;
}
.detail-meta {
color: #3f51b5;
font-size: 14px;
}
.detail-meta strong {
font-size: 16px;
margin: 0 4px;
}
.detail-actions {
display: flex;
gap: 12px;
align-items: center;
}
2025-10-30 08:52:48 +08:00
.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" >
2025-11-04 13:38:21 +08:00
< button class = "btn btn-primary" onclick = "openImportModal()" >
< span > 📥< / span > 许可导入
< / button >
2025-10-30 11:48:15 +08:00
< 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-11-04 13:38:21 +08:00
<!-- 许可导入模态窗口 -->
< div class = "modal" id = "importModal" >
< div class = "modal-content import-modal-content" >
< div class = "modal-header" style = "display:flex; justify-content: space-between; align-items: center;" >
< h3 style = "color:#3949ab; margin:0; display:flex; align-items:center; gap:8px;" >
< span > 📥< / span > 许可导入( Excel)
< / h3 >
< button class = "btn btn-warning btn-sm" onclick = "closeImportModal()" > 关闭< / button >
< / div >
< div class = "modal-body" id = "importModalBody" > < / 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 16:41:35 +08:00
let currentPermit = null;
let currentPermitDetails = null;
let pendingDangerOperation = null; // 待执行的危险操作
let isDeletingPermit = false;
2025-11-03 11:30:38 +08:00
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-11-03 16:41:35 +08:00
let expandedSnapshotGroups = new Set();
2025-10-30 08:52:48 +08:00
2025-11-04 13:38:21 +08:00
const permitImportState = {
uploading: false,
sessionId: '',
filename: '',
totalRows: 0,
sheetSummaries: [],
selectedSheets: new Set(),
overrides: new Map(),
error: '',
success: '',
commitLoading: false,
editedBy: '',
changeSummary: ''
};
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;
2025-11-03 16:41:35 +08:00
currentPermitDetails = null;
2025-10-30 09:54:36 +08:00
// 更新步骤
goToStep(2);
}
// 选择主题
async function selectTheme(themeId, themeName) {
// 保存到历史栈
historyStack.push({ step: currentStep, theme: currentTheme });
currentTheme = { id: themeId, name: themeName };
currentPermit = null;
2025-11-03 16:41:35 +08:00
currentPermitDetails = null;
2025-10-30 09:54:36 +08:00
// 更新步骤
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 };
2025-11-03 16:41:35 +08:00
currentPermitDetails = null;
2025-10-30 09:54:36 +08:00
// 更新步骤
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;
}
if (targetStep < = 3) {
currentPermit = null;
2025-11-03 16:41:35 +08:00
currentPermitDetails = null;
2025-10-30 09:54:36 +08:00
}
goToStep(targetStep);
}
// 返回首页
function goHome() {
currentRegion = null;
currentTheme = null;
currentPermit = null;
2025-11-03 16:41:35 +08:00
currentPermitDetails = null;
2025-10-30 09:54:36 +08:00
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');
2025-11-03 16:41:35 +08:00
currentPermitDetails = permit;
const riskCount = Array.isArray(permit.risks) ? permit.risks.length : 0;
if (currentPermit) {
currentPermit = { ...currentPermit, riskCount };
}
const deleteButtonLabel = isDeletingPermit ? '删除中...' : '删除许可';
const deleteButtonDisabled = isDeletingPermit ? 'disabled' : '';
2025-10-30 08:52:48 +08:00
let html = '< div class = "details-content" > ';
2025-11-03 16:41:35 +08:00
html += `
< div class = "detail-toolbar" >
< div class = "detail-meta" >
风险条目:< strong > ${riskCount}< / strong > 个
< / div >
< div class = "detail-actions" >
< button class = "btn btn-danger" id = "deletePermitBtn" $ { deleteButtonDisabled } onclick = "confirmDeleteCurrentPermit()" > ${deleteButtonLabel}< / button >
< / div >
< / div >
`;
2025-10-30 08:52:48 +08:00
// 许可基本信息
html += `
< div class = "detail-section" >
< h3 > 许可信息< / h3 >
< div class = "detail-content" >
< p > < strong > 许可名称:< / strong > ${permit.name}< / p >
2025-11-04 13:38:21 +08:00
${permit.permit_source & & permit.permit_source.source_name ? `< p style = "margin-top: 10px;" > < strong > 数据来源:< / strong > ${escapeHtml(permit.permit_source.source_name)}< / p > ` : ''}
2025-10-30 08:52:48 +08:00
${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;
}
2025-11-03 16:41:35 +08:00
function confirmDeleteCurrentPermit() {
if (isDeletingPermit) {
return;
}
if (!currentRegion || !currentTheme || !currentPermit) {
alert('请先选择要删除的许可');
return;
}
const riskCount = currentPermit.riskCount !== undefined
? currentPermit.riskCount
: (currentPermitDetails & & Array.isArray(currentPermitDetails.risks) ? currentPermitDetails.risks.length : 0);
const confirmMessage = `确定要删除「${currentRegion.name} › ${currentTheme.name} › ${currentPermit.name}」吗?\n\n` +
`此操作会删除 ${riskCount} 条风险关联(如存在)。系统会备份当前风险快照,但删除后需通过快照管理页面手动恢复。`;
if (!confirm(confirmMessage)) {
return;
}
const summaryInput = prompt('请输入删除说明(可选,用于快照对比):', '');
if (summaryInput === null) {
return;
}
const changeSummary = summaryInput.trim();
deleteCurrentPermit(changeSummary);
}
async function deleteCurrentPermit(changeSummary) {
if (isDeletingPermit) {
return;
}
if (!currentRegion || !currentTheme || !currentPermit) {
alert('当前上下文缺失,无法删除');
return;
}
isDeletingPermit = true;
toggleDeletePermitButton(true);
try {
const payload = {
region_id: currentRegion.id,
theme_id: currentTheme.id,
permit_id: currentPermit.id
};
if (changeSummary) {
payload.change_summary = changeSummary;
}
const response = await fetch('/fs-ai-asistant/api/workflow/lawrisk/admin/permits', {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const data = await response.json();
if (response.ok & & data.success) {
const snapshotCount = data.data & & typeof data.data.snapshot_count === 'number'
? data.data.snapshot_count
: 0;
const deletedRisks = data.data & & data.data.deleted_rows
? (data.data.deleted_rows.region_permit_risks || 0)
: 0;
const remainingPermits = data.data & & typeof data.data.remaining_theme_permits === 'number'
? data.data.remaining_theme_permits
: null;
const themeDetached = !!(data.data & & data.data.theme_detached);
const snapshotBatchId = data.data & & data.data.snapshot_batch_id;
let successMessage = `✅ 删除成功!\n\n已备份 ${snapshotCount} 条风险快照,并删除 ${deletedRisks} 条风险关联。`;
if (themeDetached) {
successMessage += '\n对应主题已与该地区解除关联。';
} else if (remainingPermits !== null) {
successMessage += `\n该主题在此地区仍剩余 ${remainingPermits} 个许可。`;
}
if (snapshotBatchId) {
successMessage += `\n快照批次: ${snapshotBatchId}`;
}
alert(successMessage);
const modal = document.getElementById('checkpointModal');
if (modal & & modal.classList.contains('show')) {
if ((!permitRiskSnapshotState.regionFilter || permitRiskSnapshotState.regionFilter === currentRegion.id) & &
(!permitRiskSnapshotState.permitFilter || permitRiskSnapshotState.permitFilter === currentPermit.id)) {
await refreshPermitRiskSnapshots(false);
}
}
currentPermitDetails = null;
quickJump(3);
} else {
const message = data & & data.message ? data.message : `删除失败( HTTP ${response.status}) `;
alert(`❌ 删除失败:${message}`);
}
} catch (error) {
alert(`❌ 删除失败:${error.message}`);
} finally {
isDeletingPermit = false;
toggleDeletePermitButton(false);
}
}
function toggleDeletePermitButton(disabled) {
const btn = document.getElementById('deletePermitBtn');
if (!btn) {
return;
}
btn.disabled = disabled;
btn.textContent = disabled ? '删除中...' : '删除许可';
}
2025-10-30 08:52:48 +08:00
// 更新步骤指示器
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-11-04 13:38:21 +08:00
// ================ 许可导入功能 ================
function openImportModal() {
const modal = document.getElementById('importModal');
if (!modal) return;
modal.classList.add('show');
if (!permitImportState.sessionId) {
permitImportState.selectedSheets = new Set();
permitImportState.overrides = new Map();
permitImportState.error = '';
permitImportState.success = '';
}
renderImportModal();
}
function closeImportModal() {
const modal = document.getElementById('importModal');
if (!modal) return;
modal.classList.remove('show');
}
function toggleSheetSelection(sheetName, forceChecked) {
if (!sheetName) return;
const shouldSelect = typeof forceChecked === 'boolean'
? forceChecked
: !permitImportState.selectedSheets.has(sheetName);
if (shouldSelect) {
permitImportState.selectedSheets.add(sheetName);
} else {
permitImportState.selectedSheets.delete(sheetName);
permitImportState.overrides.delete(sheetName);
}
renderImportModal();
}
function toggleSheetSelectionFromEvent(el) {
if (!el || !el.dataset) return;
toggleSheetSelection(el.dataset.sheet || '', el.checked);
}
function toggleOverridePermit(sheetName, permitName, forceChecked) {
if (!sheetName || !permitName) return;
if (!permitImportState.overrides.has(sheetName)) {
permitImportState.overrides.set(sheetName, new Set());
}
const set = permitImportState.overrides.get(sheetName);
const shouldCheck = typeof forceChecked === 'boolean'
? forceChecked
: !set.has(permitName);
if (shouldCheck) {
set.add(permitName);
} else {
set.delete(permitName);
}
if (set.size === 0) {
permitImportState.overrides.delete(sheetName);
}
renderImportModal();
}
function toggleOverridePermitFromEvent(el) {
if (!el || !el.dataset) return;
toggleOverridePermit(el.dataset.sheet || '', el.dataset.permit || '', el.checked);
}
function updateImportEditedBy(value) {
permitImportState.editedBy = value;
}
function updateImportChangeSummary(value) {
permitImportState.changeSummary = value;
}
async function handleImportFile(input) {
if (!input || !input.files || !input.files.length) {
return;
}
const file = input.files[0];
const formData = new FormData();
formData.append('file', file);
permitImportState.uploading = true;
permitImportState.error = '';
permitImportState.success = '';
renderImportModal();
try {
const response = await fetch('/fs-ai-asistant/api/workflow/lawrisk/admin/permit-import/upload', {
method: 'POST',
body: formData
});
const data = await parseJsonResponse(response);
if (data.success) {
const payload = data.data || {};
permitImportState.sessionId = payload.session_id || payload.sessionId || '';
permitImportState.filename = payload.filename || (file & & file.name) || '';
permitImportState.totalRows = payload.total_rows || payload.totalRows || 0;
permitImportState.sheetSummaries = payload.sheet_summaries || payload.sheetSummaries || [];
permitImportState.selectedSheets = new Set(
(permitImportState.sheetSummaries || []).map(item => item.sheet_name)
);
permitImportState.overrides = new Map();
permitImportState.success = `解析完成:${permitImportState.sheetSummaries.length} 个 Sheet, ${permitImportState.totalRows} 条风险记录`;
} else {
permitImportState.error = data.message || '解析失败, 请检查Excel格式';
permitImportState.sessionId = '';
permitImportState.sheetSummaries = [];
permitImportState.selectedSheets = new Set();
permitImportState.overrides = new Map();
}
} catch (error) {
permitImportState.error = error.message || '文件上传失败';
permitImportState.sessionId = '';
permitImportState.sheetSummaries = [];
permitImportState.selectedSheets = new Set();
permitImportState.overrides = new Map();
} finally {
permitImportState.uploading = false;
renderImportModal();
}
}
async function submitImport() {
if (!permitImportState.sessionId) {
permitImportState.error = '请先上传并解析Excel文件';
renderImportModal();
return;
}
if (permitImportState.selectedSheets.size === 0) {
permitImportState.error = '请选择至少一个Sheet进行导入';
renderImportModal();
return;
}
const payload = {
session_id: permitImportState.sessionId,
sheet_names: Array.from(permitImportState.selectedSheets),
overrides: {},
};
permitImportState.overrides.forEach((set, sheet) => {
if (set.size) {
payload.overrides[sheet] = Array.from(set);
}
});
if (permitImportState.editedBy & & permitImportState.editedBy.trim()) {
payload.edited_by = permitImportState.editedBy.trim();
}
if (permitImportState.changeSummary & & permitImportState.changeSummary.trim()) {
payload.change_summary = permitImportState.changeSummary.trim();
}
permitImportState.commitLoading = true;
permitImportState.error = '';
permitImportState.success = '';
renderImportModal();
try {
const response = await fetch('/fs-ai-asistant/api/workflow/lawrisk/admin/permit-import/commit', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const data = await parseJsonResponse(response);
if (data.success) {
const result = data.data || {};
const created = (result.created_permits || []).length;
const overwritten = (result.overwritten_permits || []).length;
permitImportState.success = `导入成功:新增 ${created} 个许可,覆盖 ${overwritten} 个许可。`;
permitImportState.sessionId = '';
permitImportState.sheetSummaries = [];
permitImportState.selectedSheets = new Set();
permitImportState.overrides = new Map();
await Promise.all([
refreshPermitRiskSnapshots(false),
refreshCheckpointList(false)
]);
// 如果当前在首页,刷新地区列表,确保新地区可见
if (currentStep === 1) {
await loadRegions();
}
} else {
permitImportState.error = data.message || '导入失败';
}
} catch (error) {
permitImportState.error = error.message || '导入失败';
} finally {
permitImportState.commitLoading = false;
renderImportModal();
}
}
function renderImportModal() {
const container = document.getElementById('importModalBody');
if (!container) return;
const state = permitImportState;
let html = '< div class = "import-section" > ';
html += '< h3 > < span > 📄< / span > 上传 Excel< / h3 > ';
html += '< div class = "import-upload-area" > ';
html += '< input type = "file" accept = ".xlsx,.xlsm" onchange = "handleImportFile(this)" > ';
if (state.sessionId) {
const sheetCount = state.sheetSummaries ? state.sheetSummaries.length : 0;
html += `< div class = "import-meta" > 当前会话:${escapeHtml(state.filename || '(未命名)')} | Sheet ${sheetCount} 个 | 风险 ${state.totalRows || 0} 条< / div > `;
} else {
html += '< div class = "import-meta" > 请选择包含许可数据的 Excel 文件,系统会自动解析所有 Sheet, 并以区划为单位生成导入任务。< / div > ';
}
html += '< / div > < / div > ';
if (state.error) {
html += `< div class = "import-error" > ${escapeHtml(state.error)}< / div > `;
}
if (state.success) {
html += `< div class = "import-success" > ${escapeHtml(state.success)}< / div > `;
}
if (state.uploading) {
html += '< div class = "loading" style = "margin: 8px 0;" > 正在解析 Excel...< / div > ';
}
if (state.sessionId & & state.sheetSummaries & & state.sheetSummaries.length) {
html += '< div class = "import-section" > ';
html += '< h3 > < span > 🗂️< / span > 选择导入的 Sheet< / h3 > ';
html += '< div class = "import-sheet-list" > ';
state.sheetSummaries.forEach(summary => {
const sheetName = summary.sheet_name || '';
const selected = state.selectedSheets.has(sheetName);
const duplicates = summary.duplicate_permits || summary.duplicatePermits || [];
const newPermits = summary.new_permits || summary.newPermits || [];
const overrideSet = state.overrides.get(sheetName) || new Set();
const missingRegion = summary.missing_region || summary.missingRegion;
const originalSheets = summary.original_sheet_names || summary.originalSheetNames || [];
const originalLabel = originalSheets.length
? originalSheets.map(name => escapeHtml(name)).join('、')
: escapeHtml(sheetName);
html += `< div class = "import-sheet-card ${selected ? 'selected' : ''}" > `;
html += '< div class = "import-sheet-header" > ';
html += `< label class = "import-checkbox" > < input type = "checkbox" data-sheet = "${escapeHtml(sheetName)}" $ { selected ? ' checked ' : ' ' } onchange = "toggleSheetSelectionFromEvent(this)" > < span class = "import-sheet-title" > ${escapeHtml(sheetName)}${missingRegion ? '< span style = "margin-left:6px;color:#c62828;font-size:12px;" > (新地区)< / span > ' : ''}< / span > < / label > `;
html += `< div class = "import-sheet-meta" > 许可 ${summary.permit_count || 0} | 风险 ${summary.risk_count || summary.row_count || 0}< / div > `;
html += '< / div > ';
html += `< div class = "import-sheet-submeta" > 来源 Sheet: ${originalLabel}< / div > `;
if (newPermits.length) {
html += `< div class = "import-meta" style = "margin-bottom:8px; color:#2e7d32;" > 新增许可 ${newPermits.length} 项< / div > `;
}
if (duplicates.length) {
html += '< div class = "import-duplicate-panel" > ';
html += '< div class = "import-duplicate-title" > 检测到已存在的许可,勾选代表同意覆盖:< / div > ';
duplicates.forEach(name => {
const checked = overrideSet.has(name);
const disabledAttr = selected ? '' : 'disabled';
html += `< label class = "import-checkbox" > < input type = "checkbox" data-sheet = "${escapeHtml(sheetName)}" data-permit = "${escapeHtml(name)}" $ { checked ? ' checked ' : ' ' } $ { disabledAttr } onchange = "toggleOverridePermitFromEvent(this)" > < span > ${escapeHtml(name)}< / span > < / label > `;
});
html += '< / div > ';
}
html += '< / div > ';
});
html += '< / div > < / div > ';
}
html += '< div class = "import-section" > ';
html += '< h3 > < span > 📝< / span > 导入说明< / h3 > ';
html += '< div class = "import-form-grid" > ';
html += `< input type = "text" placeholder = "编辑人(可选)" value = "${escapeHtml(state.editedBy)}" oninput = "updateImportEditedBy(this.value)" > `;
html += `< textarea placeholder = "变更摘要 / 导入说明(可选)" oninput = "updateImportChangeSummary(this.value)" > ${escapeHtml(state.changeSummary)}< / textarea > `;
html += '< / div > ';
html += '< / div > ';
const commitDisabled = !state.sessionId || state.selectedSheets.size === 0 || state.commitLoading;
const commitLabel = state.commitLoading ? '导入中...' : '开始导入';
html += '< div class = "import-actions" > ';
html += '< div class = "import-hint" > 导入前会自动创建风险快照,可在“检查点管理”中查看恢复。< / div > ';
html += `< button class = "btn btn-warning" onclick = "submitImport()" $ { commitDisabled ? ' disabled ' : ' ' } > ${commitLabel}< / button > `;
html += '< / div > ';
container.innerHTML = html;
}
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 = '';
2025-11-03 16:41:35 +08:00
expandedSnapshotGroups = new Set();
2025-11-03 11:30:38 +08:00
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();
}
});
2025-11-04 13:38:21 +08:00
document.getElementById('importModal').addEventListener('click', function(e) {
if (e.target === this) {
closeImportModal();
}
});
2025-10-30 11:48:15 +08:00
// 加载检查点列表(保持兼容性)
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 => {
2025-11-03 16:41:35 +08:00
if (entry.type === 'snapshot-group') {
const group = entry.raw || {};
const groupKey = group.groupKey || group.snapshot_batch_id || '';
const groupKeyJs = String(groupKey).replace(/'/g, "\\'");
const riskItems = group.items || [];
const riskCount = riskItems.length;
const primary = riskItems[0] || {};
const permitName = escapeHtml(group.permit_name || primary.permit_name || '-');
const regionName = escapeHtml(group.region_name || primary.region_name || '-');
const riskFull = primary.risk_content || '';
2025-11-03 11:30:38 +08:00
const riskPreview = escapeHtml(truncateText(riskFull, 160));
2025-11-04 13:38:21 +08:00
const sourceNameRaw = primary.permit_source_name || group.permit_source_name || '';
const sourceTag = sourceNameRaw ? `< span > 来源:${escapeHtml(sourceNameRaw)}< / span > ` : '';
2025-11-03 11:30:38 +08:00
const legalSegments = [];
2025-11-03 16:41:35 +08:00
if (primary.legal_basis) {
legalSegments.push(`📕 ${escapeHtml(primary.legal_basis)}`);
2025-11-03 11:30:38 +08:00
}
2025-11-03 16:41:35 +08:00
if (primary.document_no) {
legalSegments.push(`📄 ${escapeHtml(primary.document_no)}`);
2025-11-03 11:30:38 +08:00
}
const legalHtml = legalSegments.length ? `< div class = "timeline-meta" style = "gap:6px;" > ${legalSegments.join('< span > |< / span > ')}< / div > ` : '';
2025-11-03 16:41:35 +08:00
const editorsText = (group.editors & & group.editors.length)
? escapeHtml(group.editors.join('、'))
: '—';
const isExpanded = expandedSnapshotGroups.has(groupKey);
const expandIcon = isExpanded ? '▲' : '▼';
const toggleLabel = isExpanded ? '收起明细' : '展开明细';
const changeSummaryMerged = group.change_summaries & & group.change_summaries.length
? `< div class = "timeline-note" > 备注:${escapeHtml(group.change_summaries.join('; '))}< / div > `
: '';
const statusTag = primary.permit_status ? `< span class = "snapshot-status-tag" > ${escapeHtml(primary.permit_status)}< / span > ` : '';
const toggleButton = riskCount > 0
? `< button class = "btn btn-warning btn-sm" onclick = "toggleSnapshotGroup('${groupKeyJs}')" > ${expandIcon} ${toggleLabel}< / button > `
: '';
const restoreButton = `< button class = "btn btn-primary btn-sm" onclick = "confirmRestoreSnapshotBatch('${groupKeyJs}')" > < span > 🛠️< / span > 恢复< / button > `;
const detailItemsHtml = riskItems.map(detail => {
const detailLegal = [];
if (detail.legal_basis) {
detailLegal.push(`📕 ${escapeHtml(detail.legal_basis)}`);
}
if (detail.document_no) {
detailLegal.push(`📄 ${escapeHtml(detail.document_no)}`);
}
const detailMetaParts = [];
if (detail.edited_by) {
detailMetaParts.push(`编辑人:${escapeHtml(detail.edited_by)}`);
}
2025-11-04 13:38:21 +08:00
if (detail.permit_source_name) {
detailMetaParts.push(`来源:${escapeHtml(detail.permit_source_name)}`);
}
2025-11-03 16:41:35 +08:00
detailMetaParts.push(...detailLegal);
const detailMetaHtml = detailMetaParts.length
? `< div class = "snapshot-detail-meta" > ${detailMetaParts.join('< span > |< / span > ')}< / div > `
: '';
const detailStatusTag = detail.permit_status ? `< span class = "snapshot-status-tag" > ${escapeHtml(detail.permit_status)}< / span > ` : '';
const detailNote = detail.change_summary ? `< div class = "timeline-note" style = "margin-top:6px;" > 备注:${escapeHtml(detail.change_summary)}< / div > ` : '';
return `
< div class = "snapshot-detail-item" >
< div class = "snapshot-detail-header" >
< span > 版本 ${escapeHtml(String(detail.version || 0))}< / span >
< span > 风险ID: ${escapeHtml(detail.risk_id || '-')}< / span >
${detailStatusTag}
< / div >
< div class = "timeline-content" > ${escapeHtml(detail.risk_content || '—')}< / div >
${detailMetaHtml}
${detailNote}
< / div >
`;
}).join('');
const detailListHtml = (isExpanded & & riskCount > 0)
? `
< div class = "snapshot-detail-list expanded" >
${detailItemsHtml}
< / div >
`
: '';
const metaHtml = `
< div class = "timeline-meta" >
${statusTag}
< span > 风险条目:${riskCount} 个< / span >
< span > 编辑人:${editorsText}< / span >
2025-11-04 13:38:21 +08:00
${sourceTag}
2025-11-03 16:41:35 +08:00
< / div >
`;
2025-11-03 11:30:38 +08:00
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" >
2025-11-03 16:41:35 +08:00
< div class = "timeline-title" > 风险快照 · ${riskCount > 1 ? `批次(${riskCount} 条)` : `版本 ${escapeHtml(String(primary.version || 0))}`}< / div >
2025-11-03 11:30:38 +08:00
< 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}
2025-11-03 16:41:35 +08:00
${metaHtml}
${changeSummaryMerged}
< div class = "timeline-actions" >
${toggleButton}
${restoreButton}
2025-11-03 11:30:38 +08:00
< / div >
2025-11-03 16:41:35 +08:00
${detailListHtml}
2025-11-03 11:30:38 +08:00
< / 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 > `;
}
2025-11-03 16:41:35 +08:00
const snapshotGroupCount = new Set((snapshotState.snapshots || []).map(item => item.snapshot_batch_id || item.snapshot_id)).size;
const snapshotRangeText = totalSnapshots
? `风险快照 ${snapshotStart}-${snapshotEnd} / ${totalSnapshots}`
: '风险快照 0 / 0';
const snapshotBatchText = `快照批次 ${snapshotGroupCount} 个`;
2025-11-03 11:30:38 +08:00
const checkpointSummaryText = `检查点 ${checkpointList.length} 个`;
html += `
< div class = "timeline-footer" >
2025-11-03 16:41:35 +08:00
< div class = "snapshot-count" > ${snapshotBatchText}| ${snapshotRangeText}, ${checkpointSummaryText}< / div >
2025-11-03 11:30:38 +08:00
< 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;
2025-11-03 16:41:35 +08:00
expandedSnapshotGroups = new Set();
2025-11-03 11:30:38 +08:00
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 || '响应解析失败'
};
}
}
2025-11-03 16:41:35 +08:00
function getSnapshotGroupItems(batchId) {
if (!batchId) return [];
return (permitRiskSnapshotState.snapshots || []).filter(item => {
const key = item.snapshot_batch_id || item.snapshot_id;
return key === batchId;
});
}
function toggleSnapshotGroup(batchId) {
if (!batchId) return;
if (expandedSnapshotGroups.has(batchId)) {
expandedSnapshotGroups.delete(batchId);
} else {
expandedSnapshotGroups.add(batchId);
}
renderCheckpointManager(checkpointListCache);
}
function confirmRestoreSnapshotBatch(batchId) {
if (!batchId) {
alert('未找到对应的快照批次');
return;
}
const groupItems = getSnapshotGroupItems(batchId);
if (groupItems.length === 0) {
alert('未找到对应的快照记录');
return;
}
const primary = groupItems[0];
const regionName = primary.region_name || '未知地区';
const permitName = primary.permit_name || '未知许可';
const riskCount = groupItems.length;
const confirmMessage = `确定要从快照恢复「${regionName} › ${permitName}」吗?\n\n` +
`该操作将重新建立 ${riskCount} 条风险关联、许可明细以及相关主题/范围配置。`;
if (!confirm(confirmMessage)) {
return;
}
const summaryInput = prompt('请输入恢复说明(可选):', '');
if (summaryInput === null) {
return;
}
const changeSummary = summaryInput.trim();
restoreSnapshotBatch(batchId, changeSummary, groupItems);
}
async function restoreSnapshotBatch(batchId, changeSummary, groupItems) {
try {
const payload = {};
if (changeSummary) {
payload.change_summary = changeSummary;
}
const response = await fetch(`/fs-ai-asistant/api/workflow/lawrisk/admin/permit-risk-snapshots/${batchId}/restore`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const data = await parseJsonResponse(response);
if (data & & data.success) {
const restoredCount = data.data & & typeof data.data.restored_risk_count === 'number'
? data.data.restored_risk_count
: (groupItems ? groupItems.length : 0);
alert(`✅ 恢复成功!已恢复 ${restoredCount} 条风险关联。`);
await refreshPermitRiskSnapshots(false);
if (groupItems & & groupItems.length > 0) {
const targetRegionId = groupItems[0].region_id;
const targetPermitId = groupItems[0].permit_id;
if (currentRegion & & currentRegion.id === targetRegionId) {
if (currentTheme) {
await loadPermits(currentTheme.id, currentTheme.name);
}
if (currentPermit & & currentPermit.id === targetPermitId) {
await showPermitDetails();
}
}
}
} else {
const message = data & & data.message ? data.message : `恢复失败( HTTP ${response.status}) `;
alert(`❌ 恢复失败:${message}`);
}
} catch (error) {
alert(`❌ 恢复失败:${error.message}`);
}
}
2025-11-03 11:30:38 +08:00
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 = [];
2025-11-03 16:41:35 +08:00
const snapshotGroups = new Map();
2025-11-03 11:30:38 +08:00
(snapshotItems || []).forEach(item => {
2025-11-03 16:41:35 +08:00
const groupKey = item.snapshot_batch_id || item.snapshot_id;
if (!snapshotGroups.has(groupKey)) {
snapshotGroups.set(groupKey, {
key: groupKey,
items: [],
});
}
snapshotGroups.get(groupKey).items.push(item);
});
snapshotGroups.forEach(group => {
group.items.sort((a, b) => {
const timeA = Date.parse(a.created_at || '') || 0;
const timeB = Date.parse(b.created_at || '') || 0;
return timeB - timeA;
});
let latestIso = '';
let latestValue = 0;
group.items.forEach(item => {
const value = Date.parse(item.created_at || '') || 0;
if (value > latestValue) {
latestValue = value;
latestIso = item.created_at || '';
}
});
const firstItem = group.items[0] || {};
const displayIso = latestIso || firstItem.created_at || '';
const timeValue = displayIso ? Date.parse(displayIso) || 0 : latestValue;
const editors = Array.from(new Set(group.items.map(entry => entry.edited_by).filter(Boolean)));
const summaries = group.items.map(entry => entry.change_summary).filter(Boolean);
2025-11-03 11:30:38 +08:00
items.push({
2025-11-03 16:41:35 +08:00
type: 'snapshot-group',
2025-11-03 11:30:38 +08:00
timeValue,
2025-11-03 16:41:35 +08:00
timeText: displayIso ? formatIsoDatetime(displayIso) : '',
raw: {
groupKey: group.key,
created_at: displayIso,
items: group.items,
region_name: firstItem.region_name || '',
region_id: firstItem.region_id || '',
permit_name: firstItem.permit_name || '',
permit_id: firstItem.permit_id || '',
editors,
change_summaries: summaries,
snapshot_batch_id: firstItem.snapshot_batch_id || firstItem.snapshot_id || group.key,
},
2025-11-03 11:30:38 +08:00
});
});
(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 >