Files
law-kb/app/static/js/browse.js
T
freedak 641e33b834 feat: QYLAW 法律法规知识库
- 语义检索(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>
2026-08-07 14:55:25 +08:00

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);
}
},
};