|
|
|
|
@ -13,9 +13,12 @@ import {
|
|
|
|
|
type CrawlResolveResult,
|
|
|
|
|
fetchWeeklyBrief,
|
|
|
|
|
fetchWeeklyBriefs,
|
|
|
|
|
fetchWeeklyBriefWeekOptions,
|
|
|
|
|
generateWeeklyBrief,
|
|
|
|
|
downloadWeeklyBriefDocx,
|
|
|
|
|
type WeeklyBriefDetail,
|
|
|
|
|
type WeeklyBriefRow,
|
|
|
|
|
type WeeklyBriefWeekOption,
|
|
|
|
|
} from '@/api/admin/assets'
|
|
|
|
|
import { fetchDictByCode } from '@/api/admin/dict'
|
|
|
|
|
import {
|
|
|
|
|
@ -58,6 +61,8 @@ 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 briefWeekOptions = ref<WeeklyBriefWeekOption[]>([])
|
|
|
|
|
const briefWeekOffset = ref(0)
|
|
|
|
|
const form = ref({
|
|
|
|
|
target_type: 'paper' as TargetType,
|
|
|
|
|
request_url: 'https://arxiv.org/',
|
|
|
|
|
@ -521,10 +526,21 @@ async function loadBriefs() {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function loadBriefWeekOptions() {
|
|
|
|
|
try {
|
|
|
|
|
briefWeekOptions.value = await fetchWeeklyBriefWeekOptions()
|
|
|
|
|
if (briefWeekOptions.value.length && !briefWeekOptions.value.some((o) => o.offset === briefWeekOffset.value)) {
|
|
|
|
|
briefWeekOffset.value = briefWeekOptions.value[0].offset
|
|
|
|
|
}
|
|
|
|
|
} catch {
|
|
|
|
|
briefWeekOptions.value = []
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function onGenerateBrief() {
|
|
|
|
|
briefGenerating.value = true
|
|
|
|
|
try {
|
|
|
|
|
const brief = await generateWeeklyBrief()
|
|
|
|
|
const brief = await generateWeeklyBrief({ week_offset: briefWeekOffset.value })
|
|
|
|
|
ElMessage.success('周报已生成')
|
|
|
|
|
briefPage.value = 1
|
|
|
|
|
await loadBriefs()
|
|
|
|
|
@ -548,29 +564,34 @@ async function openBriefDetail(id: number) {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function copyBriefMarkdown() {
|
|
|
|
|
if (!briefDetail.value?.markdown) return
|
|
|
|
|
async function copyBriefContent() {
|
|
|
|
|
const text = briefDetail.value?.content?.trim()
|
|
|
|
|
if (!text) {
|
|
|
|
|
ElMessage.warning('暂无简报正文,请重新生成')
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
try {
|
|
|
|
|
await navigator.clipboard.writeText(briefDetail.value.markdown)
|
|
|
|
|
ElMessage.success('已复制 Markdown')
|
|
|
|
|
await navigator.clipboard.writeText(text)
|
|
|
|
|
ElMessage.success('已复制简报内容')
|
|
|
|
|
} 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)
|
|
|
|
|
async function downloadBriefDocx(brief?: WeeklyBriefDetail | WeeklyBriefRow | null) {
|
|
|
|
|
if (!brief?.id) return
|
|
|
|
|
const filename = `高校科技成果周报_${brief.week_start}_${brief.week_end}.docx`
|
|
|
|
|
try {
|
|
|
|
|
await downloadWeeklyBriefDocx(brief.id, filename)
|
|
|
|
|
ElMessage.success('Word 简报已开始下载')
|
|
|
|
|
} catch (e: unknown) {
|
|
|
|
|
ElMessage.error(e instanceof Error ? e.message : '下载失败')
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
usePageLoad(async () => {
|
|
|
|
|
await resetPage()
|
|
|
|
|
await loadBriefWeekOptions()
|
|
|
|
|
await loadBriefs()
|
|
|
|
|
})
|
|
|
|
|
</script>
|
|
|
|
|
@ -740,12 +761,22 @@ usePageLoad(async () => {
|
|
|
|
|
<div>
|
|
|
|
|
<h3 class="brief-card-title">AI 科技成果周报</h3>
|
|
|
|
|
<p class="brief-card-desc">
|
|
|
|
|
基于爬虫入库的论文与资讯,按周自动生成 Markdown 简报(默认统计上一自然周周一至周日)。
|
|
|
|
|
基于爬虫入库的论文与资讯,按周自动生成 Markdown 简报。可选择本周(截至今日)或历史自然周。
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
<el-button type="primary" :loading="briefGenerating" @click="onGenerateBrief">
|
|
|
|
|
生成上周简报
|
|
|
|
|
</el-button>
|
|
|
|
|
<div class="brief-generate-actions">
|
|
|
|
|
<el-select v-model="briefWeekOffset" class="brief-week-select" placeholder="选择统计周">
|
|
|
|
|
<el-option
|
|
|
|
|
v-for="opt in briefWeekOptions"
|
|
|
|
|
:key="opt.offset"
|
|
|
|
|
:label="`${opt.label}(${opt.week_start} ~ ${opt.week_end})`"
|
|
|
|
|
:value="opt.offset"
|
|
|
|
|
/>
|
|
|
|
|
</el-select>
|
|
|
|
|
<el-button type="primary" :loading="briefGenerating" @click="onGenerateBrief">
|
|
|
|
|
生成简报
|
|
|
|
|
</el-button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<el-table v-loading="briefLoading" :data="briefItems" row-key="id" size="small">
|
|
|
|
|
@ -762,9 +793,10 @@ usePageLoad(async () => {
|
|
|
|
|
<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">
|
|
|
|
|
<el-table-column label="操作" width="180" fixed="right">
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
<el-button type="primary" link @click="openBriefDetail(row.id)">查看</el-button>
|
|
|
|
|
<el-button type="primary" link :disabled="!row.has_docx" @click="downloadBriefDocx(row)">下载 Word</el-button>
|
|
|
|
|
</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
</el-table>
|
|
|
|
|
@ -790,14 +822,23 @@ usePageLoad(async () => {
|
|
|
|
|
<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 v-if="briefDetail.stats?.high_value_papers_count != null">高价值 {{ briefDetail.stats.high_value_papers_count }} 篇</span>
|
|
|
|
|
<span>资讯 {{ briefDetail.stats?.news_count ?? 0 }} 条</span>
|
|
|
|
|
<span>老师 {{ briefDetail.stats?.teachers_count ?? 0 }} 位</span>
|
|
|
|
|
</div>
|
|
|
|
|
<pre v-if="briefDetail" class="brief-markdown">{{ briefDetail.markdown }}</pre>
|
|
|
|
|
<el-input
|
|
|
|
|
v-if="briefDetail"
|
|
|
|
|
:model-value="briefDetail.content || '暂无简报正文,请重新生成'"
|
|
|
|
|
type="textarea"
|
|
|
|
|
:rows="18"
|
|
|
|
|
readonly
|
|
|
|
|
class="brief-content-input"
|
|
|
|
|
/>
|
|
|
|
|
<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>
|
|
|
|
|
<el-button :disabled="!briefDetail?.content" @click="copyBriefContent">复制内容</el-button>
|
|
|
|
|
<el-button type="primary" :disabled="!briefDetail?.has_docx" @click="downloadBriefDocx(briefDetail)">下载 Word 简报</el-button>
|
|
|
|
|
</template>
|
|
|
|
|
</el-dialog>
|
|
|
|
|
|
|
|
|
|
@ -939,6 +980,15 @@ usePageLoad(async () => {
|
|
|
|
|
gap: 16px;
|
|
|
|
|
margin-bottom: 16px;
|
|
|
|
|
}
|
|
|
|
|
.brief-generate-actions {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
}
|
|
|
|
|
.brief-week-select {
|
|
|
|
|
width: 320px;
|
|
|
|
|
}
|
|
|
|
|
.brief-card-title {
|
|
|
|
|
margin: 0 0 6px;
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
@ -963,19 +1013,11 @@ usePageLoad(async () => {
|
|
|
|
|
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;
|
|
|
|
|
.brief-content-input :deep(textarea) {
|
|
|
|
|
font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif;
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
line-height: 1.65;
|
|
|
|
|
background: var(--el-fill-color-light);
|
|
|
|
|
border: 1px solid var(--el-border-color-lighter);
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
color: var(--el-text-color-primary);
|
|
|
|
|
}
|
|
|
|
|
.brief-loading {
|
|
|
|
|
min-height: 240px;
|
|
|
|
|
|