feat: add visibility filter for permit management and fix V2 visibility logic

- Added 'Enabled Status' dropdown filter to db_admin.html for filtering permits by visibility (visible/hidden/all)
- Updated admin_permits_advanced_filter API to accept and process visibility parameter
- Modified filter_permits_advanced in licensing_repo.py to filter by is_v2_visible column
- Fixed role-based access control to allow department_admin to toggle permit visibility
- Improved parameter parsing in API endpoints for more robust handling
This commit is contained in:
Codex Agent 2025-12-29 15:54:53 +08:00
parent b532c46dc1
commit c55170208b
4 changed files with 343 additions and 66 deletions

View File

@ -922,6 +922,38 @@ def admin_permits():
return jsonify({"success": False, "message": str(exc)}), 500
@v2_bp.route('/admin/permits/visibility', methods=['POST'])
def admin_toggle_permit_visibility():
"""Toggle the visibility of a permit in V2 API retrieval."""
admin_user, error = _admin_guard(prefer_json=True, roles=("admin", "department_admin"))
if error:
return error
data = request.get_json() or {}
region_id = data.get("region_id")
permit_id = data.get("permit_id")
is_visible = data.get("is_v2_visible")
if not region_id or not permit_id or is_visible is None:
return jsonify({"success": False, "message": "region_id, permit_id and is_v2_visible are required"}), 400
try:
from lawrisk.services.licensing_repo import update_permit_v2_visibility
operator = (admin_user or {}).get("username") or "admin"
success = update_permit_v2_visibility(
region_id=region_id,
permit_id=permit_id,
is_visible=bool(is_visible),
operator=operator
)
if success:
return jsonify({"success": True, "message": "Visibility updated"})
else:
return jsonify({"success": False, "message": "Permit details not found or update failed"}), 404
except Exception as exc:
return jsonify({"success": False, "message": str(exc)}), 500
@v2_bp.route('/admin/permit-import/upload', methods=['POST'])
def admin_permit_import_upload():
"""Upload Excel workbook and start an import session."""
@ -1596,70 +1628,46 @@ def admin_permits_advanced_filter():
"""
try:
# Parse parameters from query string or request body
# Parse parameters from query/body in a more unified way
if request.method == 'GET':
regions = request.args.getlist('regions[]') or request.args.getlist('region')
themes = request.args.getlist('themes[]') or request.args.getlist('theme')
departments = request.args.getlist('departments[]') or request.args.getlist('department')
search_text = request.args.get('search_text') or request.args.get('q')
try:
limit = int(request.args.get('limit', '100'))
except (TypeError, ValueError):
limit = 100
try:
offset = int(request.args.get('offset', '0'))
except (TypeError, ValueError):
offset = 0
visibility = request.args.get('visibility')
limit = request.args.get('limit', '100')
offset = request.args.get('offset', '0')
else:
if request.is_json:
payload = request.get_json(silent=True) or {}
else:
payload = request.form.to_dict(flat=True) if request.form else {}
# Handle array parameters
payload = request.get_json(silent=True) or request.form
regions = payload.getlist('regions[]') if hasattr(payload, 'getlist') else payload.get('regions', [])
if isinstance(regions, str):
regions = [regions]
regions = regions or payload.getlist('region') if hasattr(payload, 'getlist') else payload.get('region', [])
if isinstance(regions, str):
regions = [regions]
themes = payload.getlist('themes[]') if hasattr(payload, 'getlist') else payload.get('themes', [])
if isinstance(themes, str):
themes = [themes]
themes = themes or payload.getlist('theme') if hasattr(payload, 'getlist') else payload.get('theme', [])
if isinstance(themes, str):
themes = [themes]
departments = payload.getlist('departments[]') if hasattr(payload, 'getlist') else payload.get('departments', [])
if isinstance(departments, str):
departments = [departments]
departments = departments or payload.getlist('department') if hasattr(payload, 'getlist') else payload.get('department', [])
if isinstance(departments, str):
departments = [departments]
search_text = payload.get('search_text') or payload.get('q')
try:
limit = int(payload.get('limit', '100'))
except (TypeError, ValueError):
limit = 100
try:
offset = int(payload.get('offset', '0'))
except (TypeError, ValueError):
offset = 0
visibility = payload.get('visibility')
limit = payload.get('limit', '100')
offset = payload.get('offset', '0')
# Normalize parameters - convert to lists if not already
if isinstance(regions, str):
regions = [regions]
if isinstance(themes, str):
themes = [themes]
if isinstance(departments, str):
departments = [departments]
# Filter out empty values
# Normalize parameters
if isinstance(regions, str): regions = [regions]
if isinstance(themes, str): themes = [themes]
if isinstance(departments, str): departments = [departments]
regions = [r.strip() for r in regions if r and r.strip()] if regions else None
themes = [t.strip() for t in themes if t and t.strip()] if themes else None
departments = [d.strip() for d in departments if d and d.strip()] if departments else None
search_text = search_text.strip() if search_text else None
search_text = (search_text or "").strip() or None
visibility = (visibility or "").strip().lower() or None
try:
limit = int(limit)
except (ValueError, TypeError):
limit = 100
try:
offset = int(offset)
except (ValueError, TypeError):
offset = 0
print(f"[DEBUG] admin_permits_advanced_filter params: search={search_text}, visibility={visibility}, regions={regions}")
# Execute filtering
result = filter_permits_advanced(
@ -1667,6 +1675,7 @@ def admin_permits_advanced_filter():
themes=themes,
departments=departments,
search_text=search_text,
visibility=visibility,
limit=limit,
offset=offset,
)

View File

@ -10,6 +10,7 @@ from lawrisk.services.licensing_repo import (
load_theme_payload,
load_permits_and_risks,
find_permit_contexts_by_name,
_ensure_v2_visibility_column,
)
from lawrisk.services.lawrisk_service import ChatClient
@ -200,18 +201,21 @@ def _get_preset_questions_pool() -> List[str]:
"""
from lawrisk.services.licensing_repo import _lic_pg_conn
# Query themes that have at least one permit
# Query themes that have at least one visible permit
sql = """
SELECT DISTINCT t.name AS theme_name
FROM themes t
JOIN region_theme_permits rtp ON rtp.theme_id = t.id
JOIN region_permit_details rpd ON rpd.region_id = rtp.region_id AND rpd.permit_id = rtp.permit_id
WHERE t.name NOT IN ('不涉及', '', '所有主题事项')
AND COALESCE(rpd.is_v2_visible, true) = true
ORDER BY t.name
"""
questions: List[str] = []
try:
with _lic_pg_conn() as conn:
_ensure_v2_visibility_column(conn)
cur = conn.cursor()
cur.execute(sql)
for (theme_name,) in cur.fetchall():
@ -313,6 +317,7 @@ def search_v2(
ctx["region_id"],
ctx["theme_id"],
permit_id=ctx["permit_id"],
only_visible=True,
)
if not permits:
continue
@ -368,7 +373,7 @@ def search_v2(
if ":" not in option_id:
continue
region_id, theme_id = option_id.split(":", 1)
payload = load_theme_payload(region_id, theme_id)
payload = load_theme_payload(region_id, theme_id, only_visible=True)
# Sanitize permits for V2 API (V2 should only expose external contact info)
for permit in payload.get("permits", []):

View File

@ -68,6 +68,9 @@ _PERMIT_THEME_OVERRIDE_SCHEMA_LOCK = threading.Lock()
_PERMIT_APPROVAL_DEPARTMENTS_SCHEMA_READY: Optional[bool] = None
_PERMIT_APPROVAL_DEPARTMENTS_SCHEMA_LOCK = threading.Lock()
_V2_VISIBILITY_SCHEMA_READY: Optional[bool] = None
_V2_VISIBILITY_SCHEMA_LOCK = threading.Lock()
_IMPORT_HEADER_ALIASES: Dict[str, Set[str]] = {
"permit_name": {
"许可事项",
@ -3200,6 +3203,36 @@ def _fetch_permit_all_theme_flags(
return {str(permit_id): True for (permit_id,) in rows}
def update_permit_v2_visibility(
region_id: str, permit_id: str, is_visible: bool, operator: str = "admin"
) -> bool:
"""Toggle the visibility of a permit in V2 API retrieval for a specific region."""
with _lic_pg_conn() as conn:
_ensure_v2_visibility_column(conn)
cur = conn.cursor()
cur.execute(
"""
UPDATE region_permit_details
SET is_v2_visible = %s, updated_at = now()
WHERE region_id = %s AND permit_id = %s
""",
(is_visible, region_id, permit_id),
)
success = cur.rowcount > 0
if success:
conn.commit()
log_operation(
operator=operator,
operation_type="UPDATE",
target_type="PERMIT_VISIBILITY",
target_id=permit_id,
target_name=f"Visibility set to {is_visible}",
change_summary=f"Updated v2_visibility for permit {permit_id} in region {region_id} to {is_visible}",
details={"region_id": region_id, "permit_id": permit_id, "is_v2_visible": is_visible},
)
return success
def _permit_binds_all_themes(conn: pg.Connection, region_id: str, permit_id: str) -> bool:
"""Check override flag for a single region-permit pair."""
global _PERMIT_THEME_OVERRIDE_SCHEMA_READY
@ -3351,10 +3384,17 @@ def list_region_theme_options() -> List[Dict[str, str]]:
FROM region_themes rt
JOIN regions r ON r.id = rt.region_id
JOIN themes t ON t.id = rt.theme_id
WHERE EXISTS (
SELECT 1 FROM region_theme_permits rtp
JOIN region_permit_details rpd ON rpd.region_id = rtp.region_id AND rpd.permit_id = rtp.permit_id
WHERE rtp.region_id = rt.region_id AND rtp.theme_id = rt.theme_id
AND COALESCE(rpd.is_v2_visible, true) = true
)
ORDER BY r.name, t.name
"""
out: List[Dict[str, str]] = []
with _lic_pg_conn() as conn:
_ensure_v2_visibility_column(conn)
cur = conn.cursor()
cur.execute(sql)
for region_id, region_name, theme_id, theme_name in cur.fetchall():
@ -3528,10 +3568,12 @@ def list_region_permit_catalog(region_id: str) -> List[Dict[str, Any]]:
p.name AS permit_name,
rtp.theme_id,
COALESCE(t.name, '') AS theme_name,
COUNT(rpr.risk_id) OVER (PARTITION BY rtp.permit_id) AS risk_total
COUNT(rpr.risk_id) OVER (PARTITION BY rtp.permit_id) AS risk_total,
COALESCE(rpd.is_v2_visible, true) AS is_v2_visible
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_details rpd ON rpd.region_id = rtp.region_id AND rpd.permit_id = rtp.permit_id
LEFT JOIN region_permit_risks rpr
ON rpr.region_id = rtp.region_id
AND rpr.permit_id = rtp.permit_id
@ -3540,9 +3582,10 @@ def list_region_permit_catalog(region_id: str) -> List[Dict[str, Any]]:
"""
catalog_map: "OrderedDict[str, Dict[str, Any]]" = OrderedDict()
with _lic_pg_conn() as conn:
_ensure_v2_visibility_column(conn)
cur = conn.cursor()
cur.execute(sql, (region_id,))
for permit_id, permit_name, theme_id, theme_name, risk_total in cur.fetchall():
for permit_id, permit_name, theme_id, theme_name, risk_total, v2_visible in cur.fetchall():
pid = str(permit_id)
entry = catalog_map.setdefault(
pid,
@ -3550,6 +3593,7 @@ def list_region_permit_catalog(region_id: str) -> List[Dict[str, Any]]:
"id": pid,
"name": str(permit_name),
"risk_count": int(risk_total or 0),
"is_v2_visible": bool(v2_visible),
"theme": {"id": "", "name": ""},
"themes": [],
},
@ -3713,7 +3757,10 @@ def _load_permit_sources_for_region(
def load_permits_and_risks(
region_id: str, theme_id: Optional[str] = None, permit_id: Optional[str] = None
region_id: str,
theme_id: Optional[str] = None,
permit_id: Optional[str] = None,
only_visible: bool = False
) -> List[Dict[str, object]]:
"""Return permits with attached risk entries for a region (optionally filtered by theme)."""
# Ensure optional permit file tables exist before running user queries.
@ -3747,7 +3794,8 @@ def load_permits_and_risks(
rpd.filler_name,
COALESCE(pad.department_name, rpd.unit_name) AS unit_name,
rpd.source_update_date,
rpd.contact_info
rpd.contact_info,
COALESCE(rpd.is_v2_visible, true) AS is_v2_visible
FROM region_permit_details rpd
JOIN permits p ON p.id = rpd.permit_id
LEFT JOIN permit_approval_departments pad
@ -3770,6 +3818,8 @@ def load_permits_and_risks(
if permit_id is not None:
sql += " AND rpd.permit_id = %s"
params.append(permit_id)
if only_visible:
sql += " AND COALESCE(rpd.is_v2_visible, true) = true"
sql += """
ORDER BY p.name, LENGTH(rpr.serial_number), rpr.serial_number, rk.risk_content
@ -3777,6 +3827,7 @@ def load_permits_and_risks(
permits: Dict[str, Dict[str, object]] = {}
risk_seen_map: Dict[str, Set[str]] = {} # pid -> set of risk_ids
with _lic_pg_conn() as conn:
_ensure_v2_visibility_column(conn)
_ensure_contact_info_column(conn)
cur = conn.cursor()
cur.execute(sql, tuple(params))
@ -3801,6 +3852,7 @@ def load_permits_and_risks(
unit_name,
source_update_date,
contact_info,
v2_visible,
) = row
pid = str(permit_id)
theme_id_value = str(row_theme_id) if row_theme_id else ""
@ -4252,7 +4304,8 @@ def find_permit_contexts_by_name(permit_name: str) -> List[Dict[str, str]]:
rtp.theme_id,
t.name AS theme_name,
p.id AS permit_id,
p.name AS permit_name
p.name AS permit_name,
COALESCE(rpd.is_v2_visible, true) AS is_v2_visible
FROM region_permit_details rpd
JOIN permits p ON p.id = rpd.permit_id
JOIN regions r ON r.id = rpd.region_id
@ -4263,6 +4316,7 @@ def find_permit_contexts_by_name(permit_name: str) -> List[Dict[str, str]]:
"""
ordered: OrderedDict[Tuple[str, str], Dict[str, str]] = OrderedDict()
with _lic_pg_conn() as conn:
_ensure_v2_visibility_column(conn)
cur = conn.cursor()
cur.execute(sql, (permit_name,))
rows = cur.fetchall()
@ -4276,7 +4330,8 @@ def find_permit_contexts_by_name(permit_name: str) -> List[Dict[str, str]]:
rtp.theme_id,
t.name AS theme_name,
p.id AS permit_id,
p.name AS permit_name
p.name AS permit_name,
COALESCE(rpd.is_v2_visible, true) AS is_v2_visible
FROM region_permit_details rpd
JOIN permits p ON p.id = rpd.permit_id
JOIN regions r ON r.id = rpd.region_id
@ -4289,7 +4344,7 @@ def find_permit_contexts_by_name(permit_name: str) -> List[Dict[str, str]]:
rows = cur.fetchall()
for row in rows:
region_id, region_name, theme_id, theme_name, permit_id, canonical_name = row
region_id, region_name, theme_id, theme_name, permit_id, canonical_name, v2_visible = row
rid = str(region_id)
pid = str(permit_id)
tid = str(theme_id) if theme_id else ""
@ -4304,11 +4359,12 @@ def find_permit_contexts_by_name(permit_name: str) -> List[Dict[str, str]]:
"theme_name": tname,
"permit_id": pid,
"permit_name": str(canonical_name),
"is_v2_visible": bool(v2_visible),
}
return list(ordered.values())
return [item for item in ordered.values() if item.get("is_v2_visible")]
def load_theme_payload(region_id: str, theme_id: str) -> Dict[str, object]:
def load_theme_payload(region_id: str, theme_id: str, only_visible: bool = False) -> Dict[str, object]:
"""Assemble full data bundle for a region-theme selection."""
info_sql = """
SELECT r.id, r.name, t.id, t.name
@ -4326,7 +4382,7 @@ def load_theme_payload(region_id: str, theme_id: str) -> Dict[str, object]:
raise ValueError("Region/theme combination not found")
region_uuid, region_name, theme_uuid, theme_name = row
permits = load_permits_and_risks(region_id, theme_id)
permits = load_permits_and_risks(region_id, theme_id, only_visible=only_visible)
return {
"region": {"id": str(region_uuid), "name": str(region_name)},
"theme": {"id": str(theme_uuid), "name": str(theme_name)},
@ -6170,23 +6226,25 @@ def filter_permits_advanced(
themes: Optional[List[str]] = None,
departments: Optional[List[str]] = None,
search_text: Optional[str] = None,
visibility: Optional[str] = None, # 'visible', 'hidden' or None/all
limit: int = 100,
offset: int = 0,
) -> Dict[str, Any]:
"""Filter permits using multiple dimensions (region, theme, department, search text).
"""Filter permits using multiple dimensions (region, theme, department, search text, visibility).
Args:
regions: List of region IDs to filter by (supports multi-select)
themes: List of theme IDs to filter by (supports multi-select)
departments: List of department IDs to filter by (supports multi-select)
search_text: Search in permit name
visibility: Filter by v2 visibility ('visible', 'hidden')
limit: Maximum number of results to return
offset: Offset for pagination
Returns:
Dictionary containing filtered permits and metadata
"""
print(f"[DEBUG] filter_permits_advanced called with limit={limit}, offset={offset}")
print(f"[DEBUG] filter_permits_advanced called with limit={limit}, offset={offset}, visibility={visibility}")
# Use subquery to avoid DISTINCT with window functions issue
# Subquery to get unique permits matching filters with pagination
# We use a CTE to ensure limit/offset apply to unique permits, not to rows (which can duplicate per theme)
@ -6212,6 +6270,11 @@ def filter_permits_advanced(
base_where += f" AND LOWER(p.name) LIKE LOWER(%s)"
base_params.append(f"%{search_text}%")
if visibility == 'visible':
base_where += " AND (rpd.is_v2_visible IS TRUE OR rpd.is_v2_visible IS NULL)"
elif visibility == 'hidden':
base_where += " AND rpd.is_v2_visible IS FALSE"
sql = f"""
WITH filtered_p AS (
SELECT rpd.permit_id, rpd.region_id
@ -6236,7 +6299,8 @@ def filter_permits_advanced(
rtp.theme_id,
t.name AS theme_name,
COALESCE(risk_counts.risk_count, 0) AS risk_count,
COALESCE(theme_counts.theme_count, 0) AS theme_count
COALESCE(theme_counts.theme_count, 0) AS theme_count,
COALESCE(rpd.is_v2_visible, true) AS is_v2_visible
FROM filtered_p fp
JOIN region_permit_details rpd ON rpd.permit_id = fp.permit_id AND rpd.region_id = fp.region_id
JOIN permits p ON p.id = rpd.permit_id
@ -6280,6 +6344,7 @@ def filter_permits_advanced(
theme_name,
risk_count,
theme_count,
v2_visible,
) in cur.fetchall():
pid = str(permit_id)
key = f"{pid}_{rid}"
@ -6294,6 +6359,7 @@ def filter_permits_advanced(
"themes": [],
"risk_count": int(risk_count or 0),
"theme_count": int(theme_count or 0),
"is_v2_visible": bool(v2_visible),
}
if tid or theme_name:
@ -6340,8 +6406,72 @@ def filter_permits_advanced(
}
def _ensure_v2_visibility_column(conn: Optional[pg.Connection] = None) -> None:
"""Ensure that the is_v2_visible column exists in region_permit_details."""
global _V2_VISIBILITY_SCHEMA_READY
if _V2_VISIBILITY_SCHEMA_READY:
return
with _V2_VISIBILITY_SCHEMA_LOCK:
if _V2_VISIBILITY_SCHEMA_READY:
return
sql = "ALTER TABLE region_permit_details ADD COLUMN IF NOT EXISTS is_v2_visible BOOLEAN DEFAULT TRUE"
if conn is not None:
original_autocommit = conn.autocommit
try:
conn.autocommit = True
cur = conn.cursor()
cur.execute(sql)
finally:
conn.autocommit = original_autocommit
else:
with _lic_pg_conn(autocommit=True) as ensure_conn:
cur = ensure_conn.cursor()
cur.execute(sql)
_V2_VISIBILITY_SCHEMA_READY = True
def _ensure_contact_info_column(conn: pg.Connection) -> None:
"Ensure that the contact_info column exists in region_permit_details."
# This check is now redundant since schema fix script was run, but kept for safety
pass
def update_permit_v2_visibility(
region_id: str,
permit_id: str,
is_visible: bool,
operator: str = "admin"
) -> bool:
"""Update the is_v2_visible flag for a specific region-permit pair."""
_ensure_v2_visibility_column()
sql = """
UPDATE region_permit_details
SET is_v2_visible = %s,
updated_at = now()
WHERE region_id = %s AND permit_id = %s
"""
with _lic_pg_conn() as conn:
cur = conn.cursor()
cur.execute(sql, (is_visible, region_id, permit_id))
count = cur.rowcount
conn.commit()
if count > 0:
log_operation(
operator=operator,
operation_type="UPDATE",
target_type="PERMIT_VISIBILITY",
target_id=f"{region_id}:{permit_id}",
target_name=f"Permit {permit_id} in region {region_id}",
change_summary=f"Set is_v2_visible to {is_visible}",
details={"is_visible": is_visible, "region_id": region_id, "permit_id": permit_id}
)
return True
return False

View File

@ -2329,6 +2329,57 @@
max-height: 600px;
overflow-y: auto;
}
/* Toggle Switch Styles */
.switch {
position: relative;
display: inline-block;
width: 44px;
height: 22px;
}
.switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
transition: .3s;
border-radius: 22px;
}
.slider:before {
position: absolute;
content: "";
height: 16px;
width: 16px;
left: 3px;
bottom: 3px;
background-color: white;
transition: .3s;
border-radius: 50%;
}
input:checked+.slider {
background-color: #10b981;
}
input:checked+.slider:before {
transform: translateX(22px);
}
input:disabled+.slider {
opacity: 0.5;
cursor: not-allowed;
}
</style>
</head>
@ -2514,6 +2565,17 @@
</div>
</div>
<!-- 启用状态筛选 -->
<div style="display: flex; flex-direction: column; gap: 8px;">
<label style="font-size: 13px; font-weight: 600; color: #555;">启用状态</label>
<select id="filterVisibility"
style="padding: 10px; border: 1px solid #ddd; border-radius: 6px; font-size: 14px; background: white;">
<option value="all">全部状态</option>
<option value="visible">已启用</option>
<option value="hidden">已隐藏</option>
</select>
</div>
<!-- 搜索关键词 -->
<div style="display: flex; flex-direction: column; gap: 8px;">
<label style="font-size: 13px; font-weight: 600; color: #555;">搜索关键词</label>
@ -6424,6 +6486,8 @@
const departmentCheckboxes = document.querySelectorAll('input[name="departmentFilter"]:checked');
const departments = Array.from(departmentCheckboxes).map(cb => cb.value);
const visibility = document.getElementById('filterVisibility')?.value || 'all';
const searchText = document.getElementById('filterSearchText')?.value || '';
const filters = {
@ -6432,7 +6496,8 @@
departments: departments.length > 0 ? departments : null,
search_text: searchText.trim() || null,
limit: permitPageSize,
offset: permitCurrentPage * permitPageSize
offset: permitCurrentPage * permitPageSize,
visibility: visibility !== 'all' ? visibility : null
};
// 显示加载状态
@ -6808,6 +6873,63 @@
}
}
// 切换许可事项可见性
async function togglePermitVisibility(permitId, regionId, currentStatus, event) {
const nextStatus = !currentStatus;
// 立即禁用开关防止重复点击
if (event && event.target) {
event.target.disabled = true;
}
try {
const response = await fetch('/fs-ai-asistant/api/workflow/lawrisk/admin/permits/visibility', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
permit_id: permitId,
region_id: regionId,
is_v2_visible: nextStatus
})
});
const data = await response.json();
if (!data.success) {
throw new Error(data.message || '更新失败');
}
// 立即更新前端状态参数以便下次点击
if (event && event.target) {
event.target.setAttribute('onclick', `togglePermitVisibility('${permitId}', '${regionId}', ${nextStatus}, event)`);
// 可选:给个微弱的小提示
const row = event.target.closest('tr');
if (row) {
const originalBg = row.style.backgroundColor;
row.style.backgroundColor = '#f0fdf4';
setTimeout(() => row.style.backgroundColor = originalBg, 500);
}
} else {
applyPermitFilter();
}
} catch (error) {
console.error('更新可见性失败:', error);
showAlert('error', '更新失败:' + error.message);
// 恢复原状
if (event && event.target) {
event.target.checked = currentStatus;
}
} finally {
if (event && event.target) {
event.target.disabled = false;
}
}
}
// 删除许可事项
async function deletePermit(permitId, regionId) {
if (!confirm('确定要删除该许可事项吗?此操作不可恢复,并且会创建风险快照。')) {
@ -6873,6 +6995,7 @@
<th style="padding: 14px 16px; text-align: left; font-weight: 600; color: #555; font-size: 14px; width: 180px;">行政区域</th>
<th style="padding: 14px 16px; text-align: left; font-weight: 600; color: #555; font-size: 14px; width: 120px;">主题</th>
<th style="padding: 14px 16px; text-align: left; font-weight: 600; color: #555; font-size: 14px; width: 100px;">风险数</th>
<th style="padding: 14px 16px; text-align: left; font-weight: 600; color: #555; font-size: 14px; width: 100px;">启用</th>
<th style="padding: 14px 16px; text-align: left; font-weight: 600; color: #555; font-size: 14px; width: 180px;">操作</th>
</tr>
</thead>
@ -6895,6 +7018,13 @@
<td style="padding: 16px; color: #666;">${escapeHtml(permit.region?.name || '-')}</td>
<td style="padding: 16px; color: #666;">${permit.theme_count || 0} 个</td>
<td style="padding: 16px; color: #666;">${permit.risk_count || 0}</td>
<td style="padding: 16px;">
<label class="switch">
<input type="checkbox" ${permit.is_v2_visible ? 'checked' : ''}
onclick="togglePermitVisibility('${permit.id}', '${regionId}', ${permit.is_v2_visible}, event)">
<span class="slider"></span>
</label>
</td>
<td style="padding: 16px;">
<button onclick="viewPermitDetail('${permit.id}', '${regionId}')" style="padding: 6px 12px; background: #2c5282; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 13px; margin-right: 8px;">
查看
@ -6968,6 +7098,9 @@
if (departmentSelect) departmentSelect.value = '';
if (searchInput) searchInput.value = '';
const filterVisibility = document.getElementById('filterVisibility');
if (filterVisibility) filterVisibility.value = 'all';
permitCurrentPage = 0;
// 清空结果