feat: show permit themes in db admin
This commit is contained in:
parent
772354bd01
commit
fd82b757fe
|
|
@ -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())
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
Loading…
Reference in New Issue