feat: 优化许可导入区划合并与来源展示
This commit is contained in:
parent
a5bc3c41b7
commit
5b86bd8799
File diff suppressed because it is too large
Load Diff
|
|
@ -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>
|
||||
`;
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue