Files
PlantAI-rag-new/frontend/src/components/bench/SourceRow.vue
T
jerryandClaude Fable 5 1e94de7464 PlantAI-rag-new 首次提交:文献知识工作台
- BFF 薄适配层(FastAPI,8090):QA 代理/SSE 中继/Neo4j 直连/抽取代理/会话持久化
- Vue3 前端:五相位检索流水线、文献来源与证据链、知识图谱、提取任务四 tab
- 四档响应式(桌面/笔记本/平板/手机)、深浅主题
- 81 个 FR 全部实现,测试追溯矩阵 81/81,后端 121 测/前端 156 测
- 真实凭据已脱敏,运行时配置在 backend/.env(不入 git)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-25 20:47:52 +08:00

141 lines
4.5 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!-- SourceRow.vue —— 文献来源单条(B3 归属)
FR-SOURCE-001/002/003:mono 列表视觉、字段缺失不渲染占位、外链优先级 url>doi>pmid>pmcid。 -->
<script setup>
import { computed, ref, watch } from 'vue';
import { T } from '../../i18n/labels.js';
const props = defineProps({
source: { type: Object, required: true },
index: { type: Number, default: 0 },
count: { type: Number, default: null }, // 会话级聚合的出现次数
highlighted: { type: Boolean, default: false }, // FR-CHAT-015:引用 chip 定位高亮
});
const expanded = ref(false);
const rootEl = ref(null);
// 高亮激活时滚动到可视区(FR-CHAT-015 滚动定位)
watch(
() => props.highlighted,
(on) => {
if (on && rootEl.value) {
rootEl.value.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
}
},
);
/** 外链优先级:url > doi > pmid > pmcid;均无 → null(不显示链接图标,FR-SOURCE-003) */
const linkHref = computed(() => {
const s = props.source || {};
if (s.url) return s.url;
if (s.doi) return `https://doi.org/${s.doi}`;
if (s.pmid) return `https://pubmed.ncbi.nlm.nih.gov/${s.pmid}/`;
if (s.pmcid) return `https://www.ncbi.nlm.nih.gov/pmc/articles/${s.pmcid}/`;
return null;
});
const metaParts = computed(() => {
const s = props.source || {};
const parts = [];
if (s.pmid) parts.push(`PMID:${s.pmid}`);
if (s.doi) parts.push(`DOI:${s.doi}`);
if (s.score != null) parts.push(`${T.score} ${Number(s.score).toFixed(2)}`);
if (props.count != null && props.count > 1) parts.push(`×${props.count}`);
return parts;
});
</script>
<template>
<div ref="rootEl" class="ft-item source-row" :class="{ 'hl-active': highlighted }">
<span class="ft-ico idx">{{ index + 1 }}</span>
<div class="src-main">
<div class="src-line1">
<span class="ft-name" :title="source.title || ''">{{ source.title }}</span>
</div>
<div class="ft-size src-meta">{{ metaParts.join(' · ') }}</div>
<div v-if="source.snippet" class="src-snippet" :class="{ clamp: !expanded }" @click="expanded = !expanded">
{{ source.snippet }}
</div>
</div>
<a
v-if="linkHref"
class="ft-dl"
:href="linkHref"
target="_blank"
rel="noopener"
:title="linkHref"
@click.stop
>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" />
<path d="M15 3h6v6" />
<path d="M10 14L21 3" />
</svg>
</a>
</div>
</template>
<style scoped>
.source-row {
padding: 6px 8px;
border-radius: var(--pa-r-sm);
font-family: var(--pa-font-mono);
font-size: 12.5px;
transition: background 0.15s var(--pa-ease);
}
.source-row:hover { background: var(--pa-surface-2); }
.source-row.hl-active {
background: color-mix(in srgb, var(--pa-g-to) 14%, transparent);
border-radius: var(--pa-r-sm);
animation: hl-fade 2s var(--pa-ease-out) forwards;
}
@keyframes hl-fade {
0% { background: color-mix(in srgb, var(--pa-g-to) 26%, transparent); }
100% { background: color-mix(in srgb, var(--pa-g-to) 6%, transparent); }
}
.ft-item { display: flex; align-items: flex-start; gap: 8px; }
.ft-ico { flex: none; width: 16px; text-align: center; }
.idx { color: var(--pa-ink-subtle); font-size: 11px; line-height: 1.7; }
.src-main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 3px; }
.src-line1 { display: flex; align-items: baseline; min-width: 0; }
.ft-name {
flex: 1;
min-width: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
color: var(--pa-ink);
font-size: 12.5px;
}
.src-meta { font-size: 11px; color: var(--pa-ink-subtle); }
.ft-size { font-size: 11px; color: var(--pa-ink-subtle); }
.src-snippet {
font-size: 11.5px;
line-height: 1.6;
color: var(--pa-ink-muted);
font-family: var(--pa-font-ui);
cursor: pointer;
word-break: break-word;
}
.src-snippet.clamp {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.ft-dl {
flex: none;
color: var(--pa-ink-subtle);
display: inline-flex;
align-items: center;
padding-top: 2px;
transition: opacity 0.15s var(--pa-ease), color 0.15s var(--pa-ease);
}
.ft-dl svg { width: 13px; height: 13px; }
.ft-dl:hover { color: var(--pa-g-to); }
/* 移动端触摸:链接图标常驻(FR-RESP-007 由 media (hover:none) 兜底,此处对齐) */
@media (hover: none) {
.ft-dl { opacity: 1; }
}
</style>