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 011bcf0..31ba43d 100644 --- a/ui/package.json +++ b/ui/package.json @@ -35,7 +35,7 @@ "mammoth": "^1.12.0", "markdown-it": "^14.1.1", "markdown-it-anchor": "^9.2.0", - "p-queue": "^8.1.1", + "vue-reactive-queue": "^0.0.1", "turndown": "^7.2.4", "vue": "^3.5.32" }, diff --git a/ui/pnpm-lock.yaml b/ui/pnpm-lock.yaml index 6523811..9d71132 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.1) - p-queue: - specifier: ^8.1.1 - version: 8.1.1 turndown: specifier: ^7.2.4 version: 7.2.4 vue: specifier: ^3.5.32 version: 3.5.32(typescript@5.5.4) + vue-reactive-queue: + specifier: ^0.0.1 + version: 0.0.1(vue@3.5.32(typescript@5.5.4)) devDependencies: '@halo-dev/ui-plugin-bundler-kit': specifier: 2.22.0 @@ -1995,9 +1995,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'} @@ -2545,14 +2542,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==} @@ -2624,10 +2613,6 @@ packages: resolution: {integrity: sha512-UMz42UD0UY0EApS0ZL9o1XnLhSTtvvvLe5Dc2H2O56fvRZi+KulDyf5ctDhhtYJBGKStV2FL1fy6253cmLgqVQ==} engines: {node: '>=4'} - postcss@8.5.6: - resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} - engines: {node: ^10 || ^12 || >=14} - postcss@8.5.8: resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==} engines: {node: ^10 || ^12 || >=14} @@ -3230,6 +3215,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: @@ -3272,10 +3262,6 @@ packages: resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} engines: {node: '>=12'} - webpack-sources@3.3.3: - resolution: {integrity: sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==} - engines: {node: '>=10.13.0'} - webpack-sources@3.3.4: resolution: {integrity: sha512-7tP1PdV4vF+lYPnkMR0jMY5/la2ub5Fc/8VQrrU+lXkiM6C4TjVfGw7iKfyhnTQOsD+6Q/iKw0eFciziRgD58Q==} engines: {node: '>=10.13.0'} @@ -5393,8 +5379,6 @@ snapshots: event-target-shim@5.0.1: {} - eventemitter3@5.0.1: {} - events@3.3.0: {} evp_bytestokey@1.0.3: @@ -5995,13 +5979,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: {} @@ -6070,12 +6047,6 @@ snapshots: cssesc: 3.0.0 util-deprecate: 1.0.2 - postcss@8.5.6: - dependencies: - nanoid: 3.3.11 - picocolors: 1.1.1 - source-map-js: 1.2.1 - postcss@8.5.8: dependencies: nanoid: 3.3.11 @@ -6665,7 +6636,7 @@ snapshots: vite@5.4.9(@types/node@24.12.2)(sass@1.80.3)(terser@5.42.0): dependencies: esbuild: 0.21.5 - postcss: 8.5.6 + postcss: 8.5.8 rollup: 4.24.0 optionalDependencies: '@types/node': 24.12.2 @@ -6708,6 +6679,10 @@ snapshots: '@vue/compiler-sfc': 3.5.32 vue: 3.5.32(typescript@5.5.4) + vue-reactive-queue@0.0.1(vue@3.5.32(typescript@5.5.4)): + dependencies: + vue: 3.5.32(typescript@5.5.4) + vue-resize@2.0.0-alpha.1(vue@3.5.32(typescript@5.5.4)): dependencies: vue: 3.5.32(typescript@5.5.4) @@ -6752,8 +6727,6 @@ snapshots: webidl-conversions@7.0.0: {} - webpack-sources@3.3.3: {} - webpack-sources@3.3.4: {} webpack-virtual-modules@0.6.2: {} @@ -6783,7 +6756,7 @@ snapshots: tapable: 2.2.2 terser-webpack-plugin: 5.3.14(webpack@5.99.9) watchpack: 2.4.4 - webpack-sources: 3.3.3 + webpack-sources: 3.3.4 transitivePeerDependencies: - '@swc/core' - esbuild diff --git a/ui/src/views/tabs/MarkdownImport.vue b/ui/src/views/tabs/MarkdownImport.vue index 8e6f1d8..ce25972 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'
- +