From 71076761852534be304f5daee2c2a578e9f0748f Mon Sep 17 00:00:00 2001 From: Ryan Wang Date: Tue, 10 Feb 2026 23:03:08 +0800 Subject: [PATCH] Refactor markdown import Signed-off-by: Ryan Wang --- settings.gradle | 2 +- ui/package.json | 4 +- ui/pnpm-lock.yaml | 35 ++---- ui/src/views/tabs/MarkdownImport.vue | 157 +++++++-------------------- ui/src/views/tabs/WordImport.vue | 150 +++++++------------------ 5 files changed, 97 insertions(+), 251 deletions(-) diff --git a/settings.gradle b/settings.gradle index 2123a19..fa55876 100644 --- a/settings.gradle +++ b/settings.gradle @@ -4,4 +4,4 @@ pluginManagement { } } rootProject.name = 'plugin-content-tools' -include 'ui' \ No newline at end of file +include 'ui' diff --git a/ui/package.json b/ui/package.json index a87e7be..03ac4ce 100644 --- a/ui/package.json +++ b/ui/package.json @@ -35,9 +35,9 @@ "mammoth": "^1.11.0", "markdown-it": "^14.1.0", "markdown-it-anchor": "^9.2.0", - "p-queue": "^8.1.1", "turndown": "^7.2.2", - "vue": "^3.5.27" + "vue": "^3.5.27", + "vue-reactive-queue": "^0.0.1" }, "devDependencies": { "@halo-dev/ui-plugin-bundler-kit": "2.22.0", diff --git a/ui/pnpm-lock.yaml b/ui/pnpm-lock.yaml index e2ea723..691df28 100644 --- a/ui/pnpm-lock.yaml +++ b/ui/pnpm-lock.yaml @@ -53,15 +53,15 @@ importers: markdown-it-anchor: specifier: ^9.2.0 version: 9.2.0(@types/markdown-it@14.1.2)(markdown-it@14.1.0) - p-queue: - specifier: ^8.1.1 - version: 8.1.1 turndown: specifier: ^7.2.2 version: 7.2.2 vue: specifier: ^3.5.27 version: 3.5.27(typescript@5.5.4) + vue-reactive-queue: + specifier: ^0.0.1 + version: 0.0.1(vue@3.5.27(typescript@5.5.4)) devDependencies: '@halo-dev/ui-plugin-bundler-kit': specifier: 2.22.0 @@ -1633,9 +1633,6 @@ packages: resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} engines: {node: '>=6'} - eventemitter3@5.0.1: - resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} - events@3.3.0: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} @@ -2157,14 +2154,6 @@ packages: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} - p-queue@8.1.1: - resolution: {integrity: sha512-aNZ+VfjobsWryoiPnEApGGmf5WmNsCo9xu8dfaYamG5qaLP7ClhLN6NgsFe6SwJ2UbLEBK5dv9x8Mn5+RVhMWQ==} - engines: {node: '>=18'} - - p-timeout@6.1.4: - resolution: {integrity: sha512-MyIV3ZA/PmyBN/ud8vV9XzwTrNtR4jFrObymZYnZqMmW0zA8Z17vnT0rBgFE/TlohB+YCHqXMgZzb3Csp49vqg==} - engines: {node: '>=14.16'} - package-manager-detector@1.3.0: resolution: {integrity: sha512-ZsEbbZORsyHuO00lY1kV3/t72yp6Ysay6Pd17ZAlNGuGwmWDLCJxFpRs0IzfXfj1o4icJOkUEioexFHzyPurSQ==} @@ -2765,6 +2754,11 @@ packages: vue: optional: true + vue-reactive-queue@0.0.1: + resolution: {integrity: sha512-b+dEyMihaBK+AGN23iDQs8GyGWBZmjh8yM2GZ0LqKCyHAUX/DEgXb31pH4N/XHarC+5aop0O3lmFBdBUN6IQOQ==} + peerDependencies: + vue: '>=3.0.0' + vue-resize@2.0.0-alpha.1: resolution: {integrity: sha512-7+iqOueLU7uc9NrMfrzbG8hwMqchfVfSzpVlCMeJQe4pyibqyoifDNbKTZvwxZKDvGkB+PdFeKvnGZMoEb8esg==} peerDependencies: @@ -4591,8 +4585,6 @@ snapshots: event-target-shim@5.0.1: {} - eventemitter3@5.0.1: {} - events@3.3.0: {} evp_bytestokey@1.0.3: @@ -5132,13 +5124,6 @@ snapshots: dependencies: p-limit: 3.1.0 - p-queue@8.1.1: - dependencies: - eventemitter3: 5.0.1 - p-timeout: 6.1.4 - - p-timeout@6.1.4: {} - package-manager-detector@1.3.0: {} pako@1.0.11: {} @@ -5730,6 +5715,10 @@ snapshots: '@vue/compiler-sfc': 3.5.27 vue: 3.5.27(typescript@5.5.4) + vue-reactive-queue@0.0.1(vue@3.5.27(typescript@5.5.4)): + dependencies: + vue: 3.5.27(typescript@5.5.4) + vue-resize@2.0.0-alpha.1(vue@3.5.27(typescript@5.5.4)): dependencies: vue: 3.5.27(typescript@5.5.4) diff --git a/ui/src/views/tabs/MarkdownImport.vue b/ui/src/views/tabs/MarkdownImport.vue index 681f3db..a1c401a 100644 --- a/ui/src/views/tabs/MarkdownImport.vue +++ b/ui/src/views/tabs/MarkdownImport.vue @@ -15,8 +15,8 @@ import { } from '@halo-dev/components'; import { utils } from '@halo-dev/ui-shared'; import { useSessionStorage } from '@vueuse/core'; -import PQueue from 'p-queue'; import { computed, reactive, ref } from 'vue'; +import { useQueue } from 'vue-reactive-queue'; import MingcuteCheckCircleFill from '~icons/mingcute/check-circle-fill'; import MingcuteCloseCircleLine from '~icons/mingcute/close-circle-line'; import MingcuteLoading3Fill from '~icons/mingcute/loading-3-fill'; @@ -29,39 +29,18 @@ const imageInput = ref(null); const convertToHtml = ref(false); const publishAfterImport = ref(true); -interface ImportItem { - id: string; - file: File; - filename: string; - status: 'pending' | 'processing' | 'success' | 'error'; - error?: string; -} +const { tasks, add, stats, clear, resume, retry, pause } = useQueue({ + concurrency: 3, + immediate: false, + onFinished() { + pause(); + }, +}); -const importQueue = reactive([]); const imageFiles = reactive([]); -const queue = new PQueue({ concurrency: 3 }); - -const queueStats = computed(() => { - const total = importQueue.length; - const pending = importQueue.filter((item) => item.status === 'pending').length; - const processing = importQueue.filter((item) => item.status === 'processing').length; - const success = importQueue.filter((item) => item.status === 'success').length; - const error = importQueue.filter((item) => item.status === 'error').length; - - return { - total, - pending, - processing, - success, - error, - queueSize: queue.size, - queuePending: queue.pending, - imageCount: imageFiles.length, - }; -}); const isBusy = computed(() => { - return queueStats.value.processing > 0; + return stats.value.running > 0; }); async function readFileContent(file: File): Promise { @@ -80,17 +59,12 @@ function onFilesSelected(event: Event) { for (let i = 0; i < files.length; i++) { const file = files[i]; if (file.type === 'text/markdown' || file.name.endsWith('.md')) { - const isDuplicate = importQueue.some( - (item) => item.file.name === file.name && item.file.size === file.size + const isDuplicate = tasks.value.some( + (item) => item.meta?.name === file.name && item.meta?.size === file.size ); if (!isDuplicate) { - importQueue.push({ - id: utils.id.uuid(), - file, - filename: file.name, - status: 'pending', - }); + add(async () => await processItem(file), file); } } } @@ -102,7 +76,7 @@ function onFolderSelected(event: Event) { const files = (event.target as HTMLInputElement).files; if (!files || files.length === 0) return; - const existingFileNames = new Set(importQueue.map((item) => item.filename)); + const existingFileNames = new Set(tasks.value.map((item) => item.meta?.name)); for (let i = 0; i < files.length; i++) { const file = files[i]; @@ -110,12 +84,7 @@ function onFolderSelected(event: Event) { if (file.type === 'text/markdown' || file.name.endsWith('.md')) { if (!existingFileNames.has(file.name)) { - importQueue.push({ - id: utils.id.uuid(), - file, - filename: file.name, - status: 'pending', - }); + add(async () => await processItem(file), file); existingFileNames.add(file.name); } } else if (isImageFile(file)) { @@ -199,61 +168,25 @@ async function processMarkdownImages(markdownContent: string): Promise { return processedContent; } -async function processItem(item: ImportItem): Promise { - item.status = 'processing'; - - try { - const raw = await readFileContent(item.file); - - await createPost(item, raw); - item.status = 'success'; - } catch (error) { - console.error('Failed to process item:', error); - item.status = 'error'; - item.error = error instanceof Error ? error.message : String(error); - throw error; - } -} - -async function handleStart() { - if (isBusy.value) return; - - const pendingItems = importQueue.filter( - (item) => item.status === 'pending' || item.status === 'error' - ); - - if (pendingItems.length === 0) return; - - const promises = pendingItems.map((item) => queue.add(() => processItem(item))); - await Promise.allSettled(promises); +async function processItem(file: File): Promise { + const raw = await readFileContent(file); + await createPost(file, raw); } function handleRetryAll() { if (isBusy.value) return; - importQueue - .filter((item) => item.status === 'error') + tasks.value + .filter((item) => item.status === 'rejected') .forEach((item) => { - item.status = 'pending'; - item.error = undefined; + retry(item.id); }); - - handleStart(); -} - -function retryItem(item: ImportItem) { - if (isBusy.value) return; - - item.status = 'pending'; - item.error = undefined; - - queue.add(() => processItem(item)); } -async function createPost(item: ImportItem, raw: string) { +async function createPost(file: File, raw: string) { const processedRaw = await processMarkdownImages(raw); - const fileName = item.file.name.replace(/\.md$/, ''); + const fileName = file.name.replace(/\.md$/, ''); const parsedMatter = readMatter(processedRaw); @@ -332,10 +265,6 @@ async function createPost(item: ImportItem, raw: string) { } } -function handleClear() { - importQueue.length = 0; -} - function handleClearImages() { imageFiles.length = 0; } @@ -372,9 +301,7 @@ const showAlert = useSessionStorage('plugin:content-tools:markdown-import-alert' 选择 Markdown 文件 选择 Markdown 文件夹 选择图片文件夹 - - 清空文件 - + 清空文件 清空图片 @@ -424,42 +351,42 @@ const showAlert = useSessionStorage('plugin:content-tools:markdown-import-alert' > -
+
- 总计: {{ queueStats.total }} + 总计: {{ tasks.length }} 待处理: - {{ queueStats.pending }} + {{ stats.pending }} 处理中: - {{ queueStats.processing }} + {{ stats.running }} - 成功: {{ queueStats.success }} + 成功: {{ stats.completed }} 失败: - - {{ queueStats.error }} + + {{ stats.failed }} - - 图片: {{ queueStats.imageCount }} + + 图片: {{ imageFiles.length }}
- + 重试所有 开始导入 @@ -468,14 +395,14 @@ const showAlert = useSessionStorage('plugin:content-tools:markdown-import-alert'
- +