diff --git a/src/api/admin/assets.ts b/src/api/admin/assets.ts index ff14216..cf1484e 100644 --- a/src/api/admin/assets.ts +++ b/src/api/admin/assets.ts @@ -308,7 +308,10 @@ export async function fetchRadarMap() { export interface WeeklyBriefStats { papers_count?: number + papers_analyzed?: number + high_value_papers_count?: number news_count?: number + news_analyzed?: number teachers_count?: number references_count?: number sections?: Record @@ -321,10 +324,12 @@ export interface WeeklyBriefRow { title: string stats?: WeeklyBriefStats | null generated_at?: string | null + has_docx?: boolean } export interface WeeklyBriefDetail extends WeeklyBriefRow { - markdown: string + docx_path?: string | null + content?: string } export async function fetchWeeklyBriefs(params: Record) { @@ -337,7 +342,38 @@ export async function fetchWeeklyBrief(id: number) { return data.data } -export async function generateWeeklyBrief(payload: { week_start?: string; week_end?: string } = {}) { +export interface WeeklyBriefWeekOption { + offset: number + label: string + week_start: string + week_end: string +} + +export async function fetchWeeklyBriefWeekOptions() { + const { data } = await http.get>('/admin/v1/weekly-briefs/week-options') + return data.data.items +} + +export async function generateWeeklyBrief(payload: { + week_start?: string + week_end?: string + week_offset?: number +} = {}) { const { data } = await http.post>('/admin/v1/weekly-briefs/generate', payload) return data.data } + +export async function downloadWeeklyBriefDocx(id: number, filename: string) { + const res = await http.get(`/admin/v1/weekly-briefs/${id}/download`, { + responseType: 'blob', + }) + const blob = new Blob([res.data], { + type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + }) + const url = URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = filename + a.click() + URL.revokeObjectURL(url) +} diff --git a/src/api/admin/demands.ts b/src/api/admin/demands.ts index b0cfd5f..9dd2d18 100644 --- a/src/api/admin/demands.ts +++ b/src/api/admin/demands.ts @@ -11,6 +11,10 @@ export interface DemandRow { status_dict_item_id: number status_item?: DictItemBrief | null title: string + industry_type?: string | null + main_business?: string | null + finance_amount?: string | null + job_positions?: string | null content: string contact_name?: string | null company?: string | null diff --git a/src/utils/admin-list.ts b/src/utils/admin-list.ts index 2f54cfd..52d5761 100644 --- a/src/utils/admin-list.ts +++ b/src/utils/admin-list.ts @@ -44,5 +44,7 @@ export function demandStatusClass(value?: string | null): string { } export function demandTypeClass(value?: string | null): string { - return value === 'tech' ? 'type-badge-brand' : 'type-badge-secondary' + if (value === 'finance') return 'type-badge-brand' + if (value === 'hire') return 'type-badge-info' + return 'type-badge-secondary' } diff --git a/src/views/assets/crawler/index.vue b/src/views/assets/crawler/index.vue index 3e4ef69..fbef73a 100644 --- a/src/views/assets/crawler/index.vue +++ b/src/views/assets/crawler/index.vue @@ -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(null) +const briefWeekOptions = ref([]) +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() }) @@ -740,12 +761,22 @@ usePageLoad(async () => {

AI 科技成果周报

- 基于爬虫入库的论文与资讯,按周自动生成 Markdown 简报(默认统计上一自然周周一至周日)。 + 基于爬虫入库的论文与资讯,按周自动生成 Markdown 简报。可选择本周(截至今日)或历史自然周。

- - 生成上周简报 - +
+ + + + + 生成简报 + +
@@ -762,9 +793,10 @@ usePageLoad(async () => { - + @@ -790,14 +822,23 @@ usePageLoad(async () => {
统计周期:{{ formatWeekRange(briefDetail.week_start, briefDetail.week_end) }} 论文 {{ briefDetail.stats?.papers_count ?? 0 }} 篇 + 高价值 {{ briefDetail.stats.high_value_papers_count }} 篇 资讯 {{ briefDetail.stats?.news_count ?? 0 }} 条 + 老师 {{ briefDetail.stats?.teachers_count ?? 0 }} 位
-
{{ briefDetail.markdown }}
+
@@ -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; diff --git a/src/views/demands/index.vue b/src/views/demands/index.vue index 7a16477..d623c91 100644 --- a/src/views/demands/index.vue +++ b/src/views/demands/index.vue @@ -191,7 +191,9 @@ usePageLoad(async () => { - + + + @@ -239,6 +241,26 @@ usePageLoad(async () => { + + + + + + + + + + + + + + + + + + + + @@ -252,7 +274,7 @@ usePageLoad(async () => { - + @@ -299,7 +321,16 @@ usePageLoad(async () => { - + @@ -370,6 +401,12 @@ usePageLoad(async () => { border-radius: 6px; margin-bottom: 12px; } +.follow-demand-meta { + margin-top: 6px; + font-size: 13px; + color: #6b7280; + line-height: 1.5; +} .follow-history-list { display: grid; gap: 10px;