|
|
|
|
@ -11,6 +11,11 @@ import {
|
|
|
|
|
type CrawlJobResult,
|
|
|
|
|
type CrawlParamField,
|
|
|
|
|
type CrawlResolveResult,
|
|
|
|
|
fetchWeeklyBrief,
|
|
|
|
|
fetchWeeklyBriefs,
|
|
|
|
|
generateWeeklyBrief,
|
|
|
|
|
type WeeklyBriefDetail,
|
|
|
|
|
type WeeklyBriefRow,
|
|
|
|
|
} from '@/api/admin/assets'
|
|
|
|
|
import { fetchDictByCode } from '@/api/admin/dict'
|
|
|
|
|
import { ElMessage } from 'element-plus'
|
|
|
|
|
@ -35,6 +40,13 @@ const teacherLeadItems = ref<CrawlJobItemRow[]>([])
|
|
|
|
|
const teacherItems = ref<CrawlJobItemRow[]>([])
|
|
|
|
|
const resultLoading = ref(false)
|
|
|
|
|
const newsCategoryOptions = ref<NewsCategoryOpt[]>([])
|
|
|
|
|
const briefLoading = ref(false)
|
|
|
|
|
const briefGenerating = ref(false)
|
|
|
|
|
const briefItems = ref<WeeklyBriefRow[]>([])
|
|
|
|
|
const briefMeta = ref({ current_page: 1, per_page: 10, total: 0 })
|
|
|
|
|
const briefPage = ref(1)
|
|
|
|
|
const briefDialog = ref(false)
|
|
|
|
|
const briefDetail = ref<WeeklyBriefDetail | null>(null)
|
|
|
|
|
const form = ref({
|
|
|
|
|
target_type: 'paper' as TargetType,
|
|
|
|
|
request_url: 'https://arxiv.org/',
|
|
|
|
|
@ -391,7 +403,84 @@ function goLibrary() {
|
|
|
|
|
const canViewResult = () =>
|
|
|
|
|
lastResult.value?.status === 'completed' && (lastResult.value.items_fetched ?? 0) > 0
|
|
|
|
|
|
|
|
|
|
usePageLoad(resetPage)
|
|
|
|
|
function formatWeekRange(start?: string | null, end?: string | null) {
|
|
|
|
|
if (!start || !end) return '—'
|
|
|
|
|
return `${start} 至 ${end}`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function formatBriefGeneratedAt(iso?: string | null) {
|
|
|
|
|
if (!iso) return '—'
|
|
|
|
|
const d = new Date(iso)
|
|
|
|
|
if (Number.isNaN(d.getTime())) return '—'
|
|
|
|
|
const pad = (n: number) => String(n).padStart(2, '0')
|
|
|
|
|
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function loadBriefs() {
|
|
|
|
|
briefLoading.value = true
|
|
|
|
|
try {
|
|
|
|
|
const res = await fetchWeeklyBriefs({ page: briefPage.value, page_size: briefMeta.value.per_page })
|
|
|
|
|
briefItems.value = res.items
|
|
|
|
|
briefMeta.value = res.meta
|
|
|
|
|
} catch {
|
|
|
|
|
briefItems.value = []
|
|
|
|
|
} finally {
|
|
|
|
|
briefLoading.value = false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function onGenerateBrief() {
|
|
|
|
|
briefGenerating.value = true
|
|
|
|
|
try {
|
|
|
|
|
const brief = await generateWeeklyBrief()
|
|
|
|
|
ElMessage.success('周报已生成')
|
|
|
|
|
briefPage.value = 1
|
|
|
|
|
await loadBriefs()
|
|
|
|
|
await openBriefDetail(brief.id)
|
|
|
|
|
} catch (e: unknown) {
|
|
|
|
|
const msg = e instanceof Error ? e.message : '周报生成失败'
|
|
|
|
|
ElMessage.error(msg)
|
|
|
|
|
} finally {
|
|
|
|
|
briefGenerating.value = false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function openBriefDetail(id: number) {
|
|
|
|
|
briefDialog.value = true
|
|
|
|
|
briefDetail.value = null
|
|
|
|
|
try {
|
|
|
|
|
briefDetail.value = await fetchWeeklyBrief(id)
|
|
|
|
|
} catch {
|
|
|
|
|
ElMessage.error('加载周报失败')
|
|
|
|
|
briefDialog.value = false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function copyBriefMarkdown() {
|
|
|
|
|
if (!briefDetail.value?.markdown) return
|
|
|
|
|
try {
|
|
|
|
|
await navigator.clipboard.writeText(briefDetail.value.markdown)
|
|
|
|
|
ElMessage.success('已复制 Markdown')
|
|
|
|
|
} catch {
|
|
|
|
|
ElMessage.error('复制失败')
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function downloadBriefMarkdown() {
|
|
|
|
|
if (!briefDetail.value?.markdown) return
|
|
|
|
|
const blob = new Blob([briefDetail.value.markdown], { type: 'text/markdown;charset=utf-8' })
|
|
|
|
|
const url = URL.createObjectURL(blob)
|
|
|
|
|
const a = document.createElement('a')
|
|
|
|
|
a.href = url
|
|
|
|
|
a.download = `AI科技成果简报_${briefDetail.value.week_start}_${briefDetail.value.week_end}.md`
|
|
|
|
|
a.click()
|
|
|
|
|
URL.revokeObjectURL(url)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
usePageLoad(async () => {
|
|
|
|
|
await resetPage()
|
|
|
|
|
await loadBriefs()
|
|
|
|
|
})
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<template>
|
|
|
|
|
@ -535,6 +624,72 @@ usePageLoad(resetPage)
|
|
|
|
|
</div>
|
|
|
|
|
</el-card>
|
|
|
|
|
|
|
|
|
|
<el-card shadow="never" class="admin-list-card brief-card">
|
|
|
|
|
<div class="brief-card-head">
|
|
|
|
|
<div>
|
|
|
|
|
<h3 class="brief-card-title">AI 科技成果周报</h3>
|
|
|
|
|
<p class="brief-card-desc">
|
|
|
|
|
基于爬虫入库的论文与资讯,按周自动生成 Markdown 简报(默认统计上一自然周周一至周日)。
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
<el-button type="primary" :loading="briefGenerating" @click="onGenerateBrief">
|
|
|
|
|
生成上周简报
|
|
|
|
|
</el-button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<el-table v-loading="briefLoading" :data="briefItems" row-key="id" size="small">
|
|
|
|
|
<el-table-column prop="title" label="简报标题" min-width="260" show-overflow-tooltip />
|
|
|
|
|
<el-table-column label="统计周期" width="200">
|
|
|
|
|
<template #default="{ row }">{{ formatWeekRange(row.week_start, row.week_end) }}</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
<el-table-column label="论文" width="70" align="center">
|
|
|
|
|
<template #default="{ row }">{{ row.stats?.papers_count ?? 0 }}</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
<el-table-column label="资讯" width="70" align="center">
|
|
|
|
|
<template #default="{ row }">{{ row.stats?.news_count ?? 0 }}</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
<el-table-column label="生成时间" width="160">
|
|
|
|
|
<template #default="{ row }">{{ formatBriefGeneratedAt(row.generated_at) }}</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
<el-table-column label="操作" width="120" fixed="right">
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
<el-button type="primary" link @click="openBriefDetail(row.id)">查看</el-button>
|
|
|
|
|
</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
</el-table>
|
|
|
|
|
<div class="brief-pager">
|
|
|
|
|
<el-pagination
|
|
|
|
|
v-model:current-page="briefPage"
|
|
|
|
|
layout="total, prev, pager, next"
|
|
|
|
|
:total="briefMeta.total"
|
|
|
|
|
:page-size="briefMeta.per_page"
|
|
|
|
|
@current-change="loadBriefs"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</el-card>
|
|
|
|
|
|
|
|
|
|
<el-dialog
|
|
|
|
|
v-model="briefDialog"
|
|
|
|
|
:title="briefDetail?.title || 'AI 科技成果周报'"
|
|
|
|
|
width="920px"
|
|
|
|
|
top="4vh"
|
|
|
|
|
destroy-on-close
|
|
|
|
|
class="brief-dialog"
|
|
|
|
|
>
|
|
|
|
|
<div v-if="briefDetail" class="brief-dialog-meta">
|
|
|
|
|
<span>统计周期:{{ formatWeekRange(briefDetail.week_start, briefDetail.week_end) }}</span>
|
|
|
|
|
<span>论文 {{ briefDetail.stats?.papers_count ?? 0 }} 篇</span>
|
|
|
|
|
<span>资讯 {{ briefDetail.stats?.news_count ?? 0 }} 条</span>
|
|
|
|
|
</div>
|
|
|
|
|
<pre v-if="briefDetail" class="brief-markdown">{{ briefDetail.markdown }}</pre>
|
|
|
|
|
<div v-else v-loading="true" class="brief-loading" />
|
|
|
|
|
<template #footer>
|
|
|
|
|
<el-button @click="briefDialog = false">关闭</el-button>
|
|
|
|
|
<el-button :disabled="!briefDetail" @click="copyBriefMarkdown">复制 Markdown</el-button>
|
|
|
|
|
<el-button type="primary" :disabled="!briefDetail" @click="downloadBriefMarkdown">下载 .md</el-button>
|
|
|
|
|
</template>
|
|
|
|
|
</el-dialog>
|
|
|
|
|
|
|
|
|
|
<el-dialog
|
|
|
|
|
v-model="resultDialog"
|
|
|
|
|
title="本次抓取结果"
|
|
|
|
|
@ -660,4 +815,55 @@ usePageLoad(resetPage)
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
color: var(--el-text-color-secondary);
|
|
|
|
|
}
|
|
|
|
|
.brief-card {
|
|
|
|
|
margin-top: 16px;
|
|
|
|
|
}
|
|
|
|
|
.brief-card-head {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: flex-start;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
gap: 16px;
|
|
|
|
|
margin-bottom: 16px;
|
|
|
|
|
}
|
|
|
|
|
.brief-card-title {
|
|
|
|
|
margin: 0 0 6px;
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
}
|
|
|
|
|
.brief-card-desc {
|
|
|
|
|
margin: 0;
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
color: var(--el-text-color-secondary);
|
|
|
|
|
line-height: 1.5;
|
|
|
|
|
}
|
|
|
|
|
.brief-pager {
|
|
|
|
|
margin-top: 16px;
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: flex-end;
|
|
|
|
|
}
|
|
|
|
|
.brief-dialog-meta {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
gap: 16px;
|
|
|
|
|
margin-bottom: 12px;
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
color: var(--el-text-color-secondary);
|
|
|
|
|
}
|
|
|
|
|
.brief-markdown {
|
|
|
|
|
margin: 0;
|
|
|
|
|
padding: 16px;
|
|
|
|
|
max-height: 62vh;
|
|
|
|
|
overflow: auto;
|
|
|
|
|
white-space: pre-wrap;
|
|
|
|
|
word-break: break-word;
|
|
|
|
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
line-height: 1.65;
|
|
|
|
|
background: var(--el-fill-color-light);
|
|
|
|
|
border: 1px solid var(--el-border-color-lighter);
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
}
|
|
|
|
|
.brief-loading {
|
|
|
|
|
min-height: 240px;
|
|
|
|
|
}
|
|
|
|
|
</style>
|
|
|
|
|
|