|
|
@@ -336,6 +336,25 @@
|
|
|
max-height: 300px; overflow-y: auto;
|
|
|
white-space: pre-wrap; word-break: break-all;
|
|
|
}
|
|
|
+ /* ── 原文卡片(按 source_id=case_id 懒加载;标题+缩略图+正文,可整体展开/收起)── */
|
|
|
+ .src-block { border: 1px solid var(--border); border-radius: 8px; margin: 10px 0; background: #F8FAFC; overflow: hidden; }
|
|
|
+ .src-head { display: flex; align-items: center; gap: 8px; padding: 8px 11px; cursor: pointer; user-select: none; }
|
|
|
+ .src-head:hover { background: #F1F5F9; }
|
|
|
+ .src-caret { flex: 0 0 auto; color: var(--text-muted); font-size: 11px; transition: transform .15s; }
|
|
|
+ .src-block.open .src-caret { transform: rotate(90deg); }
|
|
|
+ .src-label { flex: 0 0 auto; font-size: 11px; font-weight: 700; color: var(--primary); background: var(--primary-light); padding: 1px 7px; border-radius: 4px; }
|
|
|
+ .src-title { flex: 1; min-width: 0; font-weight: 600; font-size: 13px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: var(--text); }
|
|
|
+ .src-link { flex: 0 0 auto; font-size: 12px; color: var(--primary); text-decoration: none; }
|
|
|
+ .src-case { flex: 0 0 auto; font-size: 11px; font-family: 'Courier New', monospace; color: var(--text-muted); background: var(--bg); border: 1px solid var(--border); padding: 1px 7px; border-radius: 4px; white-space: nowrap; }
|
|
|
+ .src-body { padding: 0 11px 11px; }
|
|
|
+ .src-block:not(.open) .src-body { display: none; }
|
|
|
+ .src-thumbs { display: flex; gap: 8px; overflow-x: auto; padding: 4px 2px 10px; scrollbar-width: thin; }
|
|
|
+ .src-thumbs img { flex: 0 0 auto; height: 96px; width: auto; max-width: 150px; border-radius: 8px; border: 1px solid var(--border); object-fit: cover; background: #F0EFE9; cursor: zoom-in; }
|
|
|
+ .src-text { font-size: 13px; line-height: 1.7; color: var(--text-sub); white-space: pre-wrap; }
|
|
|
+ .src-text.clamp { max-height: 88px; overflow: hidden; -webkit-mask-image: linear-gradient(180deg,#000 62%,transparent); mask-image: linear-gradient(180deg,#000 62%,transparent); }
|
|
|
+ .src-text-toggle { margin-top: 4px; background: none; border: none; color: var(--primary); cursor: pointer; font-size: 12px; padding: 2px 0; }
|
|
|
+ .src-loading, .src-empty { font-size: 12px; color: var(--text-muted); padding: 8px 2px; }
|
|
|
+
|
|
|
/* ── 工序步骤表(复用自工序解构的 .steps 表格)─────────────────── */
|
|
|
.steps-wrap { overflow-x: auto; margin-top: 6px; border: 1px solid var(--border); border-radius: 6px; }
|
|
|
.steps { width: 100%; min-width: 1500px; table-layout: fixed; border-collapse: collapse; font-size: 12px; }
|
|
|
@@ -1116,6 +1135,80 @@ function markToolClamps() {
|
|
|
});
|
|
|
}
|
|
|
|
|
|
+// ── 原文块(按 source_id=case_id 懒加载,复用 #dataset 的取数/图片策略)──────────
|
|
|
+// 图片:优先浏览器直连 CDN(referrerpolicy=no-referrer 多能绕防盗链),失败回退同源代理 /api/img。
|
|
|
+const _imgProxy = u => (/^https?:\/\//.test(u || '') ? '/api/img?u=' + encodeURIComponent(u) : (u || ''));
|
|
|
+const _imgDirect = u => (/^https?:\/\//.test(u || '') ? u : _imgProxy(u));
|
|
|
+function _imgFallback(el, proxy) {
|
|
|
+ el.onerror = null;
|
|
|
+ if (!el.dataset.fb) { el.dataset.fb = '1'; el.onerror = () => _imgFallback(el, proxy); el.src = proxy; return; }
|
|
|
+ el.style.opacity = 0.25;
|
|
|
+}
|
|
|
+function _srcImg(u) {
|
|
|
+ return `<img src="${esc(_imgDirect(u))}" referrerpolicy="no-referrer" loading="lazy" onerror="_imgFallback(this,'${esc(_imgProxy(u))}')">`;
|
|
|
+}
|
|
|
+// 折叠态的原文条:标题先用知识里的 source_title 占位,展开后用真实原文标题覆盖。
|
|
|
+function renderSourceBlock(hit) {
|
|
|
+ const caseId = hit.source_id || '';
|
|
|
+ if (!caseId) return '';
|
|
|
+ const placeholder = esc(hit.source_title || hit.title || '(展开加载原文)');
|
|
|
+ return `<div class="src-block" id="src-${esc(hit.id)}">
|
|
|
+ <div class="src-head" onclick="toggleKbSrc('${esc(caseId)}','${esc(hit.id)}')">
|
|
|
+ <span class="src-caret">▸</span>
|
|
|
+ <span class="src-label">原文</span>
|
|
|
+ <span class="src-title">${placeholder}</span>
|
|
|
+ <a class="src-link" target="_blank" rel="noreferrer" onclick="event.stopPropagation()" hidden>原文 ↗</a>
|
|
|
+ <span class="src-case" title="case_id" onclick="event.stopPropagation()">${esc(caseId)}</span>
|
|
|
+ </div>
|
|
|
+ <div class="src-body"><div class="src-loading">展开加载原文…</div></div>
|
|
|
+ </div>`;
|
|
|
+}
|
|
|
+function _srcBodyHtml(p) {
|
|
|
+ const imgs = (p.images || []).filter(Boolean);
|
|
|
+ const thumbs = imgs.length
|
|
|
+ ? `<div class="src-thumbs">${imgs.map(u => `<a href="${esc(u)}" target="_blank" rel="noreferrer">${_srcImg(u)}</a>`).join('')}</div>`
|
|
|
+ : '';
|
|
|
+ const body = p.body
|
|
|
+ ? `<div class="src-text clamp">${esc(p.body)}</div><button class="src-text-toggle" hidden onclick="toggleKbText(this)">展开 ▼</button>`
|
|
|
+ : '';
|
|
|
+ return (thumbs + body) || '<div class="src-empty">(无正文/配图)</div>';
|
|
|
+}
|
|
|
+async function toggleKbSrc(caseId, hitId) {
|
|
|
+ const block = document.getElementById('src-' + hitId);
|
|
|
+ if (!block) return;
|
|
|
+ const open = block.classList.toggle('open');
|
|
|
+ if (!open || block.dataset.loaded) return;
|
|
|
+ block.dataset.loaded = '1';
|
|
|
+ const body = block.querySelector('.src-body');
|
|
|
+ try {
|
|
|
+ const r = await fetch('/api/post_by_case?case_id=' + encodeURIComponent(caseId));
|
|
|
+ if (!r.ok) throw new Error(r.status);
|
|
|
+ const p = await r.json();
|
|
|
+ const titleEl = block.querySelector('.src-title');
|
|
|
+ if (p.title) titleEl.textContent = p.title;
|
|
|
+ const linkEl = block.querySelector('.src-link');
|
|
|
+ if (p.url) { linkEl.href = p.url; linkEl.hidden = false; }
|
|
|
+ body.innerHTML = _srcBodyHtml(p);
|
|
|
+ requestAnimationFrame(() => _markKbSrcClamp(hitId));
|
|
|
+ } catch (e) {
|
|
|
+ block.dataset.loaded = ''; // 失败允许重试
|
|
|
+ body.innerHTML = '<div class="src-empty">原文加载失败,点击标题重试</div>';
|
|
|
+ }
|
|
|
+}
|
|
|
+function toggleKbText(btn) {
|
|
|
+ const t = btn.previousElementSibling;
|
|
|
+ const clamped = t.classList.toggle('clamp');
|
|
|
+ btn.textContent = clamped ? '展开 ▼' : '收起 ▲';
|
|
|
+}
|
|
|
+// 仅当正文在定高下溢出时才显示「展开」
|
|
|
+function _markKbSrcClamp(hitId) {
|
|
|
+ const block = document.getElementById('src-' + hitId);
|
|
|
+ const t = block && block.querySelector('.src-text');
|
|
|
+ const btn = block && block.querySelector('.src-text-toggle');
|
|
|
+ if (!t || !btn) return;
|
|
|
+ btn.hidden = !(t.scrollHeight > t.clientHeight + 2);
|
|
|
+}
|
|
|
+
|
|
|
function renderCard(hit) {
|
|
|
let titleHtml = esc(hit.title || '(无标题)');
|
|
|
if (hit.highlight?.title?.[0]) titleHtml = hit.highlight.title[0];
|
|
|
@@ -1183,14 +1276,14 @@ function renderCard(hit) {
|
|
|
? new Date(hit.updated_at).toLocaleDateString('zh-CN') : '';
|
|
|
|
|
|
// 正文展开区:content 可能是一条工序解构(procedure,含 steps)或一个工具解构(tool,含工具名称)。
|
|
|
- // 工序 → 工序步骤表;工具 → 工具解构表;都解析不出时回退为格式化 JSON。
|
|
|
+ // 工序 → 工序步骤表;工具 → 直接展示 content 的格式化 JSON(不再渲染工具解构表)。
|
|
|
let contentSection = '';
|
|
|
if (hit.content) {
|
|
|
let inner = '';
|
|
|
try {
|
|
|
const obj = JSON.parse(hit.content);
|
|
|
if (obj && Array.isArray(obj.steps) && obj.steps.length) inner = renderSteps(obj.steps);
|
|
|
- else if (obj && ('工具名称' in obj || '实质作用域' in obj || '创作层级' in obj)) inner = renderToolTable(obj);
|
|
|
+ // 工具解构:按需求改为直接显示 content JSON,落到下方 content-pre 回退分支。
|
|
|
} catch {}
|
|
|
if (!inner) {
|
|
|
let display = hit.content;
|
|
|
@@ -1205,6 +1298,7 @@ function renderCard(hit) {
|
|
|
<div class="card-top"><div class="card-title">${titleHtml}</div>${score}</div>
|
|
|
${meta ? `<div class="card-meta">${meta}</div>` : ''}
|
|
|
${tags ? `<div class="tag-row">${tags}</div>` : ''}
|
|
|
+ ${renderSourceBlock(hit)}
|
|
|
<div class="card-foot">
|
|
|
<div class="ext-row">${extHtml}</div>
|
|
|
<span class="card-time">${updatedAt ? updatedAt + ' 更新' : ''}</span>
|