6986 lines
274 KiB
HTML
6986 lines
274 KiB
HTML
<!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, #2c5282 0%, #1e3a5f 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 {
|
||
position: relative;
|
||
text-align: center;
|
||
margin-bottom: 20px;
|
||
padding-bottom: 20px;
|
||
border-bottom: 3px solid #2c5282;
|
||
}
|
||
|
||
.header-info {
|
||
max-width: 720px;
|
||
margin: 0 auto;
|
||
}
|
||
|
||
.header h1 {
|
||
color: #333;
|
||
font-size: 28px;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.header p {
|
||
color: #666;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.tabs-container {
|
||
margin-bottom: 0;
|
||
border-bottom: 2px solid #e5e7eb;
|
||
}
|
||
|
||
.tabs-nav {
|
||
display: flex;
|
||
gap: 4px;
|
||
padding: 0 20px;
|
||
list-style: none;
|
||
margin: 0;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.tab-button {
|
||
padding: 12px 24px;
|
||
border: none;
|
||
background: transparent;
|
||
color: #6b7280;
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
cursor: pointer;
|
||
border-bottom: 3px solid transparent;
|
||
margin-bottom: -2px;
|
||
transition: all 0.3s;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.tab-button:hover {
|
||
color: #2c5282;
|
||
background: #f7fafc;
|
||
}
|
||
|
||
.tab-button.active {
|
||
color: #2c5282;
|
||
border-bottom-color: #2c5282;
|
||
background: #f7fafc;
|
||
}
|
||
|
||
.tab-badge {
|
||
background: #e2e8f0;
|
||
color: #2c5282;
|
||
padding: 2px 8px;
|
||
border-radius: 12px;
|
||
font-size: 11px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.tab-content {
|
||
display: none;
|
||
padding: 24px 20px;
|
||
animation: fadeIn 0.3s;
|
||
}
|
||
|
||
.tab-content.active {
|
||
display: block;
|
||
}
|
||
|
||
@keyframes fadeIn {
|
||
from {
|
||
opacity: 0;
|
||
transform: translateY(10px);
|
||
}
|
||
to {
|
||
opacity: 1;
|
||
transform: translateY(0);
|
||
}
|
||
}
|
||
|
||
@keyframes slideInRight {
|
||
from {
|
||
opacity: 0;
|
||
transform: translateX(100px);
|
||
}
|
||
to {
|
||
opacity: 1;
|
||
transform: translateX(0);
|
||
}
|
||
}
|
||
|
||
@keyframes fadeOut {
|
||
from {
|
||
opacity: 1;
|
||
transform: translateX(0);
|
||
}
|
||
to {
|
||
opacity: 0;
|
||
transform: translateX(100px);
|
||
}
|
||
}
|
||
|
||
.user-bar {
|
||
position: absolute;
|
||
right: 24px;
|
||
top: 50%;
|
||
transform: translateY(-50%);
|
||
padding: 12px 16px;
|
||
border: 1px solid #e5e7eb;
|
||
border-radius: 16px;
|
||
background: rgba(255, 255, 255, 0.98);
|
||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 14px;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.user-bar:hover {
|
||
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.16);
|
||
border-color: #d1d5db;
|
||
}
|
||
|
||
.user-avatar {
|
||
width: 44px;
|
||
height: 44px;
|
||
border-radius: 50%;
|
||
background: linear-gradient(135deg, #2c5282 0%, #1e3a5f 100%);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: white;
|
||
font-weight: 600;
|
||
font-size: 18px;
|
||
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.user-info {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 3px;
|
||
color: #1f2937;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.user-name {
|
||
font-weight: 600;
|
||
font-size: 15px;
|
||
color: #111827;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.user-role {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
background: linear-gradient(135deg, #f7fafc 0%, #e2e8f0 100%);
|
||
color: #2c5282;
|
||
padding: 3px 10px;
|
||
border-radius: 999px;
|
||
font-size: 11px;
|
||
font-weight: 500;
|
||
letter-spacing: 0.02em;
|
||
}
|
||
|
||
.user-time {
|
||
font-size: 11px;
|
||
color: #6b7280;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
}
|
||
|
||
.user-alert {
|
||
font-size: 12px;
|
||
color: #f59e0b;
|
||
margin-top: 2px;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.user-actions {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.user-actions button {
|
||
border: none;
|
||
border-radius: 10px;
|
||
padding: 8px 14px;
|
||
font-size: 13px;
|
||
cursor: pointer;
|
||
transition: all 0.25s ease;
|
||
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
|
||
color: white;
|
||
box-shadow: 0 4px 12px rgba(239, 68, 68, 0.3);
|
||
font-weight: 500;
|
||
}
|
||
|
||
.user-actions button:hover {
|
||
transform: translateY(-1px);
|
||
box-shadow: 0 6px 16px rgba(239, 68, 68, 0.4);
|
||
}
|
||
|
||
.user-actions button:active {
|
||
transform: translateY(0);
|
||
}
|
||
|
||
.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: #f7fafc;
|
||
color: #2c5282;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-weight: bold;
|
||
font-size: 16px;
|
||
}
|
||
|
||
.step.active .step-number {
|
||
background: #2c5282;
|
||
color: white;
|
||
}
|
||
|
||
.step-label {
|
||
font-size: 14px;
|
||
color: #666;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.step.active .step-label {
|
||
color: #2c5282;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.arrow {
|
||
color: #ccc;
|
||
font-size: 24px;
|
||
}
|
||
|
||
.content-area {
|
||
display: grid;
|
||
grid-template-columns: 350px 1fr;
|
||
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 #2c5282;
|
||
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: #2c5282;
|
||
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;
|
||
}
|
||
|
||
.selection-area {
|
||
background: white;
|
||
border-radius: 8px;
|
||
padding: 20px;
|
||
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;
|
||
}
|
||
|
||
.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: #f7fafc;
|
||
border-color: #2c5282;
|
||
transform: translateX(5px);
|
||
}
|
||
|
||
.item-list li.active {
|
||
background: #2c5282;
|
||
color: white;
|
||
border-color: #2c5282;
|
||
}
|
||
|
||
.item-list li.active:hover {
|
||
background: #5568d3;
|
||
}
|
||
|
||
.item-name {
|
||
font-size: 15px;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.item-tag {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
margin-left: 8px;
|
||
padding: 2px 8px;
|
||
border-radius: 999px;
|
||
background: rgba(102, 126, 234, 0.15);
|
||
font-size: 11px;
|
||
color: #2c5282;
|
||
}
|
||
|
||
.theme-tags {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 6px;
|
||
}
|
||
|
||
.item-count {
|
||
font-size: 12px;
|
||
background: rgba(102, 126, 234, 0.1);
|
||
padding: 4px 10px;
|
||
border-radius: 12px;
|
||
color: #2c5282;
|
||
}
|
||
|
||
.item-list li.active .item-count {
|
||
background: rgba(255, 255, 255, 0.2);
|
||
color: white;
|
||
}
|
||
|
||
.import-preview-sheet {
|
||
border: 1px solid #e5e7eb;
|
||
border-radius: 12px;
|
||
padding: 16px;
|
||
margin-top: 16px;
|
||
background: #fdfdfd;
|
||
}
|
||
|
||
.import-preview-sheet.disabled {
|
||
opacity: 0.6;
|
||
}
|
||
|
||
.preview-sheet-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.preview-sheet-title {
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
color: #1f2937;
|
||
}
|
||
|
||
.preview-sheet-meta {
|
||
font-size: 13px;
|
||
color: #6b7280;
|
||
}
|
||
|
||
.preview-permit-card {
|
||
border: 1px solid #f7fafc;
|
||
border-radius: 10px;
|
||
padding: 12px;
|
||
margin-top: 12px;
|
||
background: #f8faff;
|
||
}
|
||
|
||
.preview-permit-card.warning {
|
||
border-color: #fbbf24;
|
||
background: #fff8e1;
|
||
}
|
||
|
||
.preview-permit-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
gap: 10px;
|
||
}
|
||
|
||
.preview-permit-name {
|
||
font-weight: 600;
|
||
color: #111827;
|
||
}
|
||
|
||
.preview-badge {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
padding: 2px 8px;
|
||
border-radius: 999px;
|
||
font-size: 11px;
|
||
background: #eef2ff;
|
||
color: #4c1d95;
|
||
}
|
||
|
||
.preview-badge.duplicate {
|
||
background: #fee2e2;
|
||
color: #b91c1c;
|
||
}
|
||
|
||
.preview-permit-meta {
|
||
font-size: 12px;
|
||
color: #4b5563;
|
||
margin-top: 6px;
|
||
}
|
||
|
||
.theme-options {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
margin-top: 10px;
|
||
}
|
||
|
||
.theme-checkbox {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
font-size: 13px;
|
||
background: #fff;
|
||
border: 1px solid #e5e7eb;
|
||
border-radius: 999px;
|
||
padding: 4px 10px;
|
||
}
|
||
|
||
.theme-checkbox input {
|
||
width: 14px;
|
||
height: 14px;
|
||
}
|
||
|
||
.theme-source-badge {
|
||
font-size: 11px;
|
||
color: #6b7280;
|
||
}
|
||
|
||
.preview-empty {
|
||
padding: 12px;
|
||
border-radius: 8px;
|
||
background: #f3f4f6;
|
||
color: #6b7280;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.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: #2c5282;
|
||
font-size: 18px;
|
||
margin-bottom: 15px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
}
|
||
|
||
.checkpoint-section h3::before {
|
||
content: '🔒';
|
||
font-size: 20px;
|
||
}
|
||
|
||
.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 #2c5282;
|
||
}
|
||
|
||
.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;
|
||
}
|
||
|
||
.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;
|
||
}
|
||
|
||
.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;
|
||
}
|
||
|
||
.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: #2c5282;
|
||
}
|
||
|
||
.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: #2c5282;
|
||
color: white;
|
||
}
|
||
|
||
.btn-primary:hover:not(:disabled) {
|
||
background: #5568d3;
|
||
}
|
||
|
||
.btn-secondary {
|
||
background: #e2e8f0;
|
||
color: #334155;
|
||
}
|
||
|
||
.btn-secondary:hover:not(:disabled) {
|
||
background: #cbd5f5;
|
||
}
|
||
|
||
.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: #2c5282;
|
||
}
|
||
|
||
.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);
|
||
z-index: 9999;
|
||
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;
|
||
}
|
||
|
||
.import-modal-content {
|
||
width: 760px;
|
||
max-height: 90vh;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
@keyframes modalFadeIn {
|
||
from {
|
||
opacity: 0;
|
||
transform: translateY(-20px);
|
||
}
|
||
to {
|
||
opacity: 1;
|
||
transform: translateY(0);
|
||
}
|
||
}
|
||
|
||
.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-template-wrapper {
|
||
display: flex;
|
||
justify-content: flex-start;
|
||
margin-top: 4px;
|
||
}
|
||
|
||
.import-template-button {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
padding: 12px 18px;
|
||
border-radius: 10px;
|
||
background: linear-gradient(135deg, #5c6bc0, #3949ab);
|
||
color: #fff;
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
text-decoration: none;
|
||
box-shadow: 0 8px 22px rgba(57, 73, 171, 0.35);
|
||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||
transform: translateY(0);
|
||
transition: transform 0.2s ease, box-shadow 0.3s ease, background 0.3s ease;
|
||
}
|
||
|
||
.import-template-button:hover {
|
||
background: linear-gradient(135deg, #283593, #1a237e);
|
||
text-decoration: none;
|
||
box-shadow: 0 10px 26px rgba(26, 35, 126, 0.45);
|
||
transform: translateY(-1px);
|
||
}
|
||
|
||
.import-template-icon {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 38px;
|
||
height: 38px;
|
||
border-radius: 50%;
|
||
background: rgba(255, 255, 255, 0.25);
|
||
font-size: 18px;
|
||
}
|
||
|
||
.import-template-text {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
line-height: 1.2;
|
||
}
|
||
|
||
.import-template-title {
|
||
font-size: 15px;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.import-template-subtitle {
|
||
font-size: 12px;
|
||
font-weight: 400;
|
||
opacity: 0.85;
|
||
}
|
||
|
||
.import-template-badge {
|
||
margin-left: auto;
|
||
padding: 4px 10px;
|
||
border-radius: 999px;
|
||
background: rgba(255, 255, 255, 0.25);
|
||
color: #fff;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
letter-spacing: 1px;
|
||
}
|
||
|
||
.drag-drop-area {
|
||
border: 2px dashed #9fa8da;
|
||
border-radius: 12px;
|
||
padding: 40px;
|
||
text-align: center;
|
||
background: #f5f7ff;
|
||
cursor: pointer;
|
||
transition: all 0.3s ease;
|
||
position: relative;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.drag-drop-area:hover {
|
||
border-color: #2c5282;
|
||
background: #f0f2ff;
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 8px 20px rgba(102, 126, 234, 0.15);
|
||
}
|
||
|
||
.drag-drop-area.drag-over {
|
||
border-color: #2c5282;
|
||
border-style: solid;
|
||
background: #e8ebff;
|
||
transform: scale(1.02);
|
||
}
|
||
|
||
.drag-drop-area.drag-over::before {
|
||
content: '';
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background: linear-gradient(135deg, rgba(102, 126, 234, 0.1) 0%, rgba(118, 75, 162, 0.1) 100%);
|
||
animation: pulse 1.5s ease-in-out infinite;
|
||
}
|
||
|
||
@keyframes pulse {
|
||
0%, 100% {
|
||
opacity: 0.3;
|
||
}
|
||
50% {
|
||
opacity: 0.5;
|
||
}
|
||
}
|
||
|
||
.drag-drop-icon {
|
||
font-size: 48px;
|
||
margin-bottom: 16px;
|
||
opacity: 0.6;
|
||
}
|
||
|
||
.drag-drop-text {
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
color: #2c5282;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.drag-drop-hint {
|
||
font-size: 13px;
|
||
color: #666;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.drag-drop-or {
|
||
font-size: 12px;
|
||
color: #999;
|
||
margin: 16px 0;
|
||
position: relative;
|
||
}
|
||
|
||
.drag-drop-or::before,
|
||
.drag-drop-or::after {
|
||
content: '';
|
||
position: absolute;
|
||
top: 50%;
|
||
width: 30%;
|
||
height: 1px;
|
||
background: #ddd;
|
||
}
|
||
|
||
.drag-drop-or::before {
|
||
left: 10%;
|
||
}
|
||
|
||
.drag-drop-or::after {
|
||
right: 10%;
|
||
}
|
||
|
||
.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: #2c5282;
|
||
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;
|
||
}
|
||
|
||
.import-modal-content.import-modal-wide {
|
||
width: 92vw;
|
||
max-width: 1500px;
|
||
}
|
||
|
||
.import-stage-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
gap: 16px;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.import-stage-header h3 {
|
||
margin: 0;
|
||
color: #1e293b;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.import-preview-summary {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 12px;
|
||
font-size: 13px;
|
||
color: #475569;
|
||
}
|
||
|
||
.import-preview-tabs {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
margin: 12px 0 18px;
|
||
}
|
||
|
||
.preview-tab {
|
||
padding: 8px 14px;
|
||
border-radius: 999px;
|
||
background: #f1f5f9;
|
||
color: #475569;
|
||
font-size: 13px;
|
||
border: 1px solid transparent;
|
||
cursor: pointer;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.preview-tab:hover {
|
||
background: #e2e8f0;
|
||
}
|
||
|
||
.preview-tab.active {
|
||
background: #eef2ff;
|
||
color: #1e3a5f;
|
||
border-color: #a5b4fc;
|
||
}
|
||
|
||
.preview-tab-badge {
|
||
background: rgba(67, 56, 202, 0.15);
|
||
color: #1e3a5f;
|
||
padding: 2px 8px;
|
||
border-radius: 999px;
|
||
font-size: 11px;
|
||
}
|
||
|
||
.preview-toolbar {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
||
gap: 12px;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.preview-toolbar label {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 6px;
|
||
font-size: 13px;
|
||
color: #475569;
|
||
}
|
||
|
||
.preview-toolbar input {
|
||
padding: 10px 12px;
|
||
border-radius: 10px;
|
||
border: 1px solid #dbeafe;
|
||
background: #fff;
|
||
font-size: 14px;
|
||
transition: border-color 0.2s ease;
|
||
}
|
||
|
||
.preview-toolbar input:focus {
|
||
outline: none;
|
||
border-color: #4a5568;
|
||
}
|
||
|
||
.preview-panels {
|
||
display: grid;
|
||
grid-template-columns: 1fr;
|
||
gap: 16px;
|
||
}
|
||
|
||
.preview-permit-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||
gap: 16px;
|
||
}
|
||
|
||
.preview-permit-card {
|
||
border: 1px solid #e2e8f0;
|
||
border-radius: 14px;
|
||
padding: 16px;
|
||
background: #fff;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 10px;
|
||
min-height: 220px;
|
||
}
|
||
|
||
.preview-permit-card.duplicate {
|
||
border-color: #fcd34d;
|
||
background: #fffbeb;
|
||
}
|
||
|
||
.preview-permit-card.new {
|
||
border-color: #bbf7d0;
|
||
background: #f0fdf4;
|
||
}
|
||
|
||
.preview-permit-header {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 6px;
|
||
}
|
||
|
||
.permit-badge {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
font-size: 12px;
|
||
padding: 2px 8px;
|
||
border-radius: 999px;
|
||
}
|
||
|
||
.permit-badge.duplicate {
|
||
background: rgba(249, 115, 22, 0.15);
|
||
color: #c2410c;
|
||
}
|
||
|
||
.permit-badge.new {
|
||
background: rgba(37, 99, 235, 0.12);
|
||
color: #1d4ed8;
|
||
}
|
||
|
||
.theme-chip-grid {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
min-height: 40px;
|
||
padding: 6px 0;
|
||
}
|
||
|
||
.theme-chip {
|
||
border: 1px solid #cbd5f5;
|
||
border-radius: 999px;
|
||
padding: 6px 12px;
|
||
font-size: 13px;
|
||
color: #1e3a5f;
|
||
background: #eef2ff;
|
||
cursor: pointer;
|
||
user-select: none;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
}
|
||
.theme-chip.all-theme {
|
||
border-color: #fcd34d;
|
||
background: #fffbeb;
|
||
color: #92400e;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.theme-chip.selected {
|
||
background: linear-gradient(135deg, #718096, #4a5568);
|
||
color: #fff;
|
||
border-color: transparent;
|
||
}
|
||
|
||
.theme-chip.drag-preview {
|
||
opacity: 0.7;
|
||
}
|
||
|
||
.theme-chip-actions {
|
||
display: flex;
|
||
gap: 8px;
|
||
margin-top: 8px;
|
||
}
|
||
|
||
.preview-empty-state {
|
||
padding: 24px;
|
||
border: 1px dashed #cbd5f5;
|
||
border-radius: 12px;
|
||
text-align: center;
|
||
background: #f8fafc;
|
||
color: #64748b;
|
||
}
|
||
|
||
.import-sheet-toolbar {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
gap: 12px;
|
||
margin-bottom: 12px;
|
||
font-size: 13px;
|
||
color: #475569;
|
||
}
|
||
|
||
.import-sheet-toolbar .toolbar-actions {
|
||
display: flex;
|
||
gap: 8px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.import-stage {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 16px;
|
||
}
|
||
|
||
.preview-stage-actions {
|
||
display: flex;
|
||
gap: 8px;
|
||
flex-wrap: wrap;
|
||
justify-content: flex-end;
|
||
}
|
||
|
||
.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;
|
||
}
|
||
|
||
.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 #2c5282;
|
||
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;
|
||
}
|
||
|
||
.file-manager-modal-content {
|
||
max-width: 960px;
|
||
width: 92%;
|
||
}
|
||
|
||
.file-manager-toolbar {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 12px;
|
||
align-items: center;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.file-manager-search {
|
||
flex: 1;
|
||
min-width: 220px;
|
||
}
|
||
|
||
.file-manager-search input {
|
||
width: 100%;
|
||
padding: 10px 12px;
|
||
border: 1px solid #d1d5db;
|
||
border-radius: 8px;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.file-manager-search input:focus {
|
||
outline: none;
|
||
border-color: #4a5568;
|
||
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
|
||
}
|
||
|
||
.file-manager-table {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
}
|
||
|
||
.file-manager-table th,
|
||
.file-manager-table td {
|
||
padding: 12px 10px;
|
||
border-bottom: 1px solid #f1f5f9;
|
||
text-align: left;
|
||
vertical-align: top;
|
||
}
|
||
|
||
.file-manager-table th {
|
||
font-size: 13px;
|
||
color: #6b7280;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.03em;
|
||
}
|
||
|
||
.file-manager-file-name {
|
||
font-weight: 600;
|
||
color: #111827;
|
||
font-size: 15px;
|
||
}
|
||
|
||
.file-manager-file-meta {
|
||
font-size: 12px;
|
||
color: #6b7280;
|
||
margin-top: 4px;
|
||
}
|
||
|
||
.file-manager-tags {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 6px;
|
||
}
|
||
|
||
.file-manager-tag {
|
||
background: #eef2ff;
|
||
color: #1e3a5f;
|
||
padding: 4px 10px;
|
||
border-radius: 999px;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.file-manager-tag-muted {
|
||
background: #f3f4f6;
|
||
color: #6b7280;
|
||
}
|
||
|
||
.file-manager-empty {
|
||
text-align: center;
|
||
padding: 40px 0;
|
||
color: #6b7280;
|
||
}
|
||
|
||
.file-manager-pagination {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-top: 18px;
|
||
font-size: 13px;
|
||
color: #4b5563;
|
||
}
|
||
|
||
.file-manager-pagination button {
|
||
border: none;
|
||
background: #f3f4f6;
|
||
color: #374151;
|
||
padding: 8px 14px;
|
||
border-radius: 8px;
|
||
cursor: pointer;
|
||
transition: background 0.2s ease;
|
||
}
|
||
|
||
.file-manager-pagination button:hover:not(:disabled) {
|
||
background: #e5e7eb;
|
||
}
|
||
|
||
.file-manager-pagination button:disabled {
|
||
opacity: 0.5;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
.file-manager-actions {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
}
|
||
|
||
.file-manager-toolbar button {
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.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);
|
||
}
|
||
}
|
||
|
||
@keyframes slideInRight {
|
||
from {
|
||
opacity: 0;
|
||
transform: translateX(100px);
|
||
}
|
||
to {
|
||
opacity: 1;
|
||
transform: translateX(0);
|
||
}
|
||
}
|
||
|
||
@keyframes fadeOut {
|
||
from {
|
||
opacity: 1;
|
||
transform: translateX(0);
|
||
}
|
||
to {
|
||
opacity: 0;
|
||
transform: translateX(100px);
|
||
}
|
||
}
|
||
|
||
.detail-section {
|
||
margin-bottom: 25px;
|
||
}
|
||
|
||
.detail-section h3 {
|
||
color: #2c5282;
|
||
font-size: 16px;
|
||
margin-bottom: 10px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.detail-section h3::before {
|
||
content: '';
|
||
width: 4px;
|
||
height: 16px;
|
||
background: #2c5282;
|
||
border-radius: 2px;
|
||
}
|
||
|
||
.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;
|
||
}
|
||
|
||
.detail-content {
|
||
background: #f8f9fa;
|
||
padding: 15px;
|
||
border-radius: 6px;
|
||
border-left: 3px solid #2c5282;
|
||
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 #2c5282;
|
||
}
|
||
|
||
.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;
|
||
}
|
||
|
||
.status-unknown {
|
||
background: #f1f5f9;
|
||
color: #475569;
|
||
}
|
||
|
||
.permit-file-name {
|
||
font-weight: 600;
|
||
color: #1d4ed8;
|
||
}
|
||
|
||
.muted-text {
|
||
color: #94a3b8;
|
||
font-size: 14px;
|
||
margin: 4px 0;
|
||
}
|
||
|
||
.loading {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
color: #475569;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.loading-icon {
|
||
width: 20px;
|
||
height: 20px;
|
||
border: 3px solid #f3f3f3;
|
||
border-top: 3px solid #2c5282;
|
||
border-radius: 50%;
|
||
animation: spin 1s linear infinite;
|
||
}
|
||
|
||
@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;
|
||
}
|
||
|
||
.panel:first-child {
|
||
max-height: 300px;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.user-bar {
|
||
position: static;
|
||
transform: none;
|
||
width: 100%;
|
||
justify-content: center;
|
||
padding: 14px 16px;
|
||
}
|
||
|
||
.user-info {
|
||
align-items: center;
|
||
text-align: center;
|
||
}
|
||
|
||
.user-actions {
|
||
justify-content: center;
|
||
}
|
||
|
||
.user-time {
|
||
justify-content: center;
|
||
}
|
||
|
||
.tabs-nav {
|
||
overflow-x: auto;
|
||
padding-bottom: 8px;
|
||
}
|
||
|
||
.tab-button {
|
||
white-space: nowrap;
|
||
}
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<div class="header">
|
||
<div class="header-info">
|
||
<h1 id="pageTitle">🗃️ 管理员控制台</h1>
|
||
<p>LawRisk 法律风险提示系统 - 管理员功能面板</p>
|
||
</div>
|
||
<div class="user-bar" id="userBar">
|
||
<div class="user-avatar" id="userAvatar">U</div>
|
||
<div class="user-info">
|
||
<div class="user-name" id="userDisplayName">--</div>
|
||
<span class="user-role" id="userRole">UNAUTH</span>
|
||
<div class="user-time" id="userTime" style="display: none;">
|
||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" style="flex-shrink: 0;">
|
||
<circle cx="6" cy="6" r="5.5" stroke="#9ca3af" stroke-width="1"/>
|
||
<path d="M6 3.5V6.5L8 8" stroke="#9ca3af" stroke-width="1" stroke-linecap="round"/>
|
||
</svg>
|
||
<span id="loginTime">--</span>
|
||
</div>
|
||
<div class="user-alert" id="userStatus"></div>
|
||
</div>
|
||
<div class="user-actions">
|
||
<button type="button" class="btn-logout" id="logoutBtn">退出登录</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 标签页导航 -->
|
||
<div class="tabs-container">
|
||
<ul class="tabs-nav" id="tabsNav">
|
||
<li><button class="tab-button active" data-tab="permits-tab" onclick="switchTab('permits-tab')">
|
||
<span>📋</span> 许可事项管理
|
||
</button></li>
|
||
<li><button class="tab-button" data-tab="checkpoints-tab" onclick="switchTab('checkpoints-tab')">
|
||
<span>🔒</span> 检查点管理
|
||
</button></li>
|
||
<li><button class="tab-button" data-tab="files-tab" onclick="switchTab('files-tab')">
|
||
<span>📁</span> 文件管理
|
||
</button></li>
|
||
<li><button class="tab-button" data-tab="import-tab" onclick="switchTab('import-tab')">
|
||
<span>📥</span> 许可导入
|
||
</button></li>
|
||
</ul>
|
||
</div>
|
||
|
||
<!-- 标签页内容区域 -->
|
||
|
||
<!-- 许可事项管理标签页 -->
|
||
<div id="permits-tab" class="tab-content active">
|
||
<h2 style="color: #333; margin-bottom: 20px; display: flex; align-items: center; gap: 10px;">
|
||
<span>📋</span> 许可事项管理
|
||
</h2>
|
||
<p style="color: #666; margin-bottom: 20px;">使用筛选器快速定位许可事项,支持多维度筛选、分页浏览、风险统计等功能。</p>
|
||
|
||
<!-- 筛选器区域 -->
|
||
<div style="background: #f8f9fa; border: 1px solid #e0e0e0; border-radius: 8px; padding: 20px; margin-bottom: 20px; position: relative;">
|
||
<!-- 筛选器加载状态 -->
|
||
<div id="filterOptionsLoading" style="display: none; position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: rgba(255, 255, 255, 0.9); border-radius: 8px; z-index: 100; display: flex; flex-direction: column; align-items: center; justify-content: center;">
|
||
<div style="width: 50px; height: 50px; border: 4px solid #e0e0e0; border-top-color: #2c5282; border-radius: 50%; animation: spin 0.8s linear infinite; margin-bottom: 16px;"></div>
|
||
<div style="font-size: 14px; color: #666; font-weight: 600;">正在加载筛选选项...</div>
|
||
</div>
|
||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 16px; margin-bottom: 16px;">
|
||
<!-- 行政区域筛选 -->
|
||
<div style="display: flex; flex-direction: column; gap: 8px;">
|
||
<label style="font-size: 13px; font-weight: 600; color: #555;">行政区域 (可多选)</label>
|
||
<div style="position: relative;">
|
||
<div id="filterRegion" class="multi-select-dropdown" style="padding: 10px; border: 1px solid #ddd; border-radius: 6px; font-size: 14px; background: white; cursor: pointer;" onclick="toggleMultiSelect('regionOptions')">
|
||
<span id="regionSelectedText">全部区域</span>
|
||
<span style="float: right; color: #999;">▼</span>
|
||
</div>
|
||
<div id="regionOptions" class="multi-select-options" style="position: absolute; top: 100%; left: 0; right: 0; background: white; border: 1px solid #ddd; border-radius: 6px; margin-top: 4px; max-height: 200px; overflow-y: auto; z-index: 1000; display: none; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
|
||
<div style="padding: 8px; border-bottom: 1px solid #eee;">
|
||
<label style="display: flex; align-items: center; cursor: pointer;">
|
||
<input type="checkbox" id="regionSelectAll" onchange="selectAllRegions()" style="margin-right: 8px;">
|
||
<span style="font-weight: 600; color: #2c5282;">全选</span>
|
||
</label>
|
||
</div>
|
||
<div id="regionOptionsList"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 主题筛选 -->
|
||
<div style="display: flex; flex-direction: column; gap: 8px;">
|
||
<label style="font-size: 13px; font-weight: 600; color: #555;">主题 (可多选)</label>
|
||
<div style="position: relative;">
|
||
<div id="filterTheme" class="multi-select-dropdown" style="padding: 10px; border: 1px solid #ddd; border-radius: 6px; font-size: 14px; background: white; cursor: pointer;" onclick="toggleMultiSelect('themeOptions')">
|
||
<span id="themeSelectedText">全部主题</span>
|
||
<span style="float: right; color: #999;">▼</span>
|
||
</div>
|
||
<div id="themeOptions" class="multi-select-options" style="position: absolute; top: 100%; left: 0; right: 0; background: white; border: 1px solid #ddd; border-radius: 6px; margin-top: 4px; max-height: 200px; overflow-y: auto; z-index: 1000; display: none; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
|
||
<div style="padding: 8px; border-bottom: 1px solid #eee;">
|
||
<label style="display: flex; align-items: center; cursor: pointer;">
|
||
<input type="checkbox" id="themeSelectAll" onchange="selectAllThemes()" style="margin-right: 8px;">
|
||
<span style="font-weight: 600; color: #2c5282;">全选</span>
|
||
</label>
|
||
</div>
|
||
<div id="themeOptionsList"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 关联部门筛选 -->
|
||
<div style="display: flex; flex-direction: column; gap: 8px;">
|
||
<label style="font-size: 13px; font-weight: 600; color: #555;">关联部门 (可多选)</label>
|
||
<div style="position: relative;">
|
||
<div id="filterDepartment" class="multi-select-dropdown" style="padding: 10px; border: 1px solid #ddd; border-radius: 6px; font-size: 14px; background: white; cursor: pointer;" onclick="toggleMultiSelect('departmentOptions')">
|
||
<span id="departmentSelectedText">全部部门</span>
|
||
<span style="float: right; color: #999;">▼</span>
|
||
</div>
|
||
<div id="departmentOptions" class="multi-select-options" style="position: absolute; top: 100%; left: 0; right: 0; background: white; border: 1px solid #ddd; border-radius: 6px; margin-top: 4px; max-height: 200px; overflow-y: auto; z-index: 1000; display: none; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
|
||
<div style="padding: 8px; border-bottom: 1px solid #eee;">
|
||
<label style="display: flex; align-items: center; cursor: pointer;">
|
||
<input type="checkbox" id="departmentSelectAll" onchange="selectAllDepartments()" style="margin-right: 8px;">
|
||
<span style="font-weight: 600; color: #2c5282;">全选</span>
|
||
</label>
|
||
</div>
|
||
<div id="departmentOptionsList"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 搜索关键词 -->
|
||
<div style="display: flex; flex-direction: column; gap: 8px;">
|
||
<label style="font-size: 13px; font-weight: 600; color: #555;">搜索关键词</label>
|
||
<input type="text" id="filterSearchText" placeholder="输入许可名称关键词..." style="padding: 10px; border: 1px solid #ddd; border-radius: 6px; font-size: 14px;">
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 筛选按钮 -->
|
||
<div style="display: flex; gap: 12px;">
|
||
<button id="applyFilterBtn" onclick="applyPermitFilter()" style="padding: 10px 20px; background: #2c5282; color: white; border: none; border-radius: 6px; font-weight: 600; cursor: pointer; font-size: 14px;">
|
||
🔍 应用筛选
|
||
</button>
|
||
<button id="resetFilterBtn" onclick="resetPermitFilter()" style="padding: 10px 20px; background: #6b7280; color: white; border: none; border-radius: 6px; font-weight: 600; cursor: pointer; font-size: 14px;">
|
||
↻ 重置筛选
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 筛选结果区域 -->
|
||
<div class="panel" style="margin-top: 0;">
|
||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
|
||
<h3 style="color: #333; margin: 0; font-size: 18px;">筛选结果</h3>
|
||
<div id="resultCount" style="color: #666; font-size: 14px;">共找到 <strong style="color: #2c5282;">0</strong> 个许可事项</div>
|
||
</div>
|
||
|
||
<!-- 加载状态 -->
|
||
<div id="permitsLoading" style="display: none; text-align: center; padding: 40px; color: #999;">
|
||
<div style="display: inline-block; width: 40px; height: 40px; border: 4px solid #f3f4f6; border-top-color: #2c5282; border-radius: 50%; animation: spin 0.8s linear infinite; margin-bottom: 12px;"></div>
|
||
<div>正在加载许可数据...</div>
|
||
</div>
|
||
|
||
<!-- 错误信息 -->
|
||
<div id="permitsError" style="display: none; background: #fee2e2; border: 1px solid #fecaca; color: #991b1b; padding: 16px; border-radius: 8px; margin-bottom: 20px;">
|
||
<strong>加载失败:</strong><span id="permitsErrorMsg"></span>
|
||
</div>
|
||
|
||
<!-- 许可事项列表 -->
|
||
<div id="permitsList" style="background: white; border: 1px solid #e0e0e0; border-radius: 8px; overflow: hidden;">
|
||
<div style="text-align: center; padding: 60px 20px; color: #999;">
|
||
<div style="font-size: 48px; margin-bottom: 16px;">📋</div>
|
||
<div>请选择筛选条件并点击"应用筛选"</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 分页控制 -->
|
||
<div id="permitsPagination" style="display: none; margin-top: 20px; display: flex; justify-content: space-between; align-items: center;">
|
||
<div id="paginationInfo" style="color: #666; font-size: 14px;"></div>
|
||
<div style="display: flex; gap: 8px;">
|
||
<button id="prevPageBtn" onclick="previousPermitPage()" style="padding: 8px 16px; border: 1px solid #ddd; background: white; border-radius: 6px; cursor: pointer; font-size: 14px;">上一页</button>
|
||
<button id="nextPageBtn" onclick="nextPermitPage()" style="padding: 8px 16px; border: 1px solid #ddd; background: white; border-radius: 6px; cursor: pointer; font-size: 14px;">下一页</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 检查点管理标签页 -->
|
||
<div id="checkpoints-tab" class="tab-content">
|
||
<h2 style="color: #333; margin-bottom: 20px; display: flex; align-items: center; gap: 10px;">
|
||
<span>🔒</span> 数据库检查点管理
|
||
</h2>
|
||
<p style="color: #666; margin-bottom: 20px;">管理系统数据库备份点,创建、恢复、删除检查点。支持许可风险快照查看与管理。</p>
|
||
<div style="background: white; border-radius: 8px; padding: 20px; border: 1px solid #e0e0e0;">
|
||
<button class="btn btn-warning" onclick="openCheckpointModal()" style="margin-bottom: 20px;">
|
||
<span>🔒</span> 打开检查点管理
|
||
</button>
|
||
<div id="checkpointPreview">
|
||
<p style="color: #999; text-align: center; padding: 40px;">点击上方按钮打开检查点管理窗口</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 文件管理标签页 -->
|
||
<div id="files-tab" class="tab-content">
|
||
<h2 style="color: #333; margin-bottom: 20px; display: flex; align-items: center; gap: 10px;">
|
||
<span>📁</span> 文件管理
|
||
</h2>
|
||
<p style="color: #666; margin-bottom: 20px;">管理许可导入相关文件,查看文件关联、重新导入、删除文件等操作。</p>
|
||
<div style="background: white; border-radius: 8px; padding: 20px; border: 1px solid #e0e0e0;">
|
||
<div id="fileManagerContainer">
|
||
<!-- 文件管理内容将动态加载到这里 -->
|
||
<div style="padding: 40px; text-align: center; color: #999;">
|
||
<div class="loading">
|
||
<span class="loading-icon"></span>
|
||
正在加载文件列表...
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 许可导入标签页 -->
|
||
<div id="import-tab" class="tab-content">
|
||
<h2 style="color: #333; margin-bottom: 20px; display: flex; align-items: center; gap: 10px;">
|
||
<span>📥</span> 许可导入
|
||
</h2>
|
||
<p style="color: #666; margin-bottom: 20px;">通过Excel文件批量导入许可数据,支持多区划批量处理、主题绑定、预览确认等功能。</p>
|
||
<div style="background: white; border-radius: 8px; padding: 20px; border: 1px solid #e0e0e0;">
|
||
<div class="drag-drop-area" onclick="triggerFileUpload()" id="dragDropArea">
|
||
<div class="drag-drop-icon">📄</div>
|
||
<div class="drag-drop-text">拖拽 Excel 文件到这里</div>
|
||
<div class="drag-drop-hint">或者点击选择文件(支持 .xlsx, .xlsm 格式)</div>
|
||
<div class="drag-drop-or">或</div>
|
||
<button class="import-template-button" onclick="event.stopPropagation(); openImportModal()">
|
||
<span class="import-template-icon">⚙️</span>
|
||
<span class="import-template-text">
|
||
<span class="import-template-title">使用导入向导</span>
|
||
<span class="import-template-subtitle">打开完整导入流程</span>
|
||
</span>
|
||
</button>
|
||
<input type="file" id="dragDropFileInput" accept=".xlsx,.xlsm" style="display: none;" onchange="handleFileSelect(this.files)">
|
||
</div>
|
||
<div id="importPreview" style="margin-top: 20px;">
|
||
<p style="color: #999; text-align: center; padding: 20px;">支持拖拽上传,文件大小限制:≤ 500KB</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 危险操作确认模态框 -->
|
||
<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>
|
||
|
||
<!-- 检查点管理模态窗口 -->
|
||
<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>
|
||
|
||
<!-- 许可导入模态窗口 -->
|
||
<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>
|
||
|
||
<!-- 文件管理模态窗口 -->
|
||
<div class="modal" id="fileManagerModal">
|
||
<div class="modal-content file-manager-modal-content">
|
||
<div class="modal-header" style="display:flex; justify-content: space-between; align-items: center;">
|
||
<h3 style="color:#111827; margin:0; display:flex; align-items:center; gap:8px;">
|
||
<span>🗂️</span> 文件管理
|
||
</h3>
|
||
<button class="checkpoint-modal-close" onclick="closeFileManagerModal()">×</button>
|
||
</div>
|
||
<div class="modal-body" id="fileManagerModalBody"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
const LOGIN_PATH = '/fs-ai-asistant/lawrisk/login';
|
||
let currentUserProfile = null;
|
||
let loginTime = null;
|
||
|
||
const userNameEl = document.getElementById('userDisplayName');
|
||
const userRoleEl = document.getElementById('userRole');
|
||
const userStatusEl = document.getElementById('userStatus');
|
||
const userAvatarEl = document.getElementById('userAvatar');
|
||
const userTimeEl = document.getElementById('userTime');
|
||
const loginTimeEl = document.getElementById('loginTime');
|
||
const logoutBtn = document.getElementById('logoutBtn');
|
||
|
||
function buildLoginRedirectUrl() {
|
||
const next = encodeURIComponent(window.location.pathname + window.location.search);
|
||
return `${LOGIN_PATH}?next=${next || '%2F'}`;
|
||
}
|
||
|
||
function updateUserBanner(user, message = '') {
|
||
if (!user) {
|
||
userNameEl.textContent = '访客';
|
||
userRoleEl.textContent = 'LOGIN';
|
||
userStatusEl.textContent = message || '登录状态失效,请重新登录';
|
||
userAvatarEl.textContent = '?';
|
||
userTimeEl.style.display = 'none';
|
||
return;
|
||
}
|
||
const displayName = user.display_name || user.username;
|
||
userNameEl.textContent = displayName || user.username;
|
||
userRoleEl.textContent = (user.role || 'user').toUpperCase();
|
||
userStatusEl.textContent = message || '';
|
||
|
||
// Update avatar with first letter of username
|
||
const initial = (displayName || user.username || '?').charAt(0).toUpperCase();
|
||
userAvatarEl.textContent = initial;
|
||
|
||
// Set login time if not already set
|
||
if (!loginTime) {
|
||
loginTime = new Date();
|
||
const timeStr = loginTime.toLocaleTimeString('zh-CN', {
|
||
hour: '2-digit',
|
||
minute: '2-digit'
|
||
});
|
||
loginTimeEl.textContent = `登录于 ${timeStr}`;
|
||
userTimeEl.style.display = 'flex';
|
||
}
|
||
}
|
||
|
||
async function fetchCurrentUser(redirectOnFail = true) {
|
||
try {
|
||
const resp = await fetch('/auth/me', { headers: { Accept: 'application/json' } });
|
||
if (!resp.ok) {
|
||
throw new Error('未登录或登录已过期');
|
||
}
|
||
const data = await resp.json();
|
||
if (!data.authenticated) {
|
||
throw new Error('未登录或登录已过期');
|
||
}
|
||
currentUserProfile = data.user;
|
||
updateUserBanner(currentUserProfile);
|
||
return currentUserProfile;
|
||
} catch (error) {
|
||
currentUserProfile = null;
|
||
updateUserBanner(null, error.message);
|
||
if (redirectOnFail) {
|
||
window.location.href = buildLoginRedirectUrl();
|
||
}
|
||
return null;
|
||
}
|
||
}
|
||
|
||
async function handleLogout() {
|
||
try {
|
||
await fetch('/auth/logout', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ reason: 'user_action' }),
|
||
});
|
||
} finally {
|
||
loginTime = null; // Reset login time
|
||
window.location.href = buildLoginRedirectUrl();
|
||
}
|
||
}
|
||
|
||
if (logoutBtn) {
|
||
logoutBtn.addEventListener('click', (event) => {
|
||
event.preventDefault();
|
||
handleLogout();
|
||
});
|
||
}
|
||
|
||
// 导航状态管理
|
||
let currentStep = 1; // 1=区划, 2=事项, 3=详情
|
||
let historyStack = []; // 历史记录栈
|
||
let currentRegion = null;
|
||
let currentPermit = null;
|
||
let currentPermitDetails = null;
|
||
let pendingDangerOperation = null; // 待执行的危险操作
|
||
let isDeletingPermit = false;
|
||
|
||
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 = '';
|
||
let expandedSnapshotGroups = new Set();
|
||
|
||
const PERMIT_FILE_MAX_BYTES = 500 * 1024; // 500KB
|
||
|
||
const ALL_THEMES_SENTINEL = '__ALL_THEMES__';
|
||
const ALL_THEMES_LABEL = '所有主题';
|
||
|
||
const permitImportState = {
|
||
uploading: false,
|
||
sessionId: '',
|
||
filename: '',
|
||
totalRows: 0,
|
||
sheetSummaries: [],
|
||
selectedSheets: new Set(),
|
||
overrides: new Map(),
|
||
error: '',
|
||
success: '',
|
||
commitLoading: false,
|
||
editedBy: '',
|
||
changeSummary: '',
|
||
fileSize: 0,
|
||
previewLoading: false,
|
||
previewError: '',
|
||
previewData: null,
|
||
themeBindings: new Map(),
|
||
stage: 'upload',
|
||
activePreviewSheet: '',
|
||
previewThemeKeyword: '',
|
||
};
|
||
|
||
const fileManagerState = {
|
||
files: [],
|
||
loading: false,
|
||
error: '',
|
||
keyword: '',
|
||
keywordInput: '',
|
||
limit: 15,
|
||
offset: 0,
|
||
total: 0,
|
||
deleting: new Set(),
|
||
reimporting: new Set(),
|
||
};
|
||
let fileManagerSearchTimer = null;
|
||
|
||
const themeDragState = {
|
||
active: false,
|
||
sheetName: '',
|
||
permitName: '',
|
||
shouldSelect: true,
|
||
};
|
||
|
||
let themeSearchRenderFrame = null;
|
||
|
||
window.addEventListener('mouseup', endThemeDragSelection);
|
||
window.addEventListener('blur', endThemeDragSelection);
|
||
window.addEventListener('mouseleave', endThemeDragSelection);
|
||
|
||
const EMPTY_PLACEHOLDER = '(未填写)';
|
||
|
||
function formatNullableText(value, placeholder = EMPTY_PLACEHOLDER) {
|
||
if (value === null || value === undefined) {
|
||
return placeholder;
|
||
}
|
||
if (typeof value === 'string') {
|
||
const trimmed = value.trim();
|
||
return trimmed === '' ? placeholder : trimmed;
|
||
}
|
||
if (typeof value === 'number') {
|
||
return Number.isNaN(value) ? placeholder : String(value);
|
||
}
|
||
if (typeof value === 'object') {
|
||
try {
|
||
return JSON.stringify(value);
|
||
} catch (error) {
|
||
return placeholder;
|
||
}
|
||
}
|
||
return String(value);
|
||
}
|
||
|
||
function formatFileSize(bytes) {
|
||
const size = Number(bytes);
|
||
if (!Number.isFinite(size) || size <= 0) {
|
||
return '0 B';
|
||
}
|
||
if (size < 1024) {
|
||
return `${size.toFixed(0)} B`;
|
||
}
|
||
if (size < 1024 * 1024) {
|
||
const kb = size / 1024;
|
||
return `${Number.isInteger(kb) ? kb.toFixed(0) : kb.toFixed(1)} KB`;
|
||
}
|
||
const mb = size / (1024 * 1024);
|
||
return `${Number.isInteger(mb) ? mb.toFixed(0) : mb.toFixed(2)} MB`;
|
||
}
|
||
|
||
// 步骤配置
|
||
const steps = {
|
||
1: { title: '选择区划' },
|
||
2: { title: '选择事项' },
|
||
3: { title: '事项详情' }
|
||
};
|
||
|
||
// 加载区划列表
|
||
async function loadRegions() {
|
||
const navList = document.getElementById('navList');
|
||
navList.innerHTML = '<div class="loading"><span class="loading-icon"></span>加载区划列表...</div>';
|
||
|
||
try {
|
||
const response = await fetch('/fs-ai-asistant/api/workflow/lawrisk/admin/regions');
|
||
const data = await response.json();
|
||
|
||
if (data.success) {
|
||
navList.innerHTML = '';
|
||
|
||
if (data.data.regions.length === 0) {
|
||
navList.innerHTML = '<div class="error">未找到区划数据</div>';
|
||
return;
|
||
}
|
||
|
||
data.data.regions.forEach(region => {
|
||
const li = document.createElement('li');
|
||
li.innerHTML = `
|
||
<span class="item-name">${region.name}</span>
|
||
<span class="item-count">点击选择</span>
|
||
`;
|
||
li.onclick = () => selectRegion(region.id, region.name);
|
||
navList.appendChild(li);
|
||
});
|
||
} else {
|
||
navList.innerHTML = `<div class="error">加载区划失败:${data.message}</div>`;
|
||
}
|
||
} catch (error) {
|
||
navList.innerHTML = `<div class="error">网络错误:${error.message}</div>`;
|
||
}
|
||
}
|
||
|
||
// 加载许可列表(直接按区划聚合事项)
|
||
async function loadPermitsForRegion() {
|
||
if (!currentRegion) {
|
||
return;
|
||
}
|
||
const navList = document.getElementById('navList');
|
||
navList.innerHTML = '<div class="loading"><span class="loading-icon"></span>加载事项列表...</div>';
|
||
|
||
try {
|
||
const response = await fetch(`/fs-ai-asistant/api/workflow/lawrisk/admin/permits?region=${currentRegion.id}`);
|
||
const data = await response.json();
|
||
|
||
if (data.success) {
|
||
const permits = data.data.permits;
|
||
|
||
if (permits.length === 0) {
|
||
navList.innerHTML = `<div class="error">区划 "${currentRegion.name}" 暂无可维护的事项</div>`;
|
||
return;
|
||
}
|
||
|
||
let html = '<div class="item-list">';
|
||
permits.forEach(permit => {
|
||
const rawRiskCount = typeof permit.risk_count === 'number'
|
||
? permit.risk_count
|
||
: (Array.isArray(permit.risks) ? permit.risks.length : 0);
|
||
const riskCount = Number.isFinite(rawRiskCount) ? rawRiskCount : 0;
|
||
const themeId = permit.theme && permit.theme.id
|
||
? permit.theme.id
|
||
: (permit.theme_id || '');
|
||
const themeName = permit.theme && permit.theme.name
|
||
? permit.theme.name
|
||
: (permit.theme_name || '');
|
||
const escapedName = permit.name ? permit.name.replace(/'/g, "\\'") : '';
|
||
const escapedTheme = themeName ? themeName.replace(/'/g, "\\'") : '';
|
||
const escapedThemeId = themeId ? themeId.replace(/'/g, "\\'") : '';
|
||
html += `
|
||
<li onclick="selectPermit('${permit.id}', '${escapedName}', '${escapedThemeId}', '${escapedTheme}', ${riskCount})">
|
||
<span class="item-name">${permit.name}</span>
|
||
<span class="item-count">${riskCount} 个风险</span>
|
||
</li>
|
||
`;
|
||
});
|
||
html += '</div>';
|
||
navList.innerHTML = html;
|
||
} else {
|
||
navList.innerHTML = `<div class="error">加载事项失败:${data.message}</div>`;
|
||
}
|
||
} catch (error) {
|
||
navList.innerHTML = `<div class="error">网络错误:${error.message}</div>`;
|
||
}
|
||
}
|
||
|
||
// 选择地区
|
||
async function selectRegion(regionId, regionName) {
|
||
// 保存到历史栈
|
||
historyStack.push({ step: currentStep, region: currentRegion });
|
||
|
||
currentRegion = { id: regionId, name: regionName };
|
||
currentPermit = null;
|
||
currentPermitDetails = null;
|
||
|
||
// 更新步骤
|
||
goToStep(2);
|
||
}
|
||
|
||
// 选择许可
|
||
async function selectPermit(permitId, permitName, themeId, themeName, riskCount = 0) {
|
||
// 保存到历史栈
|
||
historyStack.push({ step: currentStep, permit: currentPermit });
|
||
|
||
currentPermit = {
|
||
id: permitId,
|
||
name: permitName,
|
||
themeId: themeId || '',
|
||
themeName: themeName || '',
|
||
riskCount: typeof riskCount === 'number' ? riskCount : 0
|
||
};
|
||
currentPermitDetails = null;
|
||
|
||
// 更新步骤
|
||
goToStep(3);
|
||
}
|
||
|
||
// 跳转到指定步骤
|
||
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;
|
||
|
||
// 更新步骤指示器
|
||
updateStepIndicator(step);
|
||
|
||
// 更新面包屑导航
|
||
updateBreadcrumb();
|
||
|
||
// 清空详情区域
|
||
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 loadPermitsForRegion();
|
||
} else if (step === 3) {
|
||
await showPermitDetails();
|
||
}
|
||
}
|
||
|
||
// 更新面包屑导航
|
||
function updateBreadcrumb() {
|
||
const breadcrumb = document.getElementById('breadcrumb');
|
||
let 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 (currentPermit) {
|
||
const permitLabel = currentPermit.themeName
|
||
? `${currentPermit.themeName} · ${currentPermit.name}`
|
||
: currentPermit.name;
|
||
html += '<span class="breadcrumb-separator">›</span>';
|
||
if (currentStep >= 3) {
|
||
html += `
|
||
<span class="breadcrumb-item">
|
||
<span class="breadcrumb-current">${permitLabel}</span>
|
||
</span>
|
||
`;
|
||
} else {
|
||
html += `
|
||
<span class="breadcrumb-item">
|
||
<a onclick="quickJump(3)">${permitLabel}</a>
|
||
</span>
|
||
`;
|
||
}
|
||
}
|
||
|
||
breadcrumb.innerHTML = html;
|
||
}
|
||
|
||
// 快速跳转到指定步骤
|
||
function quickJump(targetStep) {
|
||
// 清空历史栈中比目标步骤更晚的记录
|
||
while (historyStack.length > 0 && historyStack[historyStack.length - 1].step >= targetStep) {
|
||
historyStack.pop();
|
||
}
|
||
|
||
// 清理后续状态
|
||
if (targetStep <= 2) {
|
||
currentPermit = null;
|
||
currentPermitDetails = null;
|
||
}
|
||
|
||
goToStep(targetStep);
|
||
}
|
||
|
||
// 返回首页
|
||
function goHome() {
|
||
currentRegion = null;
|
||
currentPermit = null;
|
||
currentPermitDetails = null;
|
||
historyStack = [];
|
||
goToStep(1);
|
||
}
|
||
|
||
// 显示许可详情
|
||
async function showPermitDetails() {
|
||
const detailsArea = document.getElementById('detailsArea');
|
||
detailsArea.innerHTML = '<div class="loading"><span class="loading-icon"></span>加载许可详情...</div>';
|
||
|
||
try {
|
||
if (!currentRegion || !currentPermit) {
|
||
detailsArea.innerHTML = '<div class="error">请选择区划和事项后查看详情</div>';
|
||
return;
|
||
}
|
||
const themePart = currentPermit.themeId
|
||
? `&theme=${encodeURIComponent(currentPermit.themeId)}`
|
||
: '';
|
||
const response = await fetch(`/fs-ai-asistant/api/workflow/lawrisk/admin/permit-details?region=${currentRegion.id}&permit=${currentPermit.id}${themePart}`);
|
||
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>`;
|
||
}
|
||
}
|
||
|
||
// 回退到上一步
|
||
function goBack() {
|
||
if (historyStack.length === 0) return;
|
||
|
||
const prev = historyStack.pop();
|
||
|
||
// 恢复状态
|
||
if (prev.region) currentRegion = prev.region;
|
||
if (prev.permit) currentPermit = prev.permit;
|
||
|
||
// 跳转到上一步
|
||
goToStep(prev.step);
|
||
}
|
||
|
||
// 渲染许可详情
|
||
function renderPermitDetails(permit) {
|
||
const detailsArea = document.querySelector('.details-area');
|
||
currentPermitDetails = permit;
|
||
const riskCount = Array.isArray(permit.risks) ? permit.risks.length : 0;
|
||
const permitTheme = permit && permit.theme ? permit.theme : {};
|
||
if (currentPermit) {
|
||
currentPermit = {
|
||
...currentPermit,
|
||
riskCount,
|
||
themeId: permitTheme.id || currentPermit.themeId || '',
|
||
themeName: permitTheme.name || currentPermit.themeName || ''
|
||
};
|
||
}
|
||
const deleteButtonLabel = isDeletingPermit ? '删除中...' : '删除许可';
|
||
const deleteButtonDisabled = isDeletingPermit ? 'disabled' : '';
|
||
const permitSourceName = permit && permit.permit_source && permit.permit_source.source_name
|
||
? permit.permit_source.source_name
|
||
: '';
|
||
const permitSourceDisplay = permitSourceName ? escapeHtml(permitSourceName) : EMPTY_PLACEHOLDER;
|
||
const permitStatusClass = permit && permit.permit_status
|
||
? (permit.permit_status === 'active' ? 'status-active' : 'status-inactive')
|
||
: 'status-unknown';
|
||
const permitStatusLabel = formatNullableText(permit.permit_status, '未设置');
|
||
const permitFile = (permit && permit.permit_file) ? permit.permit_file : {};
|
||
const hasPermitFile = Boolean(permitFile.file_id);
|
||
const fileUploadedAt = permitFile.created_at ? formatIsoDatetime(permitFile.created_at) : '';
|
||
const fileUploadedBy = permitFile.uploaded_by ? escapeHtml(permitFile.uploaded_by) : '';
|
||
const downloadDisabledAttr = hasPermitFile ? '' : 'disabled';
|
||
const fileInfoText = hasPermitFile
|
||
? `<span class="permit-file-name">${escapeHtml(permitFile.filename || '原始文件')}</span>(${formatFileSize(permitFile.file_size)}${fileUploadedAt ? ` | ${fileUploadedAt}` : ''}${fileUploadedBy ? ` | 上传:${fileUploadedBy}` : ''})`
|
||
: '<span class="muted-text">暂无关联文件</span>';
|
||
const rawThemeList = Array.isArray(permit.themes) ? permit.themes.filter(Boolean) : [];
|
||
if (permitTheme && (permitTheme.id || permitTheme.name)) {
|
||
rawThemeList.push(permitTheme);
|
||
}
|
||
if (currentPermit && (currentPermit.themeId || currentPermit.themeName)) {
|
||
rawThemeList.push({
|
||
id: currentPermit.themeId || '',
|
||
name: currentPermit.themeName || '',
|
||
});
|
||
}
|
||
const themeList = [];
|
||
const seenThemeKeys = new Set();
|
||
rawThemeList.forEach(themeItem => {
|
||
if (!themeItem) {
|
||
return;
|
||
}
|
||
const id = typeof themeItem.id === 'string' ? themeItem.id.trim() : (themeItem.id ? String(themeItem.id) : '');
|
||
const name = typeof themeItem.name === 'string' ? themeItem.name.trim() : (themeItem.name ? String(themeItem.name) : '');
|
||
if (!id && !name) {
|
||
return;
|
||
}
|
||
const key = id || `name:${name}`;
|
||
if (seenThemeKeys.has(key)) {
|
||
return;
|
||
}
|
||
seenThemeKeys.add(key);
|
||
themeList.push({
|
||
id,
|
||
name: name || id || '未命名主题',
|
||
});
|
||
});
|
||
const themeListDisplay = themeList.length
|
||
? `<div class="theme-tags">${themeList.map(themeItem => `<span class="item-tag">${escapeHtml(themeItem.name)}</span>`).join('')}</div>`
|
||
: '<p class="muted-text">暂无主题关联</p>';
|
||
|
||
let html = '<div class="details-content">';
|
||
html += `
|
||
<div class="detail-toolbar">
|
||
<div class="detail-meta">
|
||
风险条目:<strong>${riskCount}</strong> 个
|
||
</div>
|
||
<div class="detail-actions">
|
||
<button class="btn" id="downloadPermitFileBtn" ${downloadDisabledAttr} onclick="downloadPermitFile()" title="${hasPermitFile ? '下载原文件' : '暂无原始文件'}">下载原文件</button>
|
||
<button class="btn btn-danger" id="deletePermitBtn" ${deleteButtonDisabled} onclick="confirmDeleteCurrentPermit()">${deleteButtonLabel}</button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
// 许可基本信息
|
||
html += `
|
||
<div class="detail-section">
|
||
<h3>许可信息</h3>
|
||
<div class="detail-content">
|
||
<p><strong>许可名称:</strong>${formatNullableText(permit.name, '(未命名)')}</p>
|
||
<p style="margin-top: 10px;"><strong>数据来源:</strong>${permitSourceDisplay}</p>
|
||
<p style="margin-top: 10px;"><strong>许可状态:</strong><span class="permit-status ${permitStatusClass}">${permitStatusLabel}</span></p>
|
||
<p style="margin-top: 10px;"><strong>原始文件:</strong>${fileInfoText}</p>
|
||
<p style="margin-top: 10px;"><strong>子项说明:</strong>${formatNullableText(permit.subitem_summary)}</p>
|
||
<p style="margin-top: 10px;"><strong>负责部门:</strong>${formatNullableText(permit.responsible_contact)}</p>
|
||
<p style="margin-top: 10px;"><strong>权限划分:</strong>${formatNullableText(permit.jurisdiction_scope)}</p>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
html += `
|
||
<div class="detail-section">
|
||
<h3>所属主题</h3>
|
||
<div class="detail-content">
|
||
${themeListDisplay}
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
// 经营范围
|
||
html += '<div class="detail-section"><h3>经营范围</h3><div class="detail-content">';
|
||
if (permit.business_scopes && permit.business_scopes.length > 0) {
|
||
permit.business_scopes.forEach(scope => {
|
||
html += `<div class="scope-item">${formatNullableText(scope.description)}</div>`;
|
||
});
|
||
} else {
|
||
html += '<p class="muted-text">暂无经营范围信息</p>';
|
||
}
|
||
html += '</div></div>';
|
||
|
||
// 法律风险
|
||
html += '<div class="detail-section"><h3>法律风险</h3><div class="detail-content">';
|
||
if (permit.risks && permit.risks.length > 0) {
|
||
permit.risks.forEach((risk, index) => {
|
||
const riskIdentifier = formatNullableText(risk.id, index + 1);
|
||
html += `
|
||
<div class="risk-item">
|
||
<h4>风险 ${riskIdentifier}</h4>
|
||
<div class="risk-field"><strong>风险内容:</strong><p>${formatNullableText(risk.risk_content)}</p></div>
|
||
<div class="risk-field"><strong>法律依据:</strong><p>${formatNullableText(risk.legal_basis)}</p></div>
|
||
<div class="risk-field"><strong>文件编号:</strong><p>${formatNullableText(risk.document_no)}</p></div>
|
||
<div class="risk-field"><strong>摘要:</strong><div style="margin-top: 5px;">${formatNullableText(risk.summary)}</div></div>
|
||
</div>
|
||
`;
|
||
});
|
||
} else {
|
||
html += '<p class="muted-text">暂无法律风险记录</p>';
|
||
}
|
||
html += '</div></div>';
|
||
|
||
html += '</div>';
|
||
detailsArea.innerHTML = html;
|
||
}
|
||
|
||
function downloadPermitFile() {
|
||
if (!currentRegion || !currentPermit || !currentPermitDetails) {
|
||
alert('请先选择许可');
|
||
return;
|
||
}
|
||
if (!currentPermitDetails.permit_file || !currentPermitDetails.permit_file.file_id) {
|
||
alert('当前许可没有可下载的原始文件');
|
||
return;
|
||
}
|
||
const url = `/fs-ai-asistant/api/workflow/lawrisk/admin/permit-file/download?region=${encodeURIComponent(currentRegion.id)}&permit=${encodeURIComponent(currentPermit.id)}`;
|
||
window.open(url, '_blank');
|
||
}
|
||
|
||
function confirmDeleteCurrentPermit() {
|
||
if (isDeletingPermit) {
|
||
return;
|
||
}
|
||
if (!currentRegion || !currentPermit) {
|
||
alert('请先选择要删除的许可');
|
||
return;
|
||
}
|
||
|
||
const riskCount = currentPermit.riskCount !== undefined
|
||
? currentPermit.riskCount
|
||
: (currentPermitDetails && Array.isArray(currentPermitDetails.risks) ? currentPermitDetails.risks.length : 0);
|
||
const pathParts = [currentRegion.name];
|
||
if (currentPermit.themeName) {
|
||
pathParts.push(currentPermit.themeName);
|
||
}
|
||
pathParts.push(currentPermit.name);
|
||
const confirmMessage = `确定要删除「${pathParts.join(' › ')}」吗?\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 || !currentPermit) {
|
||
alert('当前上下文缺失,无法删除');
|
||
return;
|
||
}
|
||
|
||
isDeletingPermit = true;
|
||
toggleDeletePermitButton(true);
|
||
|
||
try {
|
||
const payload = {
|
||
region_id: currentRegion.id,
|
||
permit_id: currentPermit.id
|
||
};
|
||
if (currentPermit.themeId) {
|
||
payload.theme_id = currentPermit.themeId;
|
||
}
|
||
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 ? '删除中...' : '删除许可';
|
||
}
|
||
|
||
// 更新步骤指示器
|
||
function updateStepIndicator(step) {
|
||
for (let i = 1; i <= 3; i++) {
|
||
const stepElement = document.getElementById(`step${i}`);
|
||
if (!stepElement) {
|
||
continue;
|
||
}
|
||
if (i <= step) {
|
||
stepElement.classList.add('active');
|
||
} else {
|
||
stepElement.classList.remove('active');
|
||
}
|
||
}
|
||
}
|
||
|
||
// ================ 许可导入功能 ================
|
||
|
||
async function openImportModal() {
|
||
const modal = document.getElementById('importModal');
|
||
if (!modal) return;
|
||
modal.classList.add('show');
|
||
permitImportState.stage = 'upload';
|
||
permitImportState.activePreviewSheet = '';
|
||
permitImportState.previewThemeKeyword = '';
|
||
endThemeDragSelection();
|
||
if (!permitImportState.sessionId) {
|
||
permitImportState.selectedSheets = new Set();
|
||
permitImportState.overrides = new Map();
|
||
permitImportState.error = '';
|
||
permitImportState.success = '';
|
||
permitImportState.filename = '';
|
||
permitImportState.totalRows = 0;
|
||
permitImportState.fileSize = 0;
|
||
}
|
||
renderImportModal();
|
||
if (permitImportState.sessionId) {
|
||
await fetchImportPreview(false);
|
||
}
|
||
}
|
||
|
||
function closeImportModal() {
|
||
const modal = document.getElementById('importModal');
|
||
if (!modal) return;
|
||
permitImportState.stage = 'upload';
|
||
permitImportState.activePreviewSheet = '';
|
||
permitImportState.previewThemeKeyword = '';
|
||
endThemeDragSelection();
|
||
cancelThemeSearchRender();
|
||
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);
|
||
permitImportState.themeBindings.delete(sheetName);
|
||
}
|
||
ensureActivePreviewSheet();
|
||
renderImportModal();
|
||
}
|
||
|
||
function toggleSheetSelectionFromEvent(el) {
|
||
if (!el || !el.dataset) return;
|
||
toggleSheetSelection(el.dataset.sheet || '', el.checked);
|
||
}
|
||
|
||
function getSelectedSheetOrder() {
|
||
const summaries = Array.isArray(permitImportState.sheetSummaries)
|
||
? permitImportState.sheetSummaries
|
||
: [];
|
||
return summaries.filter(summary => summary && permitImportState.selectedSheets.has(summary.sheet_name));
|
||
}
|
||
|
||
function ensureActivePreviewSheet() {
|
||
if (!permitImportState.selectedSheets || permitImportState.selectedSheets.size === 0) {
|
||
permitImportState.activePreviewSheet = '';
|
||
return;
|
||
}
|
||
const previewSheets = new Set(
|
||
((permitImportState.previewData && permitImportState.previewData.sheets) || []).map(sheet => sheet.sheet_name)
|
||
);
|
||
if (
|
||
permitImportState.activePreviewSheet &&
|
||
permitImportState.selectedSheets.has(permitImportState.activePreviewSheet) &&
|
||
(previewSheets.size === 0 || previewSheets.has(permitImportState.activePreviewSheet))
|
||
) {
|
||
return;
|
||
}
|
||
const ordered = getSelectedSheetOrder();
|
||
for (const summary of ordered) {
|
||
if (!summary || !summary.sheet_name) {
|
||
continue;
|
||
}
|
||
if (previewSheets.size === 0 || previewSheets.has(summary.sheet_name)) {
|
||
permitImportState.activePreviewSheet = summary.sheet_name;
|
||
return;
|
||
}
|
||
}
|
||
permitImportState.activePreviewSheet = ordered.length ? ordered[0].sheet_name : '';
|
||
}
|
||
|
||
function selectAllSheets(selectAll) {
|
||
const summaries = Array.isArray(permitImportState.sheetSummaries)
|
||
? permitImportState.sheetSummaries
|
||
: [];
|
||
if (selectAll) {
|
||
permitImportState.selectedSheets = new Set(
|
||
summaries.map(summary => summary.sheet_name).filter(Boolean)
|
||
);
|
||
} else {
|
||
permitImportState.selectedSheets = new Set();
|
||
}
|
||
ensureActivePreviewSheet();
|
||
renderImportModal();
|
||
}
|
||
|
||
async function enterImportPreviewStage() {
|
||
if (!permitImportState.sessionId) {
|
||
permitImportState.error = '请先上传并解析Excel文件';
|
||
renderImportModal();
|
||
return;
|
||
}
|
||
if (permitImportState.selectedSheets.size === 0) {
|
||
permitImportState.error = '请选择至少一个Sheet再进入预览';
|
||
renderImportModal();
|
||
return;
|
||
}
|
||
permitImportState.error = '';
|
||
permitImportState.stage = 'preview';
|
||
permitImportState.previewThemeKeyword = '';
|
||
ensureActivePreviewSheet();
|
||
renderImportModal();
|
||
await fetchImportPreview(false);
|
||
ensureActivePreviewSheet();
|
||
renderImportModal();
|
||
}
|
||
|
||
function exitImportPreviewStage() {
|
||
permitImportState.stage = 'upload';
|
||
permitImportState.previewThemeKeyword = '';
|
||
permitImportState.activePreviewSheet = '';
|
||
endThemeDragSelection();
|
||
cancelThemeSearchRender();
|
||
renderImportModal();
|
||
}
|
||
|
||
function setActivePreviewSheet(sheetName) {
|
||
if (!sheetName || permitImportState.activePreviewSheet === sheetName) {
|
||
return;
|
||
}
|
||
if (!permitImportState.selectedSheets.has(sheetName)) {
|
||
return;
|
||
}
|
||
permitImportState.activePreviewSheet = sheetName;
|
||
renderImportModal();
|
||
}
|
||
|
||
function getPreviewSheetByName(sheetName) {
|
||
if (!sheetName || !permitImportState.previewData || !Array.isArray(permitImportState.previewData.sheets)) {
|
||
return null;
|
||
}
|
||
return permitImportState.previewData.sheets.find(
|
||
sheet => sheet && sheet.sheet_name === sheetName
|
||
) || null;
|
||
}
|
||
|
||
function filterPermitsForPreview(sheet) {
|
||
if (!sheet) {
|
||
return [];
|
||
}
|
||
return Array.isArray(sheet.permits) ? sheet.permits : [];
|
||
}
|
||
|
||
function filterThemeOptionsForPreview(sheet) {
|
||
if (!sheet) {
|
||
return [];
|
||
}
|
||
const options = Array.isArray(sheet.theme_options) ? sheet.theme_options : [];
|
||
const keyword = (permitImportState.previewThemeKeyword || '').trim().toLowerCase();
|
||
if (!keyword) {
|
||
return options;
|
||
}
|
||
return options.filter(option => {
|
||
const name = (option && option.name ? option.name : '').toLowerCase();
|
||
return name.includes(keyword);
|
||
});
|
||
}
|
||
|
||
function cancelThemeSearchRender() {
|
||
if (themeSearchRenderFrame !== null) {
|
||
cancelAnimationFrame(themeSearchRenderFrame);
|
||
themeSearchRenderFrame = null;
|
||
}
|
||
}
|
||
|
||
function scheduleThemeSearchRender() {
|
||
if (permitImportState.stage !== 'preview') {
|
||
return;
|
||
}
|
||
if (themeSearchRenderFrame === null) {
|
||
themeSearchRenderFrame = requestAnimationFrame(() => {
|
||
themeSearchRenderFrame = null;
|
||
renderImportModal();
|
||
focusThemeSearchInput();
|
||
});
|
||
}
|
||
}
|
||
|
||
function focusThemeSearchInput() {
|
||
if (permitImportState.stage !== 'preview') {
|
||
return;
|
||
}
|
||
const input = document.getElementById('themeSearchInput');
|
||
if (input) {
|
||
const len = input.value.length;
|
||
input.focus();
|
||
try {
|
||
input.setSelectionRange(len, len);
|
||
} catch (err) {
|
||
/* ignore selection errors */
|
||
}
|
||
}
|
||
}
|
||
|
||
function handleThemeSearchInput(event) {
|
||
if (!event || !event.target) {
|
||
return;
|
||
}
|
||
permitImportState.previewThemeKeyword = event.target.value || '';
|
||
if (event.isComposing) {
|
||
return;
|
||
}
|
||
scheduleThemeSearchRender();
|
||
}
|
||
|
||
function handleThemeSearchCompositionEnd(event) {
|
||
if (!event || !event.target) {
|
||
return;
|
||
}
|
||
permitImportState.previewThemeKeyword = event.target.value || '';
|
||
scheduleThemeSearchRender();
|
||
}
|
||
|
||
function selectAllThemesForPermit(sheetName, permitName) {
|
||
if (!sheetName || !permitName) {
|
||
return;
|
||
}
|
||
const sheet = getPreviewSheetByName(sheetName);
|
||
if (!sheet) {
|
||
return;
|
||
}
|
||
const options = Array.isArray(sheet.theme_options) ? sheet.theme_options : [];
|
||
if (!options.length) {
|
||
return;
|
||
}
|
||
const bindingSet = getThemeBindingSet(sheetName, permitName, true);
|
||
const hasAllThemeOption = options.some(option => resolveThemeOptionNormalized(option) === ALL_THEMES_SENTINEL || option?.is_all);
|
||
if (hasAllThemeOption) {
|
||
bindingSet.clear();
|
||
bindingSet.add(ALL_THEMES_SENTINEL);
|
||
renderImportModal();
|
||
return;
|
||
}
|
||
options.forEach(option => {
|
||
const normalized = resolveThemeOptionNormalized(option);
|
||
if (normalized) {
|
||
bindingSet.delete(ALL_THEMES_SENTINEL);
|
||
bindingSet.add(normalized);
|
||
}
|
||
});
|
||
renderImportModal();
|
||
}
|
||
|
||
function clearThemesForPermit(sheetName, permitName) {
|
||
if (!sheetName || !permitName) {
|
||
return;
|
||
}
|
||
const bindingSet = getThemeBindingSet(sheetName, permitName, true);
|
||
if (!bindingSet) {
|
||
return;
|
||
}
|
||
bindingSet.clear();
|
||
renderImportModal();
|
||
}
|
||
|
||
function startThemeDragSelection(event, sheetName, permitName, themeName) {
|
||
if (event) {
|
||
event.preventDefault();
|
||
event.stopPropagation();
|
||
}
|
||
const normalizedTheme = normalizeThemeName(themeName);
|
||
if (!sheetName || !permitName || !normalizedTheme) {
|
||
return;
|
||
}
|
||
const bindingSet = getThemeBindingSet(sheetName, permitName, true);
|
||
const shouldSelect = !bindingSet.has(normalizedTheme);
|
||
themeDragState.active = true;
|
||
themeDragState.sheetName = sheetName;
|
||
themeDragState.permitName = permitName;
|
||
themeDragState.shouldSelect = shouldSelect;
|
||
toggleThemeBinding(sheetName, permitName, normalizedTheme, shouldSelect, { render: false });
|
||
}
|
||
|
||
function continueThemeDragSelection(event, sheetName, permitName, themeName) {
|
||
if (!themeDragState.active) {
|
||
return;
|
||
}
|
||
if (event) {
|
||
event.preventDefault();
|
||
event.stopPropagation();
|
||
}
|
||
if (
|
||
sheetName !== themeDragState.sheetName ||
|
||
permitName !== themeDragState.permitName
|
||
) {
|
||
return;
|
||
}
|
||
toggleThemeBinding(sheetName, permitName, themeName, themeDragState.shouldSelect, { render: false });
|
||
}
|
||
|
||
function endThemeDragSelection() {
|
||
if (!themeDragState.active) {
|
||
return;
|
||
}
|
||
themeDragState.active = false;
|
||
themeDragState.sheetName = '';
|
||
themeDragState.permitName = '';
|
||
renderImportModal();
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
function clearImportPreviewState() {
|
||
permitImportState.previewData = null;
|
||
permitImportState.previewError = '';
|
||
permitImportState.previewLoading = false;
|
||
permitImportState.themeBindings = new Map();
|
||
permitImportState.activePreviewSheet = '';
|
||
permitImportState.previewThemeKeyword = '';
|
||
cancelThemeSearchRender();
|
||
}
|
||
|
||
function bootstrapImportSessionFromPayload(payload, options = {}) {
|
||
if (!payload) {
|
||
return false;
|
||
}
|
||
const sessionId = payload.session_id || payload.sessionId || '';
|
||
if (!sessionId) {
|
||
return false;
|
||
}
|
||
const fallbackName = options.filename || '';
|
||
const fallbackSize = typeof options.fileSize === 'number' ? options.fileSize : 0;
|
||
permitImportState.sessionId = sessionId;
|
||
permitImportState.filename = payload.filename || fallbackName;
|
||
permitImportState.totalRows = payload.total_rows || payload.totalRows || 0;
|
||
permitImportState.sheetSummaries = payload.sheet_summaries || payload.sheetSummaries || [];
|
||
permitImportState.fileSize = payload.file_size || payload.fileSize || fallbackSize;
|
||
permitImportState.selectedSheets = new Set(
|
||
(permitImportState.sheetSummaries || []).map(item => item.sheet_name)
|
||
);
|
||
permitImportState.overrides = new Map();
|
||
permitImportState.success = options.successMessage
|
||
|| `解析完成:${permitImportState.sheetSummaries.length} 个 Sheet,${permitImportState.totalRows} 条风险记录`;
|
||
permitImportState.error = '';
|
||
clearImportPreviewState();
|
||
return true;
|
||
}
|
||
|
||
function normalizeThemeName(value) {
|
||
if (typeof value === 'string') {
|
||
const trimmed = value.trim();
|
||
if (!trimmed) {
|
||
return '';
|
||
}
|
||
if (trimmed.toLowerCase() === ALL_THEMES_SENTINEL.toLowerCase()) {
|
||
return ALL_THEMES_SENTINEL;
|
||
}
|
||
if (trimmed === ALL_THEMES_LABEL) {
|
||
return ALL_THEMES_SENTINEL;
|
||
}
|
||
return trimmed;
|
||
}
|
||
if (value === undefined || value === null) {
|
||
return '';
|
||
}
|
||
return String(value).trim();
|
||
}
|
||
|
||
function formatThemeBindingLabel(value) {
|
||
return normalizeThemeName(value) === ALL_THEMES_SENTINEL ? ALL_THEMES_LABEL : (value || '');
|
||
}
|
||
|
||
function resolveThemeOptionLabel(option) {
|
||
if (!option || typeof option.name !== 'string') {
|
||
return '';
|
||
}
|
||
return option.name.trim();
|
||
}
|
||
|
||
function resolveThemeOptionNormalized(option) {
|
||
const label = resolveThemeOptionLabel(option);
|
||
if (label) {
|
||
const normalizedLabel = normalizeThemeName(label);
|
||
if (normalizedLabel) {
|
||
return normalizedLabel;
|
||
}
|
||
}
|
||
const identifier = option && option.id ? String(option.id).trim() : '';
|
||
return normalizeThemeName(identifier);
|
||
}
|
||
|
||
function resolveThemeOptionBindingValue(option) {
|
||
const normalized = resolveThemeOptionNormalized(option);
|
||
if (normalized === ALL_THEMES_SENTINEL) {
|
||
return ALL_THEMES_LABEL;
|
||
}
|
||
const label = resolveThemeOptionLabel(option);
|
||
if (label) {
|
||
return label;
|
||
}
|
||
return option && option.id ? String(option.id).trim() : '';
|
||
}
|
||
|
||
function hydrateThemeBindingsFromPreview(previewData, previousBindings) {
|
||
const nextBindings = new Map();
|
||
const sheets = (previewData && previewData.sheets) || [];
|
||
sheets.forEach(sheet => {
|
||
const sheetName = sheet.sheet_name;
|
||
const sheetMap = new Map();
|
||
const prevSheetBindings = previousBindings && previousBindings.get(sheetName);
|
||
(sheet.permits || []).forEach(permit => {
|
||
const permitName = permit.permit_name;
|
||
let baseSet = prevSheetBindings && prevSheetBindings.get(permitName);
|
||
if (!baseSet || baseSet.size === 0) {
|
||
baseSet = new Set(
|
||
(permit.default_theme_names || [])
|
||
.map(normalizeThemeName)
|
||
.filter(Boolean)
|
||
);
|
||
}
|
||
const cleanedSet = new Set();
|
||
if (baseSet && baseSet.forEach) {
|
||
baseSet.forEach(name => {
|
||
const normalized = normalizeThemeName(name);
|
||
if (normalized) {
|
||
cleanedSet.add(normalized);
|
||
}
|
||
});
|
||
}
|
||
if (cleanedSet.size === 0) {
|
||
cleanedSet.add('不涉及');
|
||
}
|
||
sheetMap.set(permitName, cleanedSet);
|
||
});
|
||
nextBindings.set(sheetName, sheetMap);
|
||
});
|
||
return nextBindings;
|
||
}
|
||
|
||
function getThemeBindingSet(sheetName, permitName, createIfMissing = false) {
|
||
if (!sheetName || !permitName) {
|
||
return createIfMissing ? new Set() : null;
|
||
}
|
||
if (!permitImportState.themeBindings.has(sheetName)) {
|
||
if (!createIfMissing) {
|
||
return null;
|
||
}
|
||
permitImportState.themeBindings.set(sheetName, new Map());
|
||
}
|
||
const sheetMap = permitImportState.themeBindings.get(sheetName);
|
||
if (!sheetMap.has(permitName)) {
|
||
if (!createIfMissing) {
|
||
return null;
|
||
}
|
||
sheetMap.set(permitName, new Set());
|
||
}
|
||
return sheetMap.get(permitName);
|
||
}
|
||
|
||
function toggleThemeBinding(sheetName, permitName, themeName, forceChecked, options = {}) {
|
||
const normalizedTheme = normalizeThemeName(themeName);
|
||
if (!sheetName || !permitName || !normalizedTheme) {
|
||
return;
|
||
}
|
||
const bindingSet = getThemeBindingSet(sheetName, permitName, true);
|
||
const shouldAdd = typeof forceChecked === 'boolean'
|
||
? forceChecked
|
||
: !bindingSet.has(normalizedTheme);
|
||
if (shouldAdd) {
|
||
if (normalizedTheme === ALL_THEMES_SENTINEL) {
|
||
bindingSet.clear();
|
||
bindingSet.add(ALL_THEMES_SENTINEL);
|
||
} else {
|
||
bindingSet.delete(ALL_THEMES_SENTINEL);
|
||
bindingSet.add(normalizedTheme);
|
||
}
|
||
} else {
|
||
bindingSet.delete(normalizedTheme);
|
||
}
|
||
if (options.render === false) {
|
||
return;
|
||
}
|
||
renderImportModal();
|
||
}
|
||
|
||
function toggleThemeBindingFromEvent(el) {
|
||
if (!el || !el.dataset) {
|
||
return;
|
||
}
|
||
const sheetName = el.dataset.sheet || '';
|
||
const permitName = el.dataset.permit || '';
|
||
const themeName = el.dataset.theme || '';
|
||
toggleThemeBinding(sheetName, permitName, themeName, el.checked);
|
||
}
|
||
|
||
async function fetchImportPreview(forceReload = false) {
|
||
const sessionId = permitImportState.sessionId;
|
||
if (!sessionId) {
|
||
return;
|
||
}
|
||
if (!forceReload && permitImportState.previewData && permitImportState.previewData.session_id === sessionId) {
|
||
return;
|
||
}
|
||
permitImportState.previewLoading = true;
|
||
permitImportState.previewError = '';
|
||
renderImportModal();
|
||
|
||
try {
|
||
const params = new URLSearchParams({ session_id: sessionId, t: Date.now() });
|
||
const response = await fetch(`/fs-ai-asistant/api/workflow/lawrisk/admin/permit-import/preview?${params.toString()}`);
|
||
const data = await parseJsonResponse(response);
|
||
if (data && data.success) {
|
||
const preview = data.data || {};
|
||
const previousPreview = permitImportState.previewData;
|
||
const previousBindings = permitImportState.themeBindings;
|
||
permitImportState.previewData = preview;
|
||
permitImportState.themeBindings = hydrateThemeBindingsFromPreview(
|
||
preview,
|
||
previousPreview && previousPreview.session_id === preview.session_id ? previousBindings : null
|
||
);
|
||
ensureActivePreviewSheet();
|
||
} else {
|
||
permitImportState.previewData = null;
|
||
permitImportState.themeBindings = new Map();
|
||
permitImportState.activePreviewSheet = '';
|
||
permitImportState.previewError = data && data.message ? data.message : `预览加载失败(HTTP ${response.status})`;
|
||
}
|
||
} catch (error) {
|
||
permitImportState.previewData = null;
|
||
permitImportState.themeBindings = new Map();
|
||
permitImportState.activePreviewSheet = '';
|
||
permitImportState.previewError = error.message || '预览加载失败';
|
||
} finally {
|
||
permitImportState.previewLoading = false;
|
||
renderImportModal();
|
||
}
|
||
}
|
||
|
||
function buildThemeBindingsPayload() {
|
||
const payload = {};
|
||
permitImportState.themeBindings.forEach((permitMap, sheetName) => {
|
||
if (!permitImportState.selectedSheets.has(sheetName)) {
|
||
return;
|
||
}
|
||
const permitPayload = {};
|
||
permitMap.forEach((themeSet, permitName) => {
|
||
if (!themeSet || themeSet.size === 0) {
|
||
return;
|
||
}
|
||
permitPayload[permitName] = Array.from(themeSet);
|
||
});
|
||
if (Object.keys(permitPayload).length > 0) {
|
||
payload[sheetName] = permitPayload;
|
||
}
|
||
});
|
||
return payload;
|
||
}
|
||
|
||
function collectMissingThemeBindings() {
|
||
const missing = [];
|
||
if (!permitImportState.previewData || !Array.isArray(permitImportState.previewData.sheets)) {
|
||
return missing;
|
||
}
|
||
const sheetMap = new Map();
|
||
permitImportState.previewData.sheets.forEach(sheet => {
|
||
sheetMap.set(sheet.sheet_name, sheet);
|
||
});
|
||
permitImportState.selectedSheets.forEach(sheetName => {
|
||
const sheet = sheetMap.get(sheetName);
|
||
if (!sheet || !Array.isArray(sheet.permits)) {
|
||
return;
|
||
}
|
||
sheet.permits.forEach(permit => {
|
||
const bindingSet = getThemeBindingSet(sheetName, permit.permit_name, false);
|
||
if (!bindingSet || bindingSet.size === 0) {
|
||
const regionLabel = sheet.region_name || sheet.sheet_name || '未知地区';
|
||
missing.push(`${regionLabel} › ${permit.permit_name}`);
|
||
}
|
||
});
|
||
});
|
||
return missing;
|
||
}
|
||
|
||
async function handleImportFile(input) {
|
||
if (!input || !input.files || !input.files.length) {
|
||
return;
|
||
}
|
||
const file = input.files[0];
|
||
if (!file) {
|
||
return;
|
||
}
|
||
if (file.size > PERMIT_FILE_MAX_BYTES) {
|
||
permitImportState.error = `文件过大(最大 ${formatFileSize(PERMIT_FILE_MAX_BYTES)}),当前 ${formatFileSize(file.size)}`;
|
||
permitImportState.success = '';
|
||
permitImportState.fileSize = 0;
|
||
input.value = '';
|
||
renderImportModal();
|
||
return;
|
||
}
|
||
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 || {};
|
||
const hydrated = bootstrapImportSessionFromPayload(payload, {
|
||
filename: (file && file.name) || '',
|
||
fileSize: file.size || 0,
|
||
});
|
||
if (hydrated) {
|
||
await fetchImportPreview(true);
|
||
} else {
|
||
permitImportState.error = '解析结果无效,请重试上传';
|
||
permitImportState.sessionId = '';
|
||
permitImportState.sheetSummaries = [];
|
||
permitImportState.selectedSheets = new Set();
|
||
permitImportState.overrides = new Map();
|
||
permitImportState.fileSize = 0;
|
||
clearImportPreviewState();
|
||
}
|
||
} else {
|
||
permitImportState.error = data.message || '解析失败,请检查Excel格式';
|
||
permitImportState.sessionId = '';
|
||
permitImportState.sheetSummaries = [];
|
||
permitImportState.fileSize = 0;
|
||
permitImportState.selectedSheets = new Set();
|
||
permitImportState.overrides = new Map();
|
||
clearImportPreviewState();
|
||
}
|
||
} catch (error) {
|
||
permitImportState.error = error.message || '文件上传失败';
|
||
permitImportState.sessionId = '';
|
||
permitImportState.sheetSummaries = [];
|
||
permitImportState.selectedSheets = new Set();
|
||
permitImportState.overrides = new Map();
|
||
permitImportState.fileSize = 0;
|
||
clearImportPreviewState();
|
||
} 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;
|
||
}
|
||
if (permitImportState.previewLoading) {
|
||
permitImportState.error = '预览数据加载中,请稍候再试';
|
||
renderImportModal();
|
||
return;
|
||
}
|
||
if (permitImportState.previewError) {
|
||
permitImportState.error = '预览数据加载失败,请重试加载预览后再导入';
|
||
renderImportModal();
|
||
return;
|
||
}
|
||
if (!permitImportState.previewData || !Array.isArray(permitImportState.previewData.sheets)) {
|
||
permitImportState.error = '预览数据尚未生成,请重新上传Excel文件';
|
||
renderImportModal();
|
||
return;
|
||
}
|
||
|
||
const missingBindings = collectMissingThemeBindings();
|
||
if (missingBindings.length > 0) {
|
||
const previewText = missingBindings.slice(0, 3).join('、');
|
||
const suffix = missingBindings.length > 3 ? ' 等' : '';
|
||
permitImportState.error = `请为以下事项选择主题:${previewText}${suffix}`;
|
||
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);
|
||
}
|
||
});
|
||
|
||
const themeBindingsPayload = buildThemeBindingsPayload();
|
||
if (!Object.keys(themeBindingsPayload).length) {
|
||
permitImportState.error = '请至少为一个事项选择主题';
|
||
renderImportModal();
|
||
return;
|
||
}
|
||
payload.theme_bindings = themeBindingsPayload;
|
||
|
||
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();
|
||
permitImportState.filename = '';
|
||
permitImportState.totalRows = 0;
|
||
permitImportState.fileSize = 0;
|
||
permitImportState.stage = 'upload';
|
||
permitImportState.activePreviewSheet = '';
|
||
permitImportState.previewThemeKeyword = '';
|
||
clearImportPreviewState();
|
||
|
||
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 modalContent = document.querySelector('#importModal .import-modal-content');
|
||
if (modalContent) {
|
||
if (permitImportState.stage === 'preview') {
|
||
modalContent.classList.add('import-modal-wide');
|
||
} else {
|
||
modalContent.classList.remove('import-modal-wide');
|
||
}
|
||
}
|
||
if (permitImportState.stage === 'preview') {
|
||
container.innerHTML = renderImportPreviewStage();
|
||
} else {
|
||
container.innerHTML = renderImportUploadStage();
|
||
}
|
||
}
|
||
|
||
function renderImportUploadStage() {
|
||
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)">';
|
||
html += '<div class="import-template-wrapper">';
|
||
html += '<a class="import-template-button" href="/fs-ai-asistant/api/workflow/lawrisk/admin/permit-import/template" download>';
|
||
html += '<span class="import-template-icon">📥</span>';
|
||
html += '<span class="import-template-text"><span class="import-template-title">下载导入模板</span><span class="import-template-subtitle">包含示例字段与填写说明</span></span>';
|
||
html += '<span class="import-template-badge">推荐</span>';
|
||
html += '</a>';
|
||
html += '</div>';
|
||
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>';
|
||
}
|
||
const currentSizeLabel = state.fileSize ? formatFileSize(state.fileSize) : '未选择文件';
|
||
html += `<div class="import-meta">文件大小限制:≤ ${formatFileSize(PERMIT_FILE_MAX_BYTES)} | 当前:${currentSizeLabel}</div>`;
|
||
html += '</div></div>';
|
||
|
||
if (state.error) {
|
||
html += `<div class="import-error">${escapeHtml(state.error)}</div>`;
|
||
}
|
||
if (state.success && state.stage === 'upload') {
|
||
html += `<div class="import-success">${escapeHtml(state.success)}</div>`;
|
||
}
|
||
if (state.uploading) {
|
||
html += '<div class="loading" style="margin: 8px 0;"><span class="loading-icon"></span>正在解析 Excel...</div>';
|
||
}
|
||
|
||
if (state.sessionId && state.sheetSummaries && state.sheetSummaries.length) {
|
||
const totalSheets = state.sheetSummaries.length;
|
||
html += '<div class="import-section">';
|
||
html += '<h3><span>🗂️</span> 选择导入的区划(Sheet)</h3>';
|
||
html += '<div class="import-sheet-toolbar">';
|
||
html += `<span>已选择 ${state.selectedSheets.size}/${totalSheets} 个区划,可一次性导入多个区域</span>`;
|
||
html += '<div class="toolbar-actions">';
|
||
html += `<button class="btn btn-primary btn-sm" onclick="selectAllSheets(true)" ${state.selectedSheets.size === totalSheets ? 'disabled' : ''}>全选区划</button>`;
|
||
html += `<button class="btn btn-warning btn-sm" onclick="selectAllSheets(false)" ${state.selectedSheets.size === 0 ? 'disabled' : ''}>清空选择</button>`;
|
||
html += '</div></div>';
|
||
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>';
|
||
}
|
||
|
||
if (state.sessionId && state.previewLoading) {
|
||
html += '<div class="loading" style="margin: 12px 0;"><span class="loading-icon"></span>正在准备预览数据...</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 nextDisabled = !state.sessionId || state.selectedSheets.size === 0 || state.uploading || state.previewLoading;
|
||
html += '<div class="import-actions">';
|
||
html += '<div class="import-hint">提示:先勾选需要导入的区划,再点击下一步进入“解析预览 & 主题绑定”环节。</div>';
|
||
html += `<button class="btn btn-warning" onclick="enterImportPreviewStage()" ${nextDisabled ? 'disabled' : ''}>下一步:预览 & 主题绑定</button>`;
|
||
html += '</div>';
|
||
|
||
return html;
|
||
}
|
||
|
||
function renderImportPreviewStage() {
|
||
const state = permitImportState;
|
||
const selectedSummaries = getSelectedSheetOrder();
|
||
const selectedCount = selectedSummaries.length;
|
||
let html = '<div class="import-stage">';
|
||
html += '<div class="import-stage-header">';
|
||
html += '<h3><span>👀</span> 解析预览 & 主题绑定</h3>';
|
||
const commitDisabled = (
|
||
state.commitLoading ||
|
||
!state.sessionId ||
|
||
selectedCount === 0 ||
|
||
state.previewLoading ||
|
||
!!state.previewError ||
|
||
!state.previewData
|
||
);
|
||
const commitLabel = state.commitLoading ? '导入中...' : '确认并导入';
|
||
html += '<div class="preview-stage-actions">';
|
||
html += `<button class="btn btn-secondary btn-sm" onclick="exitImportPreviewStage()" ${state.commitLoading ? 'disabled' : ''}>上一步</button>`;
|
||
html += `<button class="btn btn-primary btn-sm" onclick="fetchImportPreview(true)" ${state.previewLoading ? 'disabled' : ''}>重新加载预览</button>`;
|
||
html += `<button class="btn btn-warning btn-sm" onclick="submitImport()" ${commitDisabled ? 'disabled' : ''}>${commitLabel}</button>`;
|
||
html += '</div></div>';
|
||
|
||
html += `<div class="import-preview-summary"><span>文件:${escapeHtml(state.filename || '(未命名)')}</span><span>已选区划:${selectedCount}</span><span>风险条目:${state.totalRows || 0}</span></div>`;
|
||
|
||
if (state.error) {
|
||
html += `<div class="import-error">${escapeHtml(state.error)}</div>`;
|
||
}
|
||
if (state.success && state.stage === 'preview') {
|
||
html += `<div class="import-success">${escapeHtml(state.success)}</div>`;
|
||
}
|
||
|
||
if (selectedCount === 0) {
|
||
html += '<div class="preview-empty-state">请返回上一步,至少勾选一个区划后再进行主题绑定。</div>';
|
||
html += '</div>';
|
||
return html;
|
||
}
|
||
|
||
html += '<div class="import-preview-tabs">';
|
||
selectedSummaries.forEach(summary => {
|
||
const sheetName = summary.sheet_name || '';
|
||
const isActive = permitImportState.activePreviewSheet === sheetName;
|
||
const sheetLabel = summary.region_name || sheetName || '未命名 Sheet';
|
||
const safeSheet = sheetName.replace(/'/g, "\\'");
|
||
html += `<button class="preview-tab ${isActive ? 'active' : ''}" onclick="setActivePreviewSheet('${safeSheet}')">`;
|
||
html += `<span>${escapeHtml(sheetLabel)}</span>`;
|
||
html += `<span class="preview-tab-badge">${summary.permit_count || 0} 项</span>`;
|
||
html += '</button>';
|
||
});
|
||
html += '</div>';
|
||
|
||
if (state.previewLoading && !state.previewData) {
|
||
html += '<div class="loading"><span class="loading-icon"></span>正在生成预览...</div>';
|
||
html += '</div>';
|
||
return html;
|
||
}
|
||
|
||
if (state.previewError) {
|
||
html += `<div class="import-error">${escapeHtml(state.previewError)}</div>`;
|
||
html += '<div style="margin-top: 12px;"><button class="btn btn-primary btn-sm" onclick="fetchImportPreview(true)">重新加载预览</button></div>';
|
||
html += '</div>';
|
||
return html;
|
||
}
|
||
|
||
if (!state.previewData || !Array.isArray(state.previewData.sheets) || state.previewData.sheets.length === 0) {
|
||
html += '<div class="preview-empty-state">预览数据暂不可用,请重新上传 Excel。</div>';
|
||
html += '</div>';
|
||
return html;
|
||
}
|
||
|
||
ensureActivePreviewSheet();
|
||
if (!permitImportState.activePreviewSheet) {
|
||
html += '<div class="preview-empty-state">请选择上方的区划标签查看详情。</div>';
|
||
html += '</div>';
|
||
return html;
|
||
}
|
||
|
||
const activeSheet = getPreviewSheetByName(permitImportState.activePreviewSheet);
|
||
if (!activeSheet) {
|
||
html += '<div class="preview-empty-state">当前区划的预览数据不存在,请重新加载。</div>';
|
||
html += '</div>';
|
||
return html;
|
||
}
|
||
|
||
if (!state.previewLoading) {
|
||
html += '<div class="preview-toolbar">';
|
||
html += `<label>主题搜索关键字<input type="text" id="themeSearchInput" placeholder="按主题名称过滤" value="${escapeHtml(state.previewThemeKeyword || '')}" oninput="handleThemeSearchInput(event)" oncompositionend="handleThemeSearchCompositionEnd(event)" autocomplete="off"></label>`;
|
||
html += '</div>';
|
||
}
|
||
|
||
if (state.previewLoading) {
|
||
html += '<div class="loading"><span class="loading-icon"></span>预览刷新中...</div>';
|
||
}
|
||
|
||
html += buildPreviewSheetContent(activeSheet);
|
||
|
||
if (state.previewLoading) {
|
||
html += '<p class="muted-text" style="text-align:center;">预览刷新中,稍后列表将自动更新</p>';
|
||
}
|
||
|
||
html += '</div>';
|
||
return html;
|
||
}
|
||
|
||
function buildPreviewSheetContent(sheet) {
|
||
if (!sheet) {
|
||
return '<div class="preview-empty-state">暂无可展示的数据</div>';
|
||
}
|
||
const regionLabel = sheet.region_name || sheet.sheet_name || '未知区划';
|
||
const filteredPermits = filterPermitsForPreview(sheet);
|
||
const totalPermits = Array.isArray(sheet.permits) ? sheet.permits.length : 0;
|
||
const themeOptions = filterThemeOptionsForPreview(sheet);
|
||
const totalThemes = Array.isArray(sheet.theme_options) ? sheet.theme_options.length : 0;
|
||
|
||
let html = '<div class="import-preview-summary">';
|
||
html += `<span>区划:${escapeHtml(regionLabel)}</span>`;
|
||
html += `<span>许可事项:${filteredPermits.length}/${totalPermits}</span>`;
|
||
html += `<span>主题候选:${themeOptions.length}/${totalThemes}</span>`;
|
||
html += '</div>';
|
||
|
||
if (sheet.missing_region) {
|
||
html += '<div class="import-error">检测到新地区,导入后会自动创建该区划,请确认名称是否正确。</div>';
|
||
}
|
||
|
||
if (!filteredPermits.length) {
|
||
html += '<div class="preview-empty-state">当前区划暂无可导入的事项。</div>';
|
||
return html;
|
||
}
|
||
|
||
html += '<div class="preview-permit-grid">';
|
||
filteredPermits.forEach(permit => {
|
||
if (!permit) {
|
||
return;
|
||
}
|
||
const bindingSet = getThemeBindingSet(sheet.sheet_name, permit.permit_name, true);
|
||
const selectedThemes = Array.from(bindingSet || []).map(formatThemeBindingLabel);
|
||
const defaultThemes = Array.isArray(permit.default_theme_names) ? permit.default_theme_names : [];
|
||
const safeSheet = (sheet.sheet_name || '').replace(/'/g, "\\'");
|
||
const safePermit = (permit.permit_name || '').replace(/'/g, "\\'");
|
||
const badgeClass = permit.is_duplicate ? 'duplicate' : (permit.is_new ? 'new' : '');
|
||
const badgeLabel = permit.is_duplicate ? '覆盖现有' : (permit.is_new ? '新增事项' : '已有事项');
|
||
|
||
html += `<div class="preview-permit-card ${badgeClass}">`;
|
||
html += '<div class="preview-permit-header">';
|
||
html += `<div style="display:flex;justify-content:space-between;align-items:center;gap:8px;"><strong>${escapeHtml(permit.permit_name)}</strong><span class="permit-badge ${badgeClass}">${badgeLabel}</span></div>`;
|
||
html += `<span class="muted-text">风险 ${permit.risk_count || 0} 条</span>`;
|
||
if (defaultThemes.length) {
|
||
html += `<span class="muted-text">Excel 主题:${defaultThemes.map(escapeHtml).join('、')}</span>`;
|
||
}
|
||
if (permit.sample_risk) {
|
||
html += `<span class="muted-text">示例风险:${escapeHtml(truncateText(permit.sample_risk, 80))}</span>`;
|
||
}
|
||
html += '</div>';
|
||
|
||
if (!themeOptions.length) {
|
||
html += '<div class="preview-empty-state" style="margin-top:12px;">当前区划暂无可选主题,请先创建主题或清空搜索条件。</div>';
|
||
} else {
|
||
html += '<div class="theme-chip-grid">';
|
||
themeOptions.forEach(option => {
|
||
const normalized = resolveThemeOptionNormalized(option);
|
||
const isAllThemeOption = normalized === ALL_THEMES_SENTINEL || Boolean(option && option.is_all);
|
||
const bindingValue = resolveThemeOptionBindingValue(option);
|
||
const label = resolveThemeOptionLabel(option) || bindingValue || '未命名主题';
|
||
const selected = normalized && bindingSet.has(normalized);
|
||
const workbookBadge = option && option.source === 'workbook' ? '<span class="theme-source-badge">Excel</span>' : '';
|
||
const specialBadge = isAllThemeOption ? '<span class="theme-source-badge">ALL</span>' : '';
|
||
const safeTheme = bindingValue.replace(/'/g, "\\'");
|
||
const displayLabel = isAllThemeOption ? ALL_THEMES_LABEL : label;
|
||
const tooltip = isAllThemeOption ? '绑定后保持与全部主题同步(含未来新增)' : displayLabel;
|
||
html += `
|
||
<div class="theme-chip ${isAllThemeOption ? 'all-theme' : ''} ${selected ? 'selected' : ''}"
|
||
title="${escapeHtml(tooltip)}"
|
||
onmousedown="startThemeDragSelection(event, '${safeSheet}', '${safePermit}', '${safeTheme}')"
|
||
onmouseenter="continueThemeDragSelection(event, '${safeSheet}', '${safePermit}', '${safeTheme}')">
|
||
${escapeHtml(displayLabel)}
|
||
${specialBadge || workbookBadge}
|
||
</div>
|
||
`;
|
||
});
|
||
html += '</div>';
|
||
html += `<div class="theme-chip-actions">
|
||
<button class="btn btn-primary btn-sm" onclick="selectAllThemesForPermit('${safeSheet}', '${safePermit}')">全选主题</button>
|
||
<button class="btn btn-warning btn-sm" onclick="clearThemesForPermit('${safeSheet}', '${safePermit}')">清空</button>
|
||
</div>`;
|
||
html += '<div class="muted-text" style="margin-top:6px;">若选择“所有主题”,该事项将自动出现在当前及未来新增的主题下。</div>';
|
||
}
|
||
|
||
if (selectedThemes.length) {
|
||
html += `<div class="preview-permit-meta">已绑定主题:${selectedThemes.map(escapeHtml).join('、')}</div>`;
|
||
} else {
|
||
html += '<div class="preview-permit-meta"><span class="muted-text">尚未选择主题,导入前必须为该事项选择至少一个主题。</span></div>';
|
||
}
|
||
|
||
html += '</div>';
|
||
});
|
||
html += '</div>';
|
||
return html;
|
||
}
|
||
|
||
|
||
// ================ 文件管理功能 ================
|
||
|
||
async function openFileManagerModal() {
|
||
const modal = document.getElementById('fileManagerModal');
|
||
if (!modal) return;
|
||
modal.classList.add('show');
|
||
renderFileManagerModal();
|
||
await fetchFileManagerData();
|
||
}
|
||
|
||
function closeFileManagerModal() {
|
||
const modal = document.getElementById('fileManagerModal');
|
||
if (!modal) return;
|
||
modal.classList.remove('show');
|
||
if (fileManagerSearchTimer) {
|
||
clearTimeout(fileManagerSearchTimer);
|
||
fileManagerSearchTimer = null;
|
||
}
|
||
}
|
||
|
||
function renderFileManagerModal() {
|
||
const modalBody = document.getElementById('fileManagerModalBody');
|
||
if (!modalBody) return;
|
||
|
||
const keywordValue = fileManagerState.keywordInput || '';
|
||
const loading = fileManagerState.loading;
|
||
const files = Array.isArray(fileManagerState.files) ? fileManagerState.files : [];
|
||
|
||
let html = '<div class="file-manager-toolbar">';
|
||
html += `
|
||
<div class="file-manager-search">
|
||
<input
|
||
type="text"
|
||
placeholder="按文件名或许可名称搜索"
|
||
value="${escapeHtml(keywordValue)}"
|
||
oninput="handleFileManagerSearchInput(event)"
|
||
onkeydown="handleFileManagerSearchKeydown(event)"
|
||
>
|
||
</div>
|
||
`;
|
||
html += '<div style="display:flex; gap:8px;">';
|
||
html += `<button class="btn btn-primary btn-sm" onclick="triggerFileManagerSearch()" ${loading ? 'disabled' : ''}><span>🔍</span> 搜索</button>`;
|
||
html += `<button class="btn btn-warning btn-sm" onclick="fetchFileManagerData()" ${loading ? 'disabled' : ''}><span>🔄</span> 刷新</button>`;
|
||
html += '</div></div>';
|
||
|
||
if (fileManagerState.error) {
|
||
html += `<div class="error">${escapeHtml(fileManagerState.error)}</div>`;
|
||
}
|
||
|
||
if (loading && !files.length) {
|
||
html += '<div class="loading"><span class="loading-icon"></span>正在加载文件...</div>';
|
||
} else if (!files.length) {
|
||
html += '<div class="file-manager-empty">暂无上传的许可导入文件</div>';
|
||
} else {
|
||
html += '<div class="file-manager-table-wrapper"><table class="file-manager-table">';
|
||
html += '<thead><tr><th>文件信息</th><th>关联许可</th><th style="width:180px;">操作</th></tr></thead><tbody>';
|
||
files.forEach(file => {
|
||
if (!file) {
|
||
return;
|
||
}
|
||
const fileId = file.file_id || '';
|
||
const safeFileId = fileId.replace(/'/g, "\\'");
|
||
const fileName = file.filename || '(未命名文件)';
|
||
const uploadedBy = file.uploaded_by || '未知用户';
|
||
const createdLabel = formatIsoDatetime(file.created_at) || '--';
|
||
const sizeLabel = formatFileSize(file.file_size || 0);
|
||
const links = Array.isArray(file.links) ? file.links : [];
|
||
let linkHtml = '';
|
||
if (!links.length) {
|
||
linkHtml = '<span class="muted-text">暂无关联许可</span>';
|
||
} else {
|
||
const maxDisplay = 4;
|
||
const displayLinks = links.slice(0, maxDisplay);
|
||
linkHtml = '<div class="file-manager-tags">';
|
||
displayLinks.forEach(link => {
|
||
const region = link && link.region_name ? link.region_name : '';
|
||
const permitName = link && link.permit_name ? link.permit_name : '';
|
||
const labelParts = [];
|
||
if (region) labelParts.push(region);
|
||
if (permitName) labelParts.push(permitName);
|
||
const label = labelParts.length ? labelParts.join(' · ') : (permitName || '未命名事项');
|
||
linkHtml += `<span class="file-manager-tag">${escapeHtml(label)}</span>`;
|
||
});
|
||
if (links.length > maxDisplay) {
|
||
linkHtml += `<span class="file-manager-tag file-manager-tag-muted">+${links.length - maxDisplay}</span>`;
|
||
}
|
||
linkHtml += '</div>';
|
||
}
|
||
const isReimporting = fileManagerState.reimporting.has(fileId);
|
||
const isDeleting = fileManagerState.deleting.has(fileId);
|
||
html += '<tr>';
|
||
html += `<td><div class="file-manager-file-name">${escapeHtml(fileName)}</div>`;
|
||
html += `<div class="file-manager-file-meta">大小:${sizeLabel} | 上传:${escapeHtml(uploadedBy)} | 时间:${escapeHtml(createdLabel)}</div></td>`;
|
||
html += `<td>${linkHtml}</td>`;
|
||
html += '<td>';
|
||
html += '<div class="file-manager-actions">';
|
||
html += `<button class="btn btn-primary btn-sm" onclick="triggerFileReimport('${safeFileId}')" ${isReimporting || loading ? 'disabled' : ''}>${isReimporting ? '处理中...' : '重新导入'}</button>`;
|
||
html += `<button class="btn btn-danger btn-sm" onclick="confirmDeleteStoredFile('${safeFileId}')" ${isDeleting || loading ? 'disabled' : ''}>${isDeleting ? '删除中...' : '删除文件'}</button>`;
|
||
html += '</div>';
|
||
html += '</td>';
|
||
html += '</tr>';
|
||
});
|
||
html += '</tbody></table></div>';
|
||
}
|
||
|
||
const limit = Math.max(1, parseInt(fileManagerState.limit, 10) || 1);
|
||
const total = Math.max(0, parseInt(fileManagerState.total, 10) || 0);
|
||
const currentOffset = Math.max(0, parseInt(fileManagerState.offset, 10) || 0);
|
||
const totalPages = Math.max(1, Math.ceil(total / limit || 1));
|
||
const currentPage = Math.min(totalPages, Math.floor(currentOffset / limit) + 1);
|
||
const disablePrev = currentOffset <= 0;
|
||
const disableNext = currentOffset + limit >= total;
|
||
|
||
html += '<div class="file-manager-pagination">';
|
||
html += `<div>共 ${total} 个文件 | 第 ${currentPage}/${totalPages} 页</div>`;
|
||
html += '<div style="display:flex; gap:8px;">';
|
||
html += `<button onclick="changeFileManagerPage(-1)" ${disablePrev ? 'disabled' : ''}>上一页</button>`;
|
||
html += `<button onclick="changeFileManagerPage(1)" ${disableNext ? 'disabled' : ''}>下一页</button>`;
|
||
html += '</div></div>';
|
||
|
||
modalBody.innerHTML = html;
|
||
}
|
||
|
||
function handleFileManagerSearchInput(event) {
|
||
if (!event || !event.target) return;
|
||
const value = event.target.value || '';
|
||
fileManagerState.keywordInput = value;
|
||
if (fileManagerSearchTimer) {
|
||
clearTimeout(fileManagerSearchTimer);
|
||
}
|
||
fileManagerSearchTimer = setTimeout(() => {
|
||
fileManagerState.keyword = value.trim();
|
||
fetchFileManagerData({ resetOffset: true });
|
||
}, 400);
|
||
}
|
||
|
||
function handleFileManagerSearchKeydown(event) {
|
||
if (!event) return;
|
||
if (event.key === 'Enter') {
|
||
event.preventDefault();
|
||
if (fileManagerSearchTimer) {
|
||
clearTimeout(fileManagerSearchTimer);
|
||
fileManagerSearchTimer = null;
|
||
}
|
||
fileManagerState.keyword = (event.target.value || '').trim();
|
||
fetchFileManagerData({ resetOffset: true });
|
||
}
|
||
}
|
||
|
||
function triggerFileManagerSearch() {
|
||
if (fileManagerSearchTimer) {
|
||
clearTimeout(fileManagerSearchTimer);
|
||
fileManagerSearchTimer = null;
|
||
}
|
||
fileManagerState.keyword = (fileManagerState.keywordInput || '').trim();
|
||
fetchFileManagerData({ resetOffset: true });
|
||
}
|
||
|
||
async function fetchFileManagerData(options = {}) {
|
||
if (options.resetOffset) {
|
||
fileManagerState.offset = 0;
|
||
}
|
||
const limit = Math.max(1, parseInt(fileManagerState.limit, 10) || 15);
|
||
const offset = Math.max(0, parseInt(fileManagerState.offset, 10) || 0);
|
||
fileManagerState.limit = limit;
|
||
fileManagerState.offset = offset;
|
||
|
||
fileManagerState.loading = true;
|
||
fileManagerState.error = '';
|
||
renderFileManagerModal();
|
||
|
||
const params = new URLSearchParams({
|
||
limit: String(limit),
|
||
offset: String(offset),
|
||
});
|
||
const keyword = (fileManagerState.keyword || '').trim();
|
||
if (keyword) {
|
||
params.set('keyword', keyword);
|
||
}
|
||
|
||
try {
|
||
const response = await fetch(`/fs-ai-asistant/api/workflow/lawrisk/admin/permit-files?${params.toString()}`);
|
||
const data = await parseJsonResponse(response);
|
||
if (data.success) {
|
||
const payload = data.data || {};
|
||
fileManagerState.files = Array.isArray(payload.files) ? payload.files : [];
|
||
const pagination = payload.pagination || {};
|
||
const nextLimit = Number(pagination.limit);
|
||
const nextOffset = Number(pagination.offset);
|
||
const nextTotal = Number(pagination.total);
|
||
if (!Number.isNaN(nextLimit) && nextLimit > 0) {
|
||
fileManagerState.limit = nextLimit;
|
||
}
|
||
if (!Number.isNaN(nextOffset) && nextOffset >= 0) {
|
||
fileManagerState.offset = nextOffset;
|
||
}
|
||
fileManagerState.total = Number.isNaN(nextTotal) ? fileManagerState.files.length : Math.max(0, nextTotal);
|
||
} else {
|
||
fileManagerState.files = [];
|
||
fileManagerState.error = data.message || '文件列表加载失败';
|
||
fileManagerState.total = 0;
|
||
}
|
||
} catch (error) {
|
||
fileManagerState.files = [];
|
||
fileManagerState.error = error.message || '文件列表加载失败';
|
||
fileManagerState.total = 0;
|
||
} finally {
|
||
fileManagerState.loading = false;
|
||
renderFileManagerModal();
|
||
}
|
||
}
|
||
|
||
function changeFileManagerPage(delta) {
|
||
if (!delta) {
|
||
return;
|
||
}
|
||
const limit = Math.max(1, parseInt(fileManagerState.limit, 10) || 1);
|
||
const total = Math.max(0, parseInt(fileManagerState.total, 10) || 0);
|
||
let nextOffset = fileManagerState.offset + delta * limit;
|
||
if (nextOffset < 0) {
|
||
nextOffset = 0;
|
||
}
|
||
if (delta > 0 && nextOffset >= total) {
|
||
return;
|
||
}
|
||
if (nextOffset === fileManagerState.offset) {
|
||
return;
|
||
}
|
||
fileManagerState.offset = nextOffset;
|
||
fetchFileManagerData();
|
||
}
|
||
|
||
async function triggerFileReimport(fileId) {
|
||
if (!fileId || fileManagerState.reimporting.has(fileId)) {
|
||
return;
|
||
}
|
||
fileManagerState.reimporting.add(fileId);
|
||
renderFileManagerModal();
|
||
try {
|
||
const response = await fetch(`/fs-ai-asistant/api/workflow/lawrisk/admin/permit-files/${fileId}/reimport`, {
|
||
method: 'POST'
|
||
});
|
||
const data = await parseJsonResponse(response);
|
||
if (data.success) {
|
||
const hydrated = bootstrapImportSessionFromPayload(data.data || {}, {
|
||
successMessage: '已载入存档文件,请完成导入流程',
|
||
});
|
||
if (!hydrated) {
|
||
throw new Error('会话数据无效');
|
||
}
|
||
closeFileManagerModal();
|
||
await openImportModal();
|
||
} else {
|
||
alert(`重新导入失败:${data.message || '服务器错误'}`);
|
||
}
|
||
} catch (error) {
|
||
alert(`重新导入失败:${error.message}`);
|
||
} finally {
|
||
fileManagerState.reimporting.delete(fileId);
|
||
renderFileManagerModal();
|
||
}
|
||
}
|
||
|
||
async function confirmDeleteStoredFile(fileId) {
|
||
if (!fileId || fileManagerState.deleting.has(fileId)) {
|
||
return;
|
||
}
|
||
const confirmed = confirm('确定要删除该文件吗?删除后关联许可将无法下载原始文件。');
|
||
if (!confirmed) {
|
||
return;
|
||
}
|
||
fileManagerState.deleting.add(fileId);
|
||
renderFileManagerModal();
|
||
try {
|
||
const response = await fetch(`/fs-ai-asistant/api/workflow/lawrisk/admin/permit-files/${fileId}`, {
|
||
method: 'DELETE'
|
||
});
|
||
const data = await parseJsonResponse(response);
|
||
if (data.success) {
|
||
await fetchFileManagerData();
|
||
} else {
|
||
alert(`删除失败:${data.message || '服务器错误'}`);
|
||
}
|
||
} catch (error) {
|
||
alert(`删除失败:${error.message}`);
|
||
} finally {
|
||
fileManagerState.deleting.delete(fileId);
|
||
renderFileManagerModal();
|
||
}
|
||
}
|
||
|
||
// ================ 检查点管理功能 ================
|
||
|
||
// 打开检查点模态窗口
|
||
async function openCheckpointModal() {
|
||
const modal = document.getElementById('checkpointModal');
|
||
modal.classList.add('show');
|
||
|
||
// 重置并显示加载状态
|
||
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 = '';
|
||
expandedSnapshotGroups = new Set();
|
||
|
||
renderCheckpointManager(checkpointListCache);
|
||
|
||
await Promise.all([
|
||
ensureRegionFilterOptions(false),
|
||
refreshPermitRiskSnapshots(false),
|
||
refreshCheckpointList(false)
|
||
]);
|
||
}
|
||
|
||
// 关闭检查点模态窗口
|
||
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();
|
||
}
|
||
|
||
// 渲染检查点管理界面
|
||
function renderCheckpointManager(checkpoints) {
|
||
if (Array.isArray(checkpoints)) {
|
||
checkpointListCache = checkpoints;
|
||
}
|
||
|
||
const modalBody = document.getElementById('checkpointModalBody');
|
||
if (!modalBody) return;
|
||
|
||
const snapshotState = permitRiskSnapshotState;
|
||
const checkpointList = checkpointListCache || [];
|
||
|
||
let html = '<div class="checkpoint-content">';
|
||
|
||
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"><span class="loading-icon"></span>加载许可风险快照与检查点...</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-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 || '';
|
||
const riskPreview = escapeHtml(truncateText(riskFull, 160));
|
||
const sourceNameRaw = primary.permit_source_name || group.permit_source_name || '';
|
||
const sourceTag = sourceNameRaw ? `<span>来源:${escapeHtml(sourceNameRaw)}</span>` : '';
|
||
const legalSegments = [];
|
||
if (primary.legal_basis) {
|
||
legalSegments.push(`📕 ${escapeHtml(primary.legal_basis)}`);
|
||
}
|
||
if (primary.document_no) {
|
||
legalSegments.push(`📄 ${escapeHtml(primary.document_no)}`);
|
||
}
|
||
const legalHtml = legalSegments.length ? `<div class="timeline-meta" style="gap:6px;">${legalSegments.join('<span>|</span>')}</div>` : '';
|
||
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)}`);
|
||
}
|
||
if (detail.permit_source_name) {
|
||
detailMetaParts.push(`来源:${escapeHtml(detail.permit_source_name)}`);
|
||
}
|
||
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>
|
||
${sourceTag}
|
||
</div>
|
||
`;
|
||
|
||
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">风险快照 · ${riskCount > 1 ? `批次(${riskCount} 条)` : `版本 ${escapeHtml(String(primary.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}
|
||
${metaHtml}
|
||
${changeSummaryMerged}
|
||
<div class="timeline-actions">
|
||
${toggleButton}
|
||
${restoreButton}
|
||
</div>
|
||
${detailListHtml}
|
||
</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 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} 个`;
|
||
const checkpointSummaryText = `检查点 ${checkpointList.length} 个`;
|
||
|
||
html += `
|
||
<div class="timeline-footer">
|
||
<div class="snapshot-count">${snapshotBatchText}|${snapshotRangeText},${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>';
|
||
|
||
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;">
|
||
<strong>💡 说明:</strong>数据库检查点文件保存在服务器的 data/checkpoints/ 目录中,重启应用后不会丢失。
|
||
每个检查点包含完整的数据库备份,可用于快速回滚。
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
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>
|
||
`;
|
||
|
||
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);
|
||
}
|
||
}
|
||
|
||
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);
|
||
} else {
|
||
permitRiskSnapshotState.loading = true;
|
||
permitRiskSnapshotState.error = '';
|
||
}
|
||
|
||
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());
|
||
|
||
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;
|
||
expandedSnapshotGroups = new Set();
|
||
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);
|
||
}
|
||
}
|
||
|
||
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();
|
||
}
|
||
|
||
|
||
// 创建检查点
|
||
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"><span class="loading-icon"></span>创建中...</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 = '';
|
||
await refreshCheckpointList();
|
||
// 显示成功消息
|
||
setTimeout(() => {
|
||
alert(`✅ 检查点创建成功!\n\nID: ${data.data.checkpoint_id}\n备份了 ${data.data.total_rows} 行数据`);
|
||
}, 100);
|
||
} else {
|
||
setTimeout(() => {
|
||
alert(`❌ 创建失败:${data.message}`);
|
||
}, 100);
|
||
}
|
||
} catch (error) {
|
||
setTimeout(() => {
|
||
alert(`❌ 网络错误:${error.message}`);
|
||
}, 100);
|
||
} finally {
|
||
btn.disabled = false;
|
||
btn.innerHTML = originalText;
|
||
}
|
||
}
|
||
|
||
// 确认恢复检查点
|
||
function confirmRestoreCheckpoint(checkpointId) {
|
||
showDangerModal(
|
||
'恢复检查点',
|
||
`您确定要恢复检查点 "${checkpointId}" 吗?`,
|
||
'此操作将覆盖当前数据库中的所有数据,且无法撤销!请确保您已经创建了新的检查点作为备份。',
|
||
() => restoreCheckpoint(checkpointId)
|
||
);
|
||
}
|
||
|
||
// 恢复检查点
|
||
async function restoreCheckpoint(checkpointId) {
|
||
closeDangerModal();
|
||
|
||
// 显示恢复进度模态框
|
||
showRestoreProgressModal(checkpointId);
|
||
|
||
try {
|
||
const response = await fetch(`/fs-ai-asistant/api/workflow/lawrisk/admin/checkpoints/${checkpointId}/restore`, {
|
||
method: 'POST'
|
||
});
|
||
const data = await response.json();
|
||
|
||
// 关闭进度模态框
|
||
closeRestoreProgressModal();
|
||
|
||
if (data.success) {
|
||
await Promise.all([
|
||
refreshCheckpointList(),
|
||
refreshPermitRiskSnapshots()
|
||
]);
|
||
setTimeout(() => {
|
||
alert(`✅ 检查点恢复成功!\n\n恢复了 ${data.data.total_rows_restored} 行数据,覆盖了 ${data.data.tables_restored} 个表。`);
|
||
}, 100);
|
||
} else {
|
||
setTimeout(() => {
|
||
alert(`❌ 恢复失败:${data.message}`);
|
||
}, 100);
|
||
}
|
||
} catch (error) {
|
||
closeRestoreProgressModal();
|
||
setTimeout(() => {
|
||
alert(`❌ 网络错误:${error.message}`);
|
||
}, 100);
|
||
}
|
||
}
|
||
|
||
// 显示恢复进度模态框
|
||
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="justify-content: center; margin: 0 auto;">
|
||
<span class="loading-icon" style="width: 50px; height: 50px; border-width: 4px;"></span>
|
||
</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();
|
||
}
|
||
}
|
||
|
||
// 删除检查点
|
||
async function deleteCheckpoint(checkpointId) {
|
||
if (!confirm(`确定要删除检查点 "${checkpointId}" 吗?此操作无法撤销。`)) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const response = await fetch(`/fs-ai-asistant/api/workflow/lawrisk/admin/checkpoints/${checkpointId}`, {
|
||
method: 'DELETE'
|
||
});
|
||
const data = await parseJsonResponse(response);
|
||
|
||
if (data.success) {
|
||
await refreshCheckpointList();
|
||
setTimeout(() => {
|
||
alert(`✅ 检查点已删除`);
|
||
}, 100);
|
||
} else {
|
||
setTimeout(() => {
|
||
alert(`❌ 删除失败:${data.message}`);
|
||
}, 100);
|
||
}
|
||
} catch (error) {
|
||
setTimeout(() => {
|
||
alert(`❌ 网络错误:${error.message}`);
|
||
}, 100);
|
||
}
|
||
}
|
||
|
||
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 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 (currentStep >= 2) {
|
||
await loadPermitsForRegion();
|
||
}
|
||
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}`);
|
||
}
|
||
}
|
||
|
||
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 = [];
|
||
const snapshotGroups = new Map();
|
||
|
||
(snapshotItems || []).forEach(item => {
|
||
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);
|
||
|
||
items.push({
|
||
type: 'snapshot-group',
|
||
timeValue,
|
||
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,
|
||
},
|
||
});
|
||
});
|
||
|
||
(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;
|
||
}
|
||
|
||
// 格式化时间戳
|
||
function formatTimestamp(timestamp) {
|
||
if (!timestamp || timestamp.length < 15) {
|
||
return timestamp || '';
|
||
}
|
||
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();
|
||
}
|
||
|
||
// 标签页切换功能
|
||
function switchTab(tabId) {
|
||
// 隐藏所有标签页内容
|
||
const tabContents = document.querySelectorAll('.tab-content');
|
||
tabContents.forEach(content => {
|
||
content.classList.remove('active');
|
||
});
|
||
|
||
// 移除所有标签按钮的激活状态
|
||
const tabButtons = document.querySelectorAll('.tab-button');
|
||
tabButtons.forEach(button => {
|
||
button.classList.remove('active');
|
||
});
|
||
|
||
// 激活选中的标签页
|
||
const selectedTab = document.getElementById(tabId);
|
||
if (selectedTab) {
|
||
selectedTab.classList.add('active');
|
||
}
|
||
|
||
// 激活选中的标签按钮
|
||
const selectedButton = document.querySelector(`[data-tab="${tabId}"]`);
|
||
if (selectedButton) {
|
||
selectedButton.classList.add('active');
|
||
}
|
||
|
||
// 根据标签页执行特定操作
|
||
if (tabId === 'permits-tab') {
|
||
loadPermitFilterOptions();
|
||
} else if (tabId === 'files-tab') {
|
||
loadFileManager();
|
||
}
|
||
|
||
// 更新URL(不刷新页面)
|
||
const url = new URL(window.location);
|
||
url.searchParams.set('tab', tabId.replace('-tab', ''));
|
||
window.history.replaceState({}, '', url);
|
||
}
|
||
|
||
// ============== 许可事项筛选器相关函数 ==============
|
||
|
||
// 区域-部门映射关系缓存
|
||
let regionDepartmentMap = {};
|
||
|
||
let permitFilterOptions = {
|
||
regions: [],
|
||
themes: [],
|
||
departments: []
|
||
};
|
||
|
||
let permitCurrentPage = 0;
|
||
let permitPageSize = 50;
|
||
let permitTotalPages = 0;
|
||
|
||
// 加载筛选选项
|
||
async function loadPermitFilterOptions() {
|
||
// 显示加载状态
|
||
const loadingElement = document.getElementById('filterOptionsLoading');
|
||
if (loadingElement) {
|
||
loadingElement.style.display = 'flex';
|
||
}
|
||
|
||
try {
|
||
const response = await fetch('/fs-ai-asistant/api/workflow/lawrisk/admin/permits/filter-options');
|
||
const data = await response.json();
|
||
|
||
if (!data.success) {
|
||
throw new Error(data.message || '加载筛选选项失败');
|
||
}
|
||
|
||
permitFilterOptions = data.data || {};
|
||
renderPermitFilterOptions();
|
||
} catch (error) {
|
||
console.error('加载筛选选项失败:', error);
|
||
// 可以在这里显示错误提示
|
||
alert('加载筛选选项失败: ' + error.message);
|
||
} finally {
|
||
// 隐藏加载状态
|
||
if (loadingElement) {
|
||
loadingElement.style.display = 'none';
|
||
}
|
||
}
|
||
}
|
||
|
||
// 渲染筛选选项
|
||
function renderPermitFilterOptions() {
|
||
// 构建区域-部门映射关系
|
||
buildRegionDepartmentMapping();
|
||
|
||
// 渲染区域选项
|
||
const regionOptionsList = document.getElementById('regionOptionsList');
|
||
if (regionOptionsList) {
|
||
regionOptionsList.innerHTML = '';
|
||
permitFilterOptions.regions.forEach(region => {
|
||
const div = document.createElement('div');
|
||
div.style.padding = '6px 12px';
|
||
div.innerHTML = `
|
||
<label style="display: flex; align-items: center; cursor: pointer;">
|
||
<input type="checkbox" name="regionFilter" value="${region.id || region}" onchange="onRegionSelectionChange()" style="margin-right: 8px;">
|
||
<span>${region.name || region}</span>
|
||
</label>
|
||
`;
|
||
regionOptionsList.appendChild(div);
|
||
});
|
||
}
|
||
|
||
// 渲染主题选项
|
||
const themeOptionsList = document.getElementById('themeOptionsList');
|
||
if (themeOptionsList) {
|
||
themeOptionsList.innerHTML = '';
|
||
permitFilterOptions.themes.forEach(theme => {
|
||
const div = document.createElement('div');
|
||
div.style.padding = '6px 12px';
|
||
div.innerHTML = `
|
||
<label style="display: flex; align-items: center; cursor: pointer;">
|
||
<input type="checkbox" name="themeFilter" value="${theme.id}" onchange="updateSelectedText('theme')" style="margin-right: 8px;">
|
||
<span>${theme.name}</span>
|
||
</label>
|
||
`;
|
||
themeOptionsList.appendChild(div);
|
||
});
|
||
}
|
||
|
||
// 渲染部门选项(显示所有部门)
|
||
renderDepartmentOptions(permitFilterOptions.departments);
|
||
|
||
// 自动加载权限范围内的许可事项
|
||
// 注意:不要在渲染过程中直接调用,需要延迟执行确保DOM更新完成
|
||
setTimeout(() => {
|
||
loadAllVisiblePermits();
|
||
}, 100);
|
||
}
|
||
|
||
// 构建区域-部门映射关系
|
||
function buildRegionDepartmentMapping() {
|
||
// 初始化映射对象
|
||
regionDepartmentMap = {};
|
||
|
||
// 获取所有部门数据
|
||
const allDepartments = permitFilterOptions.departments || [];
|
||
|
||
// 为每个区域建立部门映射
|
||
permitFilterOptions.regions.forEach(region => {
|
||
const regionId = region.id || region;
|
||
regionDepartmentMap[regionId] = [];
|
||
});
|
||
|
||
// 将部门分配到对应区域
|
||
allDepartments.forEach(dept => {
|
||
const regionId = dept.region_id;
|
||
if (regionId && regionDepartmentMap.hasOwnProperty(regionId)) {
|
||
regionDepartmentMap[regionId].push(dept);
|
||
}
|
||
});
|
||
|
||
console.log('区域-部门映射关系构建完成:', regionDepartmentMap);
|
||
}
|
||
|
||
// 自动加载所有可见的许可事项(按权限过滤)
|
||
async function loadAllVisiblePermits() {
|
||
// 重置分页到第一页
|
||
permitCurrentPage = 0;
|
||
|
||
// 显示加载状态
|
||
showPermitsLoading(true);
|
||
hidePermitsError();
|
||
|
||
try {
|
||
// 不传递任何筛选参数,直接获取权限范围内的所有许可事项
|
||
const filters = {
|
||
limit: permitPageSize,
|
||
offset: permitCurrentPage * permitPageSize
|
||
};
|
||
|
||
const params = new URLSearchParams();
|
||
Object.keys(filters).forEach(key => {
|
||
if (filters[key]) {
|
||
params.append(key, filters[key]);
|
||
}
|
||
});
|
||
|
||
console.log('自动加载许可事项,参数:', params.toString());
|
||
const response = await fetch(`/fs-ai-asistant/api/workflow/lawrisk/admin/permits/advanced-filter?${params}`);
|
||
const data = await response.json();
|
||
|
||
if (!data.success) {
|
||
throw new Error(data.message || '加载失败');
|
||
}
|
||
|
||
console.log('自动加载结果:', data.data);
|
||
renderPermitResults(data.data.permits || []);
|
||
updatePermitPagination(data.data.pagination || {});
|
||
|
||
} catch (error) {
|
||
console.error('自动加载失败:', error);
|
||
showPermitsError(error.message);
|
||
renderPermitResults([]);
|
||
updatePermitPagination({});
|
||
} finally {
|
||
showPermitsLoading(false);
|
||
}
|
||
}
|
||
|
||
// 应用筛选
|
||
async function applyPermitFilter() {
|
||
// 获取选中的区域
|
||
const regionCheckboxes = document.querySelectorAll('input[name="regionFilter"]:checked');
|
||
const regions = Array.from(regionCheckboxes).map(cb => cb.value);
|
||
|
||
// 获取选中的主题
|
||
const themeCheckboxes = document.querySelectorAll('input[name="themeFilter"]:checked');
|
||
const themes = Array.from(themeCheckboxes).map(cb => cb.value);
|
||
|
||
// 获取选中的部门
|
||
const departmentCheckboxes = document.querySelectorAll('input[name="departmentFilter"]:checked');
|
||
const departments = Array.from(departmentCheckboxes).map(cb => cb.value);
|
||
|
||
const searchText = document.getElementById('filterSearchText')?.value || '';
|
||
|
||
const filters = {
|
||
regions: regions.length > 0 ? regions : null,
|
||
themes: themes.length > 0 ? themes : null,
|
||
departments: departments.length > 0 ? departments : null,
|
||
search_text: searchText.trim() || null,
|
||
limit: permitPageSize,
|
||
offset: permitCurrentPage * permitPageSize
|
||
};
|
||
|
||
// 显示加载状态
|
||
showPermitsLoading(true);
|
||
hidePermitsError();
|
||
|
||
try {
|
||
const params = new URLSearchParams();
|
||
Object.keys(filters).forEach(key => {
|
||
if (filters[key]) {
|
||
if (Array.isArray(filters[key])) {
|
||
// 数组参数:regions[]=1®ions[]=2
|
||
filters[key].forEach(value => {
|
||
params.append(key + '[]', value);
|
||
});
|
||
} else {
|
||
params.append(key, filters[key]);
|
||
}
|
||
}
|
||
});
|
||
|
||
const response = await fetch(`/fs-ai-asistant/api/workflow/lawrisk/admin/permits/advanced-filter?${params}`);
|
||
const data = await response.json();
|
||
|
||
if (!data.success) {
|
||
throw new Error(data.message || '筛选失败');
|
||
}
|
||
|
||
renderPermitResults(data.data.permits || []);
|
||
updatePermitPagination(data.data.pagination || {});
|
||
|
||
} catch (error) {
|
||
console.error('筛选失败:', error);
|
||
showPermitsError(error.message);
|
||
renderPermitResults([]);
|
||
updatePermitPagination({});
|
||
} finally {
|
||
showPermitsLoading(false);
|
||
}
|
||
}
|
||
|
||
// 切换多选下拉菜单
|
||
function toggleMultiSelect(optionsId) {
|
||
const options = document.getElementById(optionsId);
|
||
if (options) {
|
||
options.style.display = options.style.display === 'none' ? 'block' : 'none';
|
||
}
|
||
}
|
||
|
||
// 更新选中文本显示
|
||
function updateSelectedText(type) {
|
||
const checkboxes = document.querySelectorAll(`input[name="${type}Filter"]:checked`);
|
||
const selectedText = document.getElementById(`${type}SelectedText`);
|
||
const selectAllCheckbox = document.getElementById(`${type}SelectAll`);
|
||
|
||
if (selectedText) {
|
||
if (checkboxes.length === 0) {
|
||
selectedText.textContent = `全部${type === 'region' ? '区域' : type === 'theme' ? '主题' : '部门'}`;
|
||
} else if (checkboxes.length === 1) {
|
||
const label = checkboxes[0].closest('label');
|
||
selectedText.textContent = label ? label.querySelector('span').textContent : '已选择';
|
||
} else {
|
||
selectedText.textContent = `已选择 ${checkboxes.length} 项`;
|
||
}
|
||
}
|
||
|
||
// 更新全选复选框状态
|
||
const allCheckboxes = document.querySelectorAll(`input[name="${type}Filter"]`);
|
||
if (selectAllCheckbox && allCheckboxes.length > 0) {
|
||
const checkedCount = checkboxes.length;
|
||
selectAllCheckbox.checked = checkedCount === allCheckboxes.length;
|
||
selectAllCheckbox.indeterminate = checkedCount > 0 && checkedCount < allCheckboxes.length;
|
||
}
|
||
}
|
||
|
||
// 区域选择变化处理(基于前端缓存)
|
||
function onRegionSelectionChange() {
|
||
console.log('onRegionSelectionChange 被调用'); // 调试日志
|
||
// 更新区域显示文本
|
||
updateSelectedText('region');
|
||
|
||
// 获取当前选中的区域
|
||
const regionCheckboxes = document.querySelectorAll('input[name="regionFilter"]:checked');
|
||
const selectedRegions = Array.from(regionCheckboxes).map(cb => cb.value);
|
||
console.log('选中的区域:', selectedRegions); // 调试日志
|
||
|
||
// 根据选中的区域,动态更新部门列表
|
||
if (selectedRegions.length === 1) {
|
||
// 只选择一个区域时,显示该区域的部门
|
||
const regionId = selectedRegions[0];
|
||
console.log('显示区域关联部门:', regionId); // 调试日志
|
||
const departments = regionDepartmentMap[regionId] || [];
|
||
renderDepartmentOptions(departments);
|
||
} else if (selectedRegions.length === 0) {
|
||
// 没有选择区域时,清空已选部门并显示所有部门
|
||
console.log('显示所有部门,清空部门选择'); // 调试日志
|
||
// 清空部门选择状态
|
||
const departmentCheckboxes = document.querySelectorAll('input[name="departmentFilter"]:checked');
|
||
departmentCheckboxes.forEach(cb => cb.checked = false);
|
||
updateSelectedText('department');
|
||
// 重新加载所有部门
|
||
renderDepartmentOptions(permitFilterOptions.departments || []);
|
||
} else {
|
||
// 选择多个区域时,显示所有选中区域关联的部门(去重)
|
||
console.log('显示多个区域关联部门'); // 调试日志
|
||
const allDepartments = [];
|
||
const departmentIds = new Set();
|
||
|
||
selectedRegions.forEach(regionId => {
|
||
const depts = regionDepartmentMap[regionId] || [];
|
||
depts.forEach(dept => {
|
||
if (!departmentIds.has(dept.id)) {
|
||
departmentIds.add(dept.id);
|
||
allDepartments.push(dept);
|
||
}
|
||
});
|
||
});
|
||
|
||
renderDepartmentOptions(allDepartments);
|
||
}
|
||
}
|
||
|
||
// 渲染部门选项(基于传入的部门列表)
|
||
function renderDepartmentOptions(departments) {
|
||
const departmentOptionsList = document.getElementById('departmentOptionsList');
|
||
if (departmentOptionsList) {
|
||
departmentOptionsList.innerHTML = '';
|
||
|
||
if (!departments || departments.length === 0) {
|
||
// 如果没有部门,显示提示信息
|
||
const div = document.createElement('div');
|
||
div.style.padding = '12px';
|
||
div.style.textAlign = 'center';
|
||
div.style.color = '#999';
|
||
div.style.fontSize = '13px';
|
||
div.innerHTML = '该区域暂无关联部门';
|
||
departmentOptionsList.appendChild(div);
|
||
} else {
|
||
departments.forEach(dept => {
|
||
const div = document.createElement('div');
|
||
div.style.padding = '6px 12px';
|
||
div.innerHTML = `
|
||
<label style="display: flex; align-items: center; cursor: pointer;">
|
||
<input type="checkbox" name="departmentFilter" value="${dept.id}" onchange="updateSelectedText('department')" style="margin-right: 8px;">
|
||
<span>${dept.name} (${dept.code})</span>
|
||
</label>
|
||
`;
|
||
departmentOptionsList.appendChild(div);
|
||
});
|
||
}
|
||
|
||
// 更新部门显示文本
|
||
updateSelectedText('department');
|
||
}
|
||
}
|
||
|
||
// 全选区域
|
||
function selectAllRegions() {
|
||
const selectAll = document.getElementById('regionSelectAll');
|
||
const checkboxes = document.querySelectorAll('input[name="regionFilter"]');
|
||
checkboxes.forEach(cb => cb.checked = selectAll.checked);
|
||
updateSelectedText('region');
|
||
}
|
||
|
||
// 全选主题
|
||
function selectAllThemes() {
|
||
const selectAll = document.getElementById('themeSelectAll');
|
||
const checkboxes = document.querySelectorAll('input[name="themeFilter"]');
|
||
checkboxes.forEach(cb => cb.checked = selectAll.checked);
|
||
updateSelectedText('theme');
|
||
}
|
||
|
||
// 全选部门
|
||
function selectAllDepartments() {
|
||
const selectAll = document.getElementById('departmentSelectAll');
|
||
const checkboxes = document.querySelectorAll('input[name="departmentFilter"]');
|
||
checkboxes.forEach(cb => cb.checked = selectAll.checked);
|
||
updateSelectedText('department');
|
||
}
|
||
|
||
// 查看许可详情
|
||
async function viewPermitDetail(permitId, regionId) {
|
||
if (!regionId) {
|
||
showAlert('error', '缺少区域信息,无法查看详情');
|
||
return;
|
||
}
|
||
|
||
// 显示加载动画
|
||
const loadingHtml = `
|
||
<div style="display: flex; justify-content: center; align-items: center; min-height: 200px; flex-direction: column;">
|
||
<div class="loading">
|
||
<span class="loading-icon"></span>
|
||
</div>
|
||
<p style="color: #666; margin-top: 16px; font-size: 14px;">正在加载许可详情...</p>
|
||
</div>
|
||
`;
|
||
showModal('许可事项详情', loadingHtml);
|
||
|
||
// 构建请求参数
|
||
const params = new URLSearchParams();
|
||
params.append('permit_id', permitId);
|
||
params.append('region_id', regionId);
|
||
|
||
try {
|
||
const response = await fetch(`/fs-ai-asistant/api/workflow/lawrisk/admin/permit-details?${params}`);
|
||
const data = await response.json();
|
||
|
||
if (!data.success) {
|
||
throw new Error(data.message || '获取许可详情失败');
|
||
}
|
||
|
||
const permit = data.data.permit;
|
||
const themeDisplay = data.data.theme_display;
|
||
|
||
// 构建风险列表
|
||
const risksHtml = (permit.risks || [])
|
||
.map((risk, index) => `
|
||
<div style="border: 1px solid #e0e0e0; border-radius: 6px; padding: 20px; margin-bottom: 16px; background: #fff; box-shadow: 0 1px 3px rgba(0,0,0,0.05);">
|
||
<div style="margin-bottom: 12px;">
|
||
<div style="font-size: 13px; color: #666; font-weight: 600; margin-bottom: 6px;">风险提示内容</div>
|
||
<div style="color: #1a202c; font-size: 14px; line-height: 1.8; padding: 16px; background: #f0f9ff; border-left: 4px solid #2c5282; border-radius: 4px; font-weight: 500;">${escapeHtml(risk.risk_content || '')}</div>
|
||
</div>
|
||
|
||
${risk.summary ? `
|
||
<div style="margin-bottom: 12px;">
|
||
<div style="font-size: 13px; color: #666; font-weight: 600; margin-bottom: 6px;">风险概述</div>
|
||
<div style="color: #444; font-size: 14px; line-height: 1.8; padding: 12px; background: #f9fafb; border-radius: 4px;">${escapeHtml(risk.summary || '')}</div>
|
||
</div>
|
||
` : ''}
|
||
|
||
${risk.legal_basis ? `
|
||
<div style="margin-bottom: 12px;">
|
||
<div style="font-size: 13px; color: #666; font-weight: 600; margin-bottom: 6px;">法律依据</div>
|
||
<div style="color: #444; font-size: 14px; line-height: 1.8; padding: 12px; background: #f9fafb; border-radius: 4px; font-style: italic;">${escapeHtml(risk.legal_basis)}</div>
|
||
</div>
|
||
` : ''}
|
||
|
||
${risk.document_no ? `
|
||
<div style="margin-bottom: 12px;">
|
||
<div style="font-size: 13px; color: #666; font-weight: 600; margin-bottom: 6px;">文件文号</div>
|
||
<div style="color: #444; font-size: 14px; line-height: 1.8; padding: 12px; background: #f9fafb; border-radius: 4px;">${escapeHtml(risk.document_no)}</div>
|
||
</div>
|
||
` : ''}
|
||
|
||
${risk.remarks ? `
|
||
<div style="margin-bottom: 0;">
|
||
<div style="font-size: 13px; color: #666; font-weight: 600; margin-bottom: 6px;">备注</div>
|
||
<div style="color: #444; font-size: 14px; line-height: 1.8; padding: 12px; background: #f9fafb; border-radius: 4px;">${escapeHtml(risk.remarks)}</div>
|
||
</div>
|
||
` : ''}
|
||
</div>
|
||
`).join('');
|
||
|
||
// 构建主题列表
|
||
const themesList = (permit.themes || [])
|
||
.map(t => `<span style="display: inline-block; padding: 2px 8px; background: #eef2ff; color: #4c1d95; border-radius: 4px; font-size: 12px; margin-right: 4px; margin-bottom: 4px;">${escapeHtml(t.name)}</span>`)
|
||
.join('');
|
||
|
||
const detailHtml = `
|
||
<div style="max-height: 70vh; overflow-y: auto; padding: 24px;">
|
||
<div style="background: #f8f9fa; padding: 24px; border-radius: 8px; margin-bottom: 24px;">
|
||
<h3 style="margin: 0 0 12px 0; color: #2c5282; font-size: 20px;">${escapeHtml(permit.name || '未知许可')}</h3>
|
||
<div style="display: flex; gap: 20px; flex-wrap: wrap; font-size: 14px;">
|
||
<div><strong>行政区域:</strong>${escapeHtml(permit.region?.name || '-')}</div>
|
||
<div><strong>风险数量:</strong>${permit.risks?.length || 0}</div>
|
||
</div>
|
||
</div>
|
||
|
||
${themesList ? `<div style="margin-bottom: 24px;"><strong style="display: block; margin-bottom: 8px;">关联主题:</strong>${themesList}</div>` : ''}
|
||
|
||
<!-- 许可详细信息 -->
|
||
<div style="background: #fff; border: 1px solid #e0e0e0; border-radius: 8px; padding: 20px; margin-bottom: 24px;">
|
||
<h4 style="color: #333; margin-bottom: 16px; border-bottom: 2px solid #2c5282; padding-bottom: 8px;">许可信息</h4>
|
||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 16px;">
|
||
${permit.permit_status ? `
|
||
<div>
|
||
<div style="font-size: 13px; color: #666; font-weight: 600; margin-bottom: 6px;">许可情况</div>
|
||
<div style="color: #444; font-size: 14px; padding: 10px; background: #f9fafb; border-radius: 4px;">${escapeHtml(permit.permit_status)}</div>
|
||
</div>
|
||
` : ''}
|
||
|
||
${permit.subitem_summary ? `
|
||
<div>
|
||
<div style="font-size: 13px; color: #666; font-weight: 600; margin-bottom: 6px;">许可(备案)事项子项</div>
|
||
<div style="color: #444; font-size: 14px; padding: 10px; background: #f9fafb; border-radius: 4px;">${escapeHtml(permit.subitem_summary)}</div>
|
||
</div>
|
||
` : ''}
|
||
|
||
${permit.region?.name ? `
|
||
<div>
|
||
<div style="font-size: 13px; color: #666; font-weight: 600; margin-bottom: 6px;">行政区域</div>
|
||
<div style="color: #444; font-size: 14px; padding: 10px; background: #f9fafb; border-radius: 4px;">${escapeHtml(permit.region.name)}</div>
|
||
</div>
|
||
` : ''}
|
||
|
||
${permit.responsible_contact ? `
|
||
<div>
|
||
<div style="font-size: 13px; color: #666; font-weight: 600; margin-bottom: 6px;">负责部门</div>
|
||
<div style="color: #444; font-size: 14px; padding: 10px; background: #f9fafb; border-radius: 4px;">${escapeHtml(permit.responsible_contact)}</div>
|
||
</div>
|
||
` : ''}
|
||
|
||
${permit.jurisdiction_scope ? `
|
||
<div>
|
||
<div style="font-size: 13px; color: #666; font-weight: 600; margin-bottom: 6px;">权限划分</div>
|
||
<div style="color: #444; font-size: 14px; padding: 10px; background: #f9fafb; border-radius: 4px;">${escapeHtml(permit.jurisdiction_scope)}</div>
|
||
</div>
|
||
` : ''}
|
||
|
||
${permit.business_scope ? `
|
||
<div style="grid-column: 1 / -1;">
|
||
<div style="font-size: 13px; color: #666; font-weight: 600; margin-bottom: 6px;">经营范围</div>
|
||
<div style="color: #444; font-size: 14px; padding: 10px; background: #f9fafb; border-radius: 4px; line-height: 1.8;">${escapeHtml(permit.business_scope)}</div>
|
||
</div>
|
||
` : ''}
|
||
</div>
|
||
</div>
|
||
|
||
<div>
|
||
<h4 style="color: #333; margin-bottom: 12px; border-bottom: 2px solid #2c5282; padding-bottom: 8px;">风险信息</h4>
|
||
${risksHtml || '<div style="color: #999; text-align: center; padding: 20px;">暂无风险信息</div>'}
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
showModal('许可事项详情', detailHtml);
|
||
|
||
} catch (error) {
|
||
console.error('获取许可详情失败:', error);
|
||
const errorHtml = `
|
||
<div style="display: flex; justify-content: center; align-items: center; min-height: 200px; flex-direction: column;">
|
||
<div style="color: #dc2626; font-size: 16px; margin-bottom: 16px;">❌ 获取许可详情失败</div>
|
||
<div style="color: #666; font-size: 14px; text-align: center; max-width: 400px;">${escapeHtml(error.message || '未知错误')}</div>
|
||
<button class="btn btn-warning btn-sm" style="margin-top: 20px;" onclick="closePermitDetailModal()">关闭</button>
|
||
</div>
|
||
`;
|
||
showModal('许可事项详情', errorHtml);
|
||
}
|
||
}
|
||
|
||
// 删除许可事项
|
||
async function deletePermit(permitId, regionId) {
|
||
if (!confirm('确定要删除该许可事项吗?此操作不可恢复,并且会创建风险快照。')) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const response = await fetch('/fs-ai-asistant/api/workflow/lawrisk/admin/permits', {
|
||
method: 'DELETE',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Accept': 'application/json'
|
||
},
|
||
body: JSON.stringify({
|
||
permit_id: permitId,
|
||
region_id: regionId,
|
||
edited_by: currentUser?.display_name || currentUser?.username || '未知用户',
|
||
change_summary: '通过管理界面删除许可事项'
|
||
})
|
||
});
|
||
|
||
const data = await response.json();
|
||
|
||
if (!data.success) {
|
||
throw new Error(data.message || '删除失败');
|
||
}
|
||
|
||
showAlert('success', '许可事项删除成功');
|
||
// 重新加载当前页面的数据
|
||
loadAllVisiblePermits();
|
||
|
||
} catch (error) {
|
||
console.error('删除许可失败:', error);
|
||
showAlert('error', '删除许可失败:' + error.message);
|
||
}
|
||
}
|
||
|
||
// 渲染筛选结果
|
||
function renderPermitResults(permits) {
|
||
const permitsList = document.getElementById('permitsList');
|
||
const resultCount = document.getElementById('resultCount');
|
||
|
||
if (!permitsList || !resultCount) return;
|
||
|
||
if (!permits || permits.length === 0) {
|
||
permitsList.innerHTML = `
|
||
<div style="text-align: center; padding: 60px 20px; color: #999;">
|
||
<div style="font-size: 48px; margin-bottom: 16px;">📂</div>
|
||
<div>未找到符合条件的许可事项</div>
|
||
</div>
|
||
`;
|
||
resultCount.innerHTML = `共找到 <strong style="color: #2c5282;">0</strong> 个许可事项`;
|
||
return;
|
||
}
|
||
|
||
resultCount.innerHTML = `共找到 <strong style="color: #2c5282;">${permits.length}</strong> 个许可事项`;
|
||
|
||
let html = `
|
||
<table style="width: 100%; border-collapse: collapse;">
|
||
<thead>
|
||
<tr style="background: #f8f9fa; border-bottom: 2px solid #e0e0e0;">
|
||
<th style="padding: 14px 16px; text-align: left; font-weight: 600; color: #555; font-size: 14px;">许可事项</th>
|
||
<th style="padding: 14px 16px; text-align: left; font-weight: 600; color: #555; font-size: 14px; width: 180px;">行政区域</th>
|
||
<th style="padding: 14px 16px; text-align: left; font-weight: 600; color: #555; font-size: 14px; width: 120px;">主题</th>
|
||
<th style="padding: 14px 16px; text-align: left; font-weight: 600; color: #555; font-size: 14px; width: 100px;">风险数</th>
|
||
<th style="padding: 14px 16px; text-align: left; font-weight: 600; color: #555; font-size: 14px; width: 180px;">操作</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
`;
|
||
|
||
permits.forEach(permit => {
|
||
const themesHtml = (permit.themes || [])
|
||
.map(t => `<span style="display: inline-block; padding: 2px 8px; background: #eef2ff; color: #4c1d95; border-radius: 4px; font-size: 12px; margin-right: 4px;">${escapeHtml(t.name)}</span>`)
|
||
.join('');
|
||
|
||
const regionId = permit.region?.id || '';
|
||
|
||
html += `
|
||
<tr style="border-bottom: 1px solid #f0f0f0;">
|
||
<td style="padding: 16px;">
|
||
<div style="font-weight: 600; color: #333; margin-bottom: 6px;">${escapeHtml(permit.name || '未知许可')}</div>
|
||
<div>${themesHtml}</div>
|
||
</td>
|
||
<td style="padding: 16px; color: #666;">${escapeHtml(permit.region?.name || '-')}</td>
|
||
<td style="padding: 16px; color: #666;">${permit.theme_count || 0} 个</td>
|
||
<td style="padding: 16px; color: #666;">${permit.risk_count || 0}</td>
|
||
<td style="padding: 16px;">
|
||
<button onclick="viewPermitDetail('${permit.id}', '${regionId}')" style="padding: 6px 12px; background: #2c5282; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 13px; margin-right: 8px;">
|
||
查看
|
||
</button>
|
||
<button onclick="deletePermit('${permit.id}', '${regionId}')" style="padding: 6px 12px; background: #dc2626; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 13px;">
|
||
删除
|
||
</button>
|
||
</td>
|
||
</tr>
|
||
`;
|
||
});
|
||
|
||
html += `
|
||
</tbody>
|
||
</table>
|
||
`;
|
||
|
||
permitsList.innerHTML = html;
|
||
}
|
||
|
||
// 更新分页信息
|
||
function updatePermitPagination(pagination) {
|
||
const permitsPagination = document.getElementById('permitsPagination');
|
||
const paginationInfo = document.getElementById('paginationInfo');
|
||
const prevBtn = document.getElementById('prevPageBtn');
|
||
const nextBtn = document.getElementById('nextPageBtn');
|
||
|
||
if (!permitsPagination || !paginationInfo || !prevBtn || !nextBtn) return;
|
||
|
||
if (!pagination || !pagination.total) {
|
||
permitsPagination.style.display = 'none';
|
||
return;
|
||
}
|
||
|
||
const total = pagination.total || 0;
|
||
permitTotalPages = Math.ceil(total / permitPageSize);
|
||
|
||
paginationInfo.textContent = `第 ${permitCurrentPage + 1} 页 / 共 ${permitTotalPages} 页`;
|
||
|
||
prevBtn.disabled = permitCurrentPage === 0;
|
||
nextBtn.disabled = permitCurrentPage >= permitTotalPages - 1;
|
||
|
||
permitsPagination.style.display = 'flex';
|
||
}
|
||
|
||
// 上一页
|
||
function previousPermitPage() {
|
||
if (permitCurrentPage > 0) {
|
||
permitCurrentPage--;
|
||
applyPermitFilter();
|
||
}
|
||
}
|
||
|
||
// 下一页
|
||
function nextPermitPage() {
|
||
if (permitCurrentPage < permitTotalPages - 1) {
|
||
permitCurrentPage++;
|
||
applyPermitFilter();
|
||
}
|
||
}
|
||
|
||
// 重置筛选
|
||
function resetPermitFilter() {
|
||
const regionSelect = document.getElementById('filterRegion');
|
||
const themeSelect = document.getElementById('filterTheme');
|
||
const departmentSelect = document.getElementById('filterDepartment');
|
||
const searchInput = document.getElementById('filterSearchText');
|
||
|
||
if (regionSelect) regionSelect.value = '';
|
||
if (themeSelect) themeSelect.value = '';
|
||
if (departmentSelect) departmentSelect.value = '';
|
||
if (searchInput) searchInput.value = '';
|
||
|
||
permitCurrentPage = 0;
|
||
|
||
// 清空结果
|
||
const permitsList = document.getElementById('permitsList');
|
||
const resultCount = document.getElementById('resultCount');
|
||
const permitsPagination = document.getElementById('permitsPagination');
|
||
|
||
if (permitsList) {
|
||
permitsList.innerHTML = `
|
||
<div style="text-align: center; padding: 60px 20px; color: #999;">
|
||
<div style="font-size: 48px; margin-bottom: 16px;">📋</div>
|
||
<div>请选择筛选条件并点击"应用筛选"</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
if (resultCount) {
|
||
resultCount.innerHTML = `共找到 <strong style="color: #2c5282;">0</strong> 个许可事项`;
|
||
}
|
||
|
||
if (permitsPagination) {
|
||
permitsPagination.style.display = 'none';
|
||
}
|
||
}
|
||
|
||
// 显示/隐藏加载状态
|
||
function showPermitsLoading(show) {
|
||
const loading = document.getElementById('permitsLoading');
|
||
const applyBtn = document.getElementById('applyFilterBtn');
|
||
if (loading) loading.style.display = show ? 'block' : 'none';
|
||
if (applyBtn) applyBtn.disabled = show;
|
||
}
|
||
|
||
// 显示错误信息
|
||
function showPermitsError(message) {
|
||
const error = document.getElementById('permitsError');
|
||
const errorMsg = document.getElementById('permitsErrorMsg');
|
||
if (error && errorMsg) {
|
||
errorMsg.textContent = message;
|
||
error.style.display = 'block';
|
||
}
|
||
}
|
||
|
||
// 隐藏错误信息
|
||
function hidePermitsError() {
|
||
const error = document.getElementById('permitsError');
|
||
if (error) error.style.display = 'none';
|
||
}
|
||
|
||
// HTML转义
|
||
function escapeHtml(text) {
|
||
if (!text) return '';
|
||
const div = document.createElement('div');
|
||
div.textContent = text;
|
||
return div.innerHTML;
|
||
}
|
||
|
||
// 显示通用模态框
|
||
function showModal(title, contentHtml) {
|
||
// 移除已存在的详情模态框
|
||
const existingModal = document.getElementById('permitDetailModal');
|
||
if (existingModal) {
|
||
existingModal.remove();
|
||
}
|
||
|
||
// 创建新的模态框
|
||
const modalDiv = document.createElement('div');
|
||
modalDiv.className = 'modal';
|
||
modalDiv.id = 'permitDetailModal';
|
||
modalDiv.innerHTML = `
|
||
<div class="modal-content" style="max-width: 900px; width: 90%;">
|
||
<div class="modal-header" style="display: flex; justify-content: space-between; align-items: center; padding: 20px; border-bottom: 1px solid #e5e7eb;">
|
||
<h3 style="margin: 0; color: #2c5282; font-size: 18px;">${escapeHtml(title)}</h3>
|
||
<button class="btn btn-warning btn-sm" onclick="closePermitDetailModal()">关闭</button>
|
||
</div>
|
||
<div class="modal-body" style="padding: 20px;">
|
||
${contentHtml}
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
document.body.appendChild(modalDiv);
|
||
modalDiv.classList.add('show');
|
||
}
|
||
|
||
// 关闭许可详情模态框
|
||
function closePermitDetailModal() {
|
||
const modal = document.getElementById('permitDetailModal');
|
||
if (modal) {
|
||
modal.classList.remove('show');
|
||
setTimeout(() => modal.remove(), 300);
|
||
}
|
||
}
|
||
|
||
// 加载文件管理
|
||
function loadFileManager() {
|
||
const container = document.getElementById('fileManagerContainer');
|
||
if (!container || container.dataset.loaded === 'true') {
|
||
return;
|
||
}
|
||
|
||
container.innerHTML = `
|
||
<div style="padding: 40px; text-align: center; color: #999;">
|
||
<div class="loading">
|
||
<span class="loading-icon"></span>
|
||
正在加载文件列表...
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
fetch('/fs-ai-asistant/api/workflow/lawrisk/admin/permit-files', {
|
||
credentials: 'include',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Accept': 'application/json'
|
||
}
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success && data.data && data.data.files) {
|
||
renderFileList(data.data.files);
|
||
container.dataset.loaded = 'true';
|
||
} else {
|
||
container.innerHTML = `
|
||
<div style="padding: 40px; text-align: center; color: #999;">
|
||
<p>暂无文件数据</p>
|
||
</div>
|
||
`;
|
||
}
|
||
})
|
||
.catch(error => {
|
||
container.innerHTML = `
|
||
<div style="padding: 40px; text-align: center; color: #dc2626;">
|
||
<p>加载文件列表失败</p>
|
||
<p style="font-size: 12px; color: #999;">${error.message}</p>
|
||
</div>
|
||
`;
|
||
});
|
||
}
|
||
|
||
// 渲染文件列表
|
||
function renderFileList(files) {
|
||
const container = document.getElementById('fileManagerContainer');
|
||
if (!files || files.length === 0) {
|
||
container.innerHTML = `
|
||
<div style="padding: 40px; text-align: center; color: #999;">
|
||
<p>暂无许可导入文件</p>
|
||
</div>
|
||
`;
|
||
return;
|
||
}
|
||
|
||
let html = `
|
||
<div style="margin-bottom: 20px;">
|
||
<h3 style="font-size: 16px; color: #374151; margin: 0 0 16px 0;">文件列表(共 ${files.length} 个)</h3>
|
||
<div style="overflow-x: auto;">
|
||
<table style="width: 100%; border-collapse: collapse; font-size: 13px;">
|
||
<thead>
|
||
<tr style="background: #f9fafb; border-bottom: 2px solid #e5e7eb;">
|
||
<th style="padding: 12px 8px; text-align: left; color: #6b7280; font-weight: 600;">文件名</th>
|
||
<th style="padding: 12px 8px; text-align: left; color: #6b7280; font-weight: 600;">大小</th>
|
||
<th style="padding: 12px 8px; text-align: left; color: #6b7280; font-weight: 600;">上传时间</th>
|
||
<th style="padding: 12px 8px; text-align: left; color: #6b7280; font-weight: 600;">状态</th>
|
||
<th style="padding: 12px 8px; text-align: left; color: #6b7280; font-weight: 600;">操作</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
`;
|
||
|
||
files.forEach(file => {
|
||
const fileSize = formatFileSize(file.file_size || 0);
|
||
const uploadTime = file.created_at ? new Date(file.created_at).toLocaleString('zh-CN') : '';
|
||
const linksCount = file.links ? file.links.length : 0;
|
||
const status = linksCount > 0 ? 'processed' : 'warning';
|
||
const statusClass = status === 'processed' ? 'success' : (status === 'failed' ? 'danger' : 'warning');
|
||
const statusText = status === 'processed' ? '已处理' : (status === 'failed' ? '失败' : '待处理');
|
||
|
||
html += `
|
||
<tr style="border-bottom: 1px solid #f3f4f6;">
|
||
<td style="padding: 12px 8px; color: #111827;">${escapeHtml(file.filename || '')}</td>
|
||
<td style="padding: 12px 8px; color: #6b7280;">${fileSize}</td>
|
||
<td style="padding: 12px 8px; color: #6b7280;">${uploadTime}</td>
|
||
<td style="padding: 12px 8px;">
|
||
<span style="padding: 4px 12px; border-radius: 999px; font-size: 12px; font-weight: 600; background: ${statusClass === 'success' ? '#d1fae5' : statusClass === 'danger' ? '#fee2e2' : '#fef3c7'}; color: ${statusClass === 'success' ? '#065f46' : statusClass === 'danger' ? '#991b1b' : '#92400e'};">
|
||
${statusText}
|
||
</span>
|
||
</td>
|
||
<td style="padding: 12px 8px;">
|
||
<button class="btn btn-sm" style="margin-right: 8px; background: #3b82f6; color: white; padding: 4px 12px; border: none; border-radius: 6px; cursor: pointer; font-size: 12px;" onclick="reimportFile('${file.file_id}')">
|
||
重新导入
|
||
</button>
|
||
<button class="btn btn-sm" style="background: #ef4444; color: white; padding: 4px 12px; border: none; border-radius: 6px; cursor: pointer; font-size: 12px;" onclick="deleteFile('${file.file_id}')">
|
||
删除
|
||
</button>
|
||
</td>
|
||
</tr>
|
||
`;
|
||
});
|
||
|
||
html += `
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
container.innerHTML = html;
|
||
}
|
||
|
||
// 初始化标签页
|
||
function setupTabsByRole(userRole) {
|
||
const pageTitle = document.getElementById('pageTitle');
|
||
|
||
// 更新页面标题
|
||
pageTitle.textContent = '🗃️ 管理员控制台';
|
||
|
||
// 默认激活许可事项管理标签页
|
||
const permitsTab = document.getElementById('permits-tab');
|
||
const permitsButton = document.querySelector('[data-tab="permits-tab"]');
|
||
if (permitsTab) {
|
||
permitsTab.classList.add('active');
|
||
}
|
||
if (permitsButton) {
|
||
permitsButton.classList.add('active');
|
||
}
|
||
|
||
// 加载许可数据
|
||
goToStep(1);
|
||
}
|
||
|
||
// 触发文件上传
|
||
function triggerFileUpload() {
|
||
document.getElementById('dragDropFileInput').click();
|
||
}
|
||
|
||
// 处理拖拽文件
|
||
function handleDragDropFile(files) {
|
||
if (files && files.length > 0) {
|
||
// 只处理第一个文件
|
||
const file = files[0];
|
||
const fileName = file.name;
|
||
|
||
// 检查文件类型
|
||
if (!fileName.match(/\.(xlsx|xlsm)$/i)) {
|
||
showAlert('error', '请选择 Excel 文件(.xlsx 或 .xlsm 格式)');
|
||
return;
|
||
}
|
||
|
||
// 检查文件大小(500KB)
|
||
if (file.size > 500 * 1024) {
|
||
showAlert('error', '文件大小不能超过 500KB');
|
||
return;
|
||
}
|
||
|
||
// 文件验证通过,打开导入向导并传递文件
|
||
showAlert('success', `已选择文件: ${fileName},正在打开导入向导...`);
|
||
|
||
// 先打开模态框
|
||
openImportModal();
|
||
|
||
// 等待模态框渲染完成后,设置文件并处理
|
||
setTimeout(() => {
|
||
const importModalBody = document.getElementById('importModalBody');
|
||
if (!importModalBody) {
|
||
showAlert('error', '无法打开导入向导,请重试');
|
||
return;
|
||
}
|
||
|
||
// 查找文件输入元素
|
||
const fileInput = importModalBody.querySelector('input[type="file"]');
|
||
if (!fileInput) {
|
||
showAlert('error', '导入向导初始化失败,请重试');
|
||
return;
|
||
}
|
||
|
||
// 创建 FileList 对象
|
||
const dt = new DataTransfer();
|
||
dt.items.add(file);
|
||
fileInput.files = dt.files;
|
||
|
||
// 触发 handleImportFile 处理
|
||
handleImportFile(fileInput);
|
||
|
||
showAlert('success', `正在解析文件: ${fileName}`);
|
||
}, 100);
|
||
}
|
||
}
|
||
|
||
// 处理点击选择文件
|
||
function handleFileSelect(files) {
|
||
handleDragDropFile(files);
|
||
}
|
||
|
||
// 上传文件到服务器
|
||
function uploadFileToServer(fileName) {
|
||
const fileInput = document.getElementById('dragDropFileInput');
|
||
if (fileInput.files.length === 0) {
|
||
showAlert('error', '请先选择文件');
|
||
return;
|
||
}
|
||
|
||
const formData = new FormData();
|
||
formData.append('file', fileInput.files[0]);
|
||
|
||
// 显示上传进度
|
||
const preview = document.getElementById('importPreview');
|
||
preview.innerHTML = `
|
||
<div style="padding: 20px; background: #fef3c7; border-radius: 8px; border: 1px solid #fcd34d;">
|
||
<div style="display: flex; align-items: center; gap: 12px;">
|
||
<span style="font-size: 24px;">⏳</span>
|
||
<div>
|
||
<div style="font-weight: 600; color: #92400e;">正在上传文件...</div>
|
||
<div style="font-size: 12px; color: #78350f; margin-top: 4px;">请稍候,正在处理您的文件</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
fetch('/fs-ai-asistant/api/workflow/lawrisk/admin/permit-import/upload', {
|
||
method: 'POST',
|
||
body: formData,
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
preview.innerHTML = `
|
||
<div style="padding: 20px; background: #d1fae5; border-radius: 8px; border: 1px solid #6ee7b7;">
|
||
<div style="display: flex; align-items: center; gap: 12px;">
|
||
<span style="font-size: 24px;">✓</span>
|
||
<div>
|
||
<div style="font-weight: 600; color: #065f46;">文件上传成功!</div>
|
||
<div style="font-size: 12px; color: #047857; margin-top: 4px;">请在许可导入模态窗口中查看预览并确认导入</div>
|
||
</div>
|
||
</div>
|
||
<div style="margin-top: 16px;">
|
||
<button class="btn btn-primary" onclick="openImportModal()">
|
||
<span>→</span> 进入导入流程
|
||
</button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
} else {
|
||
throw new Error(data.message || '上传失败');
|
||
}
|
||
})
|
||
.catch(error => {
|
||
preview.innerHTML = `
|
||
<div style="padding: 20px; background: #fee2e2; border-radius: 8px; border: 1px solid #fca5a5;">
|
||
<div style="display: flex; align-items: center; gap: 12px;">
|
||
<span style="font-size: 24px;">✕</span>
|
||
<div>
|
||
<div style="font-weight: 600; color: #991b1b;">上传失败</div>
|
||
<div style="font-size: 12px; color: #b91c1c; margin-top: 4px;">${error.message}</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
});
|
||
}
|
||
|
||
// 清除文件选择
|
||
function clearFileSelection() {
|
||
document.getElementById('dragDropFileInput').value = '';
|
||
document.getElementById('importPreview').innerHTML = `
|
||
<p style="color: #999; text-align: center; padding: 20px;">支持拖拽上传,文件大小限制:≤ 500KB</p>
|
||
`;
|
||
}
|
||
|
||
// 重新导入文件
|
||
function reimportFile(fileId) {
|
||
if (!confirm('确定要重新导入该文件吗?')) {
|
||
return;
|
||
}
|
||
|
||
const container = document.getElementById('fileManagerContainer');
|
||
const originalContent = container.innerHTML;
|
||
|
||
container.innerHTML = `
|
||
<div style="padding: 40px; text-align: center; color: #f59e0b;">
|
||
<div class="loading">
|
||
<span class="loading-icon"></span>
|
||
正在重新导入文件...
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
fetch(`/fs-ai-asistant/api/workflow/lawrisk/admin/permit-files/${fileId}/reimport`, {
|
||
method: 'POST',
|
||
credentials: 'include',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Accept': 'application/json'
|
||
}
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
showAlert('success', '文件重新导入成功');
|
||
loadFileManager();
|
||
} else {
|
||
throw new Error(data.message || '重新导入失败');
|
||
}
|
||
})
|
||
.catch(error => {
|
||
showAlert('error', '重新导入失败:' + error.message);
|
||
container.innerHTML = originalContent;
|
||
});
|
||
}
|
||
|
||
// 删除文件
|
||
function deleteFile(fileId) {
|
||
if (!confirm('确定要删除该文件吗?此操作不可恢复。')) {
|
||
return;
|
||
}
|
||
|
||
const container = document.getElementById('fileManagerContainer');
|
||
const originalContent = container.innerHTML;
|
||
|
||
container.innerHTML = `
|
||
<div style="padding: 40px; text-align: center; color: #dc2626;">
|
||
<div class="loading">
|
||
<span class="loading-icon"></span>
|
||
正在删除文件...
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
fetch(`/fs-ai-asistant/api/workflow/lawrisk/admin/permit-files/${fileId}`, {
|
||
method: 'DELETE',
|
||
credentials: 'include',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Accept': 'application/json'
|
||
}
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
showAlert('success', '文件删除成功');
|
||
// 重新加载文件列表
|
||
const container = document.getElementById('fileManagerContainer');
|
||
if (container) {
|
||
delete container.dataset.loaded;
|
||
loadFileManager();
|
||
}
|
||
} else {
|
||
throw new Error(data.message || '删除失败');
|
||
}
|
||
})
|
||
.catch(error => {
|
||
showAlert('error', '删除失败:' + error.message);
|
||
container.innerHTML = originalContent;
|
||
});
|
||
}
|
||
|
||
// 显示提示信息
|
||
function showAlert(type, message) {
|
||
// 创建提示框元素
|
||
const alertDiv = document.createElement('div');
|
||
alertDiv.style.cssText = `
|
||
position: fixed;
|
||
top: 20px;
|
||
right: 20px;
|
||
padding: 16px 24px;
|
||
border-radius: 8px;
|
||
color: white;
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
z-index: 10000;
|
||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
|
||
animation: slideInRight 0.3s ease;
|
||
max-width: 400px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
`;
|
||
|
||
// 设置背景色
|
||
if (type === 'success') {
|
||
alertDiv.style.background = 'linear-gradient(135deg, #10b981 0%, #059669 100%)';
|
||
alertDiv.innerHTML = `✓ ${message}`;
|
||
} else if (type === 'error') {
|
||
alertDiv.style.background = 'linear-gradient(135deg, #ef4444 0%, #dc2626 100%)';
|
||
alertDiv.innerHTML = `✕ ${message}`;
|
||
} else if (type === 'warning') {
|
||
alertDiv.style.background = 'linear-gradient(135deg, #f59e0b 0%, #d97706 100%)';
|
||
alertDiv.innerHTML = `⚠ ${message}`;
|
||
} else {
|
||
alertDiv.style.background = 'linear-gradient(135deg, #3b82f6 0%, #2563eb 100%)';
|
||
alertDiv.innerHTML = `ℹ ${message}`;
|
||
}
|
||
|
||
// 添加到页面
|
||
document.body.appendChild(alertDiv);
|
||
|
||
// 自动消失
|
||
setTimeout(() => {
|
||
alertDiv.style.animation = 'fadeOut 0.3s ease';
|
||
setTimeout(() => {
|
||
document.body.removeChild(alertDiv);
|
||
}, 300);
|
||
}, 3000);
|
||
}
|
||
|
||
// 页面加载时初始化
|
||
window.addEventListener('DOMContentLoaded', async () => {
|
||
const user = await fetchCurrentUser(true);
|
||
// 初始化标签页
|
||
setupTabsByRole(user ? user.role : 'user');
|
||
|
||
// 检查URL参数中的tab
|
||
const urlParams = new URLSearchParams(window.location.search);
|
||
const tabParam = urlParams.get('tab');
|
||
if (tabParam && document.getElementById(`${tabParam}-tab`)) {
|
||
switchTab(`${tabParam}-tab`);
|
||
}
|
||
|
||
// 初始化拖拽事件
|
||
const dragDropArea = document.getElementById('dragDropArea');
|
||
if (dragDropArea) {
|
||
// 阻止默认拖拽行为
|
||
dragDropArea.addEventListener('dragover', function(e) {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
this.classList.add('drag-over');
|
||
});
|
||
|
||
dragDropArea.addEventListener('dragleave', function(e) {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
// 只有离开拖放区域本身时才移除样式
|
||
if (e.target === this) {
|
||
this.classList.remove('drag-over');
|
||
}
|
||
});
|
||
|
||
dragDropArea.addEventListener('drop', function(e) {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
this.classList.remove('drag-over');
|
||
|
||
const files = e.dataTransfer.files;
|
||
if (files && files.length > 0) {
|
||
handleDragDropFile(files);
|
||
}
|
||
});
|
||
}
|
||
|
||
// 如果默认显示的是文件管理标签页,加载文件列表
|
||
const filesTab = document.getElementById('files-tab');
|
||
if (filesTab && filesTab.classList.contains('active')) {
|
||
loadFileManager();
|
||
}
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|