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(null) const loading = ref(true) const saving = ref(false) const saved = ref(false) const rating = ref(0) const readingStatus = ref('') const notes = ref([]) const personalTags = ref([]) const relatedItems = ref([]) const relatedSource = ref('') const relatedLoading = ref(true) const studyTypes = computed(() => computeStudyTypes(lit.value?.pub_types || [])) const field_labels: Record = { 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, } }