feat: 优化许可导入区划合并与来源展示

This commit is contained in:
Codex Agent 2025-11-04 13:38:21 +08:00
parent a5bc3c41b7
commit 5b86bd8799
2 changed files with 2601 additions and 31 deletions

File diff suppressed because it is too large Load Diff

View File

@ -761,6 +761,12 @@
animation: modalFadeIn 0.3s;
}
.import-modal-content {
width: 760px;
max-height: 90vh;
overflow-y: auto;
}
@keyframes modalFadeIn {
from {
opacity: 0;
@ -772,6 +778,167 @@
}
}
.import-section {
margin-bottom: 18px;
}
.import-section h3 {
font-size: 16px;
margin-bottom: 8px;
color: #333;
display: flex;
align-items: center;
gap: 6px;
}
.import-upload-area {
border: 1px dashed #9fa8da;
padding: 16px;
border-radius: 8px;
background: #f5f7ff;
display: flex;
flex-direction: column;
gap: 8px;
}
.import-upload-area input[type="file"] {
background: #fff;
padding: 10px;
border-radius: 6px;
border: 1px solid #c5cae9;
}
.import-meta {
font-size: 13px;
color: #555;
line-height: 1.5;
}
.import-sheet-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.import-sheet-card {
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 12px 14px;
background: #fafafa;
}
.import-sheet-card.selected {
border-color: #667eea;
background: #eef2ff;
}
.import-sheet-header {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
margin-bottom: 10px;
}
.import-sheet-title {
display: flex;
align-items: center;
gap: 8px;
font-weight: 600;
color: #3949ab;
}
.import-sheet-meta {
font-size: 12px;
color: #666;
}
.import-duplicate-panel {
background: #fff;
border-radius: 6px;
border: 1px solid #e0e0e0;
padding: 10px;
display: flex;
flex-direction: column;
gap: 6px;
}
.import-duplicate-title {
font-size: 12px;
color: #d84315;
font-weight: 600;
}
.import-checkbox {
display: flex;
align-items: center;
gap: 6px;
font-size: 13px;
color: #333;
}
.import-checkbox input[type="checkbox"] {
transform: scale(1.05);
}
.import-messages {
font-size: 13px;
line-height: 1.6;
}
.import-success {
color: #256029;
background: #e3f2e1;
padding: 8px 10px;
border-radius: 6px;
margin-bottom: 10px;
}
.import-error {
color: #c62828;
background: #ffebee;
padding: 8px 10px;
border-radius: 6px;
margin-bottom: 10px;
}
.import-form-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px;
margin-top: 10px;
}
.import-form-grid textarea {
grid-column: span 2;
resize: vertical;
min-height: 68px;
}
.import-form-grid input,
.import-form-grid textarea {
width: 100%;
padding: 8px 10px;
border: 1px solid #c5cae9;
border-radius: 6px;
font-size: 13px;
font-family: inherit;
}
.import-actions {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 16px;
gap: 16px;
}
.import-hint {
font-size: 12px;
color: #666;
line-height: 1.4;
}
.modal-header {
text-align: center;
margin-bottom: 20px;
@ -1120,6 +1287,9 @@
<!-- 检查点管理按钮 -->
<div class="checkpoint-toolbar">
<button class="btn btn-primary" onclick="openImportModal()">
<span>📥</span> 许可导入
</button>
<button class="btn btn-warning" onclick="openCheckpointModal()">
<span>🔒</span> 检查点管理
</button>
@ -1184,6 +1354,19 @@
</div>
</div>
<!-- 许可导入模态窗口 -->
<div class="modal" id="importModal">
<div class="modal-content import-modal-content">
<div class="modal-header" style="display:flex; justify-content: space-between; align-items: center;">
<h3 style="color:#3949ab; margin:0; display:flex; align-items:center; gap:8px;">
<span>📥</span> 许可导入Excel
</h3>
<button class="btn btn-warning btn-sm" onclick="closeImportModal()">关闭</button>
</div>
<div class="modal-body" id="importModalBody"></div>
</div>
</div>
<script>
// 导航状态管理
let currentStep = 1; // 1=区域, 2=主题, 3=许可, 4=详情
@ -1217,6 +1400,21 @@
let checkpointListError = '';
let expandedSnapshotGroups = new Set();
const permitImportState = {
uploading: false,
sessionId: '',
filename: '',
totalRows: 0,
sheetSummaries: [],
selectedSheets: new Set(),
overrides: new Map(),
error: '',
success: '',
commitLoading: false,
editedBy: '',
changeSummary: ''
};
// 步骤配置
const steps = {
1: { title: '选择区域', loadData: loadRegions },
@ -1572,6 +1770,7 @@
<h3>许可信息</h3>
<div class="detail-content">
<p><strong>许可名称:</strong>${permit.name}</p>
${permit.permit_source && permit.permit_source.source_name ? `<p style="margin-top: 10px;"><strong>数据来源:</strong>${escapeHtml(permit.permit_source.source_name)}</p>` : ''}
${permit.permit_status ? `<p style="margin-top: 10px;"><strong>许可状态:</strong><span class="permit-status ${permit.permit_status === 'active' ? 'status-active' : 'status-inactive'}">${permit.permit_status}</span></p>` : ''}
${permit.subitem_summary ? `<p style="margin-top: 10px;"><strong>子项说明:</strong>${permit.subitem_summary}</p>` : ''}
${permit.responsible_contact ? `<p style="margin-top: 10px;"><strong>负责部门:</strong>${permit.responsible_contact}</p>` : ''}
@ -1743,6 +1942,294 @@
}
}
// ================ 许可导入功能 ================
function openImportModal() {
const modal = document.getElementById('importModal');
if (!modal) return;
modal.classList.add('show');
if (!permitImportState.sessionId) {
permitImportState.selectedSheets = new Set();
permitImportState.overrides = new Map();
permitImportState.error = '';
permitImportState.success = '';
}
renderImportModal();
}
function closeImportModal() {
const modal = document.getElementById('importModal');
if (!modal) return;
modal.classList.remove('show');
}
function toggleSheetSelection(sheetName, forceChecked) {
if (!sheetName) return;
const shouldSelect = typeof forceChecked === 'boolean'
? forceChecked
: !permitImportState.selectedSheets.has(sheetName);
if (shouldSelect) {
permitImportState.selectedSheets.add(sheetName);
} else {
permitImportState.selectedSheets.delete(sheetName);
permitImportState.overrides.delete(sheetName);
}
renderImportModal();
}
function toggleSheetSelectionFromEvent(el) {
if (!el || !el.dataset) return;
toggleSheetSelection(el.dataset.sheet || '', el.checked);
}
function toggleOverridePermit(sheetName, permitName, forceChecked) {
if (!sheetName || !permitName) return;
if (!permitImportState.overrides.has(sheetName)) {
permitImportState.overrides.set(sheetName, new Set());
}
const set = permitImportState.overrides.get(sheetName);
const shouldCheck = typeof forceChecked === 'boolean'
? forceChecked
: !set.has(permitName);
if (shouldCheck) {
set.add(permitName);
} else {
set.delete(permitName);
}
if (set.size === 0) {
permitImportState.overrides.delete(sheetName);
}
renderImportModal();
}
function toggleOverridePermitFromEvent(el) {
if (!el || !el.dataset) return;
toggleOverridePermit(el.dataset.sheet || '', el.dataset.permit || '', el.checked);
}
function updateImportEditedBy(value) {
permitImportState.editedBy = value;
}
function updateImportChangeSummary(value) {
permitImportState.changeSummary = value;
}
async function handleImportFile(input) {
if (!input || !input.files || !input.files.length) {
return;
}
const file = input.files[0];
const formData = new FormData();
formData.append('file', file);
permitImportState.uploading = true;
permitImportState.error = '';
permitImportState.success = '';
renderImportModal();
try {
const response = await fetch('/fs-ai-asistant/api/workflow/lawrisk/admin/permit-import/upload', {
method: 'POST',
body: formData
});
const data = await parseJsonResponse(response);
if (data.success) {
const payload = data.data || {};
permitImportState.sessionId = payload.session_id || payload.sessionId || '';
permitImportState.filename = payload.filename || (file && file.name) || '';
permitImportState.totalRows = payload.total_rows || payload.totalRows || 0;
permitImportState.sheetSummaries = payload.sheet_summaries || payload.sheetSummaries || [];
permitImportState.selectedSheets = new Set(
(permitImportState.sheetSummaries || []).map(item => item.sheet_name)
);
permitImportState.overrides = new Map();
permitImportState.success = `解析完成:${permitImportState.sheetSummaries.length} 个 Sheet${permitImportState.totalRows} 条风险记录`;
} else {
permitImportState.error = data.message || '解析失败请检查Excel格式';
permitImportState.sessionId = '';
permitImportState.sheetSummaries = [];
permitImportState.selectedSheets = new Set();
permitImportState.overrides = new Map();
}
} catch (error) {
permitImportState.error = error.message || '文件上传失败';
permitImportState.sessionId = '';
permitImportState.sheetSummaries = [];
permitImportState.selectedSheets = new Set();
permitImportState.overrides = new Map();
} finally {
permitImportState.uploading = false;
renderImportModal();
}
}
async function submitImport() {
if (!permitImportState.sessionId) {
permitImportState.error = '请先上传并解析Excel文件';
renderImportModal();
return;
}
if (permitImportState.selectedSheets.size === 0) {
permitImportState.error = '请选择至少一个Sheet进行导入';
renderImportModal();
return;
}
const payload = {
session_id: permitImportState.sessionId,
sheet_names: Array.from(permitImportState.selectedSheets),
overrides: {},
};
permitImportState.overrides.forEach((set, sheet) => {
if (set.size) {
payload.overrides[sheet] = Array.from(set);
}
});
if (permitImportState.editedBy && permitImportState.editedBy.trim()) {
payload.edited_by = permitImportState.editedBy.trim();
}
if (permitImportState.changeSummary && permitImportState.changeSummary.trim()) {
payload.change_summary = permitImportState.changeSummary.trim();
}
permitImportState.commitLoading = true;
permitImportState.error = '';
permitImportState.success = '';
renderImportModal();
try {
const response = await fetch('/fs-ai-asistant/api/workflow/lawrisk/admin/permit-import/commit', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const data = await parseJsonResponse(response);
if (data.success) {
const result = data.data || {};
const created = (result.created_permits || []).length;
const overwritten = (result.overwritten_permits || []).length;
permitImportState.success = `导入成功:新增 ${created} 个许可,覆盖 ${overwritten} 个许可。`;
permitImportState.sessionId = '';
permitImportState.sheetSummaries = [];
permitImportState.selectedSheets = new Set();
permitImportState.overrides = new Map();
await Promise.all([
refreshPermitRiskSnapshots(false),
refreshCheckpointList(false)
]);
// 如果当前在首页,刷新地区列表,确保新地区可见
if (currentStep === 1) {
await loadRegions();
}
} else {
permitImportState.error = data.message || '导入失败';
}
} catch (error) {
permitImportState.error = error.message || '导入失败';
} finally {
permitImportState.commitLoading = false;
renderImportModal();
}
}
function renderImportModal() {
const container = document.getElementById('importModalBody');
if (!container) return;
const state = permitImportState;
let html = '<div class="import-section">';
html += '<h3><span>📄</span> 上传 Excel</h3>';
html += '<div class="import-upload-area">';
html += '<input type="file" accept=".xlsx,.xlsm" onchange="handleImportFile(this)">';
if (state.sessionId) {
const sheetCount = state.sheetSummaries ? state.sheetSummaries.length : 0;
html += `<div class="import-meta">当前会话:${escapeHtml(state.filename || '(未命名)')} Sheet ${sheetCount} 个 风险 ${state.totalRows || 0} 条</div>`;
} else {
html += '<div class="import-meta">请选择包含许可数据的 Excel 文件,系统会自动解析所有 Sheet并以区划为单位生成导入任务。</div>';
}
html += '</div></div>';
if (state.error) {
html += `<div class="import-error">${escapeHtml(state.error)}</div>`;
}
if (state.success) {
html += `<div class="import-success">${escapeHtml(state.success)}</div>`;
}
if (state.uploading) {
html += '<div class="loading" style="margin: 8px 0;">正在解析 Excel...</div>';
}
if (state.sessionId && state.sheetSummaries && state.sheetSummaries.length) {
html += '<div class="import-section">';
html += '<h3><span>🗂️</span> 选择导入的 Sheet</h3>';
html += '<div class="import-sheet-list">';
state.sheetSummaries.forEach(summary => {
const sheetName = summary.sheet_name || '';
const selected = state.selectedSheets.has(sheetName);
const duplicates = summary.duplicate_permits || summary.duplicatePermits || [];
const newPermits = summary.new_permits || summary.newPermits || [];
const overrideSet = state.overrides.get(sheetName) || new Set();
const missingRegion = summary.missing_region || summary.missingRegion;
const originalSheets = summary.original_sheet_names || summary.originalSheetNames || [];
const originalLabel = originalSheets.length
? originalSheets.map(name => escapeHtml(name)).join('、')
: escapeHtml(sheetName);
html += `<div class="import-sheet-card ${selected ? 'selected' : ''}">`;
html += '<div class="import-sheet-header">';
html += `<label class="import-checkbox"><input type="checkbox" data-sheet="${escapeHtml(sheetName)}" ${selected ? 'checked' : ''} onchange="toggleSheetSelectionFromEvent(this)"> <span class="import-sheet-title">${escapeHtml(sheetName)}${missingRegion ? '<span style="margin-left:6px;color:#c62828;font-size:12px;">(新地区)</span>' : ''}</span></label>`;
html += `<div class="import-sheet-meta">许可 ${summary.permit_count || 0} 风险 ${summary.risk_count || summary.row_count || 0}</div>`;
html += '</div>';
html += `<div class="import-sheet-submeta">来源 Sheet${originalLabel}</div>`;
if (newPermits.length) {
html += `<div class="import-meta" style="margin-bottom:8px; color:#2e7d32;">新增许可 ${newPermits.length} 项</div>`;
}
if (duplicates.length) {
html += '<div class="import-duplicate-panel">';
html += '<div class="import-duplicate-title">检测到已存在的许可,勾选代表同意覆盖:</div>';
duplicates.forEach(name => {
const checked = overrideSet.has(name);
const disabledAttr = selected ? '' : 'disabled';
html += `<label class="import-checkbox"><input type="checkbox" data-sheet="${escapeHtml(sheetName)}" data-permit="${escapeHtml(name)}" ${checked ? 'checked' : ''} ${disabledAttr} onchange="toggleOverridePermitFromEvent(this)"> <span>${escapeHtml(name)}</span></label>`;
});
html += '</div>';
}
html += '</div>';
});
html += '</div></div>';
}
html += '<div class="import-section">';
html += '<h3><span>📝</span> 导入说明</h3>';
html += '<div class="import-form-grid">';
html += `<input type="text" placeholder="编辑人(可选)" value="${escapeHtml(state.editedBy)}" oninput="updateImportEditedBy(this.value)">`;
html += `<textarea placeholder="变更摘要 / 导入说明(可选)" oninput="updateImportChangeSummary(this.value)">${escapeHtml(state.changeSummary)}</textarea>`;
html += '</div>';
html += '</div>';
const commitDisabled = !state.sessionId || state.selectedSheets.size === 0 || state.commitLoading;
const commitLabel = state.commitLoading ? '导入中...' : '开始导入';
html += '<div class="import-actions">';
html += '<div class="import-hint">导入前会自动创建风险快照,可在“检查点管理”中查看恢复。</div>';
html += `<button class="btn btn-warning" onclick="submitImport()" ${commitDisabled ? 'disabled' : ''}>${commitLabel}</button>`;
html += '</div>';
container.innerHTML = html;
}
// ================ 检查点管理功能 ================
// 打开检查点模态窗口
@ -1794,6 +2281,12 @@
}
});
document.getElementById('importModal').addEventListener('click', function(e) {
if (e.target === this) {
closeImportModal();
}
});
// 加载检查点列表(保持兼容性)
async function loadCheckpoints() {
await openCheckpointModal();
@ -1903,6 +2396,8 @@
const regionName = escapeHtml(group.region_name || primary.region_name || '-');
const riskFull = primary.risk_content || '';
const riskPreview = escapeHtml(truncateText(riskFull, 160));
const sourceNameRaw = primary.permit_source_name || group.permit_source_name || '';
const sourceTag = sourceNameRaw ? `<span>来源:${escapeHtml(sourceNameRaw)}</span>` : '';
const legalSegments = [];
if (primary.legal_basis) {
legalSegments.push(`📕 ${escapeHtml(primary.legal_basis)}`);
@ -1938,6 +2433,9 @@
if (detail.edited_by) {
detailMetaParts.push(`编辑人:${escapeHtml(detail.edited_by)}`);
}
if (detail.permit_source_name) {
detailMetaParts.push(`来源:${escapeHtml(detail.permit_source_name)}`);
}
detailMetaParts.push(...detailLegal);
const detailMetaHtml = detailMetaParts.length
? `<div class="snapshot-detail-meta">${detailMetaParts.join('<span>|</span>')}</div>`
@ -1971,6 +2469,7 @@
${statusTag}
<span>风险条目:${riskCount} 个</span>
<span>编辑人:${editorsText}</span>
${sourceTag}
</div>
`;