1152 lines
46 KiB
HTML
1152 lines
46 KiB
HTML
<!doctype html>
|
||
<html lang="zh-CN">
|
||
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<title>LawRisk V2 超级管理员调试台</title>
|
||
<style>
|
||
:root {
|
||
color-scheme: light;
|
||
font-family: "Noto Sans SC", "Segoe UI", "Helvetica Neue", system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
|
||
}
|
||
|
||
* {
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
body {
|
||
margin: 0;
|
||
padding: 0;
|
||
background: linear-gradient(135deg, #eef2ff, #f8fafc);
|
||
color: #0f172a;
|
||
}
|
||
|
||
.page {
|
||
max-width: 1400px;
|
||
margin: 0 auto;
|
||
padding: 28px 18px 60px;
|
||
}
|
||
|
||
header {
|
||
background: #0f172a;
|
||
color: #e2e8f0;
|
||
border-radius: 18px;
|
||
padding: 22px;
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 12px;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
box-shadow: 0 24px 60px rgba(15, 23, 42, 0.24);
|
||
}
|
||
|
||
header h1 {
|
||
margin: 0;
|
||
font-size: 24px;
|
||
letter-spacing: 0.5px;
|
||
}
|
||
|
||
header .meta {
|
||
display: flex;
|
||
gap: 12px;
|
||
flex-wrap: wrap;
|
||
align-items: center;
|
||
}
|
||
|
||
header .badge {
|
||
padding: 6px 10px;
|
||
border-radius: 10px;
|
||
font-size: 13px;
|
||
background: #22d3ee;
|
||
color: #0f172a;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.card {
|
||
background: #fff;
|
||
border-radius: 16px;
|
||
padding: 20px;
|
||
margin-top: 18px;
|
||
box-shadow: 0 16px 45px rgba(148, 163, 184, 0.25);
|
||
}
|
||
|
||
.card h2 {
|
||
margin: 0 0 12px;
|
||
font-size: 18px;
|
||
color: #0f172a;
|
||
}
|
||
|
||
.muted {
|
||
color: #475569;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
|
||
gap: 14px;
|
||
}
|
||
|
||
label {
|
||
display: block;
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
color: #1e293b;
|
||
margin-bottom: 6px;
|
||
}
|
||
|
||
input[type="text"],
|
||
input[type="number"],
|
||
select,
|
||
textarea {
|
||
width: 100%;
|
||
padding: 10px 12px;
|
||
border: 1px solid #cbd5e1;
|
||
border-radius: 10px;
|
||
font-size: 14px;
|
||
background: #f8fafc;
|
||
transition: border-color 0.15s ease, box-shadow 0.15s ease;
|
||
}
|
||
|
||
select[multiple] {
|
||
height: 120px;
|
||
}
|
||
|
||
input:focus,
|
||
select:focus,
|
||
textarea:focus {
|
||
outline: none;
|
||
border-color: #6366f1;
|
||
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.18);
|
||
background: #fff;
|
||
}
|
||
|
||
button {
|
||
border: none;
|
||
border-radius: 10px;
|
||
padding: 10px 16px;
|
||
font-size: 14px;
|
||
font-weight: 700;
|
||
cursor: pointer;
|
||
transition: transform 0.12s ease, box-shadow 0.12s ease;
|
||
}
|
||
|
||
.btn-primary {
|
||
background: linear-gradient(120deg, #6366f1, #22d3ee);
|
||
color: #0f172a;
|
||
}
|
||
|
||
.btn-secondary {
|
||
background: #e2e8f0;
|
||
color: #0f172a;
|
||
}
|
||
|
||
.btn-text {
|
||
background: transparent;
|
||
color: #2563eb;
|
||
padding: 6px 10px;
|
||
}
|
||
|
||
button:disabled {
|
||
opacity: 0.65;
|
||
cursor: not-allowed;
|
||
transform: none;
|
||
box-shadow: none;
|
||
}
|
||
|
||
button:not(:disabled):hover {
|
||
transform: translateY(-1px);
|
||
box-shadow: 0 10px 20px rgba(99, 102, 241, 0.25);
|
||
}
|
||
|
||
.actions {
|
||
display: flex;
|
||
gap: 10px;
|
||
flex-wrap: wrap;
|
||
align-items: center;
|
||
}
|
||
|
||
.status {
|
||
margin-top: 10px;
|
||
font-size: 14px;
|
||
min-height: 20px;
|
||
}
|
||
|
||
.status.ok {
|
||
color: #15803d;
|
||
}
|
||
|
||
.status.warn {
|
||
color: #b91c1c;
|
||
}
|
||
|
||
.pill {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
padding: 6px 10px;
|
||
border-radius: 999px;
|
||
background: #e0f2fe;
|
||
color: #0c4a6e;
|
||
font-size: 12px;
|
||
margin-right: 8px;
|
||
}
|
||
|
||
.pill.gray {
|
||
background: #e2e8f0;
|
||
color: #0f172a;
|
||
}
|
||
|
||
.section-title {
|
||
font-size: 16px;
|
||
margin: 18px 0 10px;
|
||
}
|
||
|
||
.subject {
|
||
border: 1px solid #e2e8f0;
|
||
border-radius: 14px;
|
||
padding: 14px;
|
||
margin-top: 12px;
|
||
background: #f8fafc;
|
||
}
|
||
|
||
.subject h3 {
|
||
margin: 0 0 6px;
|
||
font-size: 16px;
|
||
}
|
||
|
||
.subject-meta {
|
||
color: #475569;
|
||
font-size: 13px;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
table {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
margin-top: 10px;
|
||
font-size: 13px;
|
||
}
|
||
|
||
th,
|
||
td {
|
||
border: 1px solid #e2e8f0;
|
||
padding: 10px 8px;
|
||
text-align: left;
|
||
vertical-align: top;
|
||
}
|
||
|
||
th {
|
||
background: #f1f5f9;
|
||
color: #0f172a;
|
||
font-weight: 700;
|
||
}
|
||
|
||
td ul {
|
||
margin: 0;
|
||
padding-left: 16px;
|
||
}
|
||
|
||
.raw {
|
||
background: #0f172a;
|
||
color: #e2e8f0;
|
||
border-radius: 12px;
|
||
padding: 14px;
|
||
overflow: auto;
|
||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||
font-size: 12px;
|
||
max-height: 320px;
|
||
}
|
||
|
||
.raw.collapsed {
|
||
display: none;
|
||
}
|
||
|
||
.list-inline {
|
||
display: flex;
|
||
gap: 8px;
|
||
flex-wrap: wrap;
|
||
padding: 0;
|
||
margin: 0;
|
||
list-style: none;
|
||
}
|
||
|
||
.chip {
|
||
background: #e2e8f0;
|
||
border-radius: 8px;
|
||
padding: 4px 8px;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.empty {
|
||
text-align: center;
|
||
padding: 32px 12px;
|
||
color: #94a3b8;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.tag {
|
||
display: inline-block;
|
||
padding: 4px 6px;
|
||
border-radius: 6px;
|
||
background: #fef3c7;
|
||
color: #92400e;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.panel {
|
||
position: fixed;
|
||
right: 0;
|
||
top: 0;
|
||
bottom: 0;
|
||
width: 460px;
|
||
max-width: 100%;
|
||
background: #fff;
|
||
box-shadow: -6px 0 30px rgba(15, 23, 42, 0.18);
|
||
transform: translateX(100%);
|
||
transition: transform 0.25s ease;
|
||
z-index: 40;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.panel.open {
|
||
transform: translateX(0);
|
||
}
|
||
|
||
.panel-header {
|
||
position: sticky;
|
||
top: 0;
|
||
background: #fff;
|
||
padding: 16px 18px;
|
||
border-bottom: 1px solid #e2e8f0;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
gap: 10px;
|
||
}
|
||
|
||
.panel-body {
|
||
padding: 16px 18px 60px;
|
||
}
|
||
|
||
.backdrop {
|
||
position: fixed;
|
||
inset: 0;
|
||
background: rgba(15, 23, 42, 0.35);
|
||
opacity: 0;
|
||
visibility: hidden;
|
||
transition: opacity 0.2s ease;
|
||
z-index: 30;
|
||
}
|
||
|
||
.backdrop.show {
|
||
opacity: 1;
|
||
visibility: visible;
|
||
}
|
||
|
||
.loader {
|
||
display: inline-block;
|
||
width: 18px;
|
||
height: 18px;
|
||
border: 3px solid #e2e8f0;
|
||
border-top-color: #6366f1;
|
||
border-radius: 50%;
|
||
animation: spin 0.9s linear infinite;
|
||
vertical-align: middle;
|
||
margin-right: 8px;
|
||
}
|
||
|
||
.checkbox-list {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
padding: 10px;
|
||
border: 1px solid #e2e8f0;
|
||
border-radius: 12px;
|
||
background: #f8fafc;
|
||
max-height: 180px;
|
||
overflow: auto;
|
||
}
|
||
|
||
.checkbox-item {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
padding: 6px 10px;
|
||
border-radius: 10px;
|
||
background: #fff;
|
||
border: 1px solid #e2e8f0;
|
||
cursor: pointer;
|
||
font-size: 13px;
|
||
box-shadow: 0 4px 10px rgba(148, 163, 184, 0.2);
|
||
}
|
||
|
||
.checkbox-item input {
|
||
accent-color: #6366f1;
|
||
}
|
||
|
||
@keyframes spin {
|
||
to {
|
||
transform: rotate(360deg);
|
||
}
|
||
}
|
||
|
||
@media (max-width: 720px) {
|
||
header {
|
||
flex-direction: column;
|
||
align-items: flex-start;
|
||
}
|
||
|
||
.panel {
|
||
width: 100%;
|
||
}
|
||
}
|
||
</style>
|
||
</head>
|
||
|
||
<body>
|
||
<div class="page">
|
||
<header>
|
||
<div>
|
||
<h1>LawRisk V2 超级管理员调试台</h1>
|
||
<div class="muted">面向 V2 问答与许可筛选的调试工具,需已登录并具备管理员权限。</div>
|
||
</div>
|
||
<div class="meta">
|
||
<span class="badge">Super Admin Only</span>
|
||
<button class="btn-secondary" id="reload-all-btn">刷新选项缓存</button>
|
||
<button class="btn-secondary" id="logout-btn" style="background:#fecdd3;color:#7f1d1d;">退出登录</button>
|
||
</div>
|
||
</header>
|
||
|
||
<section class="card">
|
||
<div
|
||
style="display: flex; justify-content: space-between; align-items: flex-start; gap: 12px; flex-wrap: wrap;">
|
||
<div>
|
||
<h2>V2 问答接口调试</h2>
|
||
<div class="muted">选择地区 + 输入问题,可切换 debug 与 top 参数,右侧可直达许可详情。</div>
|
||
</div>
|
||
<div class="actions">
|
||
<button id="toggle-search-raw" class="btn-text" style="display:none;">显示原始 JSON</button>
|
||
</div>
|
||
</div>
|
||
|
||
<form id="search-form" style="margin-top: 10px;">
|
||
<div class="grid">
|
||
<div>
|
||
<label for="search-query">问题</label>
|
||
<input id="search-query" type="text" placeholder="例如:开办电影院需要哪些许可事项" required>
|
||
</div>
|
||
<div>
|
||
<label for="search-regions">限定地区(可多选,默认全部)</label>
|
||
<select id="search-regions" multiple></select>
|
||
</div>
|
||
<div class="grid" style="grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 10px;">
|
||
<div>
|
||
<label for="search-top">top 推荐数量</label>
|
||
<input id="search-top" type="number" min="1" max="10" value="5">
|
||
</div>
|
||
<div>
|
||
<label style="visibility:hidden;">调试</label>
|
||
<div style="display:flex; align-items:center; gap:8px; padding:10px 0;">
|
||
<input id="search-debug" type="checkbox">
|
||
<span class="muted">debug=1</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="actions" style="margin-top: 12px;">
|
||
<button type="submit" class="btn-primary" id="search-submit">发送请求</button>
|
||
<button type="button" class="btn-secondary" id="search-reset">清空</button>
|
||
</div>
|
||
</form>
|
||
<div id="search-status" class="status"></div>
|
||
<div id="search-results"></div>
|
||
<pre id="search-raw" class="raw collapsed"></pre>
|
||
</section>
|
||
|
||
<section class="card">
|
||
<div
|
||
style="display: flex; justify-content: space-between; align-items: flex-start; gap: 12px; flex-wrap: wrap;">
|
||
<div>
|
||
<h2>许可筛选与查询</h2>
|
||
<div class="muted">多维筛选(地区/主题/部门/关键词),点击行可查看详情。</div>
|
||
</div>
|
||
<div class="actions">
|
||
<button id="toggle-filter-raw" class="btn-text" style="display:none;">显示筛选 JSON</button>
|
||
</div>
|
||
</div>
|
||
|
||
<form id="filter-form" style="margin-top: 10px;">
|
||
<div class="grid">
|
||
<div>
|
||
<label>地区(多选)</label>
|
||
<div id="filter-region-box" class="checkbox-list"></div>
|
||
</div>
|
||
<div>
|
||
<label>主题(多选)</label>
|
||
<div id="filter-theme-box" class="checkbox-list"></div>
|
||
</div>
|
||
<div>
|
||
<label>部门(多选)</label>
|
||
<div id="filter-dept-box" class="checkbox-list"></div>
|
||
</div>
|
||
<div>
|
||
<label for="filter-text">许可名称关键词</label>
|
||
<input id="filter-text" type="text" placeholder="输入许可名称片段">
|
||
</div>
|
||
<div class="grid" style="grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 10px;">
|
||
<div>
|
||
<label for="filter-limit">单页数量</label>
|
||
<input id="filter-limit" type="number" min="1" max="200" value="50">
|
||
</div>
|
||
<div>
|
||
<label for="filter-offset">偏移量</label>
|
||
<input id="filter-offset" type="number" min="0" value="0">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="actions" style="margin-top: 12px;">
|
||
<button type="button" class="btn-primary" id="filter-apply">应用筛选</button>
|
||
<button type="button" class="btn-secondary" id="filter-reset">重置</button>
|
||
</div>
|
||
</form>
|
||
<div id="filter-status" class="status"></div>
|
||
<div id="filter-results"></div>
|
||
<pre id="filter-raw" class="raw collapsed"></pre>
|
||
</section>
|
||
</div>
|
||
|
||
<div class="backdrop" id="backdrop"></div>
|
||
<aside class="panel" id="detail-panel" aria-label="许可详情">
|
||
<div class="panel-header">
|
||
<strong id="detail-title">许可详情</strong>
|
||
<div class="actions">
|
||
<button class="btn-text" id="detail-raw-toggle" style="display:none;">显示详情 JSON</button>
|
||
<button class="btn-secondary" id="detail-close">关闭</button>
|
||
</div>
|
||
</div>
|
||
<div class="panel-body" id="detail-body">
|
||
<div class="muted">加载中...</div>
|
||
</div>
|
||
<pre class="raw collapsed" id="detail-raw"></pre>
|
||
</aside>
|
||
|
||
<script>
|
||
(() => {
|
||
const endpoints = {
|
||
regions: "/fs-ai-asistant/api/workflow/lawrisk/v2/regions",
|
||
search: "/fs-ai-asistant/api/workflow/lawrisk/v2",
|
||
filterOptions: "/fs-ai-asistant/api/workflow/lawrisk/admin/permits/filter-options",
|
||
advancedFilter: "/fs-ai-asistant/api/workflow/lawrisk/admin/permits/advanced-filter",
|
||
permitDetails: "/fs-ai-asistant/api/workflow/lawrisk/admin/permit-details",
|
||
};
|
||
|
||
const searchForm = document.getElementById("search-form");
|
||
const searchQuery = document.getElementById("search-query");
|
||
const searchRegions = document.getElementById("search-regions");
|
||
const searchTop = document.getElementById("search-top");
|
||
const searchDebug = document.getElementById("search-debug");
|
||
const searchStatus = document.getElementById("search-status");
|
||
const searchResults = document.getElementById("search-results");
|
||
const searchRaw = document.getElementById("search-raw");
|
||
const toggleSearchRawBtn = document.getElementById("toggle-search-raw");
|
||
const searchReset = document.getElementById("search-reset");
|
||
const searchSubmit = document.getElementById("search-submit");
|
||
|
||
const filterRegionBox = document.getElementById("filter-region-box");
|
||
const filterThemeBox = document.getElementById("filter-theme-box");
|
||
const filterDeptBox = document.getElementById("filter-dept-box");
|
||
const filterText = document.getElementById("filter-text");
|
||
const filterLimit = document.getElementById("filter-limit");
|
||
const filterOffset = document.getElementById("filter-offset");
|
||
const filterStatus = document.getElementById("filter-status");
|
||
const filterResults = document.getElementById("filter-results");
|
||
const filterRaw = document.getElementById("filter-raw");
|
||
const toggleFilterRawBtn = document.getElementById("toggle-filter-raw");
|
||
const filterApply = document.getElementById("filter-apply");
|
||
const filterReset = document.getElementById("filter-reset");
|
||
|
||
const reloadAllBtn = document.getElementById("reload-all-btn");
|
||
const logoutBtn = document.getElementById("logout-btn");
|
||
const detailPanel = document.getElementById("detail-panel");
|
||
const detailBody = document.getElementById("detail-body");
|
||
const detailTitle = document.getElementById("detail-title");
|
||
const detailClose = document.getElementById("detail-close");
|
||
const detailRaw = document.getElementById("detail-raw");
|
||
const detailRawToggle = document.getElementById("detail-raw-toggle");
|
||
const backdrop = document.getElementById("backdrop");
|
||
|
||
let lastSearchPayload = null;
|
||
let lastFilterPayload = null;
|
||
const detailCache = new Map();
|
||
|
||
document.addEventListener("DOMContentLoaded", () => {
|
||
loadRegions();
|
||
loadFilterOptions();
|
||
searchQuery.focus();
|
||
});
|
||
|
||
reloadAllBtn.addEventListener("click", () => {
|
||
loadRegions(true);
|
||
loadFilterOptions(true);
|
||
});
|
||
|
||
logoutBtn.addEventListener("click", async () => {
|
||
try {
|
||
setSearchStatus("正在退出...", "ok");
|
||
const res = await fetch("/auth/logout", {
|
||
method: "POST",
|
||
credentials: "include",
|
||
headers: { "Content-Type": "application/json" }
|
||
});
|
||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||
// 跳转登录页强制重新认证
|
||
window.location.href = "/fs-ai-asistant/api/workflow/lawrisk/login?force=1";
|
||
} catch (err) {
|
||
setSearchStatus(`退出失败:${err.message}`, "warn");
|
||
}
|
||
});
|
||
|
||
searchForm.addEventListener("submit", async (e) => {
|
||
e.preventDefault();
|
||
await runSearch();
|
||
});
|
||
|
||
searchReset.addEventListener("click", () => {
|
||
searchQuery.value = "";
|
||
Array.from(searchRegions.options).forEach(opt => opt.selected = false);
|
||
searchTop.value = 5;
|
||
searchDebug.checked = false;
|
||
searchStatus.textContent = "";
|
||
searchResults.innerHTML = "";
|
||
searchRaw.textContent = "";
|
||
searchRaw.classList.add("collapsed");
|
||
toggleSearchRawBtn.style.display = "none";
|
||
});
|
||
|
||
toggleSearchRawBtn.addEventListener("click", () => {
|
||
const hidden = searchRaw.classList.toggle("collapsed");
|
||
toggleSearchRawBtn.textContent = hidden ? "显示原始 JSON" : "隐藏原始 JSON";
|
||
});
|
||
|
||
filterApply.addEventListener("click", () => runFilter());
|
||
filterReset.addEventListener("click", () => {
|
||
[filterRegionBox, filterThemeBox, filterDeptBox].forEach(box => {
|
||
box.querySelectorAll("input[type='checkbox']").forEach(input => { input.checked = false; });
|
||
});
|
||
filterText.value = "";
|
||
filterLimit.value = 50;
|
||
filterOffset.value = 0;
|
||
filterStatus.textContent = "";
|
||
filterResults.innerHTML = "";
|
||
filterRaw.textContent = "";
|
||
filterRaw.classList.add("collapsed");
|
||
toggleFilterRawBtn.style.display = "none";
|
||
});
|
||
|
||
toggleFilterRawBtn.addEventListener("click", () => {
|
||
const hidden = filterRaw.classList.toggle("collapsed");
|
||
toggleFilterRawBtn.textContent = hidden ? "显示筛选 JSON" : "隐藏筛选 JSON";
|
||
});
|
||
|
||
detailClose.addEventListener("click", closeDetail);
|
||
backdrop.addEventListener("click", closeDetail);
|
||
detailRawToggle.addEventListener("click", () => {
|
||
const hidden = detailRaw.classList.toggle("collapsed");
|
||
detailRawToggle.textContent = hidden ? "显示详情 JSON" : "隐藏详情 JSON";
|
||
});
|
||
|
||
async function loadRegions(force = false) {
|
||
if (searchRegions.options.length > 0 && !force) return;
|
||
setSearchStatus("加载地区列表...", "ok");
|
||
try {
|
||
const res = await fetch(endpoints.regions, { credentials: "include" });
|
||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||
const payload = await res.json();
|
||
const regions = (payload && payload.data && Array.isArray(payload.data.regions)) ? payload.data.regions : [];
|
||
renderRegionOptions(searchRegions, regions, true);
|
||
setSearchStatus("地区列表已加载", "ok");
|
||
} catch (err) {
|
||
setSearchStatus(`地区列表加载失败:${err.message}`, "warn");
|
||
}
|
||
}
|
||
|
||
async function loadFilterOptions(force = false) {
|
||
if (filterRegionBox.childElementCount > 0 && !force) return;
|
||
setFilterStatus("加载筛选选项...", "ok");
|
||
try {
|
||
const res = await fetch(endpoints.filterOptions, { credentials: "include" });
|
||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||
const payload = await res.json();
|
||
if (!payload.success) throw new Error(payload.message || "接口返回失败");
|
||
const data = payload.data || {};
|
||
renderCheckboxList(filterRegionBox, data.regions || []);
|
||
renderCheckboxList(filterThemeBox, data.themes || []);
|
||
renderCheckboxList(filterDeptBox, data.departments || [], true);
|
||
setFilterStatus("筛选选项已加载", "ok");
|
||
} catch (err) {
|
||
setFilterStatus(`筛选选项加载失败:${err.message}`, "warn");
|
||
}
|
||
}
|
||
|
||
function renderRegionOptions(selectEl, regions, addAllLabel) {
|
||
selectEl.innerHTML = "";
|
||
if (addAllLabel) {
|
||
const opt = document.createElement("option");
|
||
opt.value = "";
|
||
opt.textContent = "全部地区";
|
||
selectEl.appendChild(opt);
|
||
}
|
||
regions.forEach(region => {
|
||
if (!region || !region.id) return;
|
||
const opt = document.createElement("option");
|
||
opt.value = region.id;
|
||
opt.textContent = region.name || region.id;
|
||
selectEl.appendChild(opt);
|
||
});
|
||
}
|
||
|
||
function renderCheckboxList(container, items, includeCode = false) {
|
||
container.innerHTML = "";
|
||
items.forEach(item => {
|
||
if (!item) return;
|
||
const id = item.id || "";
|
||
const name = item.name || id || "未命名";
|
||
const label = includeCode && item.code ? `${name} (${item.code})` : name;
|
||
const wrapper = document.createElement("label");
|
||
wrapper.className = "checkbox-item";
|
||
const input = document.createElement("input");
|
||
input.type = "checkbox";
|
||
input.value = id;
|
||
wrapper.appendChild(input);
|
||
const span = document.createElement("span");
|
||
span.textContent = label;
|
||
wrapper.appendChild(span);
|
||
container.appendChild(wrapper);
|
||
});
|
||
}
|
||
|
||
async function runSearch() {
|
||
const query = searchQuery.value.trim();
|
||
if (!query) {
|
||
setSearchStatus("请输入问题后再查询。", "warn");
|
||
return;
|
||
}
|
||
setSearchStatus("查询中,请稍候...", "ok", true);
|
||
searchResults.innerHTML = "";
|
||
searchRaw.textContent = "";
|
||
|
||
const params = new URLSearchParams();
|
||
params.set("query", query);
|
||
const regions = getSelectedValues(searchRegions);
|
||
regions.forEach(r => params.append("region", r));
|
||
params.set("top", searchTop.value || "5");
|
||
if (searchDebug.checked) params.set("debug", "1");
|
||
|
||
try {
|
||
const started = performance.now();
|
||
const res = await fetch(`${endpoints.search}?${params.toString()}`, { credentials: "include" });
|
||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||
const payload = await res.json();
|
||
lastSearchPayload = payload;
|
||
renderSearchPayload(payload, performance.now() - started);
|
||
} catch (err) {
|
||
setSearchStatus(`请求失败:${err.message}`, "warn");
|
||
} finally {
|
||
setLoading(searchSubmit, false);
|
||
}
|
||
}
|
||
|
||
function renderSearchPayload(payload, durationMs) {
|
||
if (!payload || typeof payload !== "object") {
|
||
setSearchStatus("响应格式不正确。", "warn");
|
||
return;
|
||
}
|
||
const success = payload.success;
|
||
const data = payload.data || {};
|
||
const subjects = Array.isArray(data.risk_subject) ? data.risk_subject : [];
|
||
if (!success) {
|
||
setSearchStatus(payload.message ? `接口返回错误:${payload.message}` : "接口执行失败。", "warn");
|
||
return;
|
||
}
|
||
|
||
const permitCount = subjects.reduce((sum, s) => sum + (Array.isArray(s.permits) ? s.permits.length : 0), 0);
|
||
const cost = data.executionTime || Math.round(durationMs || 0);
|
||
setSearchStatus(`已匹配 ${subjects.length} 个主题,包含 ${permitCount} 条许可事项(${cost} ms)。`, subjects.length ? "ok" : "warn");
|
||
|
||
searchResults.innerHTML = subjects.length ? "" : `<div class="empty">未检索到相关主题或许可事项。</div>`;
|
||
subjects.forEach((subject, idx) => {
|
||
searchResults.appendChild(renderSubjectCard(subject, idx));
|
||
});
|
||
|
||
searchRaw.textContent = JSON.stringify(payload, null, 2);
|
||
searchRaw.classList.add("collapsed");
|
||
toggleSearchRawBtn.style.display = "inline-block";
|
||
toggleSearchRawBtn.textContent = "显示原始 JSON";
|
||
}
|
||
|
||
function renderSubjectCard(subject, index) {
|
||
const wrap = document.createElement("div");
|
||
wrap.className = "subject";
|
||
|
||
const title = document.createElement("h3");
|
||
title.textContent = subject.display_name || `主题 ${index + 1}`;
|
||
wrap.appendChild(title);
|
||
|
||
const meta = document.createElement("div");
|
||
meta.className = "subject-meta";
|
||
const regionName = subject.region?.name || subject.region_name || "未知地区";
|
||
const themeName = subject.theme?.name || subject.theme_name || "未知主题";
|
||
meta.textContent = `地区:${regionName} | 主题事项:${themeName}`;
|
||
wrap.appendChild(meta);
|
||
|
||
const permits = Array.isArray(subject.permits) ? subject.permits : [];
|
||
if (!permits.length) {
|
||
wrap.appendChild(emptyHint("该主题未关联具体许可事项。"));
|
||
return wrap;
|
||
}
|
||
|
||
const table = document.createElement("table");
|
||
const thead = document.createElement("thead");
|
||
thead.innerHTML = `
|
||
<tr>
|
||
<th>许可事项</th>
|
||
<th>事项属性</th>
|
||
<th>子项概述</th>
|
||
<th>经营范围</th>
|
||
<th>风险提示</th>
|
||
<th>操作</th>
|
||
</tr>
|
||
`;
|
||
table.appendChild(thead);
|
||
const tbody = document.createElement("tbody");
|
||
permits.forEach(permit => {
|
||
const tr = document.createElement("tr");
|
||
tr.appendChild(cell(permit.name || "未命名许可"));
|
||
tr.appendChild(cell(permit.permit_status || "—"));
|
||
tr.appendChild(cell(permit.subitem_summary || "—"));
|
||
const scopes = Array.isArray(permit.business_scopes)
|
||
? permit.business_scopes.map(s => s?.description || "").filter(Boolean).join("、")
|
||
: "";
|
||
tr.appendChild(cell(scopes || "—"));
|
||
tr.appendChild(riskCell(permit.risks));
|
||
const op = document.createElement("td");
|
||
const btn = document.createElement("button");
|
||
btn.className = "btn-primary";
|
||
btn.textContent = "查看详情";
|
||
btn.addEventListener("click", () => openDetail(
|
||
permit.id,
|
||
subject.region?.id || subject.region_id || "",
|
||
subject.theme?.id || subject.theme_id || ""
|
||
));
|
||
op.appendChild(btn);
|
||
tr.appendChild(op);
|
||
tbody.appendChild(tr);
|
||
});
|
||
table.appendChild(tbody);
|
||
wrap.appendChild(table);
|
||
return wrap;
|
||
}
|
||
|
||
async function runFilter() {
|
||
setFilterStatus("筛选中...", "ok", true);
|
||
filterResults.innerHTML = "";
|
||
filterRaw.textContent = "";
|
||
const selectedRegions = getCheckedValues(filterRegionBox);
|
||
const payload = {
|
||
regions: selectedRegions,
|
||
region: selectedRegions[0] || null,
|
||
themes: getCheckedValues(filterThemeBox),
|
||
departments: getCheckedValues(filterDeptBox),
|
||
search_text: filterText.value.trim() || null,
|
||
limit: parseInt(filterLimit.value || "50", 10),
|
||
offset: parseInt(filterOffset.value || "0", 10),
|
||
};
|
||
if (!payload.regions?.length) payload.regions = null, payload.region = null;
|
||
if (!payload.themes?.length) payload.themes = null;
|
||
if (!payload.departments?.length) payload.departments = null;
|
||
|
||
try {
|
||
const res = await fetch(endpoints.advancedFilter, {
|
||
method: "POST",
|
||
credentials: "include",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify(payload),
|
||
});
|
||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||
const data = await res.json();
|
||
if (!data.success) throw new Error(data.message || "筛选失败");
|
||
lastFilterPayload = data;
|
||
renderFilterResults(data.data || {});
|
||
} catch (err) {
|
||
setFilterStatus(`筛选失败:${err.message}`, "warn");
|
||
} finally {
|
||
setLoading(filterApply, false);
|
||
}
|
||
}
|
||
|
||
function renderFilterResults(result) {
|
||
let permits = Array.isArray(result.permits) ? result.permits : [];
|
||
const selectedRegions = getCheckedValues(filterRegionBox);
|
||
if (selectedRegions.length) {
|
||
permits = permits.filter(p => selectedRegions.includes(p.region?.id || p.region_id || ""));
|
||
}
|
||
const pagination = result.pagination || {};
|
||
const total = pagination.total ?? permits.length;
|
||
const applied = [];
|
||
if (selectedRegions.length) applied.push(`地区: ${selectedRegions.join(",")}`);
|
||
const selectedThemes = getCheckedValues(filterThemeBox);
|
||
if (selectedThemes.length) applied.push(`主题: ${selectedThemes.join(",")}`);
|
||
const selectedDepts = getCheckedValues(filterDeptBox);
|
||
if (selectedDepts.length) applied.push(`部门: ${selectedDepts.join(",")}`);
|
||
if (filterText.value.trim()) applied.push(`关键词: ${filterText.value.trim()}`);
|
||
const appliedText = applied.length ? `已用筛选 => ${applied.join(" | ")}` : "未使用筛选条件";
|
||
setFilterStatus(`共 ${total} 条,当前展示 ${permits.length} 条。${appliedText}`, permits.length ? "ok" : "warn");
|
||
|
||
if (!permits.length) {
|
||
filterResults.innerHTML = `<div class="empty">未找到符合条件的许可文件</div>`;
|
||
} else {
|
||
const table = document.createElement("table");
|
||
table.innerHTML = `
|
||
<thead>
|
||
<tr>
|
||
<th>许可名称</th>
|
||
<th>地区</th>
|
||
<th>主题</th>
|
||
<th>风险数</th>
|
||
<th>操作</th>
|
||
</tr>
|
||
</thead>
|
||
`;
|
||
const tbody = document.createElement("tbody");
|
||
permits.forEach(item => {
|
||
const tr = document.createElement("tr");
|
||
tr.appendChild(cell(item.name || "未命名许可"));
|
||
tr.appendChild(cell(item.region?.name || item.region_name || "—"));
|
||
tr.appendChild(cell(renderThemesText(item.themes)));
|
||
tr.appendChild(cell(typeof item.risk_count === "number" ? item.risk_count : "—"));
|
||
const op = document.createElement("td");
|
||
const btn = document.createElement("button");
|
||
btn.className = "btn-primary";
|
||
btn.textContent = "查看详情";
|
||
btn.addEventListener("click", () => openDetail(
|
||
item.id,
|
||
item.region?.id || item.region_id || "",
|
||
(item.themes && item.themes[0]?.id) || ""
|
||
));
|
||
op.appendChild(btn);
|
||
tr.appendChild(op);
|
||
tbody.appendChild(tr);
|
||
});
|
||
table.appendChild(tbody);
|
||
filterResults.innerHTML = "";
|
||
filterResults.appendChild(table);
|
||
}
|
||
|
||
filterRaw.textContent = JSON.stringify(result, null, 2);
|
||
filterRaw.classList.add("collapsed");
|
||
toggleFilterRawBtn.style.display = "inline-block";
|
||
toggleFilterRawBtn.textContent = "显示筛选 JSON";
|
||
}
|
||
|
||
async function openDetail(permitId, regionId, themeId) {
|
||
detailTitle.textContent = "许可详情";
|
||
detailBody.innerHTML = `<div class="muted"><span class="loader"></span>加载中...</div>`;
|
||
detailRaw.textContent = "";
|
||
detailRaw.classList.add("collapsed");
|
||
detailRawToggle.style.display = "none";
|
||
showDetailPanel(true);
|
||
|
||
if (!regionId) {
|
||
const selectedRegions = getCheckedValues(filterRegionBox);
|
||
regionId = selectedRegions[0] || regionId;
|
||
}
|
||
|
||
const key = `${regionId || ""}:${permitId || ""}:${themeId || ""}`;
|
||
|
||
if (detailCache.has(key)) {
|
||
renderDetail(detailCache.get(key));
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const params = new URLSearchParams();
|
||
params.set("region", regionId);
|
||
params.set("permit", permitId);
|
||
if (themeId) params.set("theme", themeId);
|
||
const res = await fetch(`${endpoints.permitDetails}?${params.toString()}`, { credentials: "include" });
|
||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||
const payload = await res.json();
|
||
if (!payload.success) throw new Error(payload.message || "加载详情失败");
|
||
detailCache.set(key, payload.data || {});
|
||
renderDetail(payload.data || {});
|
||
detailRaw.textContent = JSON.stringify(payload, null, 2);
|
||
detailRawToggle.style.display = "inline-block";
|
||
} catch (err) {
|
||
detailBody.innerHTML = `<div class="muted">加载失败:${err.message}</div>`;
|
||
}
|
||
}
|
||
|
||
function renderDetail(data) {
|
||
if (!data || !data.permit) {
|
||
detailBody.innerHTML = `<div class="muted">未找到许可详情。</div>`;
|
||
return;
|
||
}
|
||
const permit = data.permit;
|
||
const theme = data.theme_display || permit.theme;
|
||
detailTitle.textContent = permit.name || "许可详情";
|
||
|
||
const risks = Array.isArray(permit.risks) ? permit.risks : [];
|
||
const scopes = Array.isArray(permit.business_scopes) ? permit.business_scopes : [];
|
||
const themes = Array.isArray(permit.themes) ? permit.themes : [];
|
||
const source = permit.permit_source || {};
|
||
const fileMeta = permit.permit_file || {};
|
||
|
||
const themeChips = themes.length
|
||
? `<div class="list-inline">${themes.map(t => `<span class="chip">${escapeHtml(t.name || t.id || "")}</span>`).join("")}</div>`
|
||
: `<div class="muted">暂无关联主题</div>`;
|
||
|
||
const contact = permit.responsible_contact || permit.contact || permit.department_contact;
|
||
const jurisdiction = permit.jurisdiction_scope || permit.jurisdiction || permit.scope;
|
||
const subitem = permit.subitem_summary || permit.subitem || permit.item_summary;
|
||
|
||
detailBody.innerHTML = `
|
||
<div class="section-title">基本信息</div>
|
||
<div class="pill">地区:${escapeHtml(data.region || permit.region_id || permit.region?.name || "—")}</div>
|
||
<div class="pill gray">主题:${escapeHtml(theme?.name || theme?.id || "—")}</div>
|
||
<div style="margin-top:6px;">事项属性:${escapeHtml(permit.permit_status || "—")}</div>
|
||
<div style="margin-top:6px;">子项概述:${escapeHtml(subitem || "—")}</div>
|
||
<div style="margin-top:6px;">管辖范围:${escapeHtml(jurisdiction || "—")}</div>
|
||
<div style="margin-top:6px;">责任联系方式:${escapeHtml(contact || "—")}</div>
|
||
|
||
<div class="section-title">关联主题</div>
|
||
${themeChips}
|
||
|
||
<div class="section-title">经营范围</div>
|
||
${scopes.length ? `<ul>${scopes.map(s => `<li>${escapeHtml(s.description || "")}</li>`).join("")}</ul>` : `<div class="muted">暂无经营范围</div>`}
|
||
|
||
<div class="section-title">风险提示 (${risks.length})</div>
|
||
${risks.length ? risks.map(r => renderRiskItem(r)).join("") : `<div class="muted">暂无风险提示</div>`}
|
||
|
||
<div class="section-title">来源信息</div>
|
||
<div class="muted">来源类型:${escapeHtml(source.source_type || "—")}|来源名称:${escapeHtml(source.source_name || "—")}</div>
|
||
${source.source_detail ? `<div style="margin-top:4px;">来源详情:${escapeHtml(source.source_detail)}</div>` : ""}
|
||
|
||
<div class="section-title">附件</div>
|
||
${fileMeta.filename ? `<div class="muted">文件:${escapeHtml(fileMeta.filename)}(${fileMeta.file_size || 0} bytes)</div>` : `<div class="muted">暂无上传文件</div>`}
|
||
`;
|
||
}
|
||
|
||
function renderRiskItem(risk) {
|
||
const content = escapeHtml(risk.risk_content || "");
|
||
const basis = escapeHtml(risk.legal_basis || "");
|
||
const no = escapeHtml(risk.document_no || "");
|
||
const summary = escapeMultiline(risk.summary || "");
|
||
return `
|
||
<div style="padding:8px 10px; border:1px solid #e2e8f0; border-radius:10px; margin-bottom:8px;">
|
||
<div><strong>${content || "未填写风险内容"}</strong></div>
|
||
${basis ? `<div class="muted">法律依据:${basis}</div>` : ""}
|
||
${no ? `<div class="muted">文号:${no}</div>` : ""}
|
||
${summary ? `<div style="margin-top:6px; font-size:13px; color:#0f172a;">${summary}</div>` : ""}
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
function renderThemesText(themes) {
|
||
if (!Array.isArray(themes) || !themes.length) return "—";
|
||
return themes.map(t => t?.name || t?.id || "").filter(Boolean).join("、");
|
||
}
|
||
|
||
function setSearchStatus(text, tone = "ok", loading = false) {
|
||
searchStatus.textContent = text;
|
||
searchStatus.className = `status ${tone === "warn" ? "warn" : "ok"}`;
|
||
setLoading(searchSubmit, loading);
|
||
}
|
||
|
||
function setFilterStatus(text, tone = "ok", loading = false) {
|
||
filterStatus.textContent = text;
|
||
filterStatus.className = `status ${tone === "warn" ? "warn" : "ok"}`;
|
||
setLoading(filterApply, loading);
|
||
}
|
||
|
||
function setLoading(btn, loading) {
|
||
if (!btn) return;
|
||
btn.disabled = loading;
|
||
if (loading) btn.dataset.original = btn.textContent, btn.textContent = "处理中...";
|
||
else if (btn.dataset.original) btn.textContent = btn.dataset.original;
|
||
}
|
||
|
||
function cell(text) {
|
||
const td = document.createElement("td");
|
||
td.textContent = typeof text === "string" ? text : (text ?? "—");
|
||
return td;
|
||
}
|
||
|
||
function riskCell(risks) {
|
||
const td = document.createElement("td");
|
||
if (!Array.isArray(risks) || !risks.length) {
|
||
td.textContent = "—";
|
||
return td;
|
||
}
|
||
const ul = document.createElement("ul");
|
||
risks.forEach((risk, idx) => {
|
||
const li = document.createElement("li");
|
||
const lines = [];
|
||
if (risk.risk_content) lines.push(`${idx + 1}. ${risk.risk_content}`);
|
||
if (risk.legal_basis) lines.push(`法律依据:${risk.legal_basis}`);
|
||
if (risk.document_no) lines.push(`文号:${risk.document_no}`);
|
||
lines.forEach(line => {
|
||
const div = document.createElement("div");
|
||
div.textContent = line;
|
||
li.appendChild(div);
|
||
});
|
||
ul.appendChild(li);
|
||
});
|
||
td.appendChild(ul);
|
||
return td;
|
||
}
|
||
|
||
function emptyHint(text) {
|
||
const div = document.createElement("div");
|
||
div.className = "empty";
|
||
div.textContent = text;
|
||
return div;
|
||
}
|
||
|
||
function getSelectedValues(selectEl) {
|
||
return Array.from(selectEl.selectedOptions || [])
|
||
.map(opt => opt.value)
|
||
.filter(v => v && v.trim());
|
||
}
|
||
|
||
function getCheckedValues(container) {
|
||
return Array.from(container.querySelectorAll("input[type='checkbox']:checked"))
|
||
.map(input => input.value)
|
||
.filter(v => v && v.trim());
|
||
}
|
||
|
||
function showDetailPanel(open) {
|
||
detailPanel.classList.toggle("open", open);
|
||
backdrop.classList.toggle("show", open);
|
||
}
|
||
|
||
function closeDetail() {
|
||
showDetailPanel(false);
|
||
}
|
||
|
||
function escapeHtml(text) {
|
||
const div = document.createElement("div");
|
||
div.textContent = text ?? "";
|
||
return div.innerHTML;
|
||
}
|
||
|
||
function escapeMultiline(text) {
|
||
if (!text) return "";
|
||
return escapeHtml(text).replace(/\n/g, "<br>");
|
||
}
|
||
})();
|
||
</script>
|
||
</body>
|
||
|
||
</html> |