Files
backend/frontend/src/composables/useLiteratureDetail.ts
T
34047007@qq.com a6cd99a4ca
CI / backend (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
feat: initial commit - oncology literature search platform
OncoLit: a multi-tenant oncology literature search, feed, and
collaboration platform. Built with FastAPI + Vue 3 + PostgreSQL.
Includes PubMed pipeline, drug approvals, AI summaries, and
systematic review tools.
2026-07-27 07:59:18 +08:00

217 lines
6.9 KiB
TypeScript

import { ref, onMounted, computed } from 'vue'
import { useRouter } from 'vue-router'
import { api } from '../api/client'
import { useToast } from './useToast'
import { useAuthStore } from '../stores/auth'
import { toBeijingDateTime } from '../utils/date'
import { copyToClipboard } from '../utils/clipboard'
import { computeStudyTypes } from '../constants/studyTypes'
import { trackAction } from './useAnalytics'
import type { LiteratureDetail, NoteItem, PersonalTag, SavedItem } from '../types'
export function useLiteratureDetail(pmid: number) {
const router = useRouter()
const toast = useToast()
const lit = ref<LiteratureDetail | null>(null)
const loading = ref(true)
const saving = ref(false)
const saved = ref(false)
const rating = ref(0)
const readingStatus = ref('')
const notes = ref<NoteItem[]>([])
const personalTags = ref<PersonalTag[]>([])
const relatedItems = ref<any[]>([])
const relatedSource = ref('')
const relatedLoading = ref(true)
const studyTypes = computed(() => computeStudyTypes(lit.value?.pub_types || []))
const field_labels: Record<string, string> = {
sample_size: '样本量',
age: '年龄',
female: '女性',
male: '男性',
ecog: 'ECOG',
stage: '分期',
bmi: 'BMI',
smoking: '吸烟',
alcohol: '饮酒',
prior_therapy: '既往治疗',
histology: '病理类型',
subtype: '亚型',
race: '种族',
surgery: '手术史',
comorbidity: '合并症',
}
const tables = computed(() => lit.value?.full_text_sections?.tables || [])
const isUpdated = computed(() => {
if (!lit.value?.created_at || !lit.value?.updated_at) return false
return new Date(lit.value.updated_at).getTime() > new Date(lit.value.created_at).getTime()
})
const exportOptions = [
{ label: 'BibTeX', key: 'bibtex' },
{ label: 'RIS', key: 'ris' },
{ label: 'MLA', key: 'mla' },
{ label: 'EndNote', key: 'endnote' },
{ label: 'CSV', key: 'csv' },
]
const authStore = useAuthStore()
const planType = computed(() => authStore.currentTenant?.plan_type || 'free')
const exportFormats = computed(() => {
const freeFormats = ['bibtex', 'ris', 'mla']
return exportOptions.filter(o => {
if (planType.value === 'free') return freeFormats.includes(o.key)
return true
})
})
function formatDate(d: string | undefined | null) {
if (!d) return ''
// 有时间的完整时间戳 → 北京时间;纯日期 → 直接返回
return d.length > 10 ? toBeijingDateTime(d) : d.slice(0, 10)
}
function goBack() {
router.back()
}
function copyPmid() {
const pmid = lit.value?.pmid
if (!pmid) return
copyToClipboard(String(pmid)).then(ok => {
if (ok) toast.success('已复制 PMID')
else toast.info(String(pmid))
})
}
function copyDoi() {
const doi = lit.value?.doi
if (!doi) return
copyToClipboard(doi).then(ok => {
if (ok) toast.success('已复制 DOI')
else toast.info(doi)
})
}
function openPubMed(pmid: number) {
window.open(`https://pubmed.ncbi.nlm.nih.gov/${pmid}/`, '_blank')
}
function openDOI(doi: string) {
window.open(`https://doi.org/${doi}`, '_blank')
}
async function copyCitation() {
if (!lit.value?.pmid) return
try {
const { data } = await api.get(`/literature/export/${lit.value.pmid}/citation`, { params: { fmt: 'mla' } })
const ok = await copyToClipboard(data.citation)
if (ok) toast.success('已复制引用 (MLA)')
else toast.info(data.citation)
} catch (e) { toast.apiError(e, '复制失败') }
}
function handleExport(fmt: string) {
if (!lit.value?.pmid) return
trackAction('export', 'literature', String(lit.value.pmid), { format: fmt })
const url = `/api/v1/literature/export/${lit.value.pmid}/${fmt}`
window.open(url, '_blank')
}
async function handleSave() {
saving.value = true
try {
if (saved.value) {
await api.delete(`/literature/${pmid}/save`)
saved.value = false
toast.success('已取消收藏')
} else {
await api.post(`/literature/${pmid}/save`)
saved.value = true
toast.success('已收藏')
trackAction('save_literature', 'literature', String(pmid))
}
} catch (e) { toast.apiError(e, '操作失败') }
finally { saving.value = false }
}
async function handleRate(star: number) {
try {
// 如果未收藏,先自动收藏再评分
if (!saved.value) {
await api.post(`/literature/${pmid}/save`)
saved.value = true
}
await api.post(`/settings/literature/${pmid}/rate`, { rating: star })
rating.value = star
} catch (e) { toast.apiError(e, '评分失败,请重试') }
}
async function updateReadingStatus(status: string) {
try {
await api.put(`/literature/${pmid}/reading-status`, { status })
readingStatus.value = status
} catch (e) { toast.apiError(e, '更新阅读状态失败') }
}
const READING_STATUS_OPTIONS = [
{ value: 'unread', label: '未读', color: '#999' },
{ value: 'reading', label: '在读', color: '#f0a020' },
{ value: 'completed', label: '已读', color: '#18a058' },
]
function readingStatusLabel(status: string): string {
return READING_STATUS_OPTIONS.find(o => o.value === status)?.label || status
}
function readingStatusColor(status: string): string {
return READING_STATUS_OPTIONS.find(o => o.value === status)?.color || '#999'
}
onMounted(async () => {
try {
const [litRes, notesRes, savedRes] = await Promise.all([
api.get(`/literature/${pmid}`),
api.get(`/notes/literature/${pmid}`),
api.get('/literature/saved'),
])
lit.value = litRes.data
notes.value = notesRes.data || []
const savedItem = (savedRes.data.items || []).find((i: SavedItem) => i.pmid === Number(pmid))
if (savedItem) {
saved.value = true
rating.value = savedItem.rating || 0
personalTags.value = savedItem.personal_tags || []
readingStatus.value = savedItem.reading_status || ''
}
// detail 端返回的 reading_status 优先级更高(可能刚被自动更新为 reading)
if (litRes.data.reading_status) {
readingStatus.value = litRes.data.reading_status
}
trackAction('view_literature', 'literature', String(pmid))
} finally { loading.value = false }
try {
const { data } = await api.get(`/literature/${pmid}/related`)
relatedItems.value = data.items || []
relatedSource.value = data.source || ''
} finally { relatedLoading.value = false }
})
return {
lit, loading, saving, saved, rating, readingStatus,
studyTypes, tables, field_labels, isUpdated,
notes, personalTags, relatedItems, relatedSource, relatedLoading,
formatDate, goBack, copyPmid, copyDoi, openPubMed, openDOI,
exportOptions, exportFormats, handleExport, copyCitation,
handleSave, handleRate, updateReadingStatus,
READING_STATUS_OPTIONS, readingStatusLabel, readingStatusColor,
}
}