Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 88 additions & 0 deletions components/modal/ArticleTaskDetail.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
<script setup lang="ts">
interface DetailItem {
title: string;
publishTime: string;
url: string;
}

const props = withDefaults(
defineProps<{
title: string;
description?: string;
items: DetailItem[];
collapsedCount?: number;
}>(),
{
description: '',
collapsedCount: 10,
}
);

const modal = useModal();
const expanded = ref(false);

const canExpand = computed(() => props.items.length > props.collapsedCount);
const visibleItems = computed(() => {
if (expanded.value) {
return props.items;
}
return props.items.slice(0, props.collapsedCount);
});

function closeModal() {
modal.close();
}
</script>

<template>
<UModal prevent-close>
<UCard>
<template #header>
<div class="flex items-start justify-between gap-3">
<div class="space-y-1">
<h3 class="text-base font-semibold">{{ title }}</h3>
<p v-if="description" class="text-sm text-gray-500">
{{ description }}
</p>
</div>
<UButton square variant="link" color="gray" icon="i-lucide:x" @click="closeModal" />
</div>
</template>

<div class="space-y-3">
<div class="text-sm text-gray-500">
共 {{ items.length }} 篇,当前展示 {{ visibleItems.length }} 篇
</div>

<div class="max-h-[380px] overflow-auto rounded border border-gray-200 p-2">
<div v-if="visibleItems.length === 0" class="text-sm text-gray-500">暂无明细</div>
<div v-else class="space-y-2">
<div
v-for="(item, index) in visibleItems"
:key="item.url"
class="rounded border border-gray-100 bg-gray-50 px-3 py-2"
>
<div class="text-sm font-medium break-all">{{ index + 1 }}. {{ item.title }}</div>
<div class="mt-1 text-xs text-gray-500">发布时间:{{ item.publishTime }}</div>
</div>
</div>
</div>

<div v-if="canExpand" class="flex justify-end">
<UButton
variant="ghost"
color="gray"
:label="expanded ? '收起' : `展开全部(${items.length})`"
@click="expanded = !expanded"
/>
</div>
</div>

<template #footer>
<div class="flex justify-end">
<UButton color="primary" @click="closeModal">我知道了</UButton>
</div>
</template>
</UCard>
</UModal>
</template>
19 changes: 18 additions & 1 deletion composables/useDownloader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import type { Metadata } from '~/store/v2/metadata';
import { Downloader } from '~/utils/download/Downloader';
import type { DownloaderStatus } from '~/utils/download/types';

type DownloadTaskType = 'html' | 'metadata' | 'comment' | 'fakeid';

export interface DownloadArticleOptions {
// 文章内容下载成功回调
onContent: (url: string) => void;
Expand All @@ -22,6 +24,9 @@ export interface DownloadArticleOptions {

// 修复单篇文章下载的 fakeid 专用
onFakeID: (url: string, fakeid: string) => void;

// 抓取任务结束回调(用于展示失败明细)
onFinish: (type: DownloadTaskType, status: DownloaderStatus) => void;
}

export default (options: Partial<DownloadArticleOptions> = {}) => {
Expand Down Expand Up @@ -75,6 +80,9 @@ export default (options: Partial<DownloadArticleOptions> = {}) => {
'【文章内容】抓取完成',
`本次抓取耗时 ${formatElapsedTime(seconds)}, 成功:${status.completed.length}, 失败:${status.failed.length}, 检测到已被删除:${status.deleted.length}`
);
if (typeof options.onFinish === 'function') {
options.onFinish('html', status);
}
});
downloader.on('download:stop', () => {
toast.info('HTML下载任务已停止');
Expand Down Expand Up @@ -134,6 +142,9 @@ export default (options: Partial<DownloadArticleOptions> = {}) => {
'【阅读量】抓取完成',
`本次抓取耗时 ${formatElapsedTime(seconds)}, 成功:${status.completed.length}, 失败:${status.failed.length}, 检测到已被删除:${status.deleted.length}`
);
if (typeof options.onFinish === 'function') {
options.onFinish('metadata', status);
}
});

