768 lines
22 KiB
HTML
768 lines
22 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||
<title>知识库 · 全文搜索</title>
|
||
<style>
|
||
:root {
|
||
--bg: #0f1117;
|
||
--card: #1a1d2e;
|
||
--card-hover: #22263b;
|
||
--border: #2a2e42;
|
||
--text: #e1e4ed;
|
||
--text-dim: #7a7f99;
|
||
--text-bright: #ffffff;
|
||
--accent: #6c5ce7;
|
||
--accent-glow: rgba(108,92,231,.3);
|
||
--badge-bg: #252a41;
|
||
--badge-text: #a5a9c4;
|
||
--green: #00cec9;
|
||
--amber: #fdcb6e;
|
||
--blue: #74b9ff;
|
||
--purple: #a29bfe;
|
||
--orange: #e17055;
|
||
--radius: 12px;
|
||
--radius-sm: 8px;
|
||
}
|
||
* { margin:0; padding:0; box-sizing:border-box; }
|
||
body {
|
||
background: var(--bg);
|
||
color: var(--text);
|
||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans SC", sans-serif;
|
||
min-height: 100vh;
|
||
line-height: 1.6;
|
||
}
|
||
a { color: var(--accent); text-decoration: none; }
|
||
a:hover { text-decoration: underline; }
|
||
|
||
.container { max-width: 800px; margin: 0 auto; padding: 24px 16px; }
|
||
|
||
/* Header */
|
||
.header {
|
||
text-align: center;
|
||
padding: 32px 0 16px;
|
||
}
|
||
.header h1 {
|
||
font-size: 28px;
|
||
font-weight: 700;
|
||
color: var(--text-bright);
|
||
letter-spacing: 1px;
|
||
}
|
||
.header .subtitle {
|
||
font-size: 13px;
|
||
color: var(--text-dim);
|
||
margin-top: 6px;
|
||
}
|
||
.header .stats { margin-top: 4px; font-size: 12px; color: var(--text-dim); }
|
||
|
||
/* Search box */
|
||
.search-wrap {
|
||
position: relative;
|
||
margin: 20px 0 12px;
|
||
}
|
||
.search-wrap .icon {
|
||
position: absolute;
|
||
left: 16px;
|
||
top: 50%;
|
||
transform: translateY(-50%);
|
||
width: 20px;
|
||
height: 20px;
|
||
color: var(--text-dim);
|
||
pointer-events: none;
|
||
}
|
||
.search-wrap input {
|
||
width: 100%;
|
||
padding: 14px 16px 14px 48px;
|
||
background: var(--card);
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius);
|
||
color: var(--text);
|
||
font-size: 16px;
|
||
outline: none;
|
||
transition: border-color .2s, box-shadow .2s;
|
||
}
|
||
.search-wrap input:focus {
|
||
border-color: var(--accent);
|
||
box-shadow: 0 0 0 3px var(--accent-glow);
|
||
}
|
||
.search-wrap input::placeholder { color: var(--text-dim); }
|
||
|
||
/* Filter chips */
|
||
.filters {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
margin: 12px 0;
|
||
}
|
||
.chip {
|
||
padding: 6px 16px;
|
||
border-radius: 20px;
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
background: var(--card);
|
||
border: 1px solid var(--border);
|
||
color: var(--text-dim);
|
||
cursor: pointer;
|
||
transition: all .2s;
|
||
user-select: none;
|
||
}
|
||
.chip:hover { border-color: var(--accent); color: var(--text); background: rgba(108,92,231,.06); }
|
||
.chip.active {
|
||
background: linear-gradient(135deg, var(--accent), #8b7cf7);
|
||
border-color: var(--accent);
|
||
color: #fff;
|
||
box-shadow: 0 2px 8px var(--accent-glow);
|
||
}
|
||
|
||
/* Sort bar */
|
||
.sort-bar {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
margin: 8px 0 16px;
|
||
font-size: 13px;
|
||
color: var(--text-dim);
|
||
}
|
||
.sort-bar button {
|
||
background: none;
|
||
border: 1px solid var(--border);
|
||
color: var(--text-dim);
|
||
padding: 5px 14px;
|
||
border-radius: 6px;
|
||
cursor: pointer;
|
||
font-size: 12px;
|
||
font-weight: 500;
|
||
transition: all .2s;
|
||
}
|
||
.sort-bar button:hover { border-color: var(--accent); color: var(--text); background: rgba(108,92,231,.06); }
|
||
.sort-bar button.active { border-color: var(--accent); color: var(--accent); background: rgba(108,92,231,.1); }
|
||
.sort-bar .result-count { margin-left: auto; }
|
||
|
||
/* Loading */
|
||
.loading {
|
||
text-align: center;
|
||
padding: 60px 0;
|
||
color: var(--text-dim);
|
||
}
|
||
.spinner {
|
||
width: 32px; height: 32px;
|
||
border: 3px solid var(--border);
|
||
border-top-color: var(--accent);
|
||
border-radius: 50%;
|
||
animation: spin .8s linear infinite;
|
||
margin: 0 auto 12px;
|
||
}
|
||
@keyframes spin { to { transform: rotate(360deg); } }
|
||
|
||
/* Results */
|
||
.results { margin-top: 8px; }
|
||
|
||
/* Card */
|
||
.card {
|
||
background: var(--card);
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius-sm);
|
||
padding: 14px 16px;
|
||
margin-bottom: 6px;
|
||
transition: border-color .15s, background .15s;
|
||
position: relative;
|
||
}
|
||
.card:hover { border-color: var(--accent); }
|
||
.card:active { background: var(--card-hover); }
|
||
|
||
.card-head {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
justify-content: space-between;
|
||
gap: 8px;
|
||
}
|
||
.card-title {
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
color: var(--text-bright);
|
||
line-height: 1.5;
|
||
cursor: pointer;
|
||
flex: 1;
|
||
}
|
||
.card-title:hover { color: var(--accent); }
|
||
|
||
.card-badge {
|
||
font-size: 10px;
|
||
padding: 2px 9px;
|
||
border-radius: 4px;
|
||
white-space: nowrap;
|
||
flex-shrink: 0;
|
||
font-weight: 500;
|
||
background: var(--badge-bg);
|
||
color: var(--badge-text);
|
||
line-height: 1.5;
|
||
border: none;
|
||
}
|
||
.card-badge::before {
|
||
content: '';
|
||
display: inline-block;
|
||
width: 6px;
|
||
height: 6px;
|
||
border-radius: 50%;
|
||
margin-right: 5px;
|
||
vertical-align: middle;
|
||
position: relative;
|
||
top: -1px;
|
||
}
|
||
.badge-灵感文章::before { background: #f59e0b; }
|
||
.badge-灵感日报::before { background: #10b981; }
|
||
.badge-读书笔记::before { background: #3b82f6; }
|
||
.badge-日记::before { background: #8b5cf6; }
|
||
.badge-随手记::before { background: #06b6d4; }
|
||
|
||
.card-date {
|
||
font-size: 11px;
|
||
color: var(--text-dim);
|
||
margin-top: 4px;
|
||
}
|
||
|
||
.card-excerpt {
|
||
font-size: 13px;
|
||
color: var(--text-dim);
|
||
margin-top: 6px;
|
||
line-height: 1.6;
|
||
cursor: pointer;
|
||
display: -webkit-box;
|
||
-webkit-line-clamp: 2;
|
||
-webkit-box-orient: vertical;
|
||
overflow: hidden;
|
||
transition: all .15s;
|
||
}
|
||
.card-excerpt.open {
|
||
-webkit-line-clamp: unset;
|
||
display: block;
|
||
}
|
||
.card-excerpt:hover { color: var(--text); }
|
||
.card-excerpt mark {
|
||
background: rgba(99,102,241,.2);
|
||
color: var(--accent);
|
||
padding: 0 2px;
|
||
border-radius: 2px;
|
||
font-weight: 500;
|
||
}
|
||
|
||
/* Landing — overview dashboard */
|
||
.landing {
|
||
animation: fadeIn .35s ease;
|
||
}
|
||
.landing-welcome {
|
||
text-align: center;
|
||
padding: 40px 20px 20px;
|
||
}
|
||
.landing-welcome h2 { font-size: 22px; font-weight: 700; color: var(--text-bright); margin-bottom: 6px; }
|
||
.landing-welcome p { font-size: 14px; color: var(--text-dim); }
|
||
.landing-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||
gap: 10px;
|
||
margin: 24px 0 32px;
|
||
}
|
||
.landing-card {
|
||
background: var(--card);
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius-sm);
|
||
padding: 18px 16px;
|
||
text-align: center;
|
||
transition: border-color .15s;
|
||
cursor: pointer;
|
||
}
|
||
.landing-card:hover { border-color: var(--accent); }
|
||
.landing-card .count {
|
||
font-size: 28px;
|
||
font-weight: 700;
|
||
color: var(--text-bright);
|
||
line-height: 1.2;
|
||
}
|
||
.landing-card .label {
|
||
font-size: 12px;
|
||
color: var(--text-dim);
|
||
margin-top: 4px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 5px;
|
||
}
|
||
.landing-card .label .dot {
|
||
width: 6px; height: 6px;
|
||
border-radius: 50%;
|
||
display: inline-block;
|
||
}
|
||
.landing-section {
|
||
margin-bottom: 28px;
|
||
}
|
||
.landing-section h3 {
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
color: var(--text-muted);
|
||
text-transform: uppercase;
|
||
letter-spacing: .5px;
|
||
margin-bottom: 10px;
|
||
padding-bottom: 6px;
|
||
border-bottom: 1px solid var(--border);
|
||
}
|
||
.landing-recent {
|
||
list-style: none;
|
||
padding: 0;
|
||
}
|
||
.landing-recent li {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
padding: 8px 0;
|
||
border-bottom: 1px solid rgba(0,0,0,.03);
|
||
cursor: pointer;
|
||
font-size: 13px;
|
||
transition: color .15s;
|
||
}
|
||
.landing-recent li:hover { color: var(--accent); }
|
||
.landing-recent li .recent-dot {
|
||
width: 4px; height: 4px;
|
||
border-radius: 50%;
|
||
flex-shrink: 0;
|
||
background: var(--text-muted);
|
||
}
|
||
.landing-recent li .recent-title { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||
.landing-recent li .recent-source { font-size: 11px; color: var(--text-muted); flex-shrink: 0; }
|
||
.landing-recent li .recent-date { font-size: 11px; color: var(--text-muted); flex-shrink: 0; }
|
||
|
||
/* No results */
|
||
.empty {
|
||
text-align: center;
|
||
padding: 60px 20px;
|
||
color: var(--text-dim);
|
||
}
|
||
.empty .big-icon { font-size: 48px; margin-bottom: 12px; opacity: .4; }
|
||
.empty h3 { font-size: 18px; color: var(--text); margin-bottom: 6px; }
|
||
.empty p { font-size: 14px; }
|
||
|
||
/* Pagination */
|
||
.pagination {
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
gap: 8px;
|
||
margin: 24px 0 40px;
|
||
}
|
||
.pagination button {
|
||
padding: 6px 14px;
|
||
border-radius: 6px;
|
||
border: 1px solid var(--border);
|
||
background: var(--card);
|
||
color: var(--text);
|
||
cursor: pointer;
|
||
font-size: 13px;
|
||
transition: all .15s;
|
||
}
|
||
.pagination button:hover:not(:disabled) { border-color: var(--accent); }
|
||
.pagination button.active { background: var(--accent); border-color: var(--accent); color: #fff; }
|
||
.pagination button:disabled { opacity: .3; cursor: default; }
|
||
.pagination .page-info { font-size: 13px; color: var(--text-dim); }
|
||
|
||
/* Footer */
|
||
.footer {
|
||
text-align: center;
|
||
padding: 20px;
|
||
font-size: 12px;
|
||
color: var(--text-dim);
|
||
}
|
||
|
||
/* Badge colors (dot only) */
|
||
.badge-灵感文章::before { background: #f59e0b; }
|
||
.badge-灵感日报::before { background: #10b981; }
|
||
.badge-读书笔记::before { background: #3b82f6; }
|
||
.badge-日记::before { background: #8b5cf6; }
|
||
.badge-随手记::before { background: #06b6d4; }
|
||
|
||
/* Responsive */
|
||
@media (max-width:600px) {
|
||
.container { padding: 16px 12px; }
|
||
.header h1 { font-size: 22px; }
|
||
.search-wrap input { font-size: 15px; padding: 12px 14px 12px 44px; }
|
||
.card { padding: 12px 14px; }
|
||
.card-title { font-size: 14px; }
|
||
.sort-bar { flex-wrap: wrap; }
|
||
}
|
||
|
||
/* Animations */
|
||
.card { animation: fadeIn .25s ease; }
|
||
@keyframes fadeIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<div class="container">
|
||
<div class="header">
|
||
<h1>📖 知识库</h1>
|
||
<div class="subtitle">全文搜索 · 灵感 · 日记 · 读书笔记 · 随手记</div>
|
||
<div class="stats" id="stats">—</div>
|
||
</div>
|
||
|
||
<div class="search-wrap">
|
||
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><path d="M21 21l-4.35-4.35"/></svg>
|
||
<input type="text" id="searchInput" placeholder="搜任何内容…" autofocus>
|
||
</div>
|
||
|
||
<div class="filters" id="filters">
|
||
<span class="chip active" data-source="all">全部</span>
|
||
<span class="chip" data-source="灵感文章">灵感文章</span>
|
||
<span class="chip" data-source="灵感日报">灵感日报</span>
|
||
<span class="chip" data-source="读书笔记">读书笔记</span>
|
||
<span class="chip" data-source="日记">日记</span>
|
||
<span class="chip" data-source="随手记">随手记</span>
|
||
</div>
|
||
|
||
<div class="sort-bar">
|
||
<span>排序</span>
|
||
<button class="active" data-sort="relevance">相关度</button>
|
||
<button data-sort="date">时间</button>
|
||
<span class="result-count" id="resultCount"></span>
|
||
</div>
|
||
|
||
<div id="results"><div class="loading"><div class="spinner"></div>正在搜索…</div></div>
|
||
</div>
|
||
|
||
<div class="footer">
|
||
<span>数据每日自动更新 · 北京 · Meilisearch</span>
|
||
</div>
|
||
|
||
<script>
|
||
// Config
|
||
const MEILI_URL = '/search-api/indexes/knowledge/search';
|
||
const SEARCH_KEY = '12d3464de5e0b9bb098952f1a1fe99da0b8fe9111737a71eeddb5456278f6c7c';
|
||
const PAGE_SIZE = 20;
|
||
const NAV_BASE = 'https://nav.xybkwd.top';
|
||
|
||
// State
|
||
let currentQuery = '';
|
||
let currentSource = 'all';
|
||
let currentSort = 'relevance';
|
||
let currentPage = 1;
|
||
let totalHits = 0;
|
||
let lastResults = [];
|
||
|
||
// DOM refs
|
||
const $ = id => document.getElementById(id);
|
||
const input = $('searchInput');
|
||
const resultsDiv = $('results');
|
||
const resultCount = $('resultCount');
|
||
const stats = $('stats');
|
||
|
||
const sourceColors = {
|
||
'灵感文章': ['#f59e0b', 'badge-灵感文章'],
|
||
'灵感日报': ['#10b981', 'badge-灵感日报'],
|
||
'读书笔记': ['#3b82f6', 'badge-读书笔记'],
|
||
'日记': ['#8b5cf6', 'badge-日记'],
|
||
'随手记': ['#06b6d4', 'badge-随手记'],
|
||
};
|
||
const sourceNames = ['灵感文章', '灵感日报', '读书笔记', '日记', '随手记'];
|
||
|
||
// Init
|
||
let hasLoadedOnce = false;
|
||
fetchStats();
|
||
showLanding(); // Show landing page first
|
||
|
||
// Event listeners
|
||
|
||
// Event listeners
|
||
input.addEventListener('input', debounce(() => { currentPage = 1; search(); }, 250));
|
||
input.addEventListener('keydown', e => { if (e.key === 'Escape') { input.value = ''; currentPage = 1; search(); } });
|
||
|
||
document.querySelectorAll('.chip').forEach(chip => {
|
||
chip.addEventListener('click', () => {
|
||
document.querySelectorAll('.chip').forEach(c => c.classList.remove('active'));
|
||
chip.classList.add('active');
|
||
currentSource = chip.dataset.source;
|
||
currentPage = 1;
|
||
search();
|
||
});
|
||
});
|
||
|
||
document.querySelectorAll('.sort-bar button').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
document.querySelectorAll('.sort-bar button').forEach(b => b.classList.remove('active'));
|
||
btn.classList.add('active');
|
||
currentSort = btn.dataset.sort;
|
||
currentPage = 1;
|
||
search();
|
||
});
|
||
});
|
||
|
||
// Functions
|
||
async function search() {
|
||
const q = input.value.trim();
|
||
currentQuery = q;
|
||
|
||
// No query → show landing instead of running empty search
|
||
if (!q && currentSource === 'all') {
|
||
showLanding();
|
||
resultCount.textContent = '';
|
||
return;
|
||
}
|
||
|
||
resultsDiv.innerHTML = '<div class="loading"><div class="spinner"></div>正在搜索…</div>';
|
||
|
||
try {
|
||
const params = { q, limit: PAGE_SIZE, offset: (currentPage - 1) * PAGE_SIZE };
|
||
|
||
// Use Meilisearch's crop & highlight for keyword-in-context display
|
||
params.attributesToCrop = ['content'];
|
||
params.cropLength = 200;
|
||
params.attributesToHighlight = ['content'];
|
||
params.highlightPreTag = '<mark>';
|
||
params.highlightPostTag = '</mark>';
|
||
|
||
// Build filter
|
||
const filters = [];
|
||
if (currentSource !== 'all') {
|
||
filters.push(`source = ${JSON.stringify(currentSource)}`);
|
||
}
|
||
if (filters.length) params.filter = filters.join(' AND ');
|
||
|
||
// Build sort
|
||
if (currentSort === 'date' || !q) {
|
||
// No query → default to date sort so latest content (incl. Memos) shows first
|
||
params.sort = ['date:desc'];
|
||
}
|
||
|
||
const resp = await fetch(MEILI_URL, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Authorization': `Bearer ${SEARCH_KEY}`,
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify(params),
|
||
});
|
||
const data = await resp.json();
|
||
lastResults = data.hits || [];
|
||
totalHits = data.estimatedTotalHits || 0;
|
||
render();
|
||
} catch (e) {
|
||
resultsDiv.innerHTML = `<div class="empty"><p>搜索出错:${e.message}</p></div>`;
|
||
}
|
||
}
|
||
|
||
function render() {
|
||
if (totalHits === 0) {
|
||
if (!currentQuery) {
|
||
showLanding();
|
||
return;
|
||
}
|
||
resultsDiv.innerHTML = `
|
||
<div class="empty">
|
||
<div class="big-icon">🔍</div>
|
||
<h3>没有找到相关内容</h3>
|
||
<p>试试其他关键词,或换个分类筛选</p>
|
||
</div>`;
|
||
resultCount.textContent = '';
|
||
return;
|
||
}
|
||
|
||
let html = '<div class="results">';
|
||
lastResults.forEach(h => {
|
||
const title = h.title || '(无标题)';
|
||
const src = h.source || '其他';
|
||
const badgeClass = (sourceColors[src] || ['', ''])[1];
|
||
const url = buildUrl(h);
|
||
const date = h.date || '';
|
||
|
||
// Get highlighted excerpt
|
||
let excerptHtml = '';
|
||
if (currentQuery && h._formatted && h._formatted.content) {
|
||
excerptHtml = h._formatted.content;
|
||
if (excerptHtml.startsWith('…')) excerptHtml = excerptHtml.substring(1);
|
||
if (excerptHtml.endsWith('…')) excerptHtml = excerptHtml.substring(0, excerptHtml.length - 1);
|
||
excerptHtml = excerptHtml.trim();
|
||
excerptHtml = stripHtmlTags(excerptHtml);
|
||
} else {
|
||
let cleaned = (h.content || '').substring(0, 250);
|
||
cleaned = cleaned.replace(/^---[\s\S]*?---\n*/g, '');
|
||
cleaned = cleaned.replace(/(^|\n)#[^\n]*#/g, '');
|
||
excerptHtml = escapeHtml(cleaned.trim().substring(0, 250));
|
||
}
|
||
|
||
html += `
|
||
<div class="card">
|
||
<div class="card-head">
|
||
<div class="card-title" onclick="window.open('${url}','_blank')">${escapeHtml(title)}</div>
|
||
<span class="card-badge ${badgeClass}">${src}</span>
|
||
</div>
|
||
${date ? `<div class="card-date">${date}</div>` : ''}
|
||
<div class="card-excerpt" onclick="this.classList.toggle('open')">${excerptHtml}</div>
|
||
</div>`;
|
||
});
|
||
html += '</div>';
|
||
|
||
const totalPages = Math.ceil(totalHits / PAGE_SIZE);
|
||
if (totalPages > 1) {
|
||
html += '<div class="pagination">';
|
||
html += `<button ${currentPage <= 1 ? 'disabled' : ''} onclick="goPage(${currentPage - 1})">‹</button>`;
|
||
const start = Math.max(1, currentPage - 2);
|
||
const end = Math.min(totalPages, currentPage + 2);
|
||
for (let i = start; i <= end; i++) {
|
||
html += `<button class="${i === currentPage ? 'active' : ''}" onclick="goPage(${i})">${i}</button>`;
|
||
}
|
||
html += `<button ${currentPage >= totalPages ? 'disabled' : ''} onclick="goPage(${currentPage + 1})">›</button>`;
|
||
html += `<span class="page-info">共 ${totalHits} 条 / ${totalPages} 页</span>`;
|
||
html += '</div>';
|
||
}
|
||
|
||
resultCount.textContent = `共 ${totalHits} 条结果`;
|
||
resultsDiv.innerHTML = html;
|
||
}
|
||
|
||
function showLanding() {
|
||
resultsDiv.innerHTML = '<div class="loading"><div class="spinner"></div>加载中…</div>';
|
||
resultCount.textContent = '';
|
||
|
||
const facetsReq = fetch(MEILI_URL, {
|
||
method: 'POST',
|
||
headers: { 'Authorization': `Bearer ${SEARCH_KEY}`, 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ q: '', limit: 0, facets: ['source'] }),
|
||
});
|
||
|
||
const recentReq = fetch(MEILI_URL, {
|
||
method: 'POST',
|
||
headers: { 'Authorization': `Bearer ${SEARCH_KEY}`, 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ q: '', limit: 10, sort: ['date:desc'] }),
|
||
});
|
||
|
||
Promise.all([facetsReq, recentReq])
|
||
.then(async ([fR, rR]) => {
|
||
const fData = await fR.json();
|
||
const rData = await rR.json();
|
||
const total = fData.estimatedTotalHits || 0;
|
||
const dist = (fData.facetDistribution && fData.facetDistribution.source) || {};
|
||
const recent = rData.hits || [];
|
||
|
||
let gridHtml = sourceNames.map(s => {
|
||
const c = dist[s] || 0;
|
||
const color = sourceColors[s] ? sourceColors[s][0] : '#666';
|
||
const badgeClass = sourceColors[s] ? sourceColors[s][1] : '';
|
||
return `<div class="landing-card" onclick="filterSource('${s}')">
|
||
<div class="count">${c}</div>
|
||
<div class="label"><span class="dot" style="background:${color}"></span>${s}</div>
|
||
</div>`;
|
||
}).join('');
|
||
|
||
let recentHtml = recent.map(h => {
|
||
const dotColor = (sourceColors[h.source] || ['#666'])[0];
|
||
const title = h.title || '(无标题)';
|
||
const date = h.date || '';
|
||
return `<li onclick="searchQuery('${escapeHtml(title).replace(/'/g, "\\'")}')">
|
||
<span class="recent-dot" style="background:${dotColor}"></span>
|
||
<span class="recent-title">${escapeHtml(title)}</span>
|
||
<span class="recent-source">${h.source || ''}</span>
|
||
<span class="recent-date">${date}</span>
|
||
</li>`;
|
||
}).join('');
|
||
|
||
stats.textContent = `共 ${total} 篇文档`;
|
||
resultsDiv.innerHTML = `
|
||
<div class="landing">
|
||
<div class="landing-welcome">
|
||
<h2>📖 知识库</h2>
|
||
<p>搜索你的灵感、日记和读书笔记 · 共 ${total} 篇文档</p>
|
||
</div>
|
||
<div class="landing-grid">${gridHtml}</div>
|
||
${recent.length ? `<div class="landing-section"><h3>最近更新</h3><ul class="landing-recent">${recentHtml}</ul></div>` : ''}
|
||
</div>`;
|
||
})
|
||
.catch(() => {
|
||
resultsDiv.innerHTML = `<div class="empty"><h3>知识库</h3><p>输入关键词开始搜索</p></div>`;
|
||
});
|
||
}
|
||
|
||
function filterSource(source) {
|
||
currentSource = source;
|
||
currentPage = 1;
|
||
document.querySelectorAll('.chip').forEach(c => {
|
||
c.classList.toggle('active', c.dataset.source === source);
|
||
});
|
||
search();
|
||
}
|
||
|
||
function searchQuery(q) {
|
||
input.value = q;
|
||
currentQuery = q;
|
||
currentPage = 1;
|
||
search();
|
||
}
|
||
|
||
async function fetchStats() {
|
||
try {
|
||
const resp = await fetch(MEILI_URL, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Authorization': `Bearer ${SEARCH_KEY}`,
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify({ q: '', limit: 0 }),
|
||
});
|
||
const data = await resp.json();
|
||
const total = data.estimatedTotalHits || 0;
|
||
stats.textContent = `📦 ${total} 篇文档 · 每日更新`;
|
||
} catch (e) {
|
||
stats.textContent = '📦 知识库';
|
||
}
|
||
}
|
||
|
||
function goPage(page) {
|
||
currentPage = page;
|
||
search();
|
||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||
}
|
||
|
||
function buildUrl(h) {
|
||
if (h.source === '随手记') {
|
||
if (h._memos_uid) {
|
||
return `${NAV_BASE}/reader.html?memosUid=${encodeURIComponent(h._memos_uid)}`;
|
||
}
|
||
return 'https://memo.xybkwd.top/';
|
||
}
|
||
if (h.path) {
|
||
const p = h.path.replace('/home/ubuntu/backups/tokyo/repos/', '');
|
||
if (p.startsWith('inspiration-collector/') || p.startsWith('weread-notes/')) {
|
||
const parts = p.split('/');
|
||
const repo = parts[0];
|
||
const filePath = parts.slice(1).join('/');
|
||
const rawUrl = `${NAV_BASE}/gitea-raw/fxy/${repo}/raw/branch/main/${filePath}`;
|
||
return `${NAV_BASE}/reader.html?url=${encodeURIComponent(rawUrl)}`;
|
||
}
|
||
if (p.includes('webdav-diary')) return 'https://dav.xybkwd.top/';
|
||
}
|
||
return '#';
|
||
}
|
||
|
||
function escapeHtml(s) {
|
||
const div = document.createElement('div');
|
||
div.textContent = s;
|
||
return div.innerHTML;
|
||
}
|
||
|
||
function debounce(fn, ms) {
|
||
let timer;
|
||
return (...args) => { clearTimeout(timer); timer = setTimeout(() => fn(...args), ms); };
|
||
}
|
||
|
||
function stripHtmlTags(s) {
|
||
// Strip HTML tags but preserve <mark> tags (they're our highlight markers)
|
||
// First replace <mark> and </mark> with placeholders
|
||
const OPEN_MARK = '___MARK_OPEN___';
|
||
const CLOSE_MARK = '___MARK_CLOSE___';
|
||
s = s.replace(/<mark>/g, OPEN_MARK).replace(/<\/mark>/g, CLOSE_MARK);
|
||
// Strip all other HTML tags
|
||
s = s.replace(/<[^>]*>/g, '');
|
||
// Restore <mark> tags
|
||
s = s.replace(new RegExp(OPEN_MARK, 'g'), '<mark>').replace(new RegExp(CLOSE_MARK, 'g'), '</mark>');
|
||
return s;
|
||
}
|
||
</script>
|
||
</body>
|
||
</html>
|