diff --git a/static/db_admin.html b/static/db_admin.html
index a668576..091fe47 100644
--- a/static/db_admin.html
+++ b/static/db_admin.html
@@ -421,6 +421,48 @@
margin-top: 6px;
}
+ .snapshot-detail-list {
+ margin-top: 12px;
+ display: none;
+ border-left: 2px solid #e0e0e0;
+ padding-left: 16px;
+ }
+
+ .snapshot-detail-list.expanded {
+ display: block;
+ }
+
+ .snapshot-detail-item {
+ background: #ffffff;
+ border: 1px solid #e5e7eb;
+ border-radius: 6px;
+ padding: 10px 12px;
+ margin-bottom: 10px;
+ box-shadow: 0 1px 2px rgba(0,0,0,0.05);
+ }
+
+ .snapshot-detail-item:last-child {
+ margin-bottom: 0;
+ }
+
+ .snapshot-detail-header {
+ font-size: 13px;
+ font-weight: 600;
+ color: #333;
+ margin-bottom: 6px;
+ display: flex;
+ gap: 8px;
+ flex-wrap: wrap;
+ }
+
+ .snapshot-detail-meta {
+ font-size: 12px;
+ color: #666;
+ display: flex;
+ gap: 10px;
+ flex-wrap: wrap;
+ }
+
.timeline-footer {
margin-top: 14px;
display: flex;
@@ -917,6 +959,33 @@
border-radius: 2px;
}
+ .detail-toolbar {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 12px 16px;
+ margin-bottom: 18px;
+ border-radius: 8px;
+ background: #eef2ff;
+ border: 1px solid #d7dbff;
+ }
+
+ .detail-meta {
+ color: #3f51b5;
+ font-size: 14px;
+ }
+
+ .detail-meta strong {
+ font-size: 16px;
+ margin: 0 4px;
+ }
+
+ .detail-actions {
+ display: flex;
+ gap: 12px;
+ align-items: center;
+ }
+
.detail-content {
background: #f8f9fa;
padding: 15px;
@@ -1121,8 +1190,10 @@
let historyStack = []; // 历史记录栈
let currentRegion = null;
let currentTheme = null;
- let currentPermit = null;
- let pendingDangerOperation = null; // 待执行的危险操作
+ let currentPermit = null;
+ let currentPermitDetails = null;
+ let pendingDangerOperation = null; // 待执行的危险操作
+ let isDeletingPermit = false;
const permitRiskSnapshotState = {
limit: 10,
@@ -1144,6 +1215,7 @@
let checkpointListCache = [];
let checkpointListLoading = false;
let checkpointListError = '';
+ let expandedSnapshotGroups = new Set();
// 步骤配置
const steps = {
@@ -1268,6 +1340,7 @@
currentRegion = { id: regionId, name: regionName };
currentTheme = null;
currentPermit = null;
+ currentPermitDetails = null;
// 更新步骤
goToStep(2);
@@ -1280,6 +1353,7 @@
currentTheme = { id: themeId, name: themeName };
currentPermit = null;
+ currentPermitDetails = null;
// 更新步骤
goToStep(3);
@@ -1291,6 +1365,7 @@
historyStack.push({ step: currentStep, permit: currentPermit });
currentPermit = { id: permitId, name: permitName, themeId: themeId };
+ currentPermitDetails = null;
// 更新步骤
goToStep(4);
@@ -1415,10 +1490,10 @@
// 清理后续状态
if (targetStep <= 2) {
currentTheme = null;
- currentPermit = null;
}
if (targetStep <= 3) {
currentPermit = null;
+ currentPermitDetails = null;
}
goToStep(targetStep);
@@ -1429,6 +1504,7 @@
currentRegion = null;
currentTheme = null;
currentPermit = null;
+ currentPermitDetails = null;
historyStack = [];
goToStep(1);
}
@@ -1470,7 +1546,25 @@
// 渲染许可详情
function renderPermitDetails(permit) {
const detailsArea = document.querySelector('.details-area');
+ currentPermitDetails = permit;
+ const riskCount = Array.isArray(permit.risks) ? permit.risks.length : 0;
+ if (currentPermit) {
+ currentPermit = { ...currentPermit, riskCount };
+ }
+ const deleteButtonLabel = isDeletingPermit ? '删除中...' : '删除许可';
+ const deleteButtonDisabled = isDeletingPermit ? 'disabled' : '';
+
let html = '
';
+ html += `
+
+ `;
// 许可基本信息
html += `
@@ -1525,6 +1619,118 @@
detailsArea.innerHTML = html;
}
+ function confirmDeleteCurrentPermit() {
+ if (isDeletingPermit) {
+ return;
+ }
+ if (!currentRegion || !currentTheme || !currentPermit) {
+ alert('请先选择要删除的许可');
+ return;
+ }
+
+ const riskCount = currentPermit.riskCount !== undefined
+ ? currentPermit.riskCount
+ : (currentPermitDetails && Array.isArray(currentPermitDetails.risks) ? currentPermitDetails.risks.length : 0);
+ const confirmMessage = `确定要删除「${currentRegion.name} › ${currentTheme.name} › ${currentPermit.name}」吗?\n\n` +
+ `此操作会删除 ${riskCount} 条风险关联(如存在)。系统会备份当前风险快照,但删除后需通过快照管理页面手动恢复。`;
+
+ if (!confirm(confirmMessage)) {
+ return;
+ }
+
+ const summaryInput = prompt('请输入删除说明(可选,用于快照对比):', '');
+ if (summaryInput === null) {
+ return;
+ }
+ const changeSummary = summaryInput.trim();
+
+ deleteCurrentPermit(changeSummary);
+ }
+
+ async function deleteCurrentPermit(changeSummary) {
+ if (isDeletingPermit) {
+ return;
+ }
+ if (!currentRegion || !currentTheme || !currentPermit) {
+ alert('当前上下文缺失,无法删除');
+ return;
+ }
+
+ isDeletingPermit = true;
+ toggleDeletePermitButton(true);
+
+ try {
+ const payload = {
+ region_id: currentRegion.id,
+ theme_id: currentTheme.id,
+ permit_id: currentPermit.id
+ };
+ if (changeSummary) {
+ payload.change_summary = changeSummary;
+ }
+
+ const response = await fetch('/fs-ai-asistant/api/workflow/lawrisk/admin/permits', {
+ method: 'DELETE',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(payload)
+ });
+ const data = await response.json();
+
+ if (response.ok && data.success) {
+ const snapshotCount = data.data && typeof data.data.snapshot_count === 'number'
+ ? data.data.snapshot_count
+ : 0;
+ const deletedRisks = data.data && data.data.deleted_rows
+ ? (data.data.deleted_rows.region_permit_risks || 0)
+ : 0;
+ const remainingPermits = data.data && typeof data.data.remaining_theme_permits === 'number'
+ ? data.data.remaining_theme_permits
+ : null;
+ const themeDetached = !!(data.data && data.data.theme_detached);
+ const snapshotBatchId = data.data && data.data.snapshot_batch_id;
+
+ let successMessage = `✅ 删除成功!\n\n已备份 ${snapshotCount} 条风险快照,并删除 ${deletedRisks} 条风险关联。`;
+ if (themeDetached) {
+ successMessage += '\n对应主题已与该地区解除关联。';
+ } else if (remainingPermits !== null) {
+ successMessage += `\n该主题在此地区仍剩余 ${remainingPermits} 个许可。`;
+ }
+ if (snapshotBatchId) {
+ successMessage += `\n快照批次:${snapshotBatchId}`;
+ }
+ alert(successMessage);
+
+ const modal = document.getElementById('checkpointModal');
+ if (modal && modal.classList.contains('show')) {
+ if ((!permitRiskSnapshotState.regionFilter || permitRiskSnapshotState.regionFilter === currentRegion.id) &&
+ (!permitRiskSnapshotState.permitFilter || permitRiskSnapshotState.permitFilter === currentPermit.id)) {
+ await refreshPermitRiskSnapshots(false);
+ }
+ }
+
+ currentPermitDetails = null;
+ quickJump(3);
+ } else {
+ const message = data && data.message ? data.message : `删除失败(HTTP ${response.status})`;
+ alert(`❌ 删除失败:${message}`);
+ }
+ } catch (error) {
+ alert(`❌ 删除失败:${error.message}`);
+ } finally {
+ isDeletingPermit = false;
+ toggleDeletePermitButton(false);
+ }
+ }
+
+ function toggleDeletePermitButton(disabled) {
+ const btn = document.getElementById('deletePermitBtn');
+ if (!btn) {
+ return;
+ }
+ btn.disabled = disabled;
+ btn.textContent = disabled ? '删除中...' : '删除许可';
+ }
+
// 更新步骤指示器
function updateStepIndicator(step) {
for (let i = 1; i <= 4; i++) {
@@ -1564,6 +1770,7 @@
checkpointListCache = [];
checkpointListLoading = true;
checkpointListError = '';
+ expandedSnapshotGroups = new Set();
renderCheckpointManager(checkpointListCache);
@@ -1685,23 +1892,88 @@
html += '
';
timelineItems.forEach(entry => {
- if (entry.type === 'snapshot') {
- const item = entry.raw;
- const permitName = escapeHtml(item.permit_name || '-');
- const regionName = escapeHtml(item.region_name || '-');
- const riskFull = item.risk_content || '';
+ if (entry.type === 'snapshot-group') {
+ const group = entry.raw || {};
+ const groupKey = group.groupKey || group.snapshot_batch_id || '';
+ const groupKeyJs = String(groupKey).replace(/'/g, "\\'");
+ const riskItems = group.items || [];
+ const riskCount = riskItems.length;
+ const primary = riskItems[0] || {};
+ const permitName = escapeHtml(group.permit_name || primary.permit_name || '-');
+ const regionName = escapeHtml(group.region_name || primary.region_name || '-');
+ const riskFull = primary.risk_content || '';
const riskPreview = escapeHtml(truncateText(riskFull, 160));
const legalSegments = [];
- if (item.legal_basis) {
- legalSegments.push(`📕 ${escapeHtml(item.legal_basis)}`);
+ if (primary.legal_basis) {
+ legalSegments.push(`📕 ${escapeHtml(primary.legal_basis)}`);
}
- if (item.document_no) {
- legalSegments.push(`📄 ${escapeHtml(item.document_no)}`);
+ if (primary.document_no) {
+ legalSegments.push(`📄 ${escapeHtml(primary.document_no)}`);
}
const legalHtml = legalSegments.length ? `
${legalSegments.join('|')}
` : '';
- const statusHtml = item.permit_status ? `
${escapeHtml(item.permit_status)}` : '';
- const changeSummary = item.change_summary ? `
备注:${escapeHtml(item.change_summary)}
` : '';
- const editorName = escapeHtml(item.edited_by || '—');
+ 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
+ ? `
备注:${escapeHtml(group.change_summaries.join(';'))}
`
+ : '';
+ const statusTag = primary.permit_status ? `
${escapeHtml(primary.permit_status)}` : '';
+ const toggleButton = riskCount > 0
+ ? `
`
+ : '';
+ const restoreButton = `
`;
+
+ 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)}`);
+ }
+ detailMetaParts.push(...detailLegal);
+ const detailMetaHtml = detailMetaParts.length
+ ? `
${detailMetaParts.join('|')}
`
+ : '';
+ const detailStatusTag = detail.permit_status ? `
${escapeHtml(detail.permit_status)}` : '';
+ const detailNote = detail.change_summary ? `
备注:${escapeHtml(detail.change_summary)}
` : '';
+ return `
+
+
+
${escapeHtml(detail.risk_content || '—')}
+ ${detailMetaHtml}
+ ${detailNote}
+
+ `;
+ }).join('');
+
+ const detailListHtml = (isExpanded && riskCount > 0)
+ ? `
+
+ ${detailItemsHtml}
+
+ `
+ : '';
+
+ const metaHtml = `
+
+ ${statusTag}
+ 风险条目:${riskCount} 个
+ 编辑人:${editorsText}
+
+ `;
+
const timeDisplay = escapeHtml(entry.timeText || '');
html += `
@@ -1709,18 +1981,19 @@
📝
${permitName}(${regionName})
${riskPreview || '—'}
${legalHtml}
-
`;
@@ -1777,14 +2050,16 @@
html += `
${warningMessages.join('
')}
`;
}
- const snapshotSummaryText = totalSnapshots
- ? `快照 ${snapshotStart}-${snapshotEnd} / ${totalSnapshots}`
- : '快照 0 / 0';
+ const snapshotGroupCount = new Set((snapshotState.snapshots || []).map(item => item.snapshot_batch_id || item.snapshot_id)).size;
+ const snapshotRangeText = totalSnapshots
+ ? `风险快照 ${snapshotStart}-${snapshotEnd} / ${totalSnapshots}`
+ : '风险快照 0 / 0';
+ const snapshotBatchText = `快照批次 ${snapshotGroupCount} 个`;
const checkpointSummaryText = `检查点 ${checkpointList.length} 个`;
html += `