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")
|
TRAILING_SPACE_RE = re.compile(r"[ \t]+\n")
|
||||||
EXTRA_NEWLINES_RE = re.compile(r"\n{3,}")
|
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
|
PERMIT_IMPORT_TTL_SECONDS = 1800
|
||||||
MAX_PERMIT_FILE_SIZE_BYTES = 500 * 1024 # 500 KB limit for uploaded Excel files
|
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
|
return score
|
||||||
|
|
||||||
|
|
||||||
def _split_multi_value(value: Any) -> List[str]:
|
def _split_multi_value(value: Any, *, allow_dunhao: bool = False) -> List[str]:
|
||||||
"""Split multi-value cells using common Chinese punctuation."""
|
"""Split multi-value cells using common punctuation characters.
|
||||||
|
|
||||||
|
默认不把中文顿号(、)视作分隔符,以避免误拆“文化、旅游”等合法的
|
||||||
|
许可名称。对于确实需要用顿号分隔的字段(如主题、经营范围等),调用
|
||||||
|
方可以显式传入 allow_dunhao=True。
|
||||||
|
"""
|
||||||
|
|
||||||
text = _clean_text(value)
|
text = _clean_text(value)
|
||||||
if not text:
|
if not text:
|
||||||
return []
|
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]:
|
def _clean_empty(value: Any) -> Optional[str]:
|
||||||
|
|
@ -311,9 +319,15 @@ def _normalize_import_row(
|
||||||
jurisdiction_scope = _clean_empty(
|
jurisdiction_scope = _clean_empty(
|
||||||
raw_row.get("jurisdiction_scope") or sheet_defaults.get("jurisdiction_scope")
|
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"))
|
theme_names = _split_multi_value(
|
||||||
scope_descriptions = _split_multi_value(raw_row.get("scope_text") or sheet_defaults.get("scope_text"))
|
raw_row.get("theme_names") or sheet_defaults.get("theme_names"), allow_dunhao=True
|
||||||
subitem_names = _split_multi_value(raw_row.get("subitem_text") or sheet_defaults.get("subitem_text"))
|
)
|
||||||
|
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 {
|
return {
|
||||||
"row_index": int(row_index) if isinstance(row_index, int) else row_index,
|
"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
|
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(
|
def _load_permit_scopes_for_region(
|
||||||
conn: pg.Connection, region_id: str, permit_ids: List[str]
|
conn: pg.Connection, region_id: str, permit_ids: List[str]
|
||||||
) -> Dict[str, List[Dict[str, str]]]:
|
) -> Dict[str, List[Dict[str, str]]]:
|
||||||
|
|
@ -1766,9 +1842,9 @@ def _load_permit_sources_for_region(
|
||||||
|
|
||||||
|
|
||||||
def load_permits_and_risks(
|
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]]:
|
) -> 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.
|
# Ensure optional permit file tables exist before running user queries.
|
||||||
try:
|
try:
|
||||||
_ensure_permit_file_schema()
|
_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)
|
logger.warning("[PERMIT-FILES] Failed to ensure permit file schema before loading permits: %s", exc)
|
||||||
sql = """
|
sql = """
|
||||||
SELECT
|
SELECT
|
||||||
|
rtp.theme_id,
|
||||||
|
t.name AS theme_name,
|
||||||
p.id AS permit_id,
|
p.id AS permit_id,
|
||||||
p.name AS permit_name,
|
p.name AS permit_name,
|
||||||
rk.id AS risk_id,
|
rk.id AS risk_id,
|
||||||
|
|
@ -1789,6 +1867,7 @@ def load_permits_and_risks(
|
||||||
rpd.jurisdiction_scope
|
rpd.jurisdiction_scope
|
||||||
FROM region_theme_permits rtp
|
FROM region_theme_permits rtp
|
||||||
JOIN permits p ON p.id = rtp.permit_id
|
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
|
LEFT JOIN region_permit_risks rpr
|
||||||
ON rpr.region_id = rtp.region_id
|
ON rpr.region_id = rtp.region_id
|
||||||
AND rpr.permit_id = rtp.permit_id
|
AND rpr.permit_id = rtp.permit_id
|
||||||
|
|
@ -1796,9 +1875,12 @@ def load_permits_and_risks(
|
||||||
LEFT JOIN region_permit_details rpd
|
LEFT JOIN region_permit_details rpd
|
||||||
ON rpd.region_id = rtp.region_id
|
ON rpd.region_id = rtp.region_id
|
||||||
AND rpd.permit_id = rtp.permit_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:
|
if permit_id is not None:
|
||||||
sql += " AND rtp.permit_id = %s"
|
sql += " AND rtp.permit_id = %s"
|
||||||
params.append(permit_id)
|
params.append(permit_id)
|
||||||
|
|
@ -1812,6 +1894,8 @@ def load_permits_and_risks(
|
||||||
cur.execute(sql, tuple(params))
|
cur.execute(sql, tuple(params))
|
||||||
for row in cur.fetchall():
|
for row in cur.fetchall():
|
||||||
(
|
(
|
||||||
|
row_theme_id,
|
||||||
|
row_theme_name,
|
||||||
permit_id,
|
permit_id,
|
||||||
permit_name,
|
permit_name,
|
||||||
risk_id,
|
risk_id,
|
||||||
|
|
@ -1825,6 +1909,8 @@ def load_permits_and_risks(
|
||||||
jurisdiction_scope,
|
jurisdiction_scope,
|
||||||
) = row
|
) = row
|
||||||
pid = str(permit_id)
|
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(
|
entry = permits.setdefault(
|
||||||
pid,
|
pid,
|
||||||
{
|
{
|
||||||
|
|
@ -1836,8 +1922,37 @@ def load_permits_and_risks(
|
||||||
"subitem_summary": None,
|
"subitem_summary": None,
|
||||||
"responsible_contact": None,
|
"responsible_contact": None,
|
||||||
"jurisdiction_scope": 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:
|
if entry["permit_status"] is None and permit_status:
|
||||||
entry["permit_status"] = permit_status.strip() or None
|
entry["permit_status"] = permit_status.strip() or None
|
||||||
if entry["subitem_summary"] is None and subitem_summary:
|
if entry["subitem_summary"] is None and subitem_summary:
|
||||||
|
|
@ -1895,6 +2010,8 @@ def load_permits_and_risks(
|
||||||
"created_at": None,
|
"created_at": None,
|
||||||
"uploaded_by": "",
|
"uploaded_by": "",
|
||||||
}
|
}
|
||||||
|
if "themes" not in permits[pid] or permits[pid]["themes"] is None:
|
||||||
|
permits[pid]["themes"] = []
|
||||||
return list(permits.values())
|
return list(permits.values())
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -366,6 +366,23 @@
|
||||||
font-weight: 500;
|
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 {
|
.item-count {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
background: rgba(102, 126, 234, 0.1);
|
background: rgba(102, 126, 234, 0.1);
|
||||||
|
|
@ -1508,21 +1525,16 @@
|
||||||
<div class="step-indicator">
|
<div class="step-indicator">
|
||||||
<div class="step active" id="step1">
|
<div class="step active" id="step1">
|
||||||
<div class="step-number">1</div>
|
<div class="step-number">1</div>
|
||||||
<div class="step-label">选择地区</div>
|
<div class="step-label">选择区划</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="arrow">→</div>
|
<div class="arrow">→</div>
|
||||||
<div class="step" id="step2">
|
<div class="step" id="step2">
|
||||||
<div class="step-number">2</div>
|
<div class="step-number">2</div>
|
||||||
<div class="step-label">选择主题</div>
|
<div class="step-label">选择事项</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="arrow">→</div>
|
<div class="arrow">→</div>
|
||||||
<div class="step" id="step3">
|
<div class="step" id="step3">
|
||||||
<div class="step-number">3</div>
|
<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 class="step-label">查看详情</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1540,7 +1552,7 @@
|
||||||
<div class="content-area">
|
<div class="content-area">
|
||||||
<div class="panel">
|
<div class="panel">
|
||||||
<h2 id="navTitle">
|
<h2 id="navTitle">
|
||||||
<span>选择区域</span>
|
<span>选择区划</span>
|
||||||
<div class="nav-controls">
|
<div class="nav-controls">
|
||||||
<button class="back-button" id="backButton" onclick="goBack()" disabled>← 上一步</button>
|
<button class="back-button" id="backButton" onclick="goBack()" disabled>← 上一步</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1558,7 +1570,7 @@
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<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"/>
|
<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>
|
</svg>
|
||||||
<p id="emptyMessage">请选择区域开始导航</p>
|
<p id="emptyMessage">请选择区划开始导航</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1701,10 +1713,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// 导航状态管理
|
// 导航状态管理
|
||||||
let currentStep = 1; // 1=区域, 2=主题, 3=许可, 4=详情
|
let currentStep = 1; // 1=区划, 2=事项, 3=详情
|
||||||
let historyStack = []; // 历史记录栈
|
let historyStack = []; // 历史记录栈
|
||||||
let currentRegion = null;
|
let currentRegion = null;
|
||||||
let currentTheme = null;
|
|
||||||
let currentPermit = null;
|
let currentPermit = null;
|
||||||
let currentPermitDetails = null;
|
let currentPermitDetails = null;
|
||||||
let pendingDangerOperation = null; // 待执行的危险操作
|
let pendingDangerOperation = null; // 待执行的危险操作
|
||||||
|
|
@ -1791,16 +1802,15 @@
|
||||||
|
|
||||||
// 步骤配置
|
// 步骤配置
|
||||||
const steps = {
|
const steps = {
|
||||||
1: { title: '选择区域', loadData: loadRegions },
|
1: { title: '选择区划' },
|
||||||
2: { title: '选择主题', loadData: (region) => loadThemes(region.id, region.name) },
|
2: { title: '选择事项' },
|
||||||
3: { title: '选择许可', loadData: (theme) => loadPermits(theme.id, theme.name) },
|
3: { title: '事项详情' }
|
||||||
4: { title: '许可详情', loadData: null }
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 加载地区列表
|
// 加载区划列表
|
||||||
async function loadRegions() {
|
async function loadRegions() {
|
||||||
const navList = document.getElementById('navList');
|
const navList = document.getElementById('navList');
|
||||||
navList.innerHTML = '<div class="loading"></div>加载地区列表...';
|
navList.innerHTML = '<div class="loading"></div>加载区划列表...';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/fs-ai-asistant/api/workflow/lawrisk/admin/regions');
|
const response = await fetch('/fs-ai-asistant/api/workflow/lawrisk/admin/regions');
|
||||||
|
|
@ -1810,7 +1820,7 @@
|
||||||
navList.innerHTML = '';
|
navList.innerHTML = '';
|
||||||
|
|
||||||
if (data.data.regions.length === 0) {
|
if (data.data.regions.length === 0) {
|
||||||
navList.innerHTML = '<div class="error">未找到地区数据</div>';
|
navList.innerHTML = '<div class="error">未找到区划数据</div>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1824,72 +1834,54 @@
|
||||||
navList.appendChild(li);
|
navList.appendChild(li);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
navList.innerHTML = `<div class="error">加载地区失败:${data.message}</div>`;
|
navList.innerHTML = `<div class="error">加载区划失败:${data.message}</div>`;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
navList.innerHTML = `<div class="error">网络错误:${error.message}</div>`;
|
navList.innerHTML = `<div class="error">网络错误:${error.message}</div>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 加载主题列表
|
// 加载许可列表(直接按区划聚合事项)
|
||||||
async function loadThemes(regionId, regionName) {
|
async function loadPermitsForRegion() {
|
||||||
const navList = document.getElementById('navList');
|
if (!currentRegion) {
|
||||||
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>`;
|
|
||||||
return;
|
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');
|
const navList = document.getElementById('navList');
|
||||||
navList.innerHTML = '<div class="loading"></div>加载许可列表...';
|
navList.innerHTML = '<div class="loading"></div>加载事项列表...';
|
||||||
|
|
||||||
try {
|
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();
|
const data = await response.json();
|
||||||
|
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
const permits = data.data.permits;
|
const permits = data.data.permits;
|
||||||
|
|
||||||
if (permits.length === 0) {
|
if (permits.length === 0) {
|
||||||
navList.innerHTML = `<div class="error">主题 "${themeName}" 下没有可用的许可</div>`;
|
navList.innerHTML = `<div class="error">区划 "${currentRegion.name}" 暂无可维护的事项</div>`;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let html = '<div class="item-list">';
|
let html = '<div class="item-list">';
|
||||||
permits.forEach(permit => {
|
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 += `
|
html += `
|
||||||
<li onclick="selectPermit('${permit.id}', '${permit.name.replace(/'/g, "\\'")}', '${themeId}')">
|
<li onclick="selectPermit('${permit.id}', '${escapedName}', '${escapedThemeId}', '${escapedTheme}', ${riskCount})">
|
||||||
<span class="item-name">${permit.name}</span>
|
<span class="item-name">${permit.name}${themeBadge}</span>
|
||||||
<span class="item-count">${riskCount} 个风险</span>
|
<span class="item-count">${riskCount} 个风险</span>
|
||||||
</li>
|
</li>
|
||||||
`;
|
`;
|
||||||
|
|
@ -1897,7 +1889,7 @@
|
||||||
html += '</div>';
|
html += '</div>';
|
||||||
navList.innerHTML = html;
|
navList.innerHTML = html;
|
||||||
} else {
|
} else {
|
||||||
navList.innerHTML = `<div class="error">加载许可失败:${data.message}</div>`;
|
navList.innerHTML = `<div class="error">加载事项失败:${data.message}</div>`;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
navList.innerHTML = `<div class="error">网络错误:${error.message}</div>`;
|
navList.innerHTML = `<div class="error">网络错误:${error.message}</div>`;
|
||||||
|
|
@ -1910,7 +1902,6 @@
|
||||||
historyStack.push({ step: currentStep, region: currentRegion });
|
historyStack.push({ step: currentStep, region: currentRegion });
|
||||||
|
|
||||||
currentRegion = { id: regionId, name: regionName };
|
currentRegion = { id: regionId, name: regionName };
|
||||||
currentTheme = null;
|
|
||||||
currentPermit = null;
|
currentPermit = null;
|
||||||
currentPermitDetails = null;
|
currentPermitDetails = null;
|
||||||
|
|
||||||
|
|
@ -1918,31 +1909,24 @@
|
||||||
goToStep(2);
|
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 = {
|
||||||
currentPermit = null;
|
id: permitId,
|
||||||
|
name: permitName,
|
||||||
|
themeId: themeId || '',
|
||||||
|
themeName: themeName || '',
|
||||||
|
riskCount: typeof riskCount === 'number' ? riskCount : 0
|
||||||
|
};
|
||||||
currentPermitDetails = null;
|
currentPermitDetails = null;
|
||||||
|
|
||||||
// 更新步骤
|
// 更新步骤
|
||||||
goToStep(3);
|
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) {
|
async function goToStep(step) {
|
||||||
currentStep = step;
|
currentStep = step;
|
||||||
|
|
@ -1977,10 +1961,8 @@
|
||||||
if (step === 1) {
|
if (step === 1) {
|
||||||
await loadRegions();
|
await loadRegions();
|
||||||
} else if (step === 2) {
|
} else if (step === 2) {
|
||||||
await loadThemes(currentRegion.id, currentRegion.name);
|
await loadPermitsForRegion();
|
||||||
} else if (step === 3) {
|
} else if (step === 3) {
|
||||||
await loadPermits(currentTheme.id, currentTheme.name);
|
|
||||||
} else if (step === 4) {
|
|
||||||
await showPermitDetails();
|
await showPermitDetails();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1988,16 +1970,12 @@
|
||||||
// 更新面包屑导航
|
// 更新面包屑导航
|
||||||
function updateBreadcrumb() {
|
function updateBreadcrumb() {
|
||||||
const breadcrumb = document.getElementById('breadcrumb');
|
const breadcrumb = document.getElementById('breadcrumb');
|
||||||
let html = '';
|
let html = `
|
||||||
|
|
||||||
// 总是显示"首页"
|
|
||||||
html += `
|
|
||||||
<span class="breadcrumb-item">
|
<span class="breadcrumb-item">
|
||||||
<a onclick="goHome()">首页</a>
|
<a onclick="goHome()">首页</a>
|
||||||
</span>
|
</span>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// 显示当前选择的路径
|
|
||||||
if (currentRegion) {
|
if (currentRegion) {
|
||||||
html += '<span class="breadcrumb-separator">›</span>';
|
html += '<span class="breadcrumb-separator">›</span>';
|
||||||
if (currentStep > 2) {
|
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) {
|
if (currentPermit) {
|
||||||
|
const permitLabel = currentPermit.themeName
|
||||||
|
? `${currentPermit.themeName} · ${currentPermit.name}`
|
||||||
|
: currentPermit.name;
|
||||||
html += '<span class="breadcrumb-separator">›</span>';
|
html += '<span class="breadcrumb-separator">›</span>';
|
||||||
if (currentStep > 4) {
|
if (currentStep >= 3) {
|
||||||
html += `
|
html += `
|
||||||
<span class="breadcrumb-item">
|
<span class="breadcrumb-item">
|
||||||
<a onclick="quickJump(4)">${currentPermit.name}</a>
|
<span class="breadcrumb-current">${permitLabel}</span>
|
||||||
</span>
|
</span>
|
||||||
`;
|
`;
|
||||||
} else {
|
} else {
|
||||||
html += `
|
html += `
|
||||||
<span class="breadcrumb-item">
|
<span class="breadcrumb-item">
|
||||||
<span class="breadcrumb-current">${currentPermit.name}</span>
|
<a onclick="quickJump(3)">${permitLabel}</a>
|
||||||
</span>
|
</span>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
@ -2061,9 +2025,6 @@
|
||||||
|
|
||||||
// 清理后续状态
|
// 清理后续状态
|
||||||
if (targetStep <= 2) {
|
if (targetStep <= 2) {
|
||||||
currentTheme = null;
|
|
||||||
}
|
|
||||||
if (targetStep <= 3) {
|
|
||||||
currentPermit = null;
|
currentPermit = null;
|
||||||
currentPermitDetails = null;
|
currentPermitDetails = null;
|
||||||
}
|
}
|
||||||
|
|
@ -2074,7 +2035,6 @@
|
||||||
// 返回首页
|
// 返回首页
|
||||||
function goHome() {
|
function goHome() {
|
||||||
currentRegion = null;
|
currentRegion = null;
|
||||||
currentTheme = null;
|
|
||||||
currentPermit = null;
|
currentPermit = null;
|
||||||
currentPermitDetails = null;
|
currentPermitDetails = null;
|
||||||
historyStack = [];
|
historyStack = [];
|
||||||
|
|
@ -2087,7 +2047,14 @@
|
||||||
detailsArea.innerHTML = '<div class="loading"></div>加载许可详情...';
|
detailsArea.innerHTML = '<div class="loading"></div>加载许可详情...';
|
||||||
|
|
||||||
try {
|
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();
|
const data = await response.json();
|
||||||
|
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
|
|
@ -2108,7 +2075,6 @@
|
||||||
|
|
||||||
// 恢复状态
|
// 恢复状态
|
||||||
if (prev.region) currentRegion = prev.region;
|
if (prev.region) currentRegion = prev.region;
|
||||||
if (prev.theme) currentTheme = prev.theme;
|
|
||||||
if (prev.permit) currentPermit = prev.permit;
|
if (prev.permit) currentPermit = prev.permit;
|
||||||
|
|
||||||
// 跳转到上一步
|
// 跳转到上一步
|
||||||
|
|
@ -2120,8 +2086,14 @@
|
||||||
const detailsArea = document.querySelector('.details-area');
|
const detailsArea = document.querySelector('.details-area');
|
||||||
currentPermitDetails = permit;
|
currentPermitDetails = permit;
|
||||||
const riskCount = Array.isArray(permit.risks) ? permit.risks.length : 0;
|
const riskCount = Array.isArray(permit.risks) ? permit.risks.length : 0;
|
||||||
|
const permitTheme = permit && permit.theme ? permit.theme : {};
|
||||||
if (currentPermit) {
|
if (currentPermit) {
|
||||||
currentPermit = { ...currentPermit, riskCount };
|
currentPermit = {
|
||||||
|
...currentPermit,
|
||||||
|
riskCount,
|
||||||
|
themeId: permitTheme.id || currentPermit.themeId || '',
|
||||||
|
themeName: permitTheme.name || currentPermit.themeName || ''
|
||||||
|
};
|
||||||
}
|
}
|
||||||
const deleteButtonLabel = isDeletingPermit ? '删除中...' : '删除许可';
|
const deleteButtonLabel = isDeletingPermit ? '删除中...' : '删除许可';
|
||||||
const deleteButtonDisabled = isDeletingPermit ? 'disabled' : '';
|
const deleteButtonDisabled = isDeletingPermit ? 'disabled' : '';
|
||||||
|
|
@ -2141,6 +2113,40 @@
|
||||||
const fileInfoText = hasPermitFile
|
const fileInfoText = hasPermitFile
|
||||||
? `<span class="permit-file-name">${escapeHtml(permitFile.filename || '原始文件')}</span>(${formatFileSize(permitFile.file_size)}${fileUploadedAt ? ` | ${fileUploadedAt}` : ''}${fileUploadedBy ? ` | 上传:${fileUploadedBy}` : ''})`
|
? `<span class="permit-file-name">${escapeHtml(permitFile.filename || '原始文件')}</span>(${formatFileSize(permitFile.file_size)}${fileUploadedAt ? ` | ${fileUploadedAt}` : ''}${fileUploadedBy ? ` | 上传:${fileUploadedBy}` : ''})`
|
||||||
: '<span class="muted-text">暂无关联文件</span>';
|
: '<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">';
|
let html = '<div class="details-content">';
|
||||||
html += `
|
html += `
|
||||||
|
|
@ -2171,6 +2177,15 @@
|
||||||
</div>
|
</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">';
|
html += '<div class="detail-section"><h3>经营范围</h3><div class="detail-content">';
|
||||||
if (permit.business_scopes && permit.business_scopes.length > 0) {
|
if (permit.business_scopes && permit.business_scopes.length > 0) {
|
||||||
|
|
@ -2223,7 +2238,7 @@
|
||||||
if (isDeletingPermit) {
|
if (isDeletingPermit) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!currentRegion || !currentTheme || !currentPermit) {
|
if (!currentRegion || !currentPermit) {
|
||||||
alert('请先选择要删除的许可');
|
alert('请先选择要删除的许可');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -2231,7 +2246,12 @@
|
||||||
const riskCount = currentPermit.riskCount !== undefined
|
const riskCount = currentPermit.riskCount !== undefined
|
||||||
? currentPermit.riskCount
|
? currentPermit.riskCount
|
||||||
: (currentPermitDetails && Array.isArray(currentPermitDetails.risks) ? currentPermitDetails.risks.length : 0);
|
: (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} 条风险关联(如存在)。系统会备份当前风险快照,但删除后需通过快照管理页面手动恢复。`;
|
`此操作会删除 ${riskCount} 条风险关联(如存在)。系统会备份当前风险快照,但删除后需通过快照管理页面手动恢复。`;
|
||||||
|
|
||||||
if (!confirm(confirmMessage)) {
|
if (!confirm(confirmMessage)) {
|
||||||
|
|
@ -2251,7 +2271,7 @@
|
||||||
if (isDeletingPermit) {
|
if (isDeletingPermit) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!currentRegion || !currentTheme || !currentPermit) {
|
if (!currentRegion || !currentPermit) {
|
||||||
alert('当前上下文缺失,无法删除');
|
alert('当前上下文缺失,无法删除');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -2262,9 +2282,11 @@
|
||||||
try {
|
try {
|
||||||
const payload = {
|
const payload = {
|
||||||
region_id: currentRegion.id,
|
region_id: currentRegion.id,
|
||||||
theme_id: currentTheme.id,
|
|
||||||
permit_id: currentPermit.id
|
permit_id: currentPermit.id
|
||||||
};
|
};
|
||||||
|
if (currentPermit.themeId) {
|
||||||
|
payload.theme_id = currentPermit.themeId;
|
||||||
|
}
|
||||||
if (changeSummary) {
|
if (changeSummary) {
|
||||||
payload.change_summary = changeSummary;
|
payload.change_summary = changeSummary;
|
||||||
}
|
}
|
||||||
|
|
@ -2333,8 +2355,11 @@
|
||||||
|
|
||||||
// 更新步骤指示器
|
// 更新步骤指示器
|
||||||
function updateStepIndicator(step) {
|
function updateStepIndicator(step) {
|
||||||
for (let i = 1; i <= 4; i++) {
|
for (let i = 1; i <= 3; i++) {
|
||||||
const stepElement = document.getElementById(`step${i}`);
|
const stepElement = document.getElementById(`step${i}`);
|
||||||
|
if (!stepElement) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (i <= step) {
|
if (i <= step) {
|
||||||
stepElement.classList.add('active');
|
stepElement.classList.add('active');
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -3526,8 +3551,8 @@
|
||||||
const targetRegionId = groupItems[0].region_id;
|
const targetRegionId = groupItems[0].region_id;
|
||||||
const targetPermitId = groupItems[0].permit_id;
|
const targetPermitId = groupItems[0].permit_id;
|
||||||
if (currentRegion && currentRegion.id === targetRegionId) {
|
if (currentRegion && currentRegion.id === targetRegionId) {
|
||||||
if (currentTheme) {
|
if (currentStep >= 2) {
|
||||||
await loadPermits(currentTheme.id, currentTheme.name);
|
await loadPermitsForRegion();
|
||||||
}
|
}
|
||||||
if (currentPermit && currentPermit.id === targetPermitId) {
|
if (currentPermit && currentPermit.id === targetPermitId) {
|
||||||
await showPermitDetails();
|
await showPermitDetails();
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue