641e33b834
- 语义检索(FAISS + embedding)+ 精确查找(法规名+条号) - RAG 问答(SSE 流式,支持 thinking 折叠显示) - 法规浏览(原文阅读) - 历史记录(检索+对话持久化到 SQLite) - 设置页(系统提示词/模板/LLM 参数可配置) - 检索质量评估脚本 Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
213 lines
8.8 KiB
JavaScript
213 lines
8.8 KiB
JavaScript
/**
|
|
* browse.js — 法规浏览页 + 原文详情
|
|
*/
|
|
const BrowsePage = {
|
|
state: { view: 'list', laws: [], total: 0, page: 1, pageSize: 50, category: '', province: '', keyword: '', loading: false, error: null, currentLaw: null },
|
|
|
|
render() {
|
|
if (this.state.view === 'detail' && this.state.currentLaw) {
|
|
return this.renderDetail();
|
|
}
|
|
return this.renderList();
|
|
},
|
|
|
|
renderList() {
|
|
return `
|
|
<div class="p-4 md:p-6 max-w-5xl mx-auto">
|
|
<h2 class="text-lg font-semibold mb-4">法规浏览</h2>
|
|
|
|
<!-- 筛选栏 -->
|
|
<div class="bg-white rounded-xl border border-gray-200 p-4 mb-4">
|
|
<div class="flex gap-3 flex-wrap">
|
|
<input id="browse-keyword" type="text" placeholder="法规名关键词..."
|
|
class="flex-1 min-w-[200px] rounded-lg border border-gray-200 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-gray-900"
|
|
value="${this.state.keyword}" onkeydown="if(event.key==='Enter')BrowsePage.doBrowse(1)">
|
|
<select id="browse-category" class="rounded-lg border border-gray-200 px-3 py-2 text-sm" onchange="BrowsePage.onFilterChange()">
|
|
<option value="">全部类别</option>
|
|
<option value="法律">法律</option>
|
|
<option value="行政法规">行政法规</option>
|
|
<option value="监察法规">监察法规</option>
|
|
<option value="司法解释">司法解释</option>
|
|
<option value="地方性法规">地方性法规</option>
|
|
</select>
|
|
<select id="browse-province" class="rounded-lg border border-gray-200 px-3 py-2 text-sm" onchange="BrowsePage.onFilterChange()">
|
|
<option value="">全部省份</option>
|
|
</select>
|
|
<button onclick="BrowsePage.doBrowse(1)" class="px-4 py-2 bg-gray-900 text-white text-sm rounded-lg hover:bg-gray-800">查询</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 列表 -->
|
|
<div id="browse-list">
|
|
${UI.emptyState('输入条件浏览法规', '可按类别、省份、关键词筛选')}
|
|
</div>
|
|
</div>`;
|
|
},
|
|
|
|
renderDetail() {
|
|
const law = this.state.currentLaw;
|
|
const badge = UI.categoryBadge(law.category);
|
|
const province = law.province ? `<span class="text-xs text-gray-400">[${law.province}${law.city ? ' · ' + law.city : ''}]</span>` : '';
|
|
const date = law.publish_date ? `<span class="text-xs text-gray-400">发布: ${law.publish_date}</span>` : '';
|
|
|
|
// 目录树(章节 + 条号)
|
|
const toc = (law.clauses || []).map(c => {
|
|
const chapter = c.chapter ? c.chapter : '';
|
|
return `<div class="text-xs py-1 px-2 hover:bg-gray-50 cursor-pointer rounded" onclick="BrowsePage.scrollToClause(${c.clause_id})">
|
|
<span class="text-gray-400">${chapter}</span>
|
|
<span class="text-gray-700">${c.clause_no || ''}</span>
|
|
</div>`;
|
|
}).join('');
|
|
|
|
return `
|
|
<div class="p-4 md:p-6 max-w-5xl mx-auto">
|
|
<button onclick="BrowsePage.backToList()" class="mb-4 text-sm text-gray-500 hover:text-gray-900 flex items-center gap-1">
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m15 18-6-6 6-6"/></svg>
|
|
返回列表
|
|
</button>
|
|
|
|
<div class="flex items-center gap-2 mb-2 flex-wrap">
|
|
<h2 class="text-lg font-semibold">${law.name}</h2>
|
|
${badge}
|
|
${province}
|
|
${date}
|
|
</div>
|
|
|
|
<div class="flex gap-4">
|
|
<!-- 目录树 -->
|
|
${toc ? `<aside class="w-48 shrink-0 hidden md:block">
|
|
<div class="bg-white rounded-xl border border-gray-200 p-3 sticky top-4 max-h-[calc(100vh-120px)] overflow-y-auto scroll-thin">
|
|
<div class="text-xs font-semibold text-gray-500 mb-2">目录</div>
|
|
${toc}
|
|
</div>
|
|
</aside>` : ''}
|
|
|
|
<!-- 原文 -->
|
|
<div class="flex-1 bg-white rounded-xl border border-gray-200 p-6">
|
|
<div class="md-content text-sm leading-relaxed">${marked.parse(law.content)}</div>
|
|
</div>
|
|
</div>
|
|
</div>`;
|
|
},
|
|
|
|
init() {
|
|
this.loadProvinces();
|
|
if (this.state.category) document.getElementById('browse-category').value = this.state.category;
|
|
if (this.state.province) document.getElementById('browse-province').value = this.state.province;
|
|
if (this.state.laws.length === 0) this.doBrowse(1);
|
|
},
|
|
|
|
provinces: ['北京市','天津市','河北省','山西省','内蒙古自治区','辽宁省','吉林省','黑龙江省','上海市','江苏省','浙江省','安徽省','福建省','江西省','山东省','河南省','湖北省','湖南省','广东省','广西壮族自治区','海南省','重庆市','四川省','贵州省','云南省','西藏自治区','陕西省','甘肃省','青海省','宁夏回族自治区','新疆维吾尔自治区'],
|
|
|
|
loadProvinces() {
|
|
const sel = document.getElementById('browse-province');
|
|
if (!sel) return;
|
|
this.provinces.forEach(p => {
|
|
const opt = document.createElement('option');
|
|
opt.value = p; opt.textContent = p;
|
|
sel.appendChild(opt);
|
|
});
|
|
},
|
|
|
|
onFilterChange() {
|
|
this.state.category = document.getElementById('browse-category').value;
|
|
this.state.province = document.getElementById('browse-province').value;
|
|
},
|
|
|
|
async doBrowse(page) {
|
|
const kwInput = document.getElementById('browse-keyword');
|
|
if (kwInput) this.state.keyword = kwInput.value.trim();
|
|
this.state.page = page || 1;
|
|
this.state.view = 'list';
|
|
this.state.loading = true;
|
|
this.state.error = null;
|
|
this.renderListState();
|
|
|
|
try {
|
|
const params = { page: this.state.page, page_size: this.state.pageSize };
|
|
if (this.state.category) params.category = this.state.category;
|
|
if (this.state.province) params.province = this.state.province;
|
|
if (this.state.keyword) params.keyword = this.state.keyword;
|
|
|
|
const resp = await API.listLaws(params);
|
|
this.state.laws = resp.data.results || [];
|
|
this.state.total = resp.data.total || 0;
|
|
this.state.loading = false;
|
|
this.renderListState();
|
|
} catch (e) {
|
|
this.state.loading = false;
|
|
this.state.error = e.message;
|
|
this.renderListState();
|
|
}
|
|
},
|
|
|
|
renderListState() {
|
|
const el = document.getElementById('browse-list');
|
|
if (!el) return;
|
|
|
|
if (this.state.loading) {
|
|
el.innerHTML = `<div class="flex items-center justify-center py-16 gap-3 text-gray-400">${UI.spinner(20)}<span class="text-sm">加载中...</span></div>`;
|
|
return;
|
|
}
|
|
|
|
if (this.state.error) {
|
|
el.innerHTML = UI.errorState(this.state.error, 'BrowsePage.doBrowse()');
|
|
return;
|
|
}
|
|
|
|
if (!this.state.laws.length) {
|
|
el.innerHTML = UI.emptyState('无法规', '试试调整筛选条件');
|
|
return;
|
|
}
|
|
|
|
const rows = this.state.laws.map(l => `
|
|
<div class="bg-white border border-gray-200 rounded-lg p-3 mb-2 hover:border-gray-300 transition cursor-pointer" onclick="BrowsePage.viewLaw(${l.law_id})">
|
|
<div class="flex items-center gap-2 flex-wrap">
|
|
<span class="text-sm font-medium text-gray-900">${l.name}</span>
|
|
${UI.categoryBadge(l.category)}
|
|
${l.province ? `<span class="text-xs text-gray-400">[${l.province}${l.city ? ' · ' + l.city : ''}]</span>` : ''}
|
|
${l.publish_date ? `<span class="text-xs text-gray-400">${l.publish_date}</span>` : ''}
|
|
<span class="text-xs text-gray-400 ml-auto">${l.clause_count} 条</span>
|
|
</div>
|
|
</div>`).join('');
|
|
|
|
const pager = UI.pagination(this.state.page, this.state.pageSize, this.state.total, 'BrowsePage.doBrowse');
|
|
el.innerHTML = `<div class="mb-3 text-sm text-gray-500">共 ${this.state.total} 篇法规</div>${rows}${pager}`;
|
|
},
|
|
|
|
async viewLaw(lawId) {
|
|
this.state.view = 'detail';
|
|
this.state.currentLaw = null;
|
|
const content = document.getElementById('content');
|
|
content.innerHTML = `<div class="flex items-center justify-center py-16 gap-3 text-gray-400">${UI.spinner(20)}<span class="text-sm">加载原文...</span></div>`;
|
|
|
|
try {
|
|
const resp = await API.getLaw(lawId);
|
|
this.state.currentLaw = resp.data;
|
|
content.innerHTML = this.renderDetail();
|
|
} catch (e) {
|
|
content.innerHTML = UI.errorState(e.message, `BrowsePage.viewLaw(${lawId})`);
|
|
}
|
|
},
|
|
|
|
backToList() {
|
|
this.state.view = 'list';
|
|
this.state.currentLaw = null;
|
|
const content = document.getElementById('content');
|
|
content.innerHTML = this.renderList();
|
|
this.init();
|
|
this.renderListState();
|
|
},
|
|
|
|
scrollToClause(clauseId) {
|
|
// 简单实现:滚动到条文内容(原文中条文已渲染)
|
|
// TODO: 更精确的定位需要解析原文中的条号
|
|
const el = document.getElementById(`clause-${clauseId}`);
|
|
if (el) {
|
|
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
el.classList.add('clause-highlight');
|
|
setTimeout(() => el.classList.remove('clause-highlight'), 2000);
|
|
}
|
|
},
|
|
};
|