master
lion 6 days ago
parent 27d4e8750b
commit 031d9e57f6

@ -304,3 +304,39 @@ export async function fetchRadarMap() {
const { data } = await http.get<ApiBody<RadarMapData>>('/admin/v1/radar-map')
return data.data
}
export interface WeeklyBriefStats {
papers_count?: number
news_count?: number
teachers_count?: number
references_count?: number
sections?: Record<string, number>
}
export interface WeeklyBriefRow {
id: number
week_start: string
week_end: string
title: string
stats?: WeeklyBriefStats | null
generated_at?: string | null
}
export interface WeeklyBriefDetail extends WeeklyBriefRow {
markdown: string
}
export async function fetchWeeklyBriefs(params: Record<string, unknown>) {
const { data } = await http.get<ApiBody<Paginated<WeeklyBriefRow>>>('/admin/v1/weekly-briefs', { params })
return data.data
}
export async function fetchWeeklyBrief(id: number) {
const { data } = await http.get<ApiBody<WeeklyBriefDetail>>(`/admin/v1/weekly-briefs/${id}`)
return data.data
}
export async function generateWeeklyBrief(payload: { week_start?: string; week_end?: string } = {}) {
const { data } = await http.post<ApiBody<WeeklyBriefDetail>>('/admin/v1/weekly-briefs/generate', payload)
return data.data
}

@ -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>

Loading…
Cancel
Save