await downloader.startDownload('metadata');
Expand Down Expand Up @@ -178,6 +189,9 @@ export default (options: Partial<DownloadArticleOptions> = {}) => {
'【留言内容】抓取完成',
`本次抓取耗时 ${formatElapsedTime(seconds)}, 成功:${status.completed.length}, 失败:${status.failed.length}`
);
if (typeof options.onFinish === 'function') {
options.onFinish('comment', status);
}
});

await downloader.startDownload('comments');
Expand Down Expand Up @@ -225,6 +239,9 @@ export default (options: Partial<DownloadArticleOptions> = {}) => {
'【fakeid】修复完成',
`本次耗时 ${formatElapsedTime(seconds)}, 成功:${status.completed.length}, 失败:${status.failed.length}`
);
if (typeof options.onFinish === 'function') {
options.onFinish('fakeid', status);
}
});

await downloader.startDownload('fakeid');
Expand All @@ -237,7 +254,7 @@ export default (options: Partial<DownloadArticleOptions> = {}) => {
}
}

async function download(type: 'html' | 'metadata' | 'comment' | 'fakeid', urls: string[]) {
async function download(type: DownloadTaskType, urls: string[]) {
if (type === 'html') {
await downloadArticleHTML(urls);
} else if (type === 'metadata') {
Expand Down
18 changes: 15 additions & 3 deletions composables/useExporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ import toastFactory from '~/composables/toast';
import { Exporter } from '~/utils/download/Exporter';
import type { ExporterStatus } from '~/utils/download/types';

export default () => {
export interface UseExporterOptions {
onContentMissing: (urls: string[]) => void;
}

export default (options: Partial<UseExporterOptions> = {}) => {
const toast = toastFactory();

const loading = ref(false);
Expand Down Expand Up @@ -287,10 +291,18 @@ export default () => {
function exportFile(
type: 'excel' | 'json' | 'html' | 'text' | 'markdown' | 'word' | 'pdf',
urls: string[],
contentNotDownloadedCount?: number,
contentNotDownloaded: number | string[] = [],
) {
if (needsContentFormats.has(type) && contentNotDownloadedCount) {
const contentNotDownloadedUrls = Array.isArray(contentNotDownloaded) ? contentNotDownloaded : [];
const contentNotDownloadedCount = Array.isArray(contentNotDownloaded)
? contentNotDownloaded.length
: contentNotDownloaded;

if (needsContentFormats.has(type) && contentNotDownloadedCount > 0) {
toast.warning('提示', `有 ${contentNotDownloadedCount} 篇文章尚未抓取内容,请先抓取内容后再导出`);
if (contentNotDownloadedUrls.length > 0 && typeof options.onContentMissing === 'function') {
options.onContentMissing(contentNotDownloadedUrls);
}
return;
}

Expand Down
88 changes: 79 additions & 9 deletions pages/dashboard/article.vue
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import GridAlbum from '~/components/grid/Album.vue';
import GridArticleActions from '~/components/grid/ArticleActions.vue';
import GridCoverTooltip from '~/components/grid/CoverTooltip.vue';
import GridStatusBar from '~/components/grid/StatusBar.vue';
import ArticleTaskDetailModal from '~/components/modal/ArticleTaskDetail.vue';
import AccountSelectorForArticle from '~/components/selector/AccountSelectorForArticle.vue';
import { isDev, websiteName } from '~/config';
import { sharedGridOptions } from '~/config/shared-grid-options';
Expand All @@ -31,7 +32,7 @@ import { type MpAccount } from '~/store/v2/info';
import { getMetadataCache, type Metadata } from '~/store/v2/metadata';
import type { Preferences } from '~/types/preferences';
import type { AppMsgExWithFakeID } from '~/types/types';
import type { ArticleMetadata } from '~/utils/download/types';
import type { ArticleMetadata, DownloaderStatus } from '~/utils/download/types';
import { createBooleanColumnFilterParams, createDateColumnFilterParams } from '~/utils/grid';

useHead({
Expand Down Expand Up @@ -348,6 +349,7 @@ function onFilterChanged(event: FilterChangedEvent) {
}

const preferences = usePreferences();
const modal = useModal();
const hideDeleted = computed(() => (preferences.value as unknown as Preferences).hideDeleted);

const previewArticleRef = ref<typeof PreviewArticle | null>(null);
Expand Down Expand Up @@ -408,10 +410,71 @@ function onSelectionChanged(event: SelectionChangedEvent) {
const selectedArticleUrls = computed(() => {
return selectedArticles.value.map(article => article.link);
});
const contentNotDownloadedCount = computed(() => {
return selectedArticles.value.filter(article => !article.contentDownload).length;
const contentNotDownloadedUrls = computed(() => {
return selectedArticles.value.filter(article => !article.contentDownload).map(article => article.link);
});

type TaskDetailItem = {
title: string;
publishTime: string;
url: string;
};

function findArticleByUrl(url: string): Article | undefined {
return selectedArticles.value.find(article => article.link === url) || globalRowData.find(article => article.link === url);
}

function formatArticlePublishTime(article?: Article): string {
if (!article) {
return '--';
}
const ts = article.update_time || article.create_time;
if (!ts) {
return '--';
}
return formatTimeStamp(ts);
}

function buildTaskDetailItems(urls: string[]): TaskDetailItem[] {
return urls.map(url => {
const article = findArticleByUrl(url);
return {
title: article?.title || url,
publishTime: formatArticlePublishTime(article),
url,
};
});
}

function openTaskDetailModal(title: string, description: string, urls: string[]) {
const items = buildTaskDetailItems(urls);
modal.open(ArticleTaskDetailModal, {
title,
description,
items,
collapsedCount: 10,
});
}

function handleDownloadFinish(type: 'html' | 'metadata' | 'comment' | 'fakeid', status: DownloaderStatus) {
if (status.failed.length === 0) {
return;
}

const titleMap = {
html: '文章内容抓取失败明细',
metadata: '阅读量抓取失败明细',
comment: '留言抓取失败明细',
fakeid: 'fakeid 修复失败明细',
} as const;

openTaskDetailModal(
titleMap[type],
`共 ${status.failed.length} 篇抓取失败。默认展示前 10 条,可展开查看全部。`,
status.failed
);
}

const {
loading: downloadBtnLoading,
completed_count: downloadCompletedCount,
Expand Down Expand Up @@ -489,6 +552,9 @@ const {
console.warn(`${url} not found in table data when update commentDownload`);
}
},
onFinish(type, status) {
handleDownloadFinish(type, status);
},
});

const {
Expand All @@ -497,7 +563,11 @@ const {
completed_count: exportCompletedCount,
total_count: exportTotalCount,
exportFile,
} = useExporter();
} = useExporter({
onContentMissing(urls: string[]) {
openTaskDetailModal('存在未抓取内容的文章', `共 ${urls.length} 篇未抓取内容。默认展示前 10 条,可展开查看全部。`, urls);
},
});

async function debug() {
const cache = await getDebugCache('https://mp.weixin.qq.com/s/0IEaqpJIBGykHFKqj-7xqw');
Expand Down Expand Up @@ -570,11 +640,11 @@ function copyWechatLink() {
]"
@export-article-excel="exportFile('excel', selectedArticleUrls)"
@export-article-json="exportFile('json', selectedArticleUrls)"
@export-article-html="exportFile('html', selectedArticleUrls, contentNotDownloadedCount)"
@export-article-text="exportFile('text', selectedArticleUrls, contentNotDownloadedCount)"
@export-article-markdown="exportFile('markdown', selectedArticleUrls, contentNotDownloadedCount)"
@export-article-word="exportFile('word', selectedArticleUrls, contentNotDownloadedCount)"
@export-article-pdf="exportFile('pdf', selectedArticleUrls, contentNotDownloadedCount)"
@export-article-html="exportFile('html', selectedArticleUrls, contentNotDownloadedUrls)"
@export-article-text="exportFile('text', selectedArticleUrls, contentNotDownloadedUrls)"
@export-article-markdown="exportFile('markdown', selectedArticleUrls, contentNotDownloadedUrls)"
@export-article-word="exportFile('word', selectedArticleUrls, contentNotDownloadedUrls)"
@export-article-pdf="exportFile('pdf', selectedArticleUrls, contentNotDownloadedUrls)"
>
<UButton
:loading="exportBtnLoading"
Expand Down