2025-10-30 08:52:48 +08:00
<!DOCTYPE html>
< html lang = "zh-CN" >
< head >
< meta charset = "UTF-8" >
< meta name = "viewport" content = "width=device-width, initial-scale=1.0" >
< title > 数据库维护页面 - LawRisk< / title >
< style >
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Microsoft YaHei', Arial, sans-serif;
2025-11-18 19:23:56 +08:00
background: linear-gradient(135deg, #2c5282 0%, #1e3a5f 100%);
2025-10-30 08:52:48 +08:00
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 {
2025-11-13 15:28:08 +08:00
position: relative;
2025-10-30 08:52:48 +08:00
text-align: center;
2025-11-17 15:07:14 +08:00
margin-bottom: 20px;
padding-bottom: 20px;
2025-11-18 19:23:56 +08:00
border-bottom: 3px solid #2c5282;
2025-10-30 08:52:48 +08:00
}
2025-11-13 15:28:08 +08:00
.header-info {
max-width: 720px;
margin: 0 auto;
}
2025-10-30 08:52:48 +08:00
.header h1 {
color: #333;
font-size: 28px;
margin-bottom: 10px;
}
.header p {
color: #666;
font-size: 14px;
}
2025-11-17 15:07:14 +08:00
.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 {
2025-11-18 19:23:56 +08:00
color: #2c5282;
background: #f7fafc;
2025-11-17 15:07:14 +08:00
}
.tab-button.active {
2025-11-18 19:23:56 +08:00
color: #2c5282;
border-bottom-color: #2c5282;
background: #f7fafc;
2025-11-17 15:07:14 +08:00
}
.tab-badge {
2025-11-18 19:23:56 +08:00
background: #e2e8f0;
color: #2c5282;
2025-11-17 15:07:14 +08:00
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);
}
}
2025-11-13 15:28:08 +08:00
.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%;
2025-11-18 19:23:56 +08:00
background: linear-gradient(135deg, #2c5282 0%, #1e3a5f 100%);
2025-11-13 15:28:08 +08:00
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;
2025-11-18 19:23:56 +08:00
background: linear-gradient(135deg, #f7fafc 0%, #e2e8f0 100%);
color: #2c5282;
2025-11-13 15:28:08 +08:00
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);
}
2025-10-30 08:52:48 +08:00
.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%;
2025-11-18 19:23:56 +08:00
background: #f7fafc;
color: #2c5282;
2025-10-30 08:52:48 +08:00
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
font-size: 16px;
}
.step.active .step-number {
2025-11-18 19:23:56 +08:00
background: #2c5282;
2025-10-30 08:52:48 +08:00
color: white;
}
.step-label {
font-size: 14px;
color: #666;
font-weight: 500;
}
.step.active .step-label {
2025-11-18 19:23:56 +08:00
color: #2c5282;
2025-10-30 08:52:48 +08:00
font-weight: bold;
}
.arrow {
color: #ccc;
font-size: 24px;
}
.content-area {
display: grid;
2025-10-30 09:54:36 +08:00
grid-template-columns: 350px 1fr;
2025-10-30 08:52:48 +08:00
gap: 30px;
min-height: 600px;
}
.panel {
background: #f8f9fa;
border-radius: 8px;
padding: 20px;
border: 1px solid #e0e0e0;
}
.panel h2 {
color: #333;
font-size: 18px;
margin-bottom: 15px;
padding-bottom: 10px;
2025-11-18 19:23:56 +08:00
border-bottom: 2px solid #2c5282;
2025-10-30 09:54:36 +08:00
display: flex;
justify-content: space-between;
align-items: center;
}
.nav-controls {
display: flex;
gap: 8px;
}
.back-button {
padding: 4px 12px;
background: #f0f0f0;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 13px;
color: #666;
cursor: pointer;
transition: all 0.3s;
}
.back-button:hover:not(:disabled) {
background: #e0e0e0;
border-color: #999;
}
.back-button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.breadcrumb {
background: white;
border-radius: 6px;
padding: 12px 16px;
margin-bottom: 15px;
border: 1px solid #e0e0e0;
font-size: 14px;
color: #666;
}
.breadcrumb-item {
display: inline-flex;
align-items: center;
gap: 6px;
}
.breadcrumb-item a {
2025-11-18 19:23:56 +08:00
color: #2c5282;
2025-10-30 09:54:36 +08:00
text-decoration: none;
cursor: pointer;
transition: color 0.3s;
}
.breadcrumb-item a:hover {
color: #5568d3;
text-decoration: underline;
}
.breadcrumb-separator {
color: #ccc;
margin: 0 4px;
}
.breadcrumb-current {
color: #333;
font-weight: 500;
2025-10-30 08:52:48 +08:00
}
.selection-area {
background: white;
border-radius: 8px;
padding: 20px;
2025-10-30 09:54:36 +08:00
max-height: 600px;
overflow-y: auto;
overflow-x: hidden;
position: relative;
}
/* 自定义滚动条样式 */
.selection-area::-webkit-scrollbar {
width: 8px;
}
.selection-area::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 4px;
}
.selection-area::-webkit-scrollbar-thumb {
background: #c0c0c0;
border-radius: 4px;
transition: background 0.3s;
}
.selection-area::-webkit-scrollbar-thumb:hover {
background: #a0a0a0;
2025-10-30 08:52:48 +08:00
}
.item-list {
list-style: none;
margin-top: 10px;
}
.item-list li {
padding: 12px 16px;
margin-bottom: 8px;
background: #f8f9fa;
border: 1px solid #e0e0e0;
border-radius: 6px;
cursor: pointer;
transition: all 0.3s;
display: flex;
justify-content: space-between;
align-items: center;
}
.item-list li:hover {
2025-11-18 19:23:56 +08:00
background: #f7fafc;
border-color: #2c5282;
2025-10-30 08:52:48 +08:00
transform: translateX(5px);
}
.item-list li.active {
2025-11-18 19:23:56 +08:00
background: #2c5282;
2025-10-30 08:52:48 +08:00
color: white;
2025-11-18 19:23:56 +08:00
border-color: #2c5282;
2025-10-30 08:52:48 +08:00
}
.item-list li.active:hover {
background: #5568d3;
}
.item-name {
font-size: 15px;
font-weight: 500;
}
2025-11-13 17:01:37 +08:00
.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;
2025-11-18 19:23:56 +08:00
color: #2c5282;
2025-11-13 17:01:37 +08:00
}
.theme-tags {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
2025-10-30 08:52:48 +08:00
.item-count {
font-size: 12px;
background: rgba(102, 126, 234, 0.1);
padding: 4px 10px;
border-radius: 12px;
2025-11-18 19:23:56 +08:00
color: #2c5282;
2025-10-30 08:52:48 +08:00
}
.item-list li.active .item-count {
background: rgba(255, 255, 255, 0.2);
color: white;
}
2025-11-13 19:21:59 +08:00
.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 {
2025-11-18 19:23:56 +08:00
border: 1px solid #f7fafc;
2025-11-13 19:21:59 +08:00
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;
}
2025-10-30 10:33:35 +08:00
.checkpoint-nav-item {
background: #fff3cd;
border: 2px solid #ffc107;
}
.checkpoint-nav-item:hover {
background: #ffe69c;
border-color: #ff9800;
}
.checkpoint-section {
background: white;
padding: 20px;
border-radius: 8px;
margin-bottom: 20px;
border: 1px solid #e0e0e0;
}
.checkpoint-section h3 {
2025-11-18 19:23:56 +08:00
color: #2c5282;
2025-10-30 10:33:35 +08:00
font-size: 18px;
margin-bottom: 15px;
display: flex;
align-items: center;
gap: 10px;
}
.checkpoint-section h3::before {
content: '🔒';
font-size: 20px;
}
2025-11-03 11:30:38 +08:00
.checkpoint-section.snapshot-section h3::before {
content: '🕘';
}
.snapshot-description {
font-size: 13px;
color: #555;
margin-bottom: 12px;
line-height: 1.6;
}
.timeline-list {
display: flex;
flex-direction: column;
gap: 12px;
margin-top: 12px;
}
.timeline-item {
display: flex;
gap: 14px;
background: white;
border: 1px solid #dde1ff;
border-radius: 10px;
padding: 14px 18px;
position: relative;
box-shadow: 0 4px 18px rgba(102, 126, 234, 0.08);
}
.timeline-item-snapshot {
2025-11-18 19:23:56 +08:00
border-left: 4px solid #2c5282;
2025-11-03 11:30:38 +08:00
}
.timeline-item-checkpoint {
border-left: 4px solid #ff9800;
}
.timeline-icon {
width: 40px;
height: 40px;
border-radius: 50%;
background: #eef2ff;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
flex-shrink: 0;
}
.timeline-item-checkpoint .timeline-icon {
background: #fff4e5;
}
.timeline-body {
flex: 1;
display: flex;
flex-direction: column;
gap: 8px;
}
.timeline-header {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
}
.timeline-title {
font-weight: 600;
color: #2f3e9e;
font-size: 15px;
}
.timeline-item-checkpoint .timeline-title {
color: #e65100;
}
.timeline-subtitle {
font-size: 14px;
color: #444;
font-weight: 500;
}
.timeline-time {
font-size: 12px;
color: #888;
white-space: nowrap;
}
.timeline-content {
font-size: 13px;
color: #333;
line-height: 1.6;
}
.timeline-meta {
font-size: 12px;
color: #666;
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.timeline-note {
font-size: 12px;
color: #555;
background: #f7f9ff;
padding: 8px 10px;
border-radius: 6px;
}
.timeline-stats {
font-size: 12px;
color: #333;
display: flex;
gap: 16px;
flex-wrap: wrap;
}
.timeline-actions {
display: flex;
gap: 8px;
margin-top: 6px;
}
2025-11-03 16:41:35 +08:00
.snapshot-detail-list {
margin-top: 12px;
display: none;
border-left: 2px solid #e0e0e0;
padding-left: 16px;
}
.snapshot-detail-list.expanded {
display: block;
}
.snapshot-detail-item {
background: #ffffff;
border: 1px solid #e5e7eb;
border-radius: 6px;
padding: 10px 12px;
margin-bottom: 10px;
box-shadow: 0 1px 2px rgba(0,0,0,0.05);
}
.snapshot-detail-item:last-child {
margin-bottom: 0;
}
.snapshot-detail-header {
font-size: 13px;
font-weight: 600;
color: #333;
margin-bottom: 6px;
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.snapshot-detail-meta {
font-size: 12px;
color: #666;
display: flex;
gap: 10px;
flex-wrap: wrap;
}
2025-11-03 11:30:38 +08:00
.timeline-footer {
margin-top: 14px;
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 10px;
}
.snapshot-filters {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-bottom: 15px;
align-items: center;
}
.snapshot-filters input,
.snapshot-filters select {
flex: 1 1 200px;
padding: 8px 10px;
border: 1px solid #d0d5ff;
border-radius: 6px;
font-size: 13px;
}
.snapshot-filters select:disabled {
background: #f0f2ff;
color: #aaa;
}
.snapshot-filters .filter-actions {
display: flex;
gap: 8px;
}
.snapshot-table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
}
.snapshot-table th,
.snapshot-table td {
border-bottom: 1px solid #eceffb;
padding: 10px;
text-align: left;
vertical-align: top;
}
.snapshot-table th {
background: #f4f6ff;
color: #334;
font-weight: 600;
}
.snapshot-permit-name {
font-weight: 600;
color: #333;
}
.snapshot-permit-region {
font-size: 12px;
color: #666;
margin-top: 4px;
}
.snapshot-risk-text {
color: #333;
line-height: 1.5;
}
.snapshot-risk-meta {
margin-top: 6px;
font-size: 12px;
color: #777;
}
.snapshot-version-badge {
display: inline-flex;
align-items: center;
gap: 4px;
background: rgba(102, 126, 234, 0.12);
color: #4c51bf;
padding: 4px 8px;
border-radius: 12px;
font-size: 12px;
margin-bottom: 6px;
}
.snapshot-status-tag {
display: inline-block;
font-size: 12px;
padding: 4px 8px;
border-radius: 12px;
background: #e8f5e9;
color: #2e7d32;
}
.snapshot-editor {
font-weight: 600;
color: #333;
}
.snapshot-note {
font-size: 12px;
color: #555;
margin-top: 6px;
}
.snapshot-time {
font-size: 12px;
color: #888;
margin-top: 4px;
}
.snapshot-footer {
margin-top: 12px;
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 10px;
}
.snapshot-pagination button {
min-width: 90px;
}
2025-10-30 10:33:35 +08:00
.checkpoint-form {
margin-bottom: 20px;
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
margin-bottom: 8px;
color: #333;
font-weight: 500;
}
.form-group input[type="text"] {
width: 100%;
padding: 10px 12px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 14px;
transition: border-color 0.3s;
}
.form-group input[type="text"]:focus {
outline: none;
2025-11-18 19:23:56 +08:00
border-color: #2c5282;
2025-10-30 10:33:35 +08:00
}
.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 {
2025-11-18 19:23:56 +08:00
background: #2c5282;
2025-10-30 10:33:35 +08:00
color: white;
}
.btn-primary:hover:not(:disabled) {
background: #5568d3;
}
2025-11-13 19:21:59 +08:00
.btn-secondary {
background: #e2e8f0;
color: #334155;
}
.btn-secondary:hover:not(:disabled) {
background: #cbd5f5;
}
2025-10-30 10:33:35 +08:00
.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;
2025-11-18 19:23:56 +08:00
border-color: #2c5282;
2025-10-30 10:33:35 +08:00
}
.checkpoint-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.checkpoint-id {
font-weight: bold;
color: #333;
font-size: 15px;
}
.checkpoint-timestamp {
color: #666;
font-size: 13px;
}
.checkpoint-description {
color: #555;
margin-bottom: 10px;
line-height: 1.6;
}
.checkpoint-stats {
display: flex;
gap: 15px;
margin-bottom: 10px;
font-size: 13px;
color: #666;
}
.checkpoint-stats span {
display: inline-flex;
align-items: center;
gap: 5px;
}
.checkpoint-actions {
display: flex;
gap: 10px;
margin-top: 10px;
}
.btn-sm {
padding: 6px 12px;
font-size: 13px;
}
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
2025-10-30 12:00:57 +08:00
z-index: 9999;
2025-10-30 10:33:35 +08:00
align-items: center;
justify-content: center;
}
.modal.show {
display: flex;
}
.modal-content {
background: white;
border-radius: 12px;
padding: 30px;
max-width: 500px;
width: 90%;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
animation: modalFadeIn 0.3s;
}
2025-11-04 13:38:21 +08:00
.import-modal-content {
width: 760px;
max-height: 90vh;
overflow-y: auto;
}
2025-10-30 10:33:35 +08:00
@keyframes modalFadeIn {
from {
opacity: 0;
transform: translateY(-20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
2025-11-04 13:38:21 +08:00
.import-section {
margin-bottom: 18px;
}
.import-section h3 {
font-size: 16px;
margin-bottom: 8px;
color: #333;
display: flex;
align-items: center;
gap: 6px;
}
.import-upload-area {
border: 1px dashed #9fa8da;
padding: 16px;
border-radius: 8px;
background: #f5f7ff;
display: flex;
flex-direction: column;
gap: 8px;
}
2025-11-13 15:28:08 +08:00
.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;
}
2025-11-17 15:07:14 +08:00
.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 {
2025-11-18 19:23:56 +08:00
border-color: #2c5282;
2025-11-17 15:07:14 +08:00
background: #f0f2ff;
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(102, 126, 234, 0.15);
}
.drag-drop-area.drag-over {
2025-11-18 19:23:56 +08:00
border-color: #2c5282;
2025-11-17 15:07:14 +08:00
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;
2025-11-18 19:23:56 +08:00
color: #2c5282;
2025-11-17 15:07:14 +08:00
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%;
}
2025-11-04 13:38:21 +08:00
.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 {
2025-11-18 19:23:56 +08:00
border-color: #2c5282;
2025-11-04 13:38:21 +08:00
background: #eef2ff;
}
.import-sheet-header {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
margin-bottom: 10px;
}
.import-sheet-title {
display: flex;
align-items: center;
gap: 8px;
font-weight: 600;
color: #3949ab;
}
.import-sheet-meta {
font-size: 12px;
color: #666;
}
.import-duplicate-panel {
background: #fff;
border-radius: 6px;
border: 1px solid #e0e0e0;
padding: 10px;
display: flex;
flex-direction: column;
gap: 6px;
}
.import-duplicate-title {
font-size: 12px;
color: #d84315;
font-weight: 600;
}
.import-checkbox {
display: flex;
align-items: center;
gap: 6px;
font-size: 13px;
color: #333;
}
.import-checkbox input[type="checkbox"] {
transform: scale(1.05);
}
.import-messages {
font-size: 13px;
line-height: 1.6;
}
.import-success {
color: #256029;
background: #e3f2e1;
padding: 8px 10px;
border-radius: 6px;
margin-bottom: 10px;
}
.import-error {
color: #c62828;
background: #ffebee;
padding: 8px 10px;
border-radius: 6px;
margin-bottom: 10px;
}
.import-form-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px;
margin-top: 10px;
}
.import-form-grid textarea {
grid-column: span 2;
resize: vertical;
min-height: 68px;
}
.import-form-grid input,
.import-form-grid textarea {
width: 100%;
padding: 8px 10px;
border: 1px solid #c5cae9;
border-radius: 6px;
font-size: 13px;
font-family: inherit;
}
.import-actions {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 16px;
gap: 16px;
}
.import-hint {
font-size: 12px;
color: #666;
line-height: 1.4;
}
2025-11-13 19:21:59 +08:00
.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;
2025-11-18 19:23:56 +08:00
color: #1e3a5f;
2025-11-13 19:21:59 +08:00
border-color: #a5b4fc;
}
.preview-tab-badge {
background: rgba(67, 56, 202, 0.15);
2025-11-18 19:23:56 +08:00
color: #1e3a5f;
2025-11-13 19:21:59 +08:00
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;
2025-11-18 19:23:56 +08:00
border-color: #4a5568;
2025-11-13 19:21:59 +08:00
}
.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;
2025-11-18 19:23:56 +08:00
color: #1e3a5f;
2025-11-13 19:21:59 +08:00
background: #eef2ff;
cursor: pointer;
user-select: none;
display: inline-flex;
align-items: center;
gap: 6px;
}
2025-11-14 10:32:23 +08:00
.theme-chip.all-theme {
border-color: #fcd34d;
background: #fffbeb;
color: #92400e;
font-weight: 600;
}
2025-11-13 19:21:59 +08:00
.theme-chip.selected {
2025-11-18 19:23:56 +08:00
background: linear-gradient(135deg, #718096, #4a5568);
2025-11-13 19:21:59 +08:00
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;
}
2025-10-30 10:33:35 +08:00
.modal-header {
text-align: center;
margin-bottom: 20px;
}
.modal-header h3 {
color: #dc3545;
font-size: 20px;
margin-bottom: 10px;
}
.modal-header .warning-icon {
font-size: 48px;
margin-bottom: 15px;
}
.modal-body {
margin-bottom: 20px;
}
.modal-body p {
color: #555;
line-height: 1.6;
margin-bottom: 10px;
}
.modal-footer {
display: flex;
gap: 10px;
justify-content: flex-end;
}
.danger-text {
color: #dc3545;
font-weight: bold;
}
.success-text {
color: #28a745;
font-weight: bold;
}
2025-10-30 11:48:15 +08:00
.checkpoint-toolbar {
display: flex;
justify-content: flex-end;
margin-bottom: 20px;
}
.checkpoint-toolbar .btn {
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.checkpoint-modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
z-index: 1000;
align-items: center;
justify-content: center;
padding: 20px;
}
.checkpoint-modal.show {
display: flex;
}
.checkpoint-modal-content {
background: white;
border-radius: 12px;
padding: 0;
max-width: 900px;
width: 100%;
max-height: 90vh;
display: flex;
flex-direction: column;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
animation: modalFadeIn 0.3s;
}
.checkpoint-modal-header {
padding: 20px 30px;
2025-11-18 19:23:56 +08:00
border-bottom: 2px solid #2c5282;
2025-10-30 11:48:15 +08:00
display: flex;
justify-content: space-between;
align-items: center;
}
.checkpoint-modal-header h2 {
color: #333;
font-size: 20px;
margin: 0;
}
.checkpoint-modal-close {
background: none;
border: none;
font-size: 28px;
color: #999;
cursor: pointer;
padding: 0;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
transition: all 0.3s;
}
.checkpoint-modal-close:hover {
background: #f0f0f0;
color: #333;
}
.checkpoint-modal-body {
padding: 30px;
overflow-y: auto;
flex: 1;
}
2025-11-14 10:32:23 +08:00
.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;
2025-11-18 19:23:56 +08:00
border-color: #4a5568;
2025-11-14 10:32:23 +08:00
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;
2025-11-18 19:23:56 +08:00
color: #1e3a5f;
2025-11-14 10:32:23 +08:00
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;
}
2025-10-30 08:52:48 +08:00
.details-area {
background: white;
border-radius: 8px;
padding: 20px;
min-height: 500px;
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
color: #999;
}
.empty-state svg {
width: 80px;
height: 80px;
margin-bottom: 15px;
opacity: 0.3;
}
.empty-state p {
font-size: 16px;
}
.details-content {
animation: fadeIn 0.3s;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
2025-11-17 15:07:14 +08:00
@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);
}
}
2025-10-30 08:52:48 +08:00
.detail-section {
margin-bottom: 25px;
}
.detail-section h3 {
2025-11-18 19:23:56 +08:00
color: #2c5282;
2025-10-30 08:52:48 +08:00
font-size: 16px;
margin-bottom: 10px;
display: flex;
align-items: center;
gap: 8px;
}
.detail-section h3::before {
content: '';
width: 4px;
height: 16px;
2025-11-18 19:23:56 +08:00
background: #2c5282;
2025-10-30 08:52:48 +08:00
border-radius: 2px;
}
2025-11-03 16:41:35 +08:00
.detail-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
margin-bottom: 18px;
border-radius: 8px;
background: #eef2ff;
border: 1px solid #d7dbff;
}
.detail-meta {
color: #3f51b5;
font-size: 14px;
}
.detail-meta strong {
font-size: 16px;
margin: 0 4px;
}
.detail-actions {
display: flex;
gap: 12px;
align-items: center;
}
2025-10-30 08:52:48 +08:00
.detail-content {
background: #f8f9fa;
padding: 15px;
border-radius: 6px;
2025-11-18 19:23:56 +08:00
border-left: 3px solid #2c5282;
2025-10-30 08:52:48 +08:00
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;
2025-11-18 19:23:56 +08:00
border-left: 3px solid #2c5282;
2025-10-30 08:52:48 +08:00
}
.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;
}
2025-11-13 15:28:08 +08:00
.status-unknown {
background: #f1f5f9;
color: #475569;
}
.permit-file-name {
font-weight: 600;
color: #1d4ed8;
}
.muted-text {
color: #94a3b8;
font-size: 14px;
margin: 4px 0;
}
2025-10-30 08:52:48 +08:00
.loading {
2025-11-14 10:32:23 +08:00
display: inline-flex;
align-items: center;
gap: 8px;
color: #475569;
font-size: 14px;
}
.loading-icon {
2025-10-30 08:52:48 +08:00
width: 20px;
height: 20px;
border: 3px solid #f3f3f3;
2025-11-18 19:23:56 +08:00
border-top: 3px solid #2c5282;
2025-10-30 08:52:48 +08:00
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;
}
2025-10-30 09:54:36 +08:00
.panel:first-child {
max-height: 300px;
overflow-y: auto;
}
2025-11-13 15:28:08 +08:00
.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;
}
2025-11-17 15:07:14 +08:00
.tabs-nav {
overflow-x: auto;
padding-bottom: 8px;
}
.tab-button {
white-space: nowrap;
}
2025-10-30 08:52:48 +08:00
}
< / style >
< / head >
< body >
< div class = "container" >
< div class = "header" >
2025-11-13 15:28:08 +08:00
< div class = "header-info" >
2025-11-17 15:07:14 +08:00
< h1 id = "pageTitle" > 🗃️ 管理员控制台< / h1 >
< p > LawRisk 法律风险提示系统 - 管理员功能面板< / p >
2025-11-13 15:28:08 +08:00
< / 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 >
2025-10-30 08:52:48 +08:00
< / div >
2025-11-17 15:07:14 +08:00
<!-- 标签页导航 -->
< div class = "tabs-container" >
< ul class = "tabs-nav" id = "tabsNav" >
2025-11-18 19:57:55 +08:00
< li > < button class = "tab-button active" data-tab = "permits-tab" onclick = "switchTab('permits-tab')" >
< span > 📋< / span > 许可事项管理
2025-11-17 15:07:14 +08:00
< / 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 >
<!-- 标签页内容区域 -->
2025-11-18 19:57:55 +08:00
<!-- 许可事项管理标签页 -->
2025-11-17 15:07:14 +08:00
< 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 >
2025-11-19 15:51:49 +08:00
< p style = "color: #666; margin-bottom: 20px;" > 使用筛选器快速定位许可事项,支持多维度筛选、分页浏览、风险统计等功能。< / p >
<!-- 筛选器区域 -->
2025-11-20 09:47:20 +08:00
< 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 >
2025-11-19 15:51:49 +08:00
< 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')" >
2025-11-20 09:47:20 +08:00
< span id = "regionSelectedText" > 全部区域< / span >
2025-11-19 15:51:49 +08:00
< 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 >
2025-11-17 15:07:14 +08:00
< / div >
< / div >
2025-11-19 15:51:49 +08:00
<!-- 主题筛选 -->
< 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')" >
2025-11-20 09:47:20 +08:00
< span id = "themeSelectedText" > 全部主题< / span >
2025-11-19 15:51:49 +08:00
< 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')" >
2025-11-20 09:47:20 +08:00
< span id = "departmentSelectedText" > 全部部门< / span >
2025-11-19 15:51:49 +08:00
< 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 >
2025-11-17 15:07:14 +08:00
< / div >
< / div >
2025-11-19 15:51:49 +08:00
<!-- 搜索关键词 -->
< 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 >
2025-11-17 15:07:14 +08:00
< / div >
2025-10-30 08:52:48 +08:00
< / div >
2025-10-30 11:48:15 +08:00
< / div >
2025-11-17 15:07:14 +08:00
<!-- 检查点管理标签页 -->
< 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 >
2025-10-30 08:52:48 +08:00
< / div >
2025-11-17 15:07:14 +08:00
<!-- 文件管理标签页 -->
< 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 >
2025-10-30 09:54:36 +08:00
< / div >
2025-10-30 08:52:48 +08:00
< / div >
< / div >
2025-11-17 15:07:14 +08:00
< / div >
2025-10-30 08:52:48 +08:00
2025-11-17 15:07:14 +08:00
<!-- 许可导入标签页 -->
< 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 >
2025-10-30 08:52:48 +08:00
< / div >
< / div >
< / div >
< / div >
2025-10-30 10:33:35 +08:00
<!-- 危险操作确认模态框 -->
< div class = "modal" id = "dangerModal" >
< div class = "modal-content" >
< div class = "modal-header" >
< div class = "warning-icon" > ⚠️< / div >
< h3 id = "dangerModalTitle" > 危险操作确认< / h3 >
< / div >
< div class = "modal-body" >
< p id = "dangerModalMessage" > < / p >
< p class = "danger-text" id = "dangerModalWarning" > < / p >
< / div >
< div class = "modal-footer" >
< button class = "btn" onclick = "closeDangerModal()" > 取消< / button >
< button class = "btn btn-danger" id = "dangerModalConfirmBtn" onclick = "confirmDangerOperation()" > 确认执行< / button >
< / div >
< / div >
< / div >
2025-10-30 11:48:15 +08:00
<!-- 检查点管理模态窗口 -->
< div class = "checkpoint-modal" id = "checkpointModal" >
< div class = "checkpoint-modal-content" >
< div class = "checkpoint-modal-header" >
< h2 > 🔒 数据库检查点管理< / h2 >
< button class = "checkpoint-modal-close" onclick = "closeCheckpointModal()" > × < / button >
< / div >
< div class = "checkpoint-modal-body" id = "checkpointModalBody" >
<!-- 检查点管理内容将在这里动态加载 -->
< / div >
< / div >
< / div >
2025-11-04 13:38:21 +08:00
<!-- 许可导入模态窗口 -->
< div class = "modal" id = "importModal" >
< div class = "modal-content import-modal-content" >
< div class = "modal-header" style = "display:flex; justify-content: space-between; align-items: center;" >
< h3 style = "color:#3949ab; margin:0; display:flex; align-items:center; gap:8px;" >
< span > 📥< / span > 许可导入( Excel)
< / h3 >
< button class = "btn btn-warning btn-sm" onclick = "closeImportModal()" > 关闭< / button >
< / div >
< div class = "modal-body" id = "importModalBody" > < / div >
< / div >
< / div >
2025-11-14 10:32:23 +08:00
<!-- 文件管理模态窗口 -->
< 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 >
2025-10-30 08:52:48 +08:00
< script >
2025-11-13 15:28:08 +08:00
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();
});
}
2025-10-30 09:54:36 +08:00
// 导航状态管理
2025-11-13 17:01:37 +08:00
let currentStep = 1; // 1=区划, 2=事项, 3=详情
2025-10-30 09:54:36 +08:00
let historyStack = []; // 历史记录栈
2025-10-30 08:52:48 +08:00
let currentRegion = null;
2025-11-03 16:41:35 +08:00
let currentPermit = null;
let currentPermitDetails = null;
let pendingDangerOperation = null; // 待执行的危险操作
let isDeletingPermit = false;
2025-11-03 11:30:38 +08:00
const permitRiskSnapshotState = {
limit: 10,
offset: 0,
total: 0,
regionFilter: '',
permitFilter: '',
editorFilter: '',
snapshots: [],
loading: false,
error: '',
regionOptions: [],
regionLoading: false,
regionError: '',
permitOptions: [],
permitLoading: false,
permitOptionsError: ''
};
let checkpointListCache = [];
let checkpointListLoading = false;
let checkpointListError = '';
2025-11-03 16:41:35 +08:00
let expandedSnapshotGroups = new Set();
2025-10-30 08:52:48 +08:00
2025-11-13 15:28:08 +08:00
const PERMIT_FILE_MAX_BYTES = 500 * 1024; // 500KB
2025-11-14 10:32:23 +08:00
const ALL_THEMES_SENTINEL = '__ALL_THEMES__';
const ALL_THEMES_LABEL = '所有主题';
2025-11-04 13:38:21 +08:00
const permitImportState = {
uploading: false,
sessionId: '',
filename: '',
totalRows: 0,
sheetSummaries: [],
selectedSheets: new Set(),
overrides: new Map(),
error: '',
success: '',
commitLoading: false,
editedBy: '',
2025-11-13 15:28:08 +08:00
changeSummary: '',
2025-11-13 19:21:59 +08:00
fileSize: 0,
previewLoading: false,
previewError: '',
previewData: null,
themeBindings: new Map(),
stage: 'upload',
activePreviewSheet: '',
previewThemeKeyword: '',
2025-11-04 13:38:21 +08:00
};
2025-11-14 10:32:23 +08:00
const fileManagerState = {
files: [],
loading: false,
error: '',
keyword: '',
keywordInput: '',
limit: 15,
offset: 0,
total: 0,
deleting: new Set(),
reimporting: new Set(),
};
let fileManagerSearchTimer = null;
2025-11-13 19:21:59 +08:00
const themeDragState = {
active: false,
sheetName: '',
permitName: '',
shouldSelect: true,
};
2025-11-14 10:32:23 +08:00
let themeSearchRenderFrame = null;
2025-11-13 19:21:59 +08:00
window.addEventListener('mouseup', endThemeDragSelection);
window.addEventListener('blur', endThemeDragSelection);
window.addEventListener('mouseleave', endThemeDragSelection);
2025-11-13 15:28:08 +08:00
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`;
}
2025-10-30 09:54:36 +08:00
// 步骤配置
const steps = {
2025-11-13 17:01:37 +08:00
1: { title: '选择区划' },
2: { title: '选择事项' },
3: { title: '事项详情' }
2025-10-30 09:54:36 +08:00
};
2025-11-13 17:01:37 +08:00
// 加载区划列表
2025-10-30 08:52:48 +08:00
async function loadRegions() {
2025-10-30 09:54:36 +08:00
const navList = document.getElementById('navList');
2025-11-14 10:32:23 +08:00
navList.innerHTML = '< div class = "loading" > < span class = "loading-icon" > < / span > 加载区划列表...< / div > ';
2025-10-30 09:54:36 +08:00
2025-10-30 08:52:48 +08:00
try {
const response = await fetch('/fs-ai-asistant/api/workflow/lawrisk/admin/regions');
const data = await response.json();
if (data.success) {
2025-10-30 09:54:36 +08:00
navList.innerHTML = '';
2025-10-30 08:52:48 +08:00
if (data.data.regions.length === 0) {
2025-11-13 17:01:37 +08:00
navList.innerHTML = '< div class = "error" > 未找到区划数据< / div > ';
2025-10-30 08:52:48 +08:00
return;
}
data.data.regions.forEach(region => {
const li = document.createElement('li');
li.innerHTML = `
< span class = "item-name" > ${region.name}< / span >
2025-10-30 09:54:36 +08:00
< span class = "item-count" > 点击选择< / span >
2025-10-30 08:52:48 +08:00
`;
2025-10-30 09:54:36 +08:00
li.onclick = () => selectRegion(region.id, region.name);
navList.appendChild(li);
2025-10-30 08:52:48 +08:00
});
} else {
2025-11-13 17:01:37 +08:00
navList.innerHTML = `< div class = "error" > 加载区划失败:${data.message}< / div > `;
2025-10-30 08:52:48 +08:00
}
} catch (error) {
2025-10-30 09:54:36 +08:00
navList.innerHTML = `< div class = "error" > 网络错误:${error.message}< / div > `;
2025-10-30 08:52:48 +08:00
}
}
2025-11-13 17:01:37 +08:00
// 加载许可列表(直接按区划聚合事项)
async function loadPermitsForRegion() {
if (!currentRegion) {
return;
2025-10-30 08:52:48 +08:00
}
2025-10-30 09:54:36 +08:00
const navList = document.getElementById('navList');
2025-11-14 10:32:23 +08:00
navList.innerHTML = '< div class = "loading" > < span class = "loading-icon" > < / span > 加载事项列表...< / div > ';
2025-10-30 08:52:48 +08:00
try {
2025-11-13 17:01:37 +08:00
const response = await fetch(`/fs-ai-asistant/api/workflow/lawrisk/admin/permits?region=${currentRegion.id}`);
2025-10-30 08:52:48 +08:00
const data = await response.json();
if (data.success) {
const permits = data.data.permits;
if (permits.length === 0) {
2025-11-13 17:01:37 +08:00
navList.innerHTML = `< div class = "error" > 区划 "${currentRegion.name}" 暂无可维护的事项< / div > `;
2025-10-30 08:52:48 +08:00
return;
}
let html = '< div class = "item-list" > ';
permits.forEach(permit => {
2025-11-13 17:01:37 +08:00
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, "\\'") : '';
2025-10-30 08:52:48 +08:00
html += `
2025-11-13 17:01:37 +08:00
< li onclick = "selectPermit('${permit.id}', '${escapedName}', '${escapedThemeId}', '${escapedTheme}', ${riskCount})" >
2025-11-14 10:32:23 +08:00
< span class = "item-name" > ${permit.name}< / span >
2025-10-30 08:52:48 +08:00
< span class = "item-count" > ${riskCount} 个风险< / span >
< / li >
`;
});
html += '< / div > ';
2025-10-30 09:54:36 +08:00
navList.innerHTML = html;
2025-10-30 08:52:48 +08:00
} else {
2025-11-13 17:01:37 +08:00
navList.innerHTML = `< div class = "error" > 加载事项失败:${data.message}< / div > `;
2025-10-30 08:52:48 +08:00
}
} catch (error) {
2025-10-30 09:54:36 +08:00
navList.innerHTML = `< div class = "error" > 网络错误:${error.message}< / div > `;
2025-10-30 08:52:48 +08:00
}
}
2025-10-30 09:54:36 +08:00
// 选择地区
async function selectRegion(regionId, regionName) {
// 保存到历史栈
historyStack.push({ step: currentStep, region: currentRegion });
currentRegion = { id: regionId, name: regionName };
currentPermit = null;
2025-11-03 16:41:35 +08:00
currentPermitDetails = null;
2025-10-30 09:54:36 +08:00
// 更新步骤
goToStep(2);
}
2025-10-30 08:52:48 +08:00
// 选择许可
2025-11-13 17:01:37 +08:00
async function selectPermit(permitId, permitName, themeId, themeName, riskCount = 0) {
2025-10-30 09:54:36 +08:00
// 保存到历史栈
historyStack.push({ step: currentStep, permit: currentPermit });
2025-11-13 17:01:37 +08:00
currentPermit = {
id: permitId,
name: permitName,
themeId: themeId || '',
themeName: themeName || '',
riskCount: typeof riskCount === 'number' ? riskCount : 0
};
2025-11-03 16:41:35 +08:00
currentPermitDetails = null;
2025-10-30 09:54:36 +08:00
// 更新步骤
2025-11-13 17:01:37 +08:00
goToStep(3);
2025-10-30 09:54:36 +08:00
}
// 跳转到指定步骤
async function goToStep(step) {
currentStep = step;
// 更新导航标题
document.getElementById('navTitle').querySelector('span').textContent = steps[step].title;
// 更新上一步按钮
const backButton = document.getElementById('backButton');
backButton.disabled = historyStack.length === 0;
2025-10-30 08:52:48 +08:00
// 更新步骤指示器
2025-10-30 09:54:36 +08:00
updateStepIndicator(step);
2025-10-30 08:52:48 +08:00
2025-10-30 09:54:36 +08:00
// 更新面包屑导航
updateBreadcrumb();
2025-10-30 08:52:48 +08:00
2025-10-30 09:54:36 +08:00
// 清空详情区域
const detailsArea = document.getElementById('detailsArea');
if (step === 1) {
detailsArea.innerHTML = `
< div class = "empty-state" >
< svg viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" >
< path d = "M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" / >
< / svg >
< p id = "emptyMessage" > 请选择区域开始导航< / p >
< / div >
`;
}
// 加载数据
if (step === 1) {
await loadRegions();
} else if (step === 2) {
2025-11-13 17:01:37 +08:00
await loadPermitsForRegion();
2025-10-30 09:54:36 +08:00
} else if (step === 3) {
await showPermitDetails();
}
}
// 更新面包屑导航
function updateBreadcrumb() {
const breadcrumb = document.getElementById('breadcrumb');
2025-11-13 17:01:37 +08:00
let html = `
2025-10-30 09:54:36 +08:00
< 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) {
2025-11-13 17:01:37 +08:00
const permitLabel = currentPermit.themeName
? `${currentPermit.themeName} · ${currentPermit.name}`
: currentPermit.name;
2025-10-30 09:54:36 +08:00
html += '< span class = "breadcrumb-separator" > › < / span > ';
2025-11-13 17:01:37 +08:00
if (currentStep >= 3) {
2025-10-30 09:54:36 +08:00
html += `
< span class = "breadcrumb-item" >
2025-11-13 17:01:37 +08:00
< span class = "breadcrumb-current" > ${permitLabel}< / span >
2025-10-30 09:54:36 +08:00
< / span >
`;
} else {
html += `
< span class = "breadcrumb-item" >
2025-11-13 17:01:37 +08:00
< a onclick = "quickJump(3)" > ${permitLabel}< / a >
2025-10-30 09:54:36 +08:00
< / span >
`;
}
}
breadcrumb.innerHTML = html;
}
// 快速跳转到指定步骤
function quickJump(targetStep) {
// 清空历史栈中比目标步骤更晚的记录
while (historyStack.length > 0 & & historyStack[historyStack.length - 1].step >= targetStep) {
historyStack.pop();
}
// 清理后续状态
if (targetStep < = 2) {
currentPermit = null;
2025-11-03 16:41:35 +08:00
currentPermitDetails = null;
2025-10-30 09:54:36 +08:00
}
goToStep(targetStep);
}
// 返回首页
function goHome() {
currentRegion = null;
currentPermit = null;
2025-11-03 16:41:35 +08:00
currentPermitDetails = null;
2025-10-30 09:54:36 +08:00
historyStack = [];
goToStep(1);
}
// 显示许可详情
async function showPermitDetails() {
const detailsArea = document.getElementById('detailsArea');
2025-11-14 10:32:23 +08:00
detailsArea.innerHTML = '< div class = "loading" > < span class = "loading-icon" > < / span > 加载许可详情...< / div > ';
2025-10-30 08:52:48 +08:00
try {
2025-11-13 17:01:37 +08:00
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}`);
2025-10-30 08:52:48 +08:00
const data = await response.json();
if (data.success) {
renderPermitDetails(data.data.permit);
} else {
detailsArea.innerHTML = `< div class = "error" > 加载详情失败:${data.message}< / div > `;
}
} catch (error) {
detailsArea.innerHTML = `< div class = "error" > 网络错误:${error.message}< / div > `;
}
}
2025-10-30 09:54:36 +08:00
// 回退到上一步
function goBack() {
if (historyStack.length === 0) return;
const prev = historyStack.pop();
// 恢复状态
if (prev.region) currentRegion = prev.region;
if (prev.permit) currentPermit = prev.permit;
// 跳转到上一步
goToStep(prev.step);
}
2025-10-30 08:52:48 +08:00
// 渲染许可详情
function renderPermitDetails(permit) {
const detailsArea = document.querySelector('.details-area');
2025-11-03 16:41:35 +08:00
currentPermitDetails = permit;
const riskCount = Array.isArray(permit.risks) ? permit.risks.length : 0;
2025-11-13 17:01:37 +08:00
const permitTheme = permit & & permit.theme ? permit.theme : {};
2025-11-03 16:41:35 +08:00
if (currentPermit) {
2025-11-13 17:01:37 +08:00
currentPermit = {
...currentPermit,
riskCount,
themeId: permitTheme.id || currentPermit.themeId || '',
themeName: permitTheme.name || currentPermit.themeName || ''
};
2025-11-03 16:41:35 +08:00
}
const deleteButtonLabel = isDeletingPermit ? '删除中...' : '删除许可';
const deleteButtonDisabled = isDeletingPermit ? 'disabled' : '';
2025-11-13 15:28:08 +08:00
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 > ';
2025-11-13 17:01:37 +08:00
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 > ';
2025-11-03 16:41:35 +08:00
2025-10-30 08:52:48 +08:00
let html = '< div class = "details-content" > ';
2025-11-03 16:41:35 +08:00
html += `
< div class = "detail-toolbar" >
< div class = "detail-meta" >
风险条目:< strong > ${riskCount}< / strong > 个
< / div >
< div class = "detail-actions" >
2025-11-13 15:28:08 +08:00
< button class = "btn" id = "downloadPermitFileBtn" $ { downloadDisabledAttr } onclick = "downloadPermitFile()" title = "${hasPermitFile ? '下载原文件' : '暂无原始文件'}" > 下载原文件< / button >
2025-11-03 16:41:35 +08:00
< button class = "btn btn-danger" id = "deletePermitBtn" $ { deleteButtonDisabled } onclick = "confirmDeleteCurrentPermit()" > ${deleteButtonLabel}< / button >
< / div >
< / div >
`;
2025-10-30 08:52:48 +08:00
// 许可基本信息
html += `
< div class = "detail-section" >
< h3 > 许可信息< / h3 >
< div class = "detail-content" >
2025-11-13 15:28:08 +08:00
< 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 >
2025-10-30 08:52:48 +08:00
< / div >
< / div >
`;
2025-11-13 17:01:37 +08:00
html += `
< div class = "detail-section" >
< h3 > 所属主题< / h3 >
< div class = "detail-content" >
${themeListDisplay}
< / div >
< / div >
`;
2025-10-30 08:52:48 +08:00
// 经营范围
2025-11-13 15:28:08 +08:00
html += '< div class = "detail-section" > < h3 > 经营范围< / h3 > < div class = "detail-content" > ';
2025-10-30 08:52:48 +08:00
if (permit.business_scopes & & permit.business_scopes.length > 0) {
permit.business_scopes.forEach(scope => {
2025-11-13 15:28:08 +08:00
html += `< div class = "scope-item" > ${formatNullableText(scope.description)}< / div > `;
2025-10-30 08:52:48 +08:00
});
2025-11-13 15:28:08 +08:00
} else {
html += '< p class = "muted-text" > 暂无经营范围信息< / p > ';
2025-10-30 08:52:48 +08:00
}
2025-11-13 15:28:08 +08:00
html += '< / div > < / div > ';
2025-10-30 08:52:48 +08:00
// 法律风险
2025-11-13 15:28:08 +08:00
html += '< div class = "detail-section" > < h3 > 法律风险< / h3 > < div class = "detail-content" > ';
2025-10-30 08:52:48 +08:00
if (permit.risks & & permit.risks.length > 0) {
2025-11-13 15:28:08 +08:00
permit.risks.forEach((risk, index) => {
const riskIdentifier = formatNullableText(risk.id, index + 1);
2025-10-30 08:52:48 +08:00
html += `
< div class = "risk-item" >
2025-11-13 15:28:08 +08:00
< 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 >
2025-10-30 08:52:48 +08:00
< / div >
`;
});
} else {
2025-11-13 15:28:08 +08:00
html += '< p class = "muted-text" > 暂无法律风险记录< / p > ';
2025-10-30 08:52:48 +08:00
}
2025-11-13 15:28:08 +08:00
html += '< / div > < / div > ';
2025-10-30 08:52:48 +08:00
html += '< / div > ';
detailsArea.innerHTML = html;
}
2025-11-13 15:28:08 +08:00
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');
}
2025-11-03 16:41:35 +08:00
function confirmDeleteCurrentPermit() {
if (isDeletingPermit) {
return;
}
2025-11-13 17:01:37 +08:00
if (!currentRegion || !currentPermit) {
2025-11-03 16:41:35 +08:00
alert('请先选择要删除的许可');
return;
}
const riskCount = currentPermit.riskCount !== undefined
? currentPermit.riskCount
: (currentPermitDetails & & Array.isArray(currentPermitDetails.risks) ? currentPermitDetails.risks.length : 0);
2025-11-13 17:01:37 +08:00
const pathParts = [currentRegion.name];
if (currentPermit.themeName) {
pathParts.push(currentPermit.themeName);
}
pathParts.push(currentPermit.name);
const confirmMessage = `确定要删除「${pathParts.join(' › ')}」吗?\n\n` +
2025-11-03 16:41:35 +08:00
`此操作会删除 ${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;
}
2025-11-13 17:01:37 +08:00
if (!currentRegion || !currentPermit) {
2025-11-03 16:41:35 +08:00
alert('当前上下文缺失,无法删除');
return;
}
isDeletingPermit = true;
toggleDeletePermitButton(true);
try {
const payload = {
region_id: currentRegion.id,
permit_id: currentPermit.id
};
2025-11-13 17:01:37 +08:00
if (currentPermit.themeId) {
payload.theme_id = currentPermit.themeId;
}
2025-11-03 16:41:35 +08:00
if (changeSummary) {
payload.change_summary = changeSummary;
}
const response = await fetch('/fs-ai-asistant/api/workflow/lawrisk/admin/permits', {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const data = await response.json();
if (response.ok & & data.success) {
const snapshotCount = data.data & & typeof data.data.snapshot_count === 'number'
? data.data.snapshot_count
: 0;
const deletedRisks = data.data & & data.data.deleted_rows
? (data.data.deleted_rows.region_permit_risks || 0)
: 0;
const remainingPermits = data.data & & typeof data.data.remaining_theme_permits === 'number'
? data.data.remaining_theme_permits
: null;
const themeDetached = !!(data.data & & data.data.theme_detached);
const snapshotBatchId = data.data & & data.data.snapshot_batch_id;
let successMessage = `✅ 删除成功!\n\n已备份 ${snapshotCount} 条风险快照,并删除 ${deletedRisks} 条风险关联。`;
if (themeDetached) {
successMessage += '\n对应主题已与该地区解除关联。';
} else if (remainingPermits !== null) {
successMessage += `\n该主题在此地区仍剩余 ${remainingPermits} 个许可。`;
}
if (snapshotBatchId) {
successMessage += `\n快照批次: ${snapshotBatchId}`;
}
alert(successMessage);
const modal = document.getElementById('checkpointModal');
if (modal & & modal.classList.contains('show')) {
if ((!permitRiskSnapshotState.regionFilter || permitRiskSnapshotState.regionFilter === currentRegion.id) & &
(!permitRiskSnapshotState.permitFilter || permitRiskSnapshotState.permitFilter === currentPermit.id)) {
await refreshPermitRiskSnapshots(false);
}
}
currentPermitDetails = null;
quickJump(3);
} else {
const message = data & & data.message ? data.message : `删除失败( HTTP ${response.status}) `;
alert(`❌ 删除失败:${message}`);
}
} catch (error) {
alert(`❌ 删除失败:${error.message}`);
} finally {
isDeletingPermit = false;
toggleDeletePermitButton(false);
}
}
function toggleDeletePermitButton(disabled) {
const btn = document.getElementById('deletePermitBtn');
if (!btn) {
return;
}
btn.disabled = disabled;
btn.textContent = disabled ? '删除中...' : '删除许可';
}
2025-10-30 08:52:48 +08:00
// 更新步骤指示器
function updateStepIndicator(step) {
2025-11-13 17:01:37 +08:00
for (let i = 1; i < = 3; i++) {
2025-10-30 08:52:48 +08:00
const stepElement = document.getElementById(`step${i}`);
2025-11-13 17:01:37 +08:00
if (!stepElement) {
continue;
}
2025-10-30 08:52:48 +08:00
if (i < = step) {
stepElement.classList.add('active');
} else {
stepElement.classList.remove('active');
}
}
}
2025-11-04 13:38:21 +08:00
// ================ 许可导入功能 ================
2025-11-13 19:21:59 +08:00
async function openImportModal() {
2025-11-04 13:38:21 +08:00
const modal = document.getElementById('importModal');
if (!modal) return;
modal.classList.add('show');
2025-11-13 19:21:59 +08:00
permitImportState.stage = 'upload';
permitImportState.activePreviewSheet = '';
permitImportState.previewThemeKeyword = '';
endThemeDragSelection();
2025-11-04 13:38:21 +08:00
if (!permitImportState.sessionId) {
permitImportState.selectedSheets = new Set();
permitImportState.overrides = new Map();
permitImportState.error = '';
permitImportState.success = '';
2025-11-13 15:28:08 +08:00
permitImportState.filename = '';
permitImportState.totalRows = 0;
permitImportState.fileSize = 0;
2025-11-04 13:38:21 +08:00
}
renderImportModal();
2025-11-13 19:21:59 +08:00
if (permitImportState.sessionId) {
await fetchImportPreview(false);
}
2025-11-04 13:38:21 +08:00
}
function closeImportModal() {
const modal = document.getElementById('importModal');
if (!modal) return;
2025-11-13 19:21:59 +08:00
permitImportState.stage = 'upload';
permitImportState.activePreviewSheet = '';
permitImportState.previewThemeKeyword = '';
endThemeDragSelection();
2025-11-14 10:32:23 +08:00
cancelThemeSearchRender();
2025-11-04 13:38:21 +08:00
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);
2025-11-13 19:21:59 +08:00
permitImportState.themeBindings.delete(sheetName);
2025-11-04 13:38:21 +08:00
}
2025-11-13 19:21:59 +08:00
ensureActivePreviewSheet();
2025-11-04 13:38:21 +08:00
renderImportModal();
}
function toggleSheetSelectionFromEvent(el) {
if (!el || !el.dataset) return;
toggleSheetSelection(el.dataset.sheet || '', el.checked);
}
2025-11-13 19:21:59 +08:00
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();
2025-11-14 10:32:23 +08:00
cancelThemeSearchRender();
2025-11-13 19:21:59 +08:00
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 [];
}
2025-11-14 10:32:23 +08:00
return Array.isArray(sheet.permits) ? sheet.permits : [];
2025-11-13 19:21:59 +08:00
}
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);
});
}
2025-11-14 10:32:23 +08:00
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();
}
2025-11-13 19:21:59 +08:00
function selectAllThemesForPermit(sheetName, permitName) {
if (!sheetName || !permitName) {
return;
}
const sheet = getPreviewSheetByName(sheetName);
if (!sheet) {
return;
}
2025-11-14 10:32:23 +08:00
const options = Array.isArray(sheet.theme_options) ? sheet.theme_options : [];
2025-11-13 19:21:59 +08:00
if (!options.length) {
return;
}
const bindingSet = getThemeBindingSet(sheetName, permitName, true);
2025-11-14 10:32:23 +08:00
const hasAllThemeOption = options.some(option => resolveThemeOptionNormalized(option) === ALL_THEMES_SENTINEL || option?.is_all);
if (hasAllThemeOption) {
bindingSet.clear();
bindingSet.add(ALL_THEMES_SENTINEL);
renderImportModal();
return;
}
2025-11-13 19:21:59 +08:00
options.forEach(option => {
2025-11-14 10:32:23 +08:00
const normalized = resolveThemeOptionNormalized(option);
2025-11-13 19:21:59 +08:00
if (normalized) {
2025-11-14 10:32:23 +08:00
bindingSet.delete(ALL_THEMES_SENTINEL);
2025-11-13 19:21:59 +08:00
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();
}
2025-11-04 13:38:21 +08:00
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;
}
2025-11-13 19:21:59 +08:00
function clearImportPreviewState() {
permitImportState.previewData = null;
permitImportState.previewError = '';
permitImportState.previewLoading = false;
permitImportState.themeBindings = new Map();
permitImportState.activePreviewSheet = '';
permitImportState.previewThemeKeyword = '';
2025-11-14 10:32:23 +08:00
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;
2025-11-13 19:21:59 +08:00
}
function normalizeThemeName(value) {
if (typeof value === 'string') {
2025-11-14 10:32:23 +08:00
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;
2025-11-13 19:21:59 +08:00
}
if (value === undefined || value === null) {
return '';
}
return String(value).trim();
}
2025-11-14 10:32:23 +08:00
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() : '';
}
2025-11-13 19:21:59 +08:00
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) {
2025-11-14 10:32:23 +08:00
if (normalizedTheme === ALL_THEMES_SENTINEL) {
bindingSet.clear();
bindingSet.add(ALL_THEMES_SENTINEL);
} else {
bindingSet.delete(ALL_THEMES_SENTINEL);
bindingSet.add(normalizedTheme);
}
2025-11-13 19:21:59 +08:00
} 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;
}
2025-11-04 13:38:21 +08:00
async function handleImportFile(input) {
if (!input || !input.files || !input.files.length) {
return;
}
const file = input.files[0];
2025-11-13 15:28:08 +08:00
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;
}
2025-11-04 13:38:21 +08:00
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 || {};
2025-11-14 10:32:23 +08:00
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();
}
2025-11-04 13:38:21 +08:00
} else {
permitImportState.error = data.message || '解析失败, 请检查Excel格式';
permitImportState.sessionId = '';
permitImportState.sheetSummaries = [];
2025-11-13 15:28:08 +08:00
permitImportState.fileSize = 0;
2025-11-04 13:38:21 +08:00
permitImportState.selectedSheets = new Set();
permitImportState.overrides = new Map();
2025-11-13 19:21:59 +08:00
clearImportPreviewState();
2025-11-04 13:38:21 +08:00
}
} catch (error) {
permitImportState.error = error.message || '文件上传失败';
permitImportState.sessionId = '';
permitImportState.sheetSummaries = [];
permitImportState.selectedSheets = new Set();
permitImportState.overrides = new Map();
2025-11-13 15:28:08 +08:00
permitImportState.fileSize = 0;
2025-11-13 19:21:59 +08:00
clearImportPreviewState();
2025-11-04 13:38:21 +08:00
} 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;
}
2025-11-13 19:21:59 +08:00
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;
}
2025-11-04 13:38:21 +08:00
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);
}
});
2025-11-13 19:21:59 +08:00
const themeBindingsPayload = buildThemeBindingsPayload();
if (!Object.keys(themeBindingsPayload).length) {
permitImportState.error = '请至少为一个事项选择主题';
renderImportModal();
return;
}
payload.theme_bindings = themeBindingsPayload;
2025-11-04 13:38:21 +08:00
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();
2025-11-13 15:28:08 +08:00
permitImportState.filename = '';
permitImportState.totalRows = 0;
permitImportState.fileSize = 0;
2025-11-13 19:21:59 +08:00
permitImportState.stage = 'upload';
permitImportState.activePreviewSheet = '';
permitImportState.previewThemeKeyword = '';
clearImportPreviewState();
2025-11-04 13:38:21 +08:00
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;
2025-11-13 19:21:59 +08:00
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();
}
}
2025-11-04 13:38:21 +08:00
2025-11-13 19:21:59 +08:00
function renderImportUploadStage() {
const state = permitImportState;
2025-11-04 13:38:21 +08:00
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)" > ';
2025-11-13 15:28:08 +08:00
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 > ';
2025-11-04 13:38:21 +08:00
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 > ';
}
2025-11-13 15:28:08 +08:00
const currentSizeLabel = state.fileSize ? formatFileSize(state.fileSize) : '未选择文件';
html += `< div class = "import-meta" > 文件大小限制:≤ ${formatFileSize(PERMIT_FILE_MAX_BYTES)} | 当前:${currentSizeLabel}< / div > `;
2025-11-04 13:38:21 +08:00
html += '< / div > < / div > ';
if (state.error) {
html += `< div class = "import-error" > ${escapeHtml(state.error)}< / div > `;
}
2025-11-13 19:21:59 +08:00
if (state.success & & state.stage === 'upload') {
2025-11-04 13:38:21 +08:00
html += `< div class = "import-success" > ${escapeHtml(state.success)}< / div > `;
}
if (state.uploading) {
2025-11-14 10:32:23 +08:00
html += '< div class = "loading" style = "margin: 8px 0;" > < span class = "loading-icon" > < / span > 正在解析 Excel...< / div > ';
2025-11-04 13:38:21 +08:00
}
if (state.sessionId & & state.sheetSummaries & & state.sheetSummaries.length) {
2025-11-13 19:21:59 +08:00
const totalSheets = state.sheetSummaries.length;
2025-11-04 13:38:21 +08:00
html += '< div class = "import-section" > ';
2025-11-13 19:21:59 +08:00
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 > ';
2025-11-04 13:38:21 +08:00
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 > ';
}
2025-11-13 19:21:59 +08:00
if (state.sessionId & & state.previewLoading) {
2025-11-14 10:32:23 +08:00
html += '< div class = "loading" style = "margin: 12px 0;" > < span class = "loading-icon" > < / span > 正在准备预览数据...< / div > ';
2025-11-13 19:21:59 +08:00
}
2025-11-04 13:38:21 +08:00
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 > ';
2025-11-13 19:21:59 +08:00
const nextDisabled = !state.sessionId || state.selectedSheets.size === 0 || state.uploading || state.previewLoading;
2025-11-04 13:38:21 +08:00
html += '< div class = "import-actions" > ';
2025-11-13 19:21:59 +08:00
html += '< div class = "import-hint" > 提示:先勾选需要导入的区划,再点击下一步进入“解析预览 & 主题绑定”环节。< / div > ';
html += `< button class = "btn btn-warning" onclick = "enterImportPreviewStage()" $ { nextDisabled ? ' disabled ' : ' ' } > 下一步:预览 & 主题绑定< / button > `;
2025-11-04 13:38:21 +08:00
html += '< / div > ';
2025-11-13 19:21:59 +08:00
return html;
2025-11-04 13:38:21 +08:00
}
2025-11-13 19:21:59 +08:00
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) {
2025-11-14 10:32:23 +08:00
html += '< div class = "loading" > < span class = "loading-icon" > < / span > 正在生成预览...< / div > ';
2025-11-13 19:21:59 +08:00
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" > ';
2025-11-14 10:32:23 +08:00
html += `< label > 主题搜索关键字< input type = "text" id = "themeSearchInput" placeholder = "按主题名称过滤" value = "${escapeHtml(state.previewThemeKeyword || '')}" oninput = "handleThemeSearchInput(event)" oncompositionend = "handleThemeSearchCompositionEnd(event)" autocomplete = "off" > < / label > `;
2025-11-13 19:21:59 +08:00
html += '< / div > ';
}
if (state.previewLoading) {
2025-11-14 10:32:23 +08:00
html += '< div class = "loading" > < span class = "loading-icon" > < / span > 预览刷新中...< / div > ';
2025-11-13 19:21:59 +08:00
}
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) {
2025-11-14 10:32:23 +08:00
html += '< div class = "preview-empty-state" > 当前区划暂无可导入的事项。< / div > ';
2025-11-13 19:21:59 +08:00
return html;
}
html += '< div class = "preview-permit-grid" > ';
filteredPermits.forEach(permit => {
if (!permit) {
return;
}
const bindingSet = getThemeBindingSet(sheet.sheet_name, permit.permit_name, true);
2025-11-14 10:32:23 +08:00
const selectedThemes = Array.from(bindingSet || []).map(formatThemeBindingLabel);
2025-11-13 19:21:59 +08:00
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 => {
2025-11-14 10:32:23 +08:00
const normalized = resolveThemeOptionNormalized(option);
const isAllThemeOption = normalized === ALL_THEMES_SENTINEL || Boolean(option & & option.is_all);
const bindingValue = resolveThemeOptionBindingValue(option);
const label = resolveThemeOptionLabel(option) || bindingValue || '未命名主题';
2025-11-13 19:21:59 +08:00
const selected = normalized & & bindingSet.has(normalized);
const workbookBadge = option & & option.source === 'workbook' ? '< span class = "theme-source-badge" > Excel< / span > ' : '';
2025-11-14 10:32:23 +08:00
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;
2025-11-13 19:21:59 +08:00
html += `
2025-11-14 10:32:23 +08:00
< div class = "theme-chip ${isAllThemeOption ? 'all-theme' : ''} ${selected ? 'selected' : ''}"
title="${escapeHtml(tooltip)}"
2025-11-13 19:21:59 +08:00
onmousedown="startThemeDragSelection(event, '${safeSheet}', '${safePermit}', '${safeTheme}')"
onmouseenter="continueThemeDragSelection(event, '${safeSheet}', '${safePermit}', '${safeTheme}')">
2025-11-14 10:32:23 +08:00
${escapeHtml(displayLabel)}
${specialBadge || workbookBadge}
2025-11-13 19:21:59 +08:00
< / 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 > `;
2025-11-14 10:32:23 +08:00
html += '< div class = "muted-text" style = "margin-top:6px;" > 若选择“所有主题”,该事项将自动出现在当前及未来新增的主题下。< / div > ';
2025-11-13 19:21:59 +08:00
}
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;
}
2025-11-14 10:32:23 +08:00
// ================ 文件管理功能 ================
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();
}
}
2025-10-30 10:33:35 +08:00
// ================ 检查点管理功能 ================
2025-10-30 11:48:15 +08:00
// 打开检查点模态窗口
async function openCheckpointModal() {
const modal = document.getElementById('checkpointModal');
modal.classList.add('show');
2025-10-30 10:33:35 +08:00
2025-11-03 11:30:38 +08:00
// 重置并显示加载状态
permitRiskSnapshotState.limit = 10;
permitRiskSnapshotState.offset = 0;
permitRiskSnapshotState.total = 0;
permitRiskSnapshotState.snapshots = [];
permitRiskSnapshotState.regionFilter = '';
permitRiskSnapshotState.permitFilter = '';
permitRiskSnapshotState.editorFilter = '';
permitRiskSnapshotState.loading = true;
permitRiskSnapshotState.error = '';
permitRiskSnapshotState.regionOptions = [];
permitRiskSnapshotState.permitOptions = [];
permitRiskSnapshotState.regionLoading = true;
permitRiskSnapshotState.regionError = '';
permitRiskSnapshotState.permitLoading = false;
permitRiskSnapshotState.permitOptionsError = '';
checkpointListCache = [];
checkpointListLoading = true;
checkpointListError = '';
2025-11-03 16:41:35 +08:00
expandedSnapshotGroups = new Set();
2025-11-03 11:30:38 +08:00
renderCheckpointManager(checkpointListCache);
await Promise.all([
ensureRegionFilterOptions(false),
refreshPermitRiskSnapshots(false),
refreshCheckpointList(false)
]);
2025-10-30 10:33:35 +08:00
}
2025-10-30 11:48:15 +08:00
// 关闭检查点模态窗口
function closeCheckpointModal() {
const modal = document.getElementById('checkpointModal');
modal.classList.remove('show');
}
// 点击模态窗口外部关闭
2025-11-14 10:32:23 +08:00
document.getElementById('checkpointModal').addEventListener('click', function(e) {
if (e.target === this) {
closeCheckpointModal();
}
});
2025-11-04 13:38:21 +08:00
2025-10-30 11:48:15 +08:00
// 加载检查点列表(保持兼容性)
async function loadCheckpoints() {
await openCheckpointModal();
}
2025-10-30 10:33:35 +08:00
// 渲染检查点管理界面
function renderCheckpointManager(checkpoints) {
2025-11-03 11:30:38 +08:00
if (Array.isArray(checkpoints)) {
checkpointListCache = checkpoints;
}
2025-10-30 11:48:15 +08:00
const modalBody = document.getElementById('checkpointModalBody');
2025-11-03 11:30:38 +08:00
if (!modalBody) return;
const snapshotState = permitRiskSnapshotState;
const checkpointList = checkpointListCache || [];
2025-10-30 11:48:15 +08:00
let html = '< div class = "checkpoint-content" > ';
2025-10-30 10:33:35 +08:00
2025-11-03 11:30:38 +08:00
html += `
< div class = "checkpoint-section snapshot-section" >
< h3 > 许可风险快照< / h3 >
< div class = "snapshot-description" >
每当编辑< strong > 地区 + 许可事项 + 风险提示< / strong > 组合时,系统都会自动记录一条快照,方便追踪历史变更、责任人及备注。
可通过下方筛选快速定位特定许可的修改记录。
< / div >
`;
const regionSelectDisabled = snapshotState.regionLoading ? 'disabled' : '';
const permitSelectDisabled = snapshotState.permitLoading || !snapshotState.regionFilter ? 'disabled' : '';
let regionOptionsHtml = '< option value = "" > 全部地区< / option > ';
if (snapshotState.regionLoading) {
regionOptionsHtml += '< option value = "__loading" disabled > 正在加载地区...< / option > ';
} else {
snapshotState.regionOptions.forEach(option => {
const selected = option.id === snapshotState.regionFilter ? 'selected' : '';
regionOptionsHtml += `< option value = "${escapeHtml(option.id)}" $ { selected } > ${escapeHtml(option.name)}< / option > `;
});
}
let permitOptionsHtml = '< option value = "" > 全部许可< / option > ';
if (!snapshotState.regionFilter) {
permitOptionsHtml += '< option value = "__placeholder" disabled > 请选择地区后再选择许可< / option > ';
} else if (snapshotState.permitLoading) {
permitOptionsHtml += '< option value = "__loading" disabled > 正在加载许可...< / option > ';
} else {
snapshotState.permitOptions.forEach(option => {
const selected = option.id === snapshotState.permitFilter ? 'selected' : '';
permitOptionsHtml += `< option value = "${escapeHtml(option.id)}" $ { selected } > ${escapeHtml(option.name)}< / option > `;
});
}
html += `
< form class = "snapshot-filters" onsubmit = "return applySnapshotFilters(event)" >
< select id = "snapshotFilterRegion" $ { regionSelectDisabled } onchange = "handleSnapshotRegionChange(this.value)" >
${regionOptionsHtml}
< / select >
< select id = "snapshotFilterPermit" $ { permitSelectDisabled } onchange = "handleSnapshotPermitChange(this.value)" >
${permitOptionsHtml}
< / select >
< input type = "text" id = "snapshotFilterEditor" placeholder = "编辑人(可选)" value = "${escapeHtml(snapshotState.editorFilter)}" >
< div class = "filter-actions" >
< button type = "submit" class = "btn btn-primary btn-sm" > < span > 🔍< / span > 筛选< / button >
< button type = "button" class = "btn btn-warning btn-sm" onclick = "resetSnapshotFilters()" > 重置< / button >
< button type = "button" class = "btn btn-primary btn-sm" onclick = "refreshPermitRiskSnapshots()" > < span > 🔄< / span > 刷新< / button >
< / div >
< / form >
`;
if (snapshotState.regionError) {
html += `< div class = "error" style = "margin-top: 8px;" > 地区列表加载失败:${escapeHtml(snapshotState.regionError)}< / div > `;
}
if (snapshotState.permitOptionsError) {
html += `< div class = "error" style = "margin-top: 8px;" > 许可列表加载失败:${escapeHtml(snapshotState.permitOptionsError)}< / div > `;
}
const timelineItems = buildTimelineItems(snapshotState.snapshots, checkpointList);
const totalSnapshots = snapshotState.total || snapshotState.snapshots.length;
const hasTimelineData = timelineItems.length > 0;
const loadingInProgress = (snapshotState.loading & & snapshotState.snapshots.length === 0) & & (checkpointListLoading & & checkpointList.length === 0);
if (loadingInProgress) {
2025-11-14 10:32:23 +08:00
html += '< div class = "loading" > < span class = "loading-icon" > < / span > 加载许可风险快照与检查点...< / div > ';
2025-11-03 11:30:38 +08:00
} else if (!hasTimelineData & & (snapshotState.error || checkpointListError)) {
const errorMsg = snapshotState.error || checkpointListError || '暂无数据';
html += `< div class = "error" > ${escapeHtml(errorMsg)}< / div > `;
} else if (!hasTimelineData) {
html += '< p style = "color: #999; text-align: center; padding: 20px;" > 暂无快照或检查点记录< / p > ';
} else {
const snapshotStart = totalSnapshots ? snapshotState.offset + 1 : 0;
const snapshotEnd = totalSnapshots ? Math.min(snapshotState.offset + snapshotState.limit, totalSnapshots) : 0;
const disablePrev = snapshotState.offset === 0 ? 'disabled' : '';
const disableNext = snapshotState.offset + snapshotState.limit >= totalSnapshots ? 'disabled' : '';
html += '< div class = "timeline-list" > ';
timelineItems.forEach(entry => {
2025-11-03 16:41:35 +08:00
if (entry.type === 'snapshot-group') {
const group = entry.raw || {};
const groupKey = group.groupKey || group.snapshot_batch_id || '';
const groupKeyJs = String(groupKey).replace(/'/g, "\\'");
const riskItems = group.items || [];
const riskCount = riskItems.length;
const primary = riskItems[0] || {};
const permitName = escapeHtml(group.permit_name || primary.permit_name || '-');
const regionName = escapeHtml(group.region_name || primary.region_name || '-');
const riskFull = primary.risk_content || '';
2025-11-03 11:30:38 +08:00
const riskPreview = escapeHtml(truncateText(riskFull, 160));
2025-11-04 13:38:21 +08:00
const sourceNameRaw = primary.permit_source_name || group.permit_source_name || '';
const sourceTag = sourceNameRaw ? `< span > 来源:${escapeHtml(sourceNameRaw)}< / span > ` : '';
2025-11-03 11:30:38 +08:00
const legalSegments = [];
2025-11-03 16:41:35 +08:00
if (primary.legal_basis) {
legalSegments.push(`📕 ${escapeHtml(primary.legal_basis)}`);
2025-11-03 11:30:38 +08:00
}
2025-11-03 16:41:35 +08:00
if (primary.document_no) {
legalSegments.push(`📄 ${escapeHtml(primary.document_no)}`);
2025-11-03 11:30:38 +08:00
}
const legalHtml = legalSegments.length ? `< div class = "timeline-meta" style = "gap:6px;" > ${legalSegments.join('< span > |< / span > ')}< / div > ` : '';
2025-11-03 16:41:35 +08:00
const editorsText = (group.editors & & group.editors.length)
? escapeHtml(group.editors.join('、'))
: '—';
const isExpanded = expandedSnapshotGroups.has(groupKey);
const expandIcon = isExpanded ? '▲' : '▼';
const toggleLabel = isExpanded ? '收起明细' : '展开明细';
const changeSummaryMerged = group.change_summaries & & group.change_summaries.length
? `< div class = "timeline-note" > 备注:${escapeHtml(group.change_summaries.join('; '))}< / div > `
: '';
const statusTag = primary.permit_status ? `< span class = "snapshot-status-tag" > ${escapeHtml(primary.permit_status)}< / span > ` : '';
const toggleButton = riskCount > 0
? `< button class = "btn btn-warning btn-sm" onclick = "toggleSnapshotGroup('${groupKeyJs}')" > ${expandIcon} ${toggleLabel}< / button > `
: '';
const restoreButton = `< button class = "btn btn-primary btn-sm" onclick = "confirmRestoreSnapshotBatch('${groupKeyJs}')" > < span > 🛠️< / span > 恢复< / button > `;
const detailItemsHtml = riskItems.map(detail => {
const detailLegal = [];
if (detail.legal_basis) {
detailLegal.push(`📕 ${escapeHtml(detail.legal_basis)}`);
}
if (detail.document_no) {
detailLegal.push(`📄 ${escapeHtml(detail.document_no)}`);
}
const detailMetaParts = [];
if (detail.edited_by) {
detailMetaParts.push(`编辑人:${escapeHtml(detail.edited_by)}`);
}
2025-11-04 13:38:21 +08:00
if (detail.permit_source_name) {
detailMetaParts.push(`来源:${escapeHtml(detail.permit_source_name)}`);
}
2025-11-03 16:41:35 +08:00
detailMetaParts.push(...detailLegal);
const detailMetaHtml = detailMetaParts.length
? `< div class = "snapshot-detail-meta" > ${detailMetaParts.join('< span > |< / span > ')}< / div > `
: '';
const detailStatusTag = detail.permit_status ? `< span class = "snapshot-status-tag" > ${escapeHtml(detail.permit_status)}< / span > ` : '';
const detailNote = detail.change_summary ? `< div class = "timeline-note" style = "margin-top:6px;" > 备注:${escapeHtml(detail.change_summary)}< / div > ` : '';
return `
< div class = "snapshot-detail-item" >
< div class = "snapshot-detail-header" >
< span > 版本 ${escapeHtml(String(detail.version || 0))}< / span >
< span > 风险ID: ${escapeHtml(detail.risk_id || '-')}< / span >
${detailStatusTag}
< / div >
< div class = "timeline-content" > ${escapeHtml(detail.risk_content || '—')}< / div >
${detailMetaHtml}
${detailNote}
< / div >
`;
}).join('');
const detailListHtml = (isExpanded & & riskCount > 0)
? `
< div class = "snapshot-detail-list expanded" >
${detailItemsHtml}
< / div >
`
: '';
const metaHtml = `
< div class = "timeline-meta" >
${statusTag}
< span > 风险条目:${riskCount} 个< / span >
< span > 编辑人:${editorsText}< / span >
2025-11-04 13:38:21 +08:00
${sourceTag}
2025-11-03 16:41:35 +08:00
< / div >
`;
2025-11-03 11:30:38 +08:00
const timeDisplay = escapeHtml(entry.timeText || '');
html += `
< div class = "timeline-item timeline-item-snapshot" >
< div class = "timeline-icon" > 📝< / div >
< div class = "timeline-body" >
< div class = "timeline-header" >
2025-11-03 16:41:35 +08:00
< div class = "timeline-title" > 风险快照 · ${riskCount > 1 ? `批次(${riskCount} 条)` : `版本 ${escapeHtml(String(primary.version || 0))}`}< / div >
2025-11-03 11:30:38 +08:00
< div class = "timeline-time" > ${timeDisplay}< / div >
< / div >
< div class = "timeline-subtitle" > ${permitName}< span style = "margin-left: 6px; color: #666;" > ( ${regionName}) < / span > < / div >
< div class = "timeline-content" title = "${escapeHtml(riskFull)}" > ${riskPreview || '—'}< / div >
${legalHtml}
2025-11-03 16:41:35 +08:00
${metaHtml}
${changeSummaryMerged}
< div class = "timeline-actions" >
${toggleButton}
${restoreButton}
2025-11-03 11:30:38 +08:00
< / div >
2025-11-03 16:41:35 +08:00
${detailListHtml}
2025-11-03 11:30:38 +08:00
< / div >
< / div >
`;
} else if (entry.type === 'checkpoint') {
const item = entry.raw;
const timeDisplay = escapeHtml(entry.timeText || '');
const description = item.description ? escapeHtml(item.description) : '无描述';
const tableCount = Object.keys(item.table_counts || {}).length;
const totalRows = item.total_rows || 0;
const checkpointIdRaw = item.checkpoint_id || '';
const checkpointIdDisplay = escapeHtml(checkpointIdRaw);
const checkpointIdJs = checkpointIdRaw.replace(/'/g, "\\'");
html += `
< div class = "timeline-item timeline-item-checkpoint" >
< div class = "timeline-icon" > 🗂️< / div >
< div class = "timeline-body" >
< div class = "timeline-header" >
< div class = "timeline-title" > 数据库检查点< / div >
< div class = "timeline-time" > ${timeDisplay}< / div >
< / div >
< div class = "timeline-subtitle" > ${checkpointIdDisplay}< / div >
< div class = "timeline-content" >
< div class = "timeline-note" > ${description}< / div >
< div class = "timeline-stats" >
< span > 📊 行数:< strong > ${totalRows}< / strong > < / span >
< span > 📋 表数:< strong > ${tableCount}< / strong > < / span >
< / div >
< / div >
< div class = "timeline-actions" >
< button class = "btn btn-danger btn-sm" onclick = "confirmRestoreCheckpoint('${checkpointIdJs}')" >
< span > 🔄< / span > 恢复
< / button >
< button class = "btn btn-warning btn-sm" onclick = "deleteCheckpoint('${checkpointIdJs}')" >
< span > 🗑️< / span > 删除
< / button >
< / div >
< / div >
< / div >
`;
}
});
html += '< / div > ';
const warningMessages = [];
if (snapshotState.error) {
warningMessages.push(`快照加载提示:${escapeHtml(snapshotState.error)}`);
}
if (checkpointListError) {
warningMessages.push(`检查点加载提示:${escapeHtml(checkpointListError)}`);
}
if (warningMessages.length) {
html += `< div class = "error" style = "margin-top: 10px;" > ${warningMessages.join('< br > ')}< / div > `;
}
2025-11-03 16:41:35 +08:00
const snapshotGroupCount = new Set((snapshotState.snapshots || []).map(item => item.snapshot_batch_id || item.snapshot_id)).size;
const snapshotRangeText = totalSnapshots
? `风险快照 ${snapshotStart}-${snapshotEnd} / ${totalSnapshots}`
: '风险快照 0 / 0';
const snapshotBatchText = `快照批次 ${snapshotGroupCount} 个`;
2025-11-03 11:30:38 +08:00
const checkpointSummaryText = `检查点 ${checkpointList.length} 个`;
html += `
< div class = "timeline-footer" >
2025-11-03 16:41:35 +08:00
< div class = "snapshot-count" > ${snapshotBatchText}| ${snapshotRangeText}, ${checkpointSummaryText}< / div >
2025-11-03 11:30:38 +08:00
< div class = "snapshot-pagination" >
< button class = "btn btn-warning btn-sm" onclick = "changeSnapshotPage(-1)" $ { disablePrev } > 上一页< / button >
< button class = "btn btn-warning btn-sm" onclick = "changeSnapshotPage(1)" $ { disableNext } > 下一页< / button >
< / div >
< / div >
`;
}
html += '< / div > ';
2025-10-30 13:47:13 +08:00
html += `
< div class = "checkpoint-info" style = "background: #e3f2fd; padding: 15px; border-radius: 6px; margin-bottom: 20px; border-left: 4px solid #2196f3;" >
< div style = "color: #1976d2; font-size: 13px; line-height: 1.6;" >
2025-11-03 11:30:38 +08:00
< strong > 💡 说明:< / strong > 数据库检查点文件保存在服务器的 data/checkpoints/ 目录中,重启应用后不会丢失。
每个检查点包含完整的数据库备份,可用于快速回滚。
2025-10-30 13:47:13 +08:00
< / div >
< / div >
`;
2025-10-30 10:33:35 +08:00
html += `
< div class = "checkpoint-section" >
< h3 > 创建新检查点< / h3 >
< div class = "checkpoint-form" >
< div class = "form-group" >
< label for = "checkpointDescription" > 检查点描述(可选):< / label >
< input type = "text" id = "checkpointDescription" placeholder = "例如:修改风险提示前的备份" >
< / div >
< button class = "btn btn-primary" onclick = "createCheckpoint()" >
< span > 📸< / span > 创建检查点
< / button >
< / div >
< / div >
`;
2025-11-03 11:30:38 +08:00
html += '< / div > ';
modalBody.innerHTML = html;
}
async function ensureRegionFilterOptions(showSpinner = true) {
permitRiskSnapshotState.regionLoading = true;
permitRiskSnapshotState.regionError = '';
if (showSpinner) {
renderCheckpointManager(checkpointListCache);
}
try {
const response = await fetch(`/fs-ai-asistant/api/workflow/lawrisk/admin/regions?t=${Date.now()}`);
const data = await parseJsonResponse(response);
if (data & & data.success) {
permitRiskSnapshotState.regionOptions = (data.data.regions || []).map(region => ({
id: region.id,
name: region.name
}));
permitRiskSnapshotState.regionError = '';
} else {
permitRiskSnapshotState.regionOptions = [];
permitRiskSnapshotState.regionError = data & & data.message ? data.message : `地区列表加载失败( HTTP ${response.status}) `;
}
} catch (error) {
permitRiskSnapshotState.regionOptions = [];
permitRiskSnapshotState.regionError = error.message || '地区列表加载失败';
} finally {
permitRiskSnapshotState.regionLoading = false;
renderCheckpointManager(checkpointListCache);
}
}
async function fetchPermitOptionsForRegion(regionId, showSpinner = true) {
permitRiskSnapshotState.permitLoading = true;
permitRiskSnapshotState.permitOptions = [];
permitRiskSnapshotState.permitOptionsError = '';
if (showSpinner) {
renderCheckpointManager(checkpointListCache);
}
if (!regionId) {
permitRiskSnapshotState.permitLoading = false;
renderCheckpointManager(checkpointListCache);
return;
}
try {
const response = await fetch(`/fs-ai-asistant/api/workflow/lawrisk/getPermits?region=${regionId}&t=${Date.now()}`);
const data = await parseJsonResponse(response);
if (data & & data.success) {
const permits = data.data & & data.data.permits ? data.data.permits : [];
permitRiskSnapshotState.permitOptions = permits.map(permit => ({
id: permit.id,
name: permit.name
}));
permitRiskSnapshotState.permitOptionsError = '';
} else {
permitRiskSnapshotState.permitOptions = [];
permitRiskSnapshotState.permitOptionsError = data & & data.message ? data.message : `许可列表加载失败( HTTP ${response.status}) `;
}
} catch (error) {
permitRiskSnapshotState.permitOptions = [];
permitRiskSnapshotState.permitOptionsError = error.message || '许可列表加载失败';
} finally {
permitRiskSnapshotState.permitLoading = false;
renderCheckpointManager(checkpointListCache);
}
}
async function handleSnapshotRegionChange(value) {
let normalizedValue = (value || '').trim();
if (normalizedValue === '__loading') {
normalizedValue = '';
}
permitRiskSnapshotState.regionFilter = normalizedValue;
permitRiskSnapshotState.permitFilter = '';
permitRiskSnapshotState.offset = 0;
permitRiskSnapshotState.permitOptions = [];
permitRiskSnapshotState.permitOptionsError = '';
renderCheckpointManager(checkpointListCache);
if (normalizedValue) {
await fetchPermitOptionsForRegion(normalizedValue, true);
}
}
2025-10-30 10:33:35 +08:00
2025-11-03 11:30:38 +08:00
function handleSnapshotPermitChange(value) {
let normalizedValue = (value || '').trim();
if (normalizedValue === '__placeholder' || normalizedValue === '__loading') {
normalizedValue = '';
}
permitRiskSnapshotState.permitFilter = normalizedValue;
}
async function refreshPermitRiskSnapshots(showSpinner = true) {
if (showSpinner) {
permitRiskSnapshotState.loading = true;
permitRiskSnapshotState.error = '';
renderCheckpointManager(checkpointListCache);
2025-10-30 10:33:35 +08:00
} else {
2025-11-03 11:30:38 +08:00
permitRiskSnapshotState.loading = true;
permitRiskSnapshotState.error = '';
2025-10-30 10:33:35 +08:00
}
2025-11-03 11:30:38 +08:00
const params = new URLSearchParams({
limit: permitRiskSnapshotState.limit,
offset: permitRiskSnapshotState.offset
});
if (permitRiskSnapshotState.regionFilter) {
params.append('region_id', permitRiskSnapshotState.regionFilter);
}
if (permitRiskSnapshotState.permitFilter) {
params.append('permit_id', permitRiskSnapshotState.permitFilter);
}
if (permitRiskSnapshotState.editorFilter) {
params.append('edited_by', permitRiskSnapshotState.editorFilter);
}
params.append('t', Date.now());
2025-10-30 10:33:35 +08:00
2025-11-03 11:30:38 +08:00
try {
const response = await fetch(`/fs-ai-asistant/api/workflow/lawrisk/admin/permit-risk-snapshots?${params.toString()}`);
const data = await parseJsonResponse(response);
if (data & & data.success) {
permitRiskSnapshotState.snapshots = data.data.snapshots || [];
const pagination = data.data.pagination || {};
permitRiskSnapshotState.total = pagination.total ?? permitRiskSnapshotState.snapshots.length;
permitRiskSnapshotState.limit = pagination.limit ?? permitRiskSnapshotState.limit;
permitRiskSnapshotState.offset = pagination.offset ?? permitRiskSnapshotState.offset;
permitRiskSnapshotState.error = '';
} else {
permitRiskSnapshotState.snapshots = [];
permitRiskSnapshotState.total = 0;
permitRiskSnapshotState.error = data & & data.message ? data.message : `加载失败( HTTP ${response.status}) `;
}
} catch (error) {
permitRiskSnapshotState.snapshots = [];
permitRiskSnapshotState.total = 0;
permitRiskSnapshotState.error = error.message || '网络错误';
} finally {
permitRiskSnapshotState.loading = false;
2025-11-03 16:41:35 +08:00
expandedSnapshotGroups = new Set();
2025-11-03 11:30:38 +08:00
renderCheckpointManager(checkpointListCache);
}
}
async function refreshCheckpointList(showSpinner = true) {
if (showSpinner) {
checkpointListLoading = true;
checkpointListError = '';
renderCheckpointManager(checkpointListCache);
} else {
checkpointListLoading = true;
checkpointListError = '';
}
try {
const response = await fetch(`/fs-ai-asistant/api/workflow/lawrisk/admin/checkpoints?t=${Date.now()}`);
const data = await parseJsonResponse(response);
if (data & & data.success) {
checkpointListCache = data.data.checkpoints || [];
checkpointListError = '';
} else {
checkpointListError = data & & data.message ? data.message : `加载失败( HTTP ${response.status}) `;
}
} catch (error) {
checkpointListError = error.message || '网络错误';
} finally {
checkpointListLoading = false;
renderCheckpointManager(checkpointListCache);
}
2025-10-30 10:33:35 +08:00
}
2025-11-03 11:30:38 +08:00
function applySnapshotFilters(event) {
if (event) {
event.preventDefault();
}
const regionInput = document.getElementById('snapshotFilterRegion');
const permitInput = document.getElementById('snapshotFilterPermit');
const editorInput = document.getElementById('snapshotFilterEditor');
let regionValue = regionInput ? regionInput.value.trim() : '';
if (regionValue === '__loading') {
regionValue = '';
}
let permitValue = permitInput ? permitInput.value.trim() : '';
if (permitValue === '__placeholder' || permitValue === '__loading') {
permitValue = '';
}
permitRiskSnapshotState.regionFilter = regionValue;
permitRiskSnapshotState.permitFilter = permitValue;
permitRiskSnapshotState.editorFilter = editorInput ? editorInput.value.trim() : '';
permitRiskSnapshotState.offset = 0;
refreshPermitRiskSnapshots();
return false;
}
function resetSnapshotFilters() {
permitRiskSnapshotState.regionFilter = '';
permitRiskSnapshotState.permitFilter = '';
permitRiskSnapshotState.editorFilter = '';
permitRiskSnapshotState.offset = 0;
permitRiskSnapshotState.permitOptions = [];
permitRiskSnapshotState.permitOptionsError = '';
const regionInput = document.getElementById('snapshotFilterRegion');
const permitInput = document.getElementById('snapshotFilterPermit');
const editorInput = document.getElementById('snapshotFilterEditor');
if (regionInput) regionInput.value = '';
if (permitInput) permitInput.value = '';
if (editorInput) editorInput.value = '';
refreshPermitRiskSnapshots();
}
async function changeSnapshotPage(direction) {
if (permitRiskSnapshotState.loading) {
return;
}
const step = direction > 0 ? permitRiskSnapshotState.limit : -permitRiskSnapshotState.limit;
let nextOffset = permitRiskSnapshotState.offset + step;
if (direction < 0 ) {
nextOffset = Math.max(0, nextOffset);
} else if (permitRiskSnapshotState.offset + permitRiskSnapshotState.limit >= permitRiskSnapshotState.total) {
return;
}
permitRiskSnapshotState.offset = Math.max(0, nextOffset);
await refreshPermitRiskSnapshots();
}
2025-10-30 10:33:35 +08:00
// 创建检查点
async function createCheckpoint() {
const description = document.getElementById('checkpointDescription').value;
const btn = event.target;
const originalText = btn.innerHTML;
btn.disabled = true;
2025-11-14 10:32:23 +08:00
btn.innerHTML = '< div class = "loading" > < span class = "loading-icon" > < / span > 创建中...< / div > ';
2025-10-30 10:33:35 +08:00
try {
const response = await fetch('/fs-ai-asistant/api/workflow/lawrisk/admin/checkpoints', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ description })
});
const data = await response.json();
if (data.success) {
document.getElementById('checkpointDescription').value = '';
2025-11-03 11:30:38 +08:00
await refreshCheckpointList();
2025-10-30 13:47:13 +08:00
// 显示成功消息
setTimeout(() => {
alert(`✅ 检查点创建成功!\n\nID: ${data.data.checkpoint_id}\n备份了 ${data.data.total_rows} 行数据`);
}, 100);
2025-10-30 10:33:35 +08:00
} else {
2025-10-30 13:47:13 +08:00
setTimeout(() => {
alert(`❌ 创建失败:${data.message}`);
}, 100);
2025-10-30 10:33:35 +08:00
}
} catch (error) {
2025-10-30 13:47:13 +08:00
setTimeout(() => {
alert(`❌ 网络错误:${error.message}`);
}, 100);
2025-10-30 10:33:35 +08:00
} finally {
btn.disabled = false;
btn.innerHTML = originalText;
}
}
// 确认恢复检查点
function confirmRestoreCheckpoint(checkpointId) {
showDangerModal(
'恢复检查点',
`您确定要恢复检查点 "${checkpointId}" 吗?`,
'此操作将覆盖当前数据库中的所有数据,且无法撤销!请确保您已经创建了新的检查点作为备份。',
() => restoreCheckpoint(checkpointId)
);
}
// 恢复检查点
async function restoreCheckpoint(checkpointId) {
closeDangerModal();
2025-10-30 13:52:19 +08:00
// 显示恢复进度模态框
showRestoreProgressModal(checkpointId);
2025-10-30 10:33:35 +08:00
try {
const response = await fetch(`/fs-ai-asistant/api/workflow/lawrisk/admin/checkpoints/${checkpointId}/restore`, {
method: 'POST'
});
const data = await response.json();
2025-10-30 13:52:19 +08:00
// 关闭进度模态框
closeRestoreProgressModal();
2025-10-30 10:33:35 +08:00
if (data.success) {
2025-11-03 11:30:38 +08:00
await Promise.all([
refreshCheckpointList(),
refreshPermitRiskSnapshots()
]);
2025-10-30 13:47:13 +08:00
setTimeout(() => {
alert(`✅ 检查点恢复成功!\n\n恢复了 ${data.data.total_rows_restored} 行数据,覆盖了 ${data.data.tables_restored} 个表。`);
}, 100);
2025-10-30 10:33:35 +08:00
} else {
2025-10-30 13:47:13 +08:00
setTimeout(() => {
alert(`❌ 恢复失败:${data.message}`);
}, 100);
2025-10-30 10:33:35 +08:00
}
} catch (error) {
2025-10-30 13:52:19 +08:00
closeRestoreProgressModal();
2025-10-30 13:47:13 +08:00
setTimeout(() => {
alert(`❌ 网络错误:${error.message}`);
}, 100);
2025-10-30 10:33:35 +08:00
}
}
2025-10-30 13:52:19 +08:00
// 显示恢复进度模态框
function showRestoreProgressModal(checkpointId) {
const modal = document.createElement('div');
modal.id = 'restoreProgressModal';
modal.className = 'modal show';
modal.innerHTML = `
< div class = "modal-content" style = "max-width: 450px; text-align: center; padding: 40px;" >
< div style = "margin-bottom: 20px;" >
2025-11-14 10:32:23 +08:00
< div class = "loading" style = "justify-content: center; margin: 0 auto;" >
< span class = "loading-icon" style = "width: 50px; height: 50px; border-width: 4px;" > < / span >
< / div >
2025-10-30 13:52:19 +08:00
< / div >
< h3 style = "color: #333; margin-bottom: 15px;" > 正在恢复检查点...< / h3 >
< p style = "color: #666; font-size: 14px; line-height: 1.6;" >
正在从备份恢复数据库< br >
< strong > ${checkpointId}< / strong > < br >
< span style = "color: #999; font-size: 12px; margin-top: 10px; display: block;" >
此操作可能需要几分钟时间,请耐心等待...
< / span >
< / p >
< / div >
`;
document.body.appendChild(modal);
}
// 关闭恢复进度模态框
function closeRestoreProgressModal() {
const modal = document.getElementById('restoreProgressModal');
if (modal) {
modal.remove();
}
}
2025-10-30 10:33:35 +08:00
// 删除检查点
async function deleteCheckpoint(checkpointId) {
if (!confirm(`确定要删除检查点 "${checkpointId}" 吗?此操作无法撤销。`)) {
return;
}
try {
const response = await fetch(`/fs-ai-asistant/api/workflow/lawrisk/admin/checkpoints/${checkpointId}`, {
method: 'DELETE'
});
2025-11-03 11:30:38 +08:00
const data = await parseJsonResponse(response);
2025-10-30 10:33:35 +08:00
if (data.success) {
2025-11-03 11:30:38 +08:00
await refreshCheckpointList();
2025-10-30 13:47:13 +08:00
setTimeout(() => {
alert(`✅ 检查点已删除`);
}, 100);
2025-10-30 10:33:35 +08:00
} else {
2025-10-30 13:47:13 +08:00
setTimeout(() => {
alert(`❌ 删除失败:${data.message}`);
}, 100);
2025-10-30 10:33:35 +08:00
}
} catch (error) {
2025-10-30 13:47:13 +08:00
setTimeout(() => {
alert(`❌ 网络错误:${error.message}`);
}, 100);
2025-10-30 10:33:35 +08:00
}
}
2025-11-03 11:30:38 +08:00
function truncateText(text, maxLength = 120) {
if (text === null || text === undefined) return '';
const str = String(text);
return str.length > maxLength ? `${str.slice(0, maxLength)}…` : str;
}
function formatIsoDatetime(value) {
if (!value) return '';
const date = new Date(value);
if (Number.isNaN(date.getTime())) {
return value;
}
const pad = (num) => String(num).padStart(2, '0');
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
}
function escapeHtml(str) {
if (str === null || str === undefined) return '';
return String(str)
.replace(/& /g, '& ')
.replace(/< /g, '< ')
.replace(/>/g, '> ')
.replace(/"/g, '" ')
.replace(/'/g, '' ');
}
async function parseJsonResponse(response) {
const contentType = response.headers.get('content-type') || '';
if (!contentType.includes('application/json')) {
await response.text();
return {
success: false,
message: `服务器返回非 JSON 格式( HTTP ${response.status}) `
};
}
try {
return await response.json();
} catch (err) {
return {
success: false,
message: err.message || '响应解析失败'
};
}
}
2025-11-03 16:41:35 +08:00
function getSnapshotGroupItems(batchId) {
if (!batchId) return [];
return (permitRiskSnapshotState.snapshots || []).filter(item => {
const key = item.snapshot_batch_id || item.snapshot_id;
return key === batchId;
});
}
function toggleSnapshotGroup(batchId) {
if (!batchId) return;
if (expandedSnapshotGroups.has(batchId)) {
expandedSnapshotGroups.delete(batchId);
} else {
expandedSnapshotGroups.add(batchId);
}
renderCheckpointManager(checkpointListCache);
}
function confirmRestoreSnapshotBatch(batchId) {
if (!batchId) {
alert('未找到对应的快照批次');
return;
}
const groupItems = getSnapshotGroupItems(batchId);
if (groupItems.length === 0) {
alert('未找到对应的快照记录');
return;
}
const primary = groupItems[0];
const regionName = primary.region_name || '未知地区';
const permitName = primary.permit_name || '未知许可';
const riskCount = groupItems.length;
const confirmMessage = `确定要从快照恢复「${regionName} › ${permitName}」吗?\n\n` +
`该操作将重新建立 ${riskCount} 条风险关联、许可明细以及相关主题/范围配置。`;
if (!confirm(confirmMessage)) {
return;
}
const summaryInput = prompt('请输入恢复说明(可选):', '');
if (summaryInput === null) {
return;
}
const changeSummary = summaryInput.trim();
restoreSnapshotBatch(batchId, changeSummary, groupItems);
}
async function restoreSnapshotBatch(batchId, changeSummary, groupItems) {
try {
const payload = {};
if (changeSummary) {
payload.change_summary = changeSummary;
}
const response = await fetch(`/fs-ai-asistant/api/workflow/lawrisk/admin/permit-risk-snapshots/${batchId}/restore`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const data = await parseJsonResponse(response);
if (data & & data.success) {
const restoredCount = data.data & & typeof data.data.restored_risk_count === 'number'
? data.data.restored_risk_count
: (groupItems ? groupItems.length : 0);
alert(`✅ 恢复成功!已恢复 ${restoredCount} 条风险关联。`);
await refreshPermitRiskSnapshots(false);
if (groupItems & & groupItems.length > 0) {
const targetRegionId = groupItems[0].region_id;
const targetPermitId = groupItems[0].permit_id;
if (currentRegion & & currentRegion.id === targetRegionId) {
2025-11-13 17:01:37 +08:00
if (currentStep >= 2) {
await loadPermitsForRegion();
2025-11-03 16:41:35 +08:00
}
if (currentPermit & & currentPermit.id === targetPermitId) {
await showPermitDetails();
}
}
}
} else {
const message = data & & data.message ? data.message : `恢复失败( HTTP ${response.status}) `;
alert(`❌ 恢复失败:${message}`);
}
} catch (error) {
alert(`❌ 恢复失败:${error.message}`);
}
}
2025-11-03 11:30:38 +08:00
function checkpointTimestampToIso(timestamp) {
if (!timestamp) return '';
const match = String(timestamp).match(/(\d{4})(\d{2})(\d{2})_(\d{2})(\d{2})(\d{2})/);
if (!match) return '';
return `${match[1]}-${match[2]}-${match[3]}T${match[4]}:${match[5]}:${match[6]}`;
}
function buildTimelineItems(snapshotItems, checkpointItems) {
const items = [];
2025-11-03 16:41:35 +08:00
const snapshotGroups = new Map();
2025-11-03 11:30:38 +08:00
(snapshotItems || []).forEach(item => {
2025-11-03 16:41:35 +08:00
const groupKey = item.snapshot_batch_id || item.snapshot_id;
if (!snapshotGroups.has(groupKey)) {
snapshotGroups.set(groupKey, {
key: groupKey,
items: [],
});
}
snapshotGroups.get(groupKey).items.push(item);
});
snapshotGroups.forEach(group => {
group.items.sort((a, b) => {
const timeA = Date.parse(a.created_at || '') || 0;
const timeB = Date.parse(b.created_at || '') || 0;
return timeB - timeA;
});
let latestIso = '';
let latestValue = 0;
group.items.forEach(item => {
const value = Date.parse(item.created_at || '') || 0;
if (value > latestValue) {
latestValue = value;
latestIso = item.created_at || '';
}
});
const firstItem = group.items[0] || {};
const displayIso = latestIso || firstItem.created_at || '';
const timeValue = displayIso ? Date.parse(displayIso) || 0 : latestValue;
const editors = Array.from(new Set(group.items.map(entry => entry.edited_by).filter(Boolean)));
const summaries = group.items.map(entry => entry.change_summary).filter(Boolean);
2025-11-03 11:30:38 +08:00
items.push({
2025-11-03 16:41:35 +08:00
type: 'snapshot-group',
2025-11-03 11:30:38 +08:00
timeValue,
2025-11-03 16:41:35 +08:00
timeText: displayIso ? formatIsoDatetime(displayIso) : '',
raw: {
groupKey: group.key,
created_at: displayIso,
items: group.items,
region_name: firstItem.region_name || '',
region_id: firstItem.region_id || '',
permit_name: firstItem.permit_name || '',
permit_id: firstItem.permit_id || '',
editors,
change_summaries: summaries,
snapshot_batch_id: firstItem.snapshot_batch_id || firstItem.snapshot_id || group.key,
},
2025-11-03 11:30:38 +08:00
});
});
(checkpointItems || []).forEach(item => {
const iso = checkpointTimestampToIso(item.timestamp);
const timeValue = iso ? Date.parse(iso) : 0;
items.push({
type: 'checkpoint',
timeValue,
timeText: formatTimestamp(item.timestamp),
raw: item,
iso
});
});
items.sort((a, b) => b.timeValue - a.timeValue);
return items;
}
2025-10-30 10:33:35 +08:00
// 格式化时间戳
function formatTimestamp(timestamp) {
2025-11-03 11:30:38 +08:00
if (!timestamp || timestamp.length < 15 ) {
return timestamp || '';
}
2025-10-30 10:33:35 +08:00
const year = timestamp.substring(0, 4);
const month = timestamp.substring(4, 6);
const day = timestamp.substring(6, 8);
const hour = timestamp.substring(9, 11);
const minute = timestamp.substring(11, 13);
const second = timestamp.substring(13, 15);
return `${year}-${month}-${day} ${hour}:${minute}:${second}`;
}
// ================ 危险操作确认模态框 ================
// 显示危险操作确认模态框
function showDangerModal(title, message, warning, callback) {
pendingDangerOperation = callback;
document.getElementById('dangerModalTitle').textContent = title;
document.getElementById('dangerModalMessage').textContent = message;
document.getElementById('dangerModalWarning').textContent = warning;
document.getElementById('dangerModal').classList.add('show');
}
// 关闭危险操作确认模态框
function closeDangerModal() {
document.getElementById('dangerModal').classList.remove('show');
pendingDangerOperation = null;
}
// 确认执行危险操作
function confirmDangerOperation() {
if (pendingDangerOperation & & typeof pendingDangerOperation === 'function') {
pendingDangerOperation();
}
closeDangerModal();
}
2025-11-17 15:07:14 +08:00
// 标签页切换功能
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') {
2025-11-19 15:51:49 +08:00
loadPermitFilterOptions();
2025-11-17 15:07:14 +08:00
} else if (tabId === 'files-tab') {
loadFileManager();
}
// 更新URL( 不刷新页面)
const url = new URL(window.location);
url.searchParams.set('tab', tabId.replace('-tab', ''));
window.history.replaceState({}, '', url);
}
2025-11-19 15:51:49 +08:00
// ============== 许可事项筛选器相关函数 ==============
2025-11-20 09:47:20 +08:00
// 区域-部门映射关系缓存
let regionDepartmentMap = {};
2025-11-19 15:51:49 +08:00
let permitFilterOptions = {
regions: [],
themes: [],
departments: []
};
let permitCurrentPage = 0;
let permitPageSize = 50;
let permitTotalPages = 0;
// 加载筛选选项
async function loadPermitFilterOptions() {
2025-11-20 09:47:20 +08:00
// 显示加载状态
const loadingElement = document.getElementById('filterOptionsLoading');
if (loadingElement) {
loadingElement.style.display = 'flex';
}
2025-11-19 15:51:49 +08:00
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);
2025-11-20 09:47:20 +08:00
// 可以在这里显示错误提示
alert('加载筛选选项失败: ' + error.message);
} finally {
// 隐藏加载状态
if (loadingElement) {
loadingElement.style.display = 'none';
}
2025-11-19 15:51:49 +08:00
}
}
// 渲染筛选选项
function renderPermitFilterOptions() {
2025-11-20 09:47:20 +08:00
// 构建区域-部门映射关系
buildRegionDepartmentMapping();
2025-11-19 15:51:49 +08:00
// 渲染区域选项
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);
});
}
2025-11-20 09:47:20 +08:00
// 渲染部门选项(显示所有部门)
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]);
}
2025-11-19 15:51:49 +08:00
});
2025-11-20 09:47:20 +08:00
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);
2025-11-19 15:51:49 +08:00
}
}
// 应用筛选
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& regions[]=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) {
2025-11-20 09:47:20 +08:00
selectedText.textContent = `全部${type === 'region' ? '区域' : type === 'theme' ? '主题' : '部门'}`;
2025-11-19 15:51:49 +08:00
} 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 ;
}
}
2025-11-20 09:47:20 +08:00
// 区域选择变化处理(基于前端缓存)
function onRegionSelectionChange() {
console.log('onRegionSelectionChange 被调用'); // 调试日志
2025-11-19 15:51:49 +08:00
// 更新区域显示文本
updateSelectedText('region');
// 获取当前选中的区域
const regionCheckboxes = document.querySelectorAll('input[name="regionFilter"]:checked');
const selectedRegions = Array.from(regionCheckboxes).map(cb => cb.value);
2025-11-20 09:47:20 +08:00
console.log('选中的区域:', selectedRegions); // 调试日志
2025-11-19 15:51:49 +08:00
2025-11-20 09:47:20 +08:00
// 根据选中的区域,动态更新部门列表
2025-11-19 15:51:49 +08:00
if (selectedRegions.length === 1) {
2025-11-20 09:47:20 +08:00
// 只选择一个区域时,显示该区域的部门
const regionId = selectedRegions[0];
console.log('显示区域关联部门:', regionId); // 调试日志
const departments = regionDepartmentMap[regionId] || [];
renderDepartmentOptions(departments);
2025-11-19 15:51:49 +08:00
} else if (selectedRegions.length === 0) {
2025-11-20 09:47:20 +08:00
// 没有选择区域时,清空已选部门并显示所有部门
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);
}
});
});
2025-11-19 15:51:49 +08:00
2025-11-20 09:47:20 +08:00
renderDepartmentOptions(allDepartments);
2025-11-19 15:51:49 +08:00
}
}
2025-11-20 09:47:20 +08:00
// 渲染部门选项(基于传入的部门列表)
function renderDepartmentOptions(departments) {
2025-11-19 15:51:49 +08:00
const departmentOptionsList = document.getElementById('departmentOptionsList');
if (departmentOptionsList) {
departmentOptionsList.innerHTML = '';
2025-11-20 09:47:20 +08:00
if (!departments || departments.length === 0) {
// 如果没有部门,显示提示信息
2025-11-19 15:51:49 +08:00
const div = document.createElement('div');
2025-11-20 09:47:20 +08:00
div.style.padding = '12px';
div.style.textAlign = 'center';
div.style.color = '#999';
div.style.fontSize = '13px';
div.innerHTML = '该区域暂无关联部门';
2025-11-19 15:51:49 +08:00
departmentOptionsList.appendChild(div);
2025-11-20 09:47:20 +08:00
} 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);
});
}
2025-11-19 15:51:49 +08:00
// 更新部门显示文本
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');
}
2025-11-20 09:47:20 +08:00
// 查看许可详情
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);
}
}
2025-11-19 15:51:49 +08:00
// 渲染筛选结果
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 >
2025-11-20 09:47:20 +08:00
< th style = "padding: 14px 16px; text-align: left; font-weight: 600; color: #555; font-size: 14px; width: 180px;" > 操作< / th >
2025-11-19 15:51:49 +08:00
< / 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('');
2025-11-20 09:47:20 +08:00
const regionId = permit.region?.id || '';
2025-11-19 15:51:49 +08:00
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;" >
2025-11-20 09:47:20 +08:00
< 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;" >
2025-11-19 15:51:49 +08:00
查看
< / button >
2025-11-20 09:47:20 +08:00
< 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 >
2025-11-19 15:51:49 +08:00
< / 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;
}
2025-11-20 09:47:20 +08:00
// 显示通用模态框
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);
}
2025-11-19 15:51:49 +08:00
}
2025-11-17 15:07:14 +08:00
// 加载文件管理
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;
}
2025-11-18 19:57:55 +08:00
// 初始化标签页
2025-11-17 15:07:14 +08:00
function setupTabsByRole(userRole) {
const pageTitle = document.getElementById('pageTitle');
// 更新页面标题
2025-11-18 19:57:55 +08:00
pageTitle.textContent = '🗃️ 管理员控制台';
2025-11-17 15:07:14 +08:00
2025-11-18 19:57:55 +08:00
// 默认激活许可事项管理标签页
const permitsTab = document.getElementById('permits-tab');
const permitsButton = document.querySelector('[data-tab="permits-tab"]');
if (permitsTab) {
permitsTab.classList.add('active');
2025-11-17 15:07:14 +08:00
}
2025-11-18 19:57:55 +08:00
if (permitsButton) {
permitsButton.classList.add('active');
}
// 加载许可数据
goToStep(1);
2025-11-17 15:07:14 +08:00
}
// 触发文件上传
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);
}
2025-10-30 08:52:48 +08:00
// 页面加载时初始化
2025-11-13 15:28:08 +08:00
window.addEventListener('DOMContentLoaded', async () => {
2025-11-17 15:07:14 +08:00
const user = await fetchCurrentUser(true);
2025-11-18 19:57:55 +08:00
// 初始化标签页
setupTabsByRole(user ? user.role : 'user');
2025-11-17 15:07:14 +08:00
// 检查URL参数中的tab
const urlParams = new URLSearchParams(window.location.search);
const tabParam = urlParams.get('tab');
if (tabParam & & document.getElementById(`${tabParam}-tab`)) {
2025-11-18 19:57:55 +08:00
switchTab(`${tabParam}-tab`);
2025-11-17 15:07:14 +08:00
}
// 初始化拖拽事件
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();
}
2025-10-30 08:52:48 +08:00
});
< / script >
< / body >
< / html >