feat: show permit themes in db admin

This commit is contained in:
Codex Agent 2025-11-13 17:01:37 +08:00
parent 772354bd01
commit fd82b757fe
2 changed files with 276 additions and 134 deletions

View File

@ -45,7 +45,8 @@ COLON_NEWLINE_RE = re.compile(r"\s*\n")
TRAILING_SPACE_RE = re.compile(r"[ \t]+\n")
EXTRA_NEWLINES_RE = re.compile(r"\n{3,}")
TEXT_SPLIT_PATTERN = re.compile(r"[,\uff0c;\uff1b、\n\r]+")
TEXT_SPLIT_PATTERN = re.compile(r"[,\uff0c;\uff1b\n\r]+")
TEXT_SPLIT_PATTERN_WITH_DUNHAO = re.compile(r"[,\uff0c;\uff1b、\n\r]+")
PERMIT_IMPORT_TTL_SECONDS = 1800
MAX_PERMIT_FILE_SIZE_BYTES = 500 * 1024 # 500 KB limit for uploaded Excel files
@ -260,12 +261,19 @@ def _score_import_header(canonical: str, cell_text: str, col_idx: int) -> float:
return score
def _split_multi_value(value: Any) -> List[str]:
"""Split multi-value cells using common Chinese punctuation."""
def _split_multi_value(value: Any, *, allow_dunhao: bool = False) -> List[str]:
"""Split multi-value cells using common punctuation characters.
默认不把中文顿号视作分隔符以避免误拆文化旅游等合法的
许可名称对于确实需要用顿号分隔的字段如主题经营范围等调用
方可以显式传入 allow_dunhao=True
"""
text = _clean_text(value)
if not text:
return []
return [item.strip() for item in TEXT_SPLIT_PATTERN.split(text) if item.strip()]
pattern = TEXT_SPLIT_PATTERN_WITH_DUNHAO if allow_dunhao else TEXT_SPLIT_PATTERN
return [item.strip() for item in pattern.split(text) if item.strip()]
def _clean_empty(value: Any) -> Optional[str]:
@ -311,9 +319,15 @@ def _normalize_import_row(
jurisdiction_scope = _clean_empty(
raw_row.get("jurisdiction_scope") or sheet_defaults.get("jurisdiction_scope")
)
theme_names = _split_multi_value(raw_row.get("theme_names") or sheet_defaults.get("theme_names"))
scope_descriptions = _split_multi_value(raw_row.get("scope_text") or sheet_defaults.get("scope_text"))
subitem_names = _split_multi_value(raw_row.get("subitem_text") or sheet_defaults.get("subitem_text"))
theme_names = _split_multi_value(
raw_row.get("theme_names") or sheet_defaults.get("theme_names"), allow_dunhao=True
)
scope_descriptions = _split_multi_value(
raw_row.get("scope_text") or sheet_defaults.get("scope_text"), allow_dunhao=True
)
subitem_names = _split_multi_value(
raw_row.get("subitem_text") or sheet_defaults.get("subitem_text"), allow_dunhao=True
)
return {
"row_index": int(row_index) if isinstance(row_index, int) else row_index,
@ -1700,6 +1714,68 @@ def list_permits_for_region(region: str) -> List[Dict[str, str]]:
return permits
def list_region_permit_catalog(region_id: str) -> List[Dict[str, Any]]:
"""Return permit entries for a region, including owning theme and risk count."""
sql = """
SELECT
rtp.permit_id,
p.name AS permit_name,
rtp.theme_id,
COALESCE(t.name, '') AS theme_name,
COUNT(rpr.risk_id) AS risk_count
FROM region_theme_permits rtp
JOIN permits p ON p.id = rtp.permit_id
LEFT JOIN themes t ON t.id = rtp.theme_id
LEFT JOIN region_permit_risks rpr
ON rpr.region_id = rtp.region_id
AND rpr.permit_id = rtp.permit_id
WHERE rtp.region_id = %s
GROUP BY rtp.permit_id, p.name, rtp.theme_id, t.name
ORDER BY LOWER(p.name), LOWER(COALESCE(t.name, ''))
"""
catalog: List[Dict[str, Any]] = []
with _lic_pg_conn() as conn:
cur = conn.cursor()
cur.execute(sql, (region_id,))
for permit_id, permit_name, theme_id, theme_name, risk_count in cur.fetchall():
catalog.append(
{
"id": str(permit_id),
"name": str(permit_name),
"theme": {
"id": str(theme_id) if theme_id else "",
"name": str(theme_name) if theme_name else "",
},
"risk_count": int(risk_count or 0),
}
)
return catalog
def resolve_region_permit_theme(region_id: str, permit_id: str) -> Optional[Dict[str, str]]:
"""Return the first theme associated with a region-permit pair (if any)."""
sql = """
SELECT rtp.theme_id, t.name
FROM region_theme_permits rtp
LEFT JOIN themes t ON t.id = rtp.theme_id
WHERE rtp.region_id = %s
AND rtp.permit_id = %s
ORDER BY t.name NULLS LAST
LIMIT 1
"""
with _lic_pg_conn() as conn:
cur = conn.cursor()
cur.execute(sql, (region_id, permit_id))
row = cur.fetchone()
if not row:
return None
theme_id, theme_name = row
return {
"id": str(theme_id) if theme_id else "",
"name": str(theme_name) if theme_name else "",
}
def _load_permit_scopes_for_region(
conn: pg.Connection, region_id: str, permit_ids: List[str]
) -> Dict[str, List[Dict[str, str]]]:
@ -1766,9 +1842,9 @@ def _load_permit_sources_for_region(
def load_permits_and_risks(
region_id: str, theme_id: str, permit_id: Optional[str] = None
region_id: str, theme_id: Optional[str] = None, permit_id: Optional[str] = None
) -> List[Dict[str, object]]:
"""Return permits with attached risk entries for a region-theme pair."""
"""Return permits with attached risk entries for a region (optionally filtered by theme)."""
# Ensure optional permit file tables exist before running user queries.
try:
_ensure_permit_file_schema()
@ -1776,6 +1852,8 @@ def load_permits_and_risks(
logger.warning("[PERMIT-FILES] Failed to ensure permit file schema before loading permits: %s", exc)
sql = """
SELECT
rtp.theme_id,
t.name AS theme_name,
p.id AS permit_id,
p.name AS permit_name,
rk.id AS risk_id,
@ -1789,6 +1867,7 @@ def load_permits_and_risks(
rpd.jurisdiction_scope
FROM region_theme_permits rtp
JOIN permits p ON p.id = rtp.permit_id
LEFT JOIN themes t ON t.id = rtp.theme_id
LEFT JOIN region_permit_risks rpr
ON rpr.region_id = rtp.region_id
AND rpr.permit_id = rtp.permit_id
@ -1796,9 +1875,12 @@ def load_permits_and_risks(
LEFT JOIN region_permit_details rpd
ON rpd.region_id = rtp.region_id
AND rpd.permit_id = rtp.permit_id
WHERE rtp.region_id = %s AND rtp.theme_id = %s
WHERE rtp.region_id = %s
"""
params: List[Any] = [region_id, theme_id]
params: List[Any] = [region_id]
if theme_id:
sql += " AND rtp.theme_id = %s"
params.append(theme_id)
if permit_id is not None:
sql += " AND rtp.permit_id = %s"
params.append(permit_id)
@ -1812,6 +1894,8 @@ def load_permits_and_risks(
cur.execute(sql, tuple(params))
for row in cur.fetchall():
(
row_theme_id,
row_theme_name,
permit_id,
permit_name,
risk_id,
@ -1825,6 +1909,8 @@ def load_permits_and_risks(
jurisdiction_scope,
) = row
pid = str(permit_id)
theme_id_value = str(row_theme_id) if row_theme_id else ""
theme_name_value = str(row_theme_name) if row_theme_name else ""
entry = permits.setdefault(
pid,
{
@ -1836,8 +1922,37 @@ def load_permits_and_risks(
"subitem_summary": None,
"responsible_contact": None,
"jurisdiction_scope": None,
"theme": {
"id": theme_id_value,
"name": theme_name_value,
},
"themes": [],
},
)
if theme_id_value and not entry["theme"].get("id"):
entry["theme"]["id"] = theme_id_value
if theme_name_value and not entry["theme"].get("name"):
entry["theme"]["name"] = theme_name_value
if theme_id_value or theme_name_value:
theme_list = entry.get("themes") or []
duplicate = False
for theme_entry in theme_list:
if theme_id_value:
if theme_entry.get("id") == theme_id_value:
duplicate = True
break
else:
if not theme_entry.get("id") and theme_entry.get("name") == theme_name_value:
duplicate = True
break
if not duplicate:
theme_list.append(
{
"id": theme_id_value,
"name": theme_name_value,
}
)
entry["themes"] = theme_list
if entry["permit_status"] is None and permit_status:
entry["permit_status"] = permit_status.strip() or None
if entry["subitem_summary"] is None and subitem_summary:
@ -1895,6 +2010,8 @@ def load_permits_and_risks(
"created_at": None,
"uploaded_by": "",
}
if "themes" not in permits[pid] or permits[pid]["themes"] is None:
permits[pid]["themes"] = []
return list(permits.values())

View File

@ -366,6 +366,23 @@
font-weight: 500;
}
.item-tag {
display: inline-flex;
align-items: center;
margin-left: 8px;
padding: 2px 8px;
border-radius: 999px;
background: rgba(102, 126, 234, 0.15);
font-size: 11px;
color: #4f46e5;
}
.theme-tags {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.item-count {
font-size: 12px;
background: rgba(102, 126, 234, 0.1);
@ -1508,21 +1525,16 @@
<div class="step-indicator">
<div class="step active" id="step1">
<div class="step-number">1</div>
<div class="step-label">选择</div>
<div class="step-label">选择区</div>
</div>
<div class="arrow"></div>
<div class="step" id="step2">
<div class="step-number">2</div>
<div class="step-label">选择主题</div>
<div class="step-label">选择事项</div>
</div>
<div class="arrow"></div>
<div class="step" id="step3">
<div class="step-number">3</div>
<div class="step-label">选择许可</div>
</div>
<div class="arrow"></div>
<div class="step" id="step4">
<div class="step-number">4</div>
<div class="step-label">查看详情</div>
</div>
</div>
@ -1540,7 +1552,7 @@
<div class="content-area">
<div class="panel">
<h2 id="navTitle">
<span>选择区</span>
<span>选择区</span>
<div class="nav-controls">
<button class="back-button" id="backButton" onclick="goBack()" disabled>← 上一步</button>
</div>
@ -1558,7 +1570,7 @@
<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>
<p id="emptyMessage">请选择区开始导航</p>
</div>
</div>
</div>
@ -1701,10 +1713,9 @@
}
// 导航状态管理
let currentStep = 1; // 1=区域, 2=主题, 3=许可, 4=详情
let currentStep = 1; // 1=区划, 2=事项, 3=详情
let historyStack = []; // 历史记录栈
let currentRegion = null;
let currentTheme = null;
let currentPermit = null;
let currentPermitDetails = null;
let pendingDangerOperation = null; // 待执行的危险操作
@ -1791,16 +1802,15 @@
// 步骤配置
const steps = {
1: { title: '选择区域', loadData: loadRegions },
2: { title: '选择主题', loadData: (region) => loadThemes(region.id, region.name) },
3: { title: '选择许可', loadData: (theme) => loadPermits(theme.id, theme.name) },
4: { title: '许可详情', loadData: null }
1: { title: '选择区划' },
2: { title: '选择事项' },
3: { title: '事项详情' }
};
// 加载区列表
// 加载区列表
async function loadRegions() {
const navList = document.getElementById('navList');
navList.innerHTML = '<div class="loading"></div>加载区列表...';
navList.innerHTML = '<div class="loading"></div>加载区列表...';
try {
const response = await fetch('/fs-ai-asistant/api/workflow/lawrisk/admin/regions');
@ -1810,7 +1820,7 @@
navList.innerHTML = '';
if (data.data.regions.length === 0) {
navList.innerHTML = '<div class="error">未找到区数据</div>';
navList.innerHTML = '<div class="error">未找到区数据</div>';
return;
}
@ -1824,72 +1834,54 @@
navList.appendChild(li);
});
} else {
navList.innerHTML = `<div class="error">加载区失败:${data.message}</div>`;
navList.innerHTML = `<div class="error">加载区失败:${data.message}</div>`;
}
} catch (error) {
navList.innerHTML = `<div class="error">网络错误:${error.message}</div>`;
}
}
// 加载主题列表
async function loadThemes(regionId, regionName) {
const navList = document.getElementById('navList');
navList.innerHTML = '<div class="loading"></div>加载主题列表...';
try {
const response = await fetch(`/fs-ai-asistant/api/workflow/lawrisk/admin/themes?region=${regionId}`);
const data = await response.json();
if (data.success) {
const themes = data.data.themes;
if (themes.length === 0) {
navList.innerHTML = `<div class="error">地区 "${regionName}" 下没有可用的主题</div>`;
// 加载许可列表(直接按区划聚合事项)
async function loadPermitsForRegion() {
if (!currentRegion) {
return;
}
let html = '<div class="item-list">';
themes.forEach(theme => {
html += `
<li onclick="selectTheme('${theme.id}', '${theme.name.replace(/'/g, "\\'")}')">
<span class="item-name">${theme.name}</span>
<span class="item-count">点击选择</span>
</li>
`;
});
html += '</div>';
navList.innerHTML = html;
} else {
navList.innerHTML = `<div class="error">加载主题失败:${data.message}</div>`;
}
} catch (error) {
navList.innerHTML = `<div class="error">网络错误:${error.message}</div>`;
}
}
// 加载许可列表
async function loadPermits(themeId, themeName) {
const navList = document.getElementById('navList');
navList.innerHTML = '<div class="loading"></div>加载许可列表...';
navList.innerHTML = '<div class="loading"></div>加载事项列表...';
try {
const response = await fetch(`/fs-ai-asistant/api/workflow/lawrisk/admin/permits?region=${currentRegion.id}&theme=${themeId}`);
const response = await fetch(`/fs-ai-asistant/api/workflow/lawrisk/admin/permits?region=${currentRegion.id}`);
const data = await response.json();
if (data.success) {
const permits = data.data.permits;
if (permits.length === 0) {
navList.innerHTML = `<div class="error">主题 "${themeName}" 下没有可用的许可</div>`;
navList.innerHTML = `<div class="error">区划 "${currentRegion.name}" 暂无可维护的事项</div>`;
return;
}
let html = '<div class="item-list">';
permits.forEach(permit => {
const riskCount = permit.risks ? permit.risks.length : 0;
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 themeBadge = themeName
? `<span class="item-tag">${themeName}</span>`
: '';
const escapedName = permit.name ? permit.name.replace(/'/g, "\\'") : '';
const escapedTheme = themeName ? themeName.replace(/'/g, "\\'") : '';
const escapedThemeId = themeId ? themeId.replace(/'/g, "\\'") : '';
html += `
<li onclick="selectPermit('${permit.id}', '${permit.name.replace(/'/g, "\\'")}', '${themeId}')">
<span class="item-name">${permit.name}</span>
<li onclick="selectPermit('${permit.id}', '${escapedName}', '${escapedThemeId}', '${escapedTheme}', ${riskCount})">
<span class="item-name">${permit.name}${themeBadge}</span>
<span class="item-count">${riskCount} 个风险</span>
</li>
`;
@ -1897,7 +1889,7 @@
html += '</div>';
navList.innerHTML = html;
} else {
navList.innerHTML = `<div class="error">加载许可失败:${data.message}</div>`;
navList.innerHTML = `<div class="error">加载事项失败:${data.message}</div>`;
}
} catch (error) {
navList.innerHTML = `<div class="error">网络错误:${error.message}</div>`;
@ -1910,7 +1902,6 @@
historyStack.push({ step: currentStep, region: currentRegion });
currentRegion = { id: regionId, name: regionName };
currentTheme = null;
currentPermit = null;
currentPermitDetails = null;
@ -1918,31 +1909,24 @@
goToStep(2);
}
// 选择主题
async function selectTheme(themeId, themeName) {
// 选择许可
async function selectPermit(permitId, permitName, themeId, themeName, riskCount = 0) {
// 保存到历史栈
historyStack.push({ step: currentStep, theme: currentTheme });
historyStack.push({ step: currentStep, permit: currentPermit });
currentTheme = { id: themeId, name: themeName };
currentPermit = null;
currentPermit = {
id: permitId,
name: permitName,
themeId: themeId || '',
themeName: themeName || '',
riskCount: typeof riskCount === 'number' ? riskCount : 0
};
currentPermitDetails = null;
// 更新步骤
goToStep(3);
}
// 选择许可
async function selectPermit(permitId, permitName, themeId) {
// 保存到历史栈
historyStack.push({ step: currentStep, permit: currentPermit });
currentPermit = { id: permitId, name: permitName, themeId: themeId };
currentPermitDetails = null;
// 更新步骤
goToStep(4);
}
// 跳转到指定步骤
async function goToStep(step) {
currentStep = step;
@ -1977,10 +1961,8 @@
if (step === 1) {
await loadRegions();
} else if (step === 2) {
await loadThemes(currentRegion.id, currentRegion.name);
await loadPermitsForRegion();
} else if (step === 3) {
await loadPermits(currentTheme.id, currentTheme.name);
} else if (step === 4) {
await showPermitDetails();
}
}
@ -1988,16 +1970,12 @@
// 更新面包屑导航
function updateBreadcrumb() {
const breadcrumb = document.getElementById('breadcrumb');
let html = '';
// 总是显示"首页"
html += `
let html = `
<span class="breadcrumb-item">
<a onclick="goHome()">首页</a>
</span>
`;
// 显示当前选择的路径
if (currentRegion) {
html += '<span class="breadcrumb-separator"></span>';
if (currentStep > 2) {
@ -2015,35 +1993,21 @@
}
}
if (currentTheme) {
html += '<span class="breadcrumb-separator"></span>';
if (currentStep > 3) {
html += `
<span class="breadcrumb-item">
<a onclick="quickJump(3)">${currentTheme.name}</a>
</span>
`;
} else {
html += `
<span class="breadcrumb-item">
<span class="breadcrumb-current">${currentTheme.name}</span>
</span>
`;
}
}
if (currentPermit) {
const permitLabel = currentPermit.themeName
? `${currentPermit.themeName} · ${currentPermit.name}`
: currentPermit.name;
html += '<span class="breadcrumb-separator"></span>';
if (currentStep > 4) {
if (currentStep >= 3) {
html += `
<span class="breadcrumb-item">
<a onclick="quickJump(4)">${currentPermit.name}</a>
<span class="breadcrumb-current">${permitLabel}</span>
</span>
`;
} else {
html += `
<span class="breadcrumb-item">
<span class="breadcrumb-current">${currentPermit.name}</span>
<a onclick="quickJump(3)">${permitLabel}</a>
</span>
`;
}
@ -2061,9 +2025,6 @@
// 清理后续状态
if (targetStep <= 2) {
currentTheme = null;
}
if (targetStep <= 3) {
currentPermit = null;
currentPermitDetails = null;
}
@ -2074,7 +2035,6 @@
// 返回首页
function goHome() {
currentRegion = null;
currentTheme = null;
currentPermit = null;
currentPermitDetails = null;
historyStack = [];
@ -2087,7 +2047,14 @@
detailsArea.innerHTML = '<div class="loading"></div>加载许可详情...';
try {
const response = await fetch(`/fs-ai-asistant/api/workflow/lawrisk/admin/permit-details?region=${currentRegion.id}&theme=${currentPermit.themeId}&permit=${currentPermit.id}`);
if (!currentRegion || !currentPermit) {
detailsArea.innerHTML = '<div class="error">请选择区划和事项后查看详情</div>';
return;
}
const themePart = currentPermit.themeId
? `&theme=${encodeURIComponent(currentPermit.themeId)}`
: '';
const response = await fetch(`/fs-ai-asistant/api/workflow/lawrisk/admin/permit-details?region=${currentRegion.id}&permit=${currentPermit.id}${themePart}`);
const data = await response.json();
if (data.success) {
@ -2108,7 +2075,6 @@
// 恢复状态
if (prev.region) currentRegion = prev.region;
if (prev.theme) currentTheme = prev.theme;
if (prev.permit) currentPermit = prev.permit;
// 跳转到上一步
@ -2120,8 +2086,14 @@
const detailsArea = document.querySelector('.details-area');
currentPermitDetails = permit;
const riskCount = Array.isArray(permit.risks) ? permit.risks.length : 0;
const permitTheme = permit && permit.theme ? permit.theme : {};
if (currentPermit) {
currentPermit = { ...currentPermit, riskCount };
currentPermit = {
...currentPermit,
riskCount,
themeId: permitTheme.id || currentPermit.themeId || '',
themeName: permitTheme.name || currentPermit.themeName || ''
};
}
const deleteButtonLabel = isDeletingPermit ? '删除中...' : '删除许可';
const deleteButtonDisabled = isDeletingPermit ? 'disabled' : '';
@ -2141,6 +2113,40 @@
const fileInfoText = hasPermitFile
? `<span class="permit-file-name">${escapeHtml(permitFile.filename || '原始文件')}</span>${formatFileSize(permitFile.file_size)}${fileUploadedAt ? ` ${fileUploadedAt}` : ''}${fileUploadedBy ? ` 上传:${fileUploadedBy}` : ''}`
: '<span class="muted-text">暂无关联文件</span>';
const rawThemeList = Array.isArray(permit.themes) ? permit.themes.filter(Boolean) : [];
if (permitTheme && (permitTheme.id || permitTheme.name)) {
rawThemeList.push(permitTheme);
}
if (currentPermit && (currentPermit.themeId || currentPermit.themeName)) {
rawThemeList.push({
id: currentPermit.themeId || '',
name: currentPermit.themeName || '',
});
}
const themeList = [];
const seenThemeKeys = new Set();
rawThemeList.forEach(themeItem => {
if (!themeItem) {
return;
}
const id = typeof themeItem.id === 'string' ? themeItem.id.trim() : (themeItem.id ? String(themeItem.id) : '');
const name = typeof themeItem.name === 'string' ? themeItem.name.trim() : (themeItem.name ? String(themeItem.name) : '');
if (!id && !name) {
return;
}
const key = id || `name:${name}`;
if (seenThemeKeys.has(key)) {
return;
}
seenThemeKeys.add(key);
themeList.push({
id,
name: name || id || '未命名主题',
});
});
const themeListDisplay = themeList.length
? `<div class="theme-tags">${themeList.map(themeItem => `<span class="item-tag">${escapeHtml(themeItem.name)}</span>`).join('')}</div>`
: '<p class="muted-text">暂无主题关联</p>';
let html = '<div class="details-content">';
html += `
@ -2171,6 +2177,15 @@
</div>
`;
html += `
<div class="detail-section">
<h3>所属主题</h3>
<div class="detail-content">
${themeListDisplay}
</div>
</div>
`;
// 经营范围
html += '<div class="detail-section"><h3>经营范围</h3><div class="detail-content">';
if (permit.business_scopes && permit.business_scopes.length > 0) {
@ -2223,7 +2238,7 @@
if (isDeletingPermit) {
return;
}
if (!currentRegion || !currentTheme || !currentPermit) {
if (!currentRegion || !currentPermit) {
alert('请先选择要删除的许可');
return;
}
@ -2231,7 +2246,12 @@
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` +
const pathParts = [currentRegion.name];
if (currentPermit.themeName) {
pathParts.push(currentPermit.themeName);
}
pathParts.push(currentPermit.name);
const confirmMessage = `确定要删除「${pathParts.join(' ')}」吗?\n\n` +
`此操作会删除 ${riskCount} 条风险关联(如存在)。系统会备份当前风险快照,但删除后需通过快照管理页面手动恢复。`;
if (!confirm(confirmMessage)) {
@ -2251,7 +2271,7 @@
if (isDeletingPermit) {
return;
}
if (!currentRegion || !currentTheme || !currentPermit) {
if (!currentRegion || !currentPermit) {
alert('当前上下文缺失,无法删除');
return;
}
@ -2262,9 +2282,11 @@
try {
const payload = {
region_id: currentRegion.id,
theme_id: currentTheme.id,
permit_id: currentPermit.id
};
if (currentPermit.themeId) {
payload.theme_id = currentPermit.themeId;
}
if (changeSummary) {
payload.change_summary = changeSummary;
}
@ -2333,8 +2355,11 @@
// 更新步骤指示器
function updateStepIndicator(step) {
for (let i = 1; i <= 4; i++) {
for (let i = 1; i <= 3; i++) {
const stepElement = document.getElementById(`step${i}`);
if (!stepElement) {
continue;
}
if (i <= step) {
stepElement.classList.add('active');
} else {
@ -3526,8 +3551,8 @@
const targetRegionId = groupItems[0].region_id;
const targetPermitId = groupItems[0].permit_id;
if (currentRegion && currentRegion.id === targetRegionId) {
if (currentTheme) {
await loadPermits(currentTheme.id, currentTheme.name);
if (currentStep >= 2) {
await loadPermitsForRegion();
}
if (currentPermit && currentPermit.id === targetPermitId) {
await showPermitDetails();