From 882ccdac12b888fd33663fec4f7c6b5a59abbb8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E8=87=AA=E8=BE=BE?= <980324341@qq.com> Date: Sun, 13 Sep 2026 23:16:59 +0800 Subject: [PATCH] =?UTF-8?q?refactor(=E5=93=81=E7=89=8C=E5=B7=A5=E5=85=B7?= =?UTF-8?q?=E9=A1=B5):=20=E6=8A=BD=E5=8F=96=20formatDateTime=20=E4=B8=8E?= =?UTF-8?q?=20uploadPathsToJava=20=E5=85=AC=E5=85=B1=E5=B7=A5=E5=85=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 shared/utils/datetime.ts:13 个品牌页逐字重复的 formatDateTime 收敛为一处; 语义不同的 3 个变体(toLocaleString / 字符串切片 / 支持时间戳)保留不动 - 新增 shared/utils/upload-to-java.ts:10 个品牌页的上传循环收敛,api 依赖注入便于单测; 保留 uploadOss(品牌Tab)与 returnEmptyWhenUnavailable(跟价)两处行为差异 - 补 datetime / upload-to-java 单测 10 例 净减约 327 行;vue-tsc 构建与 690 个前端单测全通过 --- .../components/BrandAppearancePatentTab.vue | 39 ++--------- .../pages/brand/components/BrandBrandTab.vue | 38 ++-------- .../brand/components/BrandCollectDataTab.vue | 39 ++--------- .../brand/components/BrandConvertTab.vue | 39 ++--------- .../pages/brand/components/BrandDedupeTab.vue | 39 ++--------- .../brand/components/BrandDeleteBrandTab.vue | 39 ++--------- .../brand/components/BrandPatrolDeleteTab.vue | 14 +--- .../brand/components/BrandPriceTrackTab.vue | 37 ++-------- .../brand/components/BrandProductRiskTab.vue | 14 +--- .../brand/components/BrandPublishTab.vue | 23 ++---- .../brand/components/BrandQueryAsinTab.vue | 14 +--- .../brand/components/BrandSimilarAsinTab.vue | 39 ++--------- .../pages/brand/components/BrandSplitTab.vue | 39 ++--------- .../brand/components/BrandWithdrawTab.vue | 14 +--- frontend-vue/src/shared/utils/datetime.ts | 20 ++++++ .../src/shared/utils/upload-to-java.ts | 47 +++++++++++++ frontend-vue/tests/datetime.test.ts | 23 ++++++ frontend-vue/tests/upload-to-java.test.ts | 70 +++++++++++++++++++ 18 files changed, 210 insertions(+), 377 deletions(-) create mode 100644 frontend-vue/src/shared/utils/datetime.ts create mode 100644 frontend-vue/src/shared/utils/upload-to-java.ts create mode 100644 frontend-vue/tests/datetime.test.ts create mode 100644 frontend-vue/tests/upload-to-java.test.ts diff --git a/frontend-vue/src/pages/brand/components/BrandAppearancePatentTab.vue b/frontend-vue/src/pages/brand/components/BrandAppearancePatentTab.vue index 0bc37320..b6c95cf9 100644 --- a/frontend-vue/src/pages/brand/components/BrandAppearancePatentTab.vue +++ b/frontend-vue/src/pages/brand/components/BrandAppearancePatentTab.vue @@ -97,7 +97,7 @@ import { type UploadedFileRef, type UploadFileVo, } from '@/shared/api/java-modules' -import { expandBrandFolderRecursive, type BrandExpandFolderItem } from '@/shared/api/brand' +import { expandBrandFolderRecursive } from '@/shared/api/brand' import { getPywebviewApi } from '@/shared/bridges/pywebview' import { getTaskPollIntervalMs } from '@/shared/task-progress-config' import { getStoredApiSecret } from '@/shared/utils/api-secret-store' @@ -110,6 +110,8 @@ import { checkSelectedFiles, } from '@/shared/dispatch-guard' import { passGuard } from '@/shared/dispatch-guard-ui' +import { formatDateTime } from '@/shared/utils/datetime' +import { uploadPathsToJava } from '@/shared/utils/upload-to-java' const selectedFileNames = ref([]) const uploadedFiles = ref([]) @@ -293,37 +295,6 @@ function loadPollingIds() { } } -function formatDateTime(value?: string) { - if (!value) return '-' - const date = new Date(value) - if (Number.isNaN(date.getTime())) return value - const year = date.getFullYear() - const month = String(date.getMonth() + 1).padStart(2, '0') - const day = String(date.getDate()).padStart(2, '0') - const hours = String(date.getHours()).padStart(2, '0') - const minutes = String(date.getMinutes()).padStart(2, '0') - const seconds = String(date.getSeconds()).padStart(2, '0') - return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}` -} - -async function uploadAppearancePathsToJava(paths: Array) { - const api = getPywebviewApi() - if (!api?.upload_file_to_java) { - throw new Error('当前桌面端未提供文件上传能力') - } - const files: UploadFileVo[] = [] - for (const item of paths) { - const filePath = typeof item === 'string' ? item : item.absolutePath - const relativePath = typeof item === 'string' ? undefined : item.relativePath - const uploaded = await api.upload_file_to_java(filePath, relativePath) - if (!uploaded?.success || !uploaded.data) { - throw new Error(uploaded?.error || uploaded?.message || `上传失败:${filePath}`) - } - files.push(uploaded.data) - } - return files -} - async function selectFiles() { const api = getPywebviewApi() if (!api?.select_brand_xlsx_files || !api.upload_file_to_java) { @@ -334,7 +305,7 @@ async function selectFiles() { if (!paths?.length) return if (!(await passGuard(checkSelectedFiles(paths, { allowedExtensions: EXCEL_EXTENSIONS })))) return try { - const files = await uploadAppearancePathsToJava(paths) + const files = await uploadPathsToJava(getPywebviewApi(), paths) uploadedFiles.value = files selectedFileNames.value = files.map((f) => f.relativePath || f.originalFilename || f.fileKey) parseResult.value = null @@ -366,7 +337,7 @@ async function selectFolder() { return } if (!(await passGuard(checkSelectedFiles(result.items, { allowedExtensions: EXCEL_EXTENSIONS })))) return - const files = await uploadAppearancePathsToJava(result.items) + const files = await uploadPathsToJava(getPywebviewApi(), result.items) uploadedFiles.value = files selectedFileNames.value = result.items.map((item) => item.relativePath || item.absolutePath) parseResult.value = null diff --git a/frontend-vue/src/pages/brand/components/BrandBrandTab.vue b/frontend-vue/src/pages/brand/components/BrandBrandTab.vue index b662fae5..7743d611 100644 --- a/frontend-vue/src/pages/brand/components/BrandBrandTab.vue +++ b/frontend-vue/src/pages/brand/components/BrandBrandTab.vue @@ -125,7 +125,8 @@ import { getTaskPollIntervalMs } from '@/shared/task-progress-config' import { createCategorizedTimers } from '@/shared/utils/categorized-timers' import type { UploadFileVo } from '@/shared/api/upload.ts' import type { UploadedFileRef } from '@/shared/api/types/upload.ts' -import type { BrandExpandFolderItem } from '@/shared/api/types/modules/brand' +import { formatDateTime } from '@/shared/utils/datetime' +import { uploadPathsToJava } from '@/shared/utils/upload-to-java' const uploadedFiles = ref([]) const strategy = ref<'Terms' | 'Simple'>('Simple') @@ -342,38 +343,7 @@ function toBrandTaskView(item: BrandTaskItem): TaskItemView { } } -function formatDateTime(value?: string) { - if (!value) return '-' - const date = new Date(value) - if (Number.isNaN(date.getTime())) return value - const year = date.getFullYear() - const month = String(date.getMonth() + 1).padStart(2, '0') - const day = String(date.getDate()).padStart(2, '0') - const hours = String(date.getHours()).padStart(2, '0') - const minutes = String(date.getMinutes()).padStart(2, '0') - const seconds = String(date.getSeconds()).padStart(2, '0') - return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}` -} - /** 上传本地 xlsx 到 Java 临时目录,成功后回填 fileUrl(服务器本地路径,Java 直接读取) */ -async function uploadBrandPathsToJava(paths: Array) { - const api = getPywebviewApi() - if (!api?.upload_file_to_java) { - throw new Error('当前环境未提供文件上传能力') - } - const files: UploadFileVo[] = [] - for (const item of paths) { - const filePath = typeof item === 'string' ? item : item.absolutePath - const relativePath = typeof item === 'string' ? undefined : item.relativePath - const uploaded = await api.upload_file_to_java(filePath, relativePath, true) - if (!uploaded?.success || !uploaded.data) { - throw new Error(uploaded?.error || uploaded?.message || `上传失败:${filePath}`) - } - files.push(uploaded.data) - } - return files -} - async function selectFiles() { const bridge = getPywebviewApi() if (!bridge?.select_brand_xlsx_files) { @@ -384,7 +354,7 @@ async function selectFiles() { if (!paths?.length) return if (!(await passGuard(checkSelectedFiles(paths, { allowedExtensions: EXCEL_EXTENSIONS })))) return try { - const files = await uploadBrandPathsToJava(paths) + const files = await uploadPathsToJava(getPywebviewApi(), paths, { uploadOss: true }) uploadedFiles.value = files ElMessage.success(`已选择并上传 ${files.length} 个 Excel 文件`) } catch (error) { @@ -412,7 +382,7 @@ async function selectFolder() { return } if (!(await passGuard(checkSelectedFiles(res.items.map((i) => i.absolutePath), { allowedExtensions: EXCEL_EXTENSIONS })))) return - const files = await uploadBrandPathsToJava(res.items) + const files = await uploadPathsToJava(getPywebviewApi(), res.items, { uploadOss: true }) uploadedFiles.value = files ElMessage.success(`已选择并上传文件夹内 ${files.length} 个 Excel 文件`) } catch (error) { diff --git a/frontend-vue/src/pages/brand/components/BrandCollectDataTab.vue b/frontend-vue/src/pages/brand/components/BrandCollectDataTab.vue index 09a4802a..9cb1785e 100644 --- a/frontend-vue/src/pages/brand/components/BrandCollectDataTab.vue +++ b/frontend-vue/src/pages/brand/components/BrandCollectDataTab.vue @@ -150,7 +150,7 @@ import AmazonToolPageShell from '@/pages/amazon/components/AmazonToolPageShell.v import ModuleTemplateDownload from '@/shared/components/ModuleTemplateDownload.vue' import TaskCenterPanel from '@/shared/components/tasks/TaskCenterPanel.vue' import type { TaskItemView, TaskStatCard } from '@/shared/components/tasks/types' -import { expandBrandFolderRecursive, type BrandExpandFolderItem } from '@/shared/api/brand' +import { expandBrandFolderRecursive } from '@/shared/api/brand' import { getPywebviewApi } from '@/shared/bridges/pywebview' import { useTaskProgressLoop } from '@/shared/composables/useTaskProgressLoop' import { createCategorizedTimers } from '@/shared/utils/categorized-timers' @@ -179,6 +179,8 @@ import { type CollectDataTaskDetailVo, type UploadFileVo, } from '@/shared/api/java-modules' +import { formatDateTime } from '@/shared/utils/datetime' +import { uploadPathsToJava } from '@/shared/utils/upload-to-java' const COUNTRY_OPTIONS = [ { code: 'DE', label: '德国' }, @@ -307,24 +309,6 @@ async function loadCountryPreference() { } } -async function uploadPathsToJava(paths: Array) { - const api = getPywebviewApi() - if (!api?.upload_file_to_java) { - throw new Error('当前桌面端未提供文件上传能力') - } - const files: UploadFileVo[] = [] - for (const item of paths) { - const filePath = typeof item === 'string' ? item : item.absolutePath - const relativePath = typeof item === 'string' ? undefined : item.relativePath - const uploaded = await api.upload_file_to_java(filePath, relativePath) - if (!uploaded?.success || !uploaded.data) { - throw new Error(uploaded?.error || uploaded?.message || `上传失败:${filePath}`) - } - files.push(uploaded.data) - } - return files -} - async function selectFiles() { const api = getPywebviewApi() if (!api?.select_brand_xlsx_files || !api.upload_file_to_java) { @@ -335,7 +319,7 @@ async function selectFiles() { const paths = await api.select_brand_xlsx_files() if (!paths?.length) return if (!(await passGuard(checkSelectedFiles(paths, { allowedExtensions: EXCEL_EXTENSIONS })))) return - const files = await uploadPathsToJava(paths) + const files = await uploadPathsToJava(getPywebviewApi(), paths) uploadedFiles.value = files selectedFileNames.value = files.map((f) => f.relativePath || f.originalFilename || f.fileKey) lastTaskId.value = null @@ -365,7 +349,7 @@ async function selectFolder() { return } if (!(await passGuard(checkSelectedFiles(result.items, { allowedExtensions: EXCEL_EXTENSIONS })))) return - const files = await uploadPathsToJava(result.items) + const files = await uploadPathsToJava(getPywebviewApi(), result.items) uploadedFiles.value = files selectedFileNames.value = result.items.map((item) => item.relativePath || item.absolutePath) lastTaskId.value = null @@ -501,19 +485,6 @@ async function pushToPythonQueue() { } } -function formatDateTime(value?: string) { - if (!value) return '-' - const date = new Date(value) - if (Number.isNaN(date.getTime())) return value - const year = date.getFullYear() - const month = String(date.getMonth() + 1).padStart(2, '0') - const day = String(date.getDate()).padStart(2, '0') - const hours = String(date.getHours()).padStart(2, '0') - const minutes = String(date.getMinutes()).padStart(2, '0') - const seconds = String(date.getSeconds()).padStart(2, '0') - return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}` -} - function normalizeTaskStatus(item: CollectDataHistoryItem) { return (item.taskStatus || '').toUpperCase() } diff --git a/frontend-vue/src/pages/brand/components/BrandConvertTab.vue b/frontend-vue/src/pages/brand/components/BrandConvertTab.vue index 3a34123c..2c4423b6 100644 --- a/frontend-vue/src/pages/brand/components/BrandConvertTab.vue +++ b/frontend-vue/src/pages/brand/components/BrandConvertTab.vue @@ -155,7 +155,6 @@ import AmazonToolPageShell from '@/pages/amazon/components/AmazonToolPageShell.v import TaskCenterPanel from '@/shared/components/tasks/TaskCenterPanel.vue' import type { TaskItemView, TaskStatCard } from '@/shared/components/tasks/types' import { expandBrandFolderRecursive } from '@/shared/api/brand' -import type { BrandExpandFolderItem } from '@/shared/api/brand' import { deleteConvertHistory, getConvertHistory, @@ -169,6 +168,8 @@ import { getPywebviewApi, type UploadedJavaFile } from '@/shared/bridges/pywebvi import { saveUrlWithProgress } from '@/shared/utils/download-progress' import { EXCEL_EXTENSIONS, checkSelectedFiles, guardBlocked } from '@/shared/dispatch-guard' import { passGuard } from '@/shared/dispatch-guard-ui' +import { formatDateTime } from '@/shared/utils/datetime' +import { uploadPathsToJava } from '@/shared/utils/upload-to-java' const convertSelectedPaths = ref([]) const convertArchiveName = ref('') @@ -210,19 +211,6 @@ function itemSource(item: TaskItemView): ConvertResultItem { return item.source as ConvertResultItem } -function formatDateTime(value?: string) { - if (!value) return '-' - const date = new Date(value) - if (Number.isNaN(date.getTime())) return value - const year = date.getFullYear() - const month = String(date.getMonth() + 1).padStart(2, '0') - const day = String(date.getDate()).padStart(2, '0') - const hours = String(date.getHours()).padStart(2, '0') - const minutes = String(date.getMinutes()).padStart(2, '0') - const seconds = String(date.getSeconds()).padStart(2, '0') - return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}` -} - function convertTaskStatusText(item: ConvertResultItem) { const status = (item.taskStatus || '').toUpperCase() if (status === 'RUNNING') return '执行中' @@ -254,25 +242,6 @@ function toConvertTaskView(item: ConvertResultItem): TaskItemView { } } -async function uploadPathsToJava(paths: Array) { - const api = getPywebviewApi() - if (!api?.upload_file_to_java) { - throw new Error('当前桌面端未提供文件上传桥接能力') - } - - const uploaded: UploadedJavaFile[] = [] - for (const item of paths) { - const filePath = typeof item === 'string' ? item : item.absolutePath - const relativePath = typeof item === 'string' ? undefined : item.relativePath - const result = await api.upload_file_to_java(filePath, relativePath) - if (!result?.success || !result.data) { - throw new Error(result?.error || result?.message || `上传失败:${filePath}`) - } - uploaded.push(result.data) - } - return uploaded -} - async function loadConvertTemplates(preferredTemplateId?: string) { const templates = await getConvertTemplates() convertTemplates.value = templates || [] @@ -300,7 +269,7 @@ async function loadConvertTemplates(preferredTemplateId?: string) { async function handleSelectedPaths(paths: string[], successMessage: string) { convertSelectedPaths.value = paths convertArchiveName.value = '' - convertUploadedFiles.value = await uploadPathsToJava(paths) + convertUploadedFiles.value = await uploadPathsToJava(getPywebviewApi(), paths) ElMessage.success(successMessage) } @@ -339,7 +308,7 @@ async function selectConvertFolder() { if (result.success && result.items?.length) { if (!(await passGuard(checkSelectedFiles(result.items, { allowedExtensions: EXCEL_EXTENSIONS })))) return convertSelectedPaths.value = result.items.map((item) => item.relativePath) - convertUploadedFiles.value = await uploadPathsToJava(result.items) + convertUploadedFiles.value = await uploadPathsToJava(getPywebviewApi(), result.items) ElMessage.success(`已选择文件夹内 ${result.items.length} 个 xlsx 文件`) return } diff --git a/frontend-vue/src/pages/brand/components/BrandDedupeTab.vue b/frontend-vue/src/pages/brand/components/BrandDedupeTab.vue index 79a94999..b131013f 100644 --- a/frontend-vue/src/pages/brand/components/BrandDedupeTab.vue +++ b/frontend-vue/src/pages/brand/components/BrandDedupeTab.vue @@ -120,12 +120,14 @@ import { ElMessage } from 'element-plus' import AmazonToolPageShell from '@/pages/amazon/components/AmazonToolPageShell.vue'; import TaskCenterPanel from '@/shared/components/tasks/TaskCenterPanel.vue' import type { TaskItemView, TaskStatCard } from '@/shared/components/tasks/types' -import { expandBrandFolderRecursive, type BrandExpandFolderItem } from '@/shared/api/brand' +import { expandBrandFolderRecursive } from '@/shared/api/brand' import { deleteDedupeHistory, getDedupeHistory, getDedupeResultDownloadUrl, getDedupeRunProgress, getExcelInfo, runDedupe, type DedupeResultItem, type DedupeRunProgressVo, type DedupeRunVo } from '@/shared/api/java-modules' import { getPywebviewApi, type UploadedJavaFile } from '@/shared/bridges/pywebview' import { saveUrlWithProgress } from '@/shared/utils/download-progress' import { EXCEL_EXTENSIONS, checkSelectedFiles, guardBlocked } from '@/shared/dispatch-guard' import { passGuard } from '@/shared/dispatch-guard-ui' +import { formatDateTime } from '@/shared/utils/datetime' +import { uploadPathsToJava } from '@/shared/utils/upload-to-java' const cleanAvailableColumns = ref([]) const cleanSelectedColumns = ref([]) @@ -144,19 +146,6 @@ const cleanDisplayPaths = computed(() => cleanSelectedPaths.value.slice(0, 8)) const latestRunId = ref('') const cleanRunStartedAt = ref('') -function formatDateTime(value?: string) { - if (!value) return '-' - const date = new Date(value) - if (Number.isNaN(date.getTime())) return value - const year = date.getFullYear() - const month = String(date.getMonth() + 1).padStart(2, '0') - const day = String(date.getDate()).padStart(2, '0') - const hours = String(date.getHours()).padStart(2, '0') - const minutes = String(date.getMinutes()).padStart(2, '0') - const seconds = String(date.getSeconds()).padStart(2, '0') - return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}` -} - function itemSource(item: TaskItemView): DedupeResultItem { return item.source as DedupeResultItem } @@ -231,24 +220,6 @@ function clearAllCleanColumns() { cleanSelectedColumns.value = [] } -async function uploadPathsToJava(paths: Array) { - const api = getPywebviewApi() - if (!api?.upload_file_to_java) { - throw new Error('当前桌面端未提供文件上传桥接能力') - } - const uploaded: UploadedJavaFile[] = [] - for (const item of paths) { - const filePath = typeof item === 'string' ? item : item.absolutePath - const relativePath = typeof item === 'string' ? undefined : item.relativePath - const result = await api.upload_file_to_java(filePath, relativePath) - if (!result?.success || !result.data) { - throw new Error(result?.error || result?.message || `上传失败:${filePath}`) - } - uploaded.push(result.data) - } - return uploaded -} - async function loadCleanHeaders(fileKey: string) { const result = await getExcelInfo(fileKey) if (!result.headers?.length) { @@ -272,7 +243,7 @@ async function loadCleanHeaders(fileKey: string) { async function handleSelectedPaths(paths: string[], successMessage: string) { cleanSelectedPaths.value = paths cleanArchiveName.value = '' - cleanUploadedFiles.value = await uploadPathsToJava(paths) + cleanUploadedFiles.value = await uploadPathsToJava(getPywebviewApi(), paths) if (cleanUploadedFiles.value.length > 0) { await loadCleanHeaders(cleanUploadedFiles.value[0].fileKey) } @@ -314,7 +285,7 @@ async function selectCleanFolder() { if (result.success && result.items?.length) { if (!(await passGuard(checkSelectedFiles(result.items, { allowedExtensions: EXCEL_EXTENSIONS })))) return cleanSelectedPaths.value = result.items.map((item) => item.relativePath) - cleanUploadedFiles.value = await uploadPathsToJava(result.items) + cleanUploadedFiles.value = await uploadPathsToJava(getPywebviewApi(), result.items) if (cleanUploadedFiles.value.length > 0) { await loadCleanHeaders(cleanUploadedFiles.value[0].fileKey) } diff --git a/frontend-vue/src/pages/brand/components/BrandDeleteBrandTab.vue b/frontend-vue/src/pages/brand/components/BrandDeleteBrandTab.vue index 4a5216be..0c1b301b 100644 --- a/frontend-vue/src/pages/brand/components/BrandDeleteBrandTab.vue +++ b/frontend-vue/src/pages/brand/components/BrandDeleteBrandTab.vue @@ -114,7 +114,7 @@ import ModuleTemplateDownload from '@/shared/components/ModuleTemplateDownload.v import TaskCenterPanel from '@/shared/components/tasks/TaskCenterPanel.vue' import type { TaskItemView, TaskStatCard } from '@/shared/components/tasks/types' import ZiniaoVersionSetting from '@/shared/components/ZiniaoVersionSetting.vue' -import { expandBrandFolderRecursive, type BrandExpandFolderItem } from '@/shared/api/brand' +import { expandBrandFolderRecursive } from '@/shared/api/brand' import { deleteDeleteBrandHistory, getDeleteBrandHistory, @@ -139,6 +139,8 @@ import { } from '@/shared/dispatch-guard' import { passGuard } from '@/shared/dispatch-guard-ui' import { useZiniaoVersion } from '@/shared/utils/ziniao-version' +import { formatDateTime } from '@/shared/utils/datetime' +import { uploadPathsToJava } from '@/shared/utils/upload-to-java' interface SessionDeleteBrandItem extends DeleteBrandResultItem { _pushed?: boolean @@ -534,19 +536,6 @@ function taskFinishedAt(item: DeleteBrandResultItem) { return taskDetails.value[item.taskId]?.task?.finishedAt ?? '' } -function formatDateTime(value?: string) { - if (!value) return '-' - const date = new Date(value) - if (Number.isNaN(date.getTime())) return value - const year = date.getFullYear() - const month = String(date.getMonth() + 1).padStart(2, '0') - const day = String(date.getDate()).padStart(2, '0') - const hours = String(date.getHours()).padStart(2, '0') - const minutes = String(date.getMinutes()).padStart(2, '0') - const seconds = String(date.getSeconds()).padStart(2, '0') - return year + '-' + month + '-' + day + ' ' + hours + ':' + minutes + ':' + seconds -} - function formatMatchResult(item: DeleteBrandResultItem) { if (item.matchStatus === 'INDEX_STALE' && isUsableMatchedItem(item)) { @@ -1023,24 +1012,6 @@ function stopPolling() { clearAutoRetryTimer() } -async function uploadPathsToJava(paths: Array) { - const api = getPywebviewApi() - if (!api?.upload_file_to_java) { - throw new Error('当前桌面端未提供文件上传桥接能力') - } - const uploaded: UploadedJavaFile[] = [] - for (const item of paths) { - const filePath = typeof item === 'string' ? item : item.absolutePath - const relativePath = typeof item === 'string' ? undefined : item.relativePath - const result = await api.upload_file_to_java(filePath, relativePath) - if (!result?.success || !result.data) { - throw new Error(result?.error || result?.message || `上传失败:${filePath}`) - } - uploaded.push(result.data) - } - return uploaded -} - async function selectFiles() { const api = getPywebviewApi() if (!api?.select_brand_xlsx_files) { @@ -1052,7 +1023,7 @@ async function selectFiles() { if (!paths?.length) return if (!(await passGuard(checkSelectedFiles(paths, { allowedExtensions: EXCEL_EXTENSIONS })))) return selectedPaths.value = paths - uploadedFiles.value = await uploadPathsToJava(paths) + uploadedFiles.value = await uploadPathsToJava(getPywebviewApi(), paths) ElMessage.success(`已选择 ${paths.length} 个删除品牌文件`) } catch (error) { // 上传失败时清空选择,避免残留上一批文件让用户误以为新文件已就绪 @@ -1079,7 +1050,7 @@ async function selectFolder() { } if (!(await passGuard(checkSelectedFiles(result.items, { allowedExtensions: EXCEL_EXTENSIONS })))) return selectedPaths.value = result.items.map((item) => item.relativePath || item.absolutePath) - uploadedFiles.value = await uploadPathsToJava(result.items) + uploadedFiles.value = await uploadPathsToJava(getPywebviewApi(), result.items) ElMessage.success(`已选择文件夹内 ${result.items.length} 个 xlsx 文件`) } catch (error) { selectedPaths.value = [] diff --git a/frontend-vue/src/pages/brand/components/BrandPatrolDeleteTab.vue b/frontend-vue/src/pages/brand/components/BrandPatrolDeleteTab.vue index ff438458..be985b8a 100644 --- a/frontend-vue/src/pages/brand/components/BrandPatrolDeleteTab.vue +++ b/frontend-vue/src/pages/brand/components/BrandPatrolDeleteTab.vue @@ -272,6 +272,7 @@ import { countryLabel, sanitizeCountryCodes, } from "@/shared/country-options"; +import { formatDateTime } from '@/shared/utils/datetime' const MAX_TRANSIENT_ERRORS = 30; /** 任务终态后等待结果文件(Java 侧异步生成)的最大轮次,12 × 10s ≈ 2 分钟 */ @@ -447,19 +448,6 @@ function createSubmissionId(taskId: number, shopName?: string) { return `patrol-delete:${taskId}:${shopName || "shop"}:${Date.now()}`; } -function formatDateTime(value?: string) { - if (!value) return "-"; - const date = new Date(value); - if (Number.isNaN(date.getTime())) return value; - const year = date.getFullYear(); - const month = String(date.getMonth() + 1).padStart(2, "0"); - const day = String(date.getDate()).padStart(2, "0"); - const hours = String(date.getHours()).padStart(2, "0"); - const minutes = String(date.getMinutes()).padStart(2, "0"); - const seconds = String(date.getSeconds()).padStart(2, "0"); - return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; -} - function formatMatchStatus(status?: string) { const value = (status || "").trim(); const map: Record = { diff --git a/frontend-vue/src/pages/brand/components/BrandPriceTrackTab.vue b/frontend-vue/src/pages/brand/components/BrandPriceTrackTab.vue index a39de0cd..18003fda 100644 --- a/frontend-vue/src/pages/brand/components/BrandPriceTrackTab.vue +++ b/frontend-vue/src/pages/brand/components/BrandPriceTrackTab.vue @@ -257,6 +257,8 @@ import { type PriceTrackShopQueueItem, type PriceTrackTaskDetailVo, } from '@/shared/api/java-modules' +import { formatDateTime } from '@/shared/utils/datetime' +import { uploadPathsToJava } from '@/shared/utils/upload-to-java' const ziniaoVersion = useZiniaoVersion() @@ -336,19 +338,6 @@ function snapTask(item: PriceTrackHistoryItem) { return item.taskId ? taskSnapshots.value[item.taskId]?.task : undefined } -function formatDateTime(value?: string) { - if (!value) return '-' - const date = new Date(value) - if (Number.isNaN(date.getTime())) return value - const year = date.getFullYear() - const month = String(date.getMonth() + 1).padStart(2, '0') - const day = String(date.getDate()).padStart(2, '0') - const hours = String(date.getHours()).padStart(2, '0') - const minutes = String(date.getMinutes()).padStart(2, '0') - const seconds = String(date.getSeconds()).padStart(2, '0') - return year + '-' + month + '-' + day + ' ' + hours + ':' + minutes + ':' + seconds -} - const pollTimer = ref(null) const pollingInFlight = ref(false) let disposed = false @@ -511,7 +500,7 @@ async function selectAsinFile() { } try { asinFiles.value = paths - asinUploadedFiles.value = await uploadAsinPathsToJava(paths) + asinUploadedFiles.value = await uploadPathsToJava(getPywebviewApi(), paths, { returnEmptyWhenUnavailable: true }) ElMessage.success(`已选择 ${paths.length} 个ASIN文件`) } catch (error) { // 上传失败时清空选择,避免本地路径残留被当作服务器路径提交给 Java @@ -537,7 +526,7 @@ async function selectAsinFolder() { } if (!(await passGuard(checkSelectedFiles(result.items, { allowedExtensions: EXCEL_CSV_EXTENSIONS })))) return asinFiles.value = result.items.map((item) => item.relativePath || item.absolutePath) - asinUploadedFiles.value = await uploadAsinPathsToJava(result.items) + asinUploadedFiles.value = await uploadPathsToJava(getPywebviewApi(), result.items, { returnEmptyWhenUnavailable: true }) ElMessage.success(`已选择文件夹内 ${result.items.length} 个ASIN文件`) } catch (error) { // 上传失败时清空选择,避免残留上一批文件让用户误以为新文件已就绪 @@ -550,24 +539,6 @@ async function selectAsinFolder() { // ========== 国家顺序相关 ========== -async function uploadAsinPathsToJava(paths: Array) { - const api = getPywebviewApi() - if (!api?.upload_file_to_java) { - return [] - } - const uploaded: UploadedJavaFile[] = [] - for (const item of paths) { - const filePath = typeof item === 'string' ? item : item.absolutePath - const relativePath = typeof item === 'string' ? undefined : item.relativePath - const result = await api.upload_file_to_java(filePath, relativePath) - if (!result?.success || !result.data) { - throw new Error(result?.error || result?.message || `上传失败:${filePath}`) - } - uploaded.push(result.data) - } - return uploaded -} - function resolveAsinRequestPaths() { // 提交上传返回的 fileKey(与去重/转换/采集等模块一致):服务端按 key 反查上传临时目录, // 不依赖上传返回的 localPath 绝对路径形态——安全加固后服务端按拼接解析绝对路径会失败。 diff --git a/frontend-vue/src/pages/brand/components/BrandProductRiskTab.vue b/frontend-vue/src/pages/brand/components/BrandProductRiskTab.vue index 96b9a4f7..425b56af 100644 --- a/frontend-vue/src/pages/brand/components/BrandProductRiskTab.vue +++ b/frontend-vue/src/pages/brand/components/BrandProductRiskTab.vue @@ -174,6 +174,7 @@ import { checkQueuePayload, guardBlocked } from '@/shared/dispatch-guard' import { passGuard } from '@/shared/dispatch-guard-ui' import ZiniaoVersionSetting from '@/shared/components/ZiniaoVersionSetting.vue' import { useZiniaoVersion } from '@/shared/utils/ziniao-version' +import { formatDateTime } from '@/shared/utils/datetime' const shopInput = ref('') const candidates = ref([]) @@ -338,19 +339,6 @@ function snapTask(item: ProductRiskHistoryItem) { return item.taskId ? taskSnapshots.value[item.taskId]?.task : undefined } -function formatDateTime(value?: string) { - if (!value) return '-' - const date = new Date(value) - if (Number.isNaN(date.getTime())) return value - const year = date.getFullYear() - const month = String(date.getMonth() + 1).padStart(2, '0') - const day = String(date.getDate()).padStart(2, '0') - const hours = String(date.getHours()).padStart(2, '0') - const minutes = String(date.getMinutes()).padStart(2, '0') - const seconds = String(date.getSeconds()).padStart(2, '0') - return year + '-' + month + '-' + day + ' ' + hours + ':' + minutes + ':' + seconds -} - const pollTimer = ref(null) const pollingInFlight = ref(false) let disposed = false diff --git a/frontend-vue/src/pages/brand/components/BrandPublishTab.vue b/frontend-vue/src/pages/brand/components/BrandPublishTab.vue index 94fc4101..84687a9f 100644 --- a/frontend-vue/src/pages/brand/components/BrandPublishTab.vue +++ b/frontend-vue/src/pages/brand/components/BrandPublishTab.vue @@ -191,7 +191,7 @@ import ModuleTemplateDownload from '@/shared/components/ModuleTemplateDownload.v import TaskCenterPanel from '@/shared/components/tasks/TaskCenterPanel.vue' import type { TaskItemView, TaskStatCard } from '@/shared/components/tasks/types' import ZiniaoVersionSetting from '@/shared/components/ZiniaoVersionSetting.vue' -import { expandBrandFolderRecursive, type BrandExpandFolderItem } from '@/shared/api/brand' +import { expandBrandFolderRecursive } from '@/shared/api/brand' import { activatePublishFile, activatePublishTask, @@ -219,6 +219,7 @@ import { } from '@/shared/dispatch-guard' import { passGuard } from '@/shared/dispatch-guard-ui' import { useZiniaoVersion, type ZiniaoVersion } from '@/shared/utils/ziniao-version' +import { uploadPathsToJava } from '@/shared/utils/upload-to-java' const COUNTRY_OPTIONS = [ { code: 'DE', label: '德国' }, @@ -476,22 +477,6 @@ function onSyncCountryChange(code: string, event: Event) { syncCountries.value = COUNTRY_OPTIONS.map((item) => item.code).filter((item) => selected.has(item)) } -async function uploadPathsToJava(paths: Array) { - const api = getPywebviewApi() - if (!api?.upload_file_to_java) throw new Error('当前桌面端未提供文件上传能力') - const uploaded: UploadedJavaFile[] = [] - for (const item of paths) { - const filePath = typeof item === 'string' ? item : item.absolutePath - const relativePath = typeof item === 'string' ? undefined : item.relativePath - const result = await api.upload_file_to_java(filePath, relativePath) - if (!result?.success || !result.data) { - throw new Error(result?.error || result?.message || `上传失败:${filePath}`) - } - uploaded.push(result.data) - } - return uploaded -} - async function selectFiles() { const api = getPywebviewApi() if (!api?.select_brand_xlsx_files || !api.upload_file_to_java) { @@ -503,7 +488,7 @@ async function selectFiles() { if (!paths?.length) return if (!(await passGuard(checkSelectedFiles(paths, { allowedExtensions: EXCEL_EXTENSIONS })))) return uploading.value = true - const uploaded = await uploadPathsToJava(paths) + const uploaded = await uploadPathsToJava(getPywebviewApi(), paths) selectedPaths.value = paths uploadedFiles.value = uploaded ElMessage.success(`已上传 ${uploaded.length} 个待上架文件`) @@ -533,7 +518,7 @@ async function selectFolder() { } if (!(await passGuard(checkSelectedFiles(result.items, { allowedExtensions: EXCEL_EXTENSIONS })))) return uploading.value = true - const uploaded = await uploadPathsToJava(result.items) + const uploaded = await uploadPathsToJava(getPywebviewApi(), result.items) selectedPaths.value = result.items.map((item) => item.relativePath || item.absolutePath) uploadedFiles.value = uploaded ElMessage.success(`已上传文件夹内 ${uploaded.length} 个 xlsx 文件`) diff --git a/frontend-vue/src/pages/brand/components/BrandQueryAsinTab.vue b/frontend-vue/src/pages/brand/components/BrandQueryAsinTab.vue index 0d429b45..a919c8c6 100644 --- a/frontend-vue/src/pages/brand/components/BrandQueryAsinTab.vue +++ b/frontend-vue/src/pages/brand/components/BrandQueryAsinTab.vue @@ -214,6 +214,7 @@ import { checkQueuePayload } from "@/shared/dispatch-guard"; import { passGuard } from "@/shared/dispatch-guard-ui"; import ZiniaoVersionSetting from "@/shared/components/ZiniaoVersionSetting.vue"; import { useZiniaoVersion } from "@/shared/utils/ziniao-version"; +import { formatDateTime } from '@/shared/utils/datetime' const MAX_TRANSIENT_ERRORS = 30; const ziniaoVersion = useZiniaoVersion(); @@ -375,19 +376,6 @@ function createSubmissionId(taskId: number, shopName?: string) { return `query-asin:${taskId}:${shopName || "shop"}:${Date.now()}`; } -function formatDateTime(value?: string) { - if (!value) return "-"; - const date = new Date(value); - if (Number.isNaN(date.getTime())) return value; - const year = date.getFullYear(); - const month = String(date.getMonth() + 1).padStart(2, "0"); - const day = String(date.getDate()).padStart(2, "0"); - const hours = String(date.getHours()).padStart(2, "0"); - const minutes = String(date.getMinutes()).padStart(2, "0"); - const seconds = String(date.getSeconds()).padStart(2, "0"); - return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; -} - function formatMatchStatus(status?: string) { const value = (status || "").trim(); const map: Record = { diff --git a/frontend-vue/src/pages/brand/components/BrandSimilarAsinTab.vue b/frontend-vue/src/pages/brand/components/BrandSimilarAsinTab.vue index 51a9203d..57722199 100644 --- a/frontend-vue/src/pages/brand/components/BrandSimilarAsinTab.vue +++ b/frontend-vue/src/pages/brand/components/BrandSimilarAsinTab.vue @@ -135,7 +135,7 @@ import { type UploadedFileRef, type UploadFileVo, } from '@/shared/api/java-modules' -import { expandBrandFolderRecursive, type BrandExpandFolderItem } from '@/shared/api/brand' +import { expandBrandFolderRecursive } from '@/shared/api/brand' import { getPywebviewApi, type ProxyMode } from '@/shared/bridges/pywebview' import { getTaskPollIntervalMs } from '@/shared/task-progress-config' import { getStoredApiSecret } from '@/shared/utils/api-secret-store' @@ -150,6 +150,8 @@ import { checkSelectedFiles, } from '@/shared/dispatch-guard' import { passGuard } from '@/shared/dispatch-guard-ui' +import { formatDateTime } from '@/shared/utils/datetime' +import { uploadPathsToJava } from '@/shared/utils/upload-to-java' const selectedFileNames = ref([]) const uploadedFiles = ref([]) @@ -300,19 +302,6 @@ function getRequiredAlipriceCredentials() { return { username, password } } -function formatDateTime(value?: string) { - if (!value) return '-' - const date = new Date(value) - if (Number.isNaN(date.getTime())) return value - const year = date.getFullYear() - const month = String(date.getMonth() + 1).padStart(2, '0') - const day = String(date.getDate()).padStart(2, '0') - const hours = String(date.getHours()).padStart(2, '0') - const minutes = String(date.getMinutes()).padStart(2, '0') - const seconds = String(date.getSeconds()).padStart(2, '0') - return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}` -} - function maskSecret(secret: string) { if (!secret) return '' if (secret.length <= 10) return '***' @@ -374,24 +363,6 @@ function loadPollingIds() { } } -async function uploadAppearancePathsToJava(paths: Array) { - const api = getPywebviewApi() - if (!api?.upload_file_to_java) { - throw new Error('当前桌面端未提供文件上传能力') - } - const files: UploadFileVo[] = [] - for (const item of paths) { - const filePath = typeof item === 'string' ? item : item.absolutePath - const relativePath = typeof item === 'string' ? undefined : item.relativePath - const uploaded = await api.upload_file_to_java(filePath, relativePath) - if (!uploaded?.success || !uploaded.data) { - throw new Error(uploaded?.error || uploaded?.message || `上传失败:${filePath}`) - } - files.push(uploaded.data) - } - return files -} - async function selectFiles() { const api = getPywebviewApi() if (!api?.select_brand_xlsx_files || !api.upload_file_to_java) { @@ -402,7 +373,7 @@ async function selectFiles() { if (!paths?.length) return if (!(await passGuard(checkSelectedFiles(paths, { allowedExtensions: EXCEL_EXTENSIONS })))) return try { - const files = await uploadAppearancePathsToJava(paths) + const files = await uploadPathsToJava(getPywebviewApi(), paths) uploadedFiles.value = files selectedFileNames.value = files.map((f) => f.relativePath || f.originalFilename || f.fileKey) parseResult.value = null @@ -434,7 +405,7 @@ async function selectFolder() { return } if (!(await passGuard(checkSelectedFiles(result.items, { allowedExtensions: EXCEL_EXTENSIONS })))) return - const files = await uploadAppearancePathsToJava(result.items) + const files = await uploadPathsToJava(getPywebviewApi(), result.items) uploadedFiles.value = files selectedFileNames.value = result.items.map((item) => item.relativePath || item.absolutePath) parseResult.value = null diff --git a/frontend-vue/src/pages/brand/components/BrandSplitTab.vue b/frontend-vue/src/pages/brand/components/BrandSplitTab.vue index b8c2804e..428dd620 100644 --- a/frontend-vue/src/pages/brand/components/BrandSplitTab.vue +++ b/frontend-vue/src/pages/brand/components/BrandSplitTab.vue @@ -141,12 +141,14 @@ import { ElMessage } from 'element-plus' import AmazonToolPageShell from '@/pages/amazon/components/AmazonToolPageShell.vue'; import TaskCenterPanel from '@/shared/components/tasks/TaskCenterPanel.vue' import type { TaskItemView, TaskStatCard } from '@/shared/components/tasks/types' -import { expandBrandFolderRecursive, type BrandExpandFolderItem } from '@/shared/api/brand' +import { expandBrandFolderRecursive } from '@/shared/api/brand' import { deleteSplitHistory, getExcelInfo, getSplitHistory, getSplitResultDownloadUrl, runSplit, type SplitResultItem } from '@/shared/api/java-modules' import { getPywebviewApi, type UploadedJavaFile } from '@/shared/bridges/pywebview' import { saveUrlWithProgress } from '@/shared/utils/download-progress' import { EXCEL_EXTENSIONS, checkSelectedFiles, guardBlocked } from '@/shared/dispatch-guard' import { passGuard } from '@/shared/dispatch-guard-ui' +import { formatDateTime } from '@/shared/utils/datetime' +import { uploadPathsToJava } from '@/shared/utils/upload-to-java' const splitSelectedPaths = ref([]) const splitArchiveName = ref('') @@ -188,19 +190,6 @@ function itemSource(item: TaskItemView): SplitResultItem { return item.source as SplitResultItem } -function formatDateTime(value?: string) { - if (!value) return '-' - const date = new Date(value) - if (Number.isNaN(date.getTime())) return value - const year = date.getFullYear() - const month = String(date.getMonth() + 1).padStart(2, '0') - const day = String(date.getDate()).padStart(2, '0') - const hours = String(date.getHours()).padStart(2, '0') - const minutes = String(date.getMinutes()).padStart(2, '0') - const seconds = String(date.getSeconds()).padStart(2, '0') - return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}` -} - function splitTaskStatusText(item: SplitResultItem) { const status = (item.taskStatus || '').toUpperCase() if (status === 'RUNNING') return '执行中' @@ -235,24 +224,6 @@ function toSplitTaskView(item: SplitResultItem): TaskItemView { } } -async function uploadPathsToJava(paths: Array) { - const api = getPywebviewApi() - if (!api?.upload_file_to_java) { - throw new Error('当前桌面端未提供文件上传桥接能力') - } - const uploaded: UploadedJavaFile[] = [] - for (const item of paths) { - const filePath = typeof item === 'string' ? item : item.absolutePath - const relativePath = typeof item === 'string' ? undefined : item.relativePath - const result = await api.upload_file_to_java(filePath, relativePath) - if (!result?.success || !result.data) { - throw new Error(result?.error || result?.message || `上传失败:${filePath}`) - } - uploaded.push(result.data) - } - return uploaded -} - async function loadSplitInfo(fileKey: string) { const result = await getExcelInfo(fileKey) splitAvailableColumns.value = result.headers || [] @@ -263,7 +234,7 @@ async function loadSplitInfo(fileKey: string) { async function handleSelectedPaths(paths: string[], successMessage: string) { splitSelectedPaths.value = paths splitArchiveName.value = '' - splitUploadedFiles.value = await uploadPathsToJava(paths) + splitUploadedFiles.value = await uploadPathsToJava(getPywebviewApi(), paths) if (splitUploadedFiles.value.length > 0) { await loadSplitInfo(splitUploadedFiles.value[0].fileKey) } @@ -305,7 +276,7 @@ async function selectSplitFolder() { if (result.success && result.items?.length) { if (!(await passGuard(checkSelectedFiles(result.items, { allowedExtensions: EXCEL_EXTENSIONS })))) return splitSelectedPaths.value = result.items.map((item) => item.relativePath) - splitUploadedFiles.value = await uploadPathsToJava(result.items) + splitUploadedFiles.value = await uploadPathsToJava(getPywebviewApi(), result.items) if (splitUploadedFiles.value.length > 0) { await loadSplitInfo(splitUploadedFiles.value[0].fileKey) } diff --git a/frontend-vue/src/pages/brand/components/BrandWithdrawTab.vue b/frontend-vue/src/pages/brand/components/BrandWithdrawTab.vue index 4a3fd519..19ab198a 100644 --- a/frontend-vue/src/pages/brand/components/BrandWithdrawTab.vue +++ b/frontend-vue/src/pages/brand/components/BrandWithdrawTab.vue @@ -254,6 +254,7 @@ import { checkQueuePayload } from "@/shared/dispatch-guard"; import { passGuard } from "@/shared/dispatch-guard-ui"; import ZiniaoVersionSetting from "@/shared/components/ZiniaoVersionSetting.vue"; import { useZiniaoVersion } from "@/shared/utils/ziniao-version"; +import { formatDateTime } from '@/shared/utils/datetime' const MAX_TRANSIENT_ERRORS = 30; const ziniaoVersion = useZiniaoVersion(); @@ -506,19 +507,6 @@ function createSubmissionId(taskId: number, shopName?: string) { return `withdraw:${taskId}:${shopName || "shop"}:${Date.now()}`; } -function formatDateTime(value?: string) { - if (!value) return "-"; - const date = new Date(value); - if (Number.isNaN(date.getTime())) return value; - const year = date.getFullYear(); - const month = String(date.getMonth() + 1).padStart(2, "0"); - const day = String(date.getDate()).padStart(2, "0"); - const hours = String(date.getHours()).padStart(2, "0"); - const minutes = String(date.getMinutes()).padStart(2, "0"); - const seconds = String(date.getSeconds()).padStart(2, "0"); - return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; -} - function formatMatchStatus(status?: string) { const value = (status || "").trim(); const map: Record = { diff --git a/frontend-vue/src/shared/utils/datetime.ts b/frontend-vue/src/shared/utils/datetime.ts new file mode 100644 index 00000000..1c86a518 --- /dev/null +++ b/frontend-vue/src/shared/utils/datetime.ts @@ -0,0 +1,20 @@ +/** + * 时间展示工具(前端各工具页统一使用)。 + */ + +/** + * 把后端时间串格式化为 `YYYY-MM-DD HH:mm:ss`(本地时区)。 + * 空值返回 '-';无法解析时原样返回,避免把后端原始串吞掉。 + */ +export function formatDateTime(value?: string): string { + if (!value) return '-' + const date = new Date(value) + if (Number.isNaN(date.getTime())) return value + const year = date.getFullYear() + const month = String(date.getMonth() + 1).padStart(2, '0') + const day = String(date.getDate()).padStart(2, '0') + const hours = String(date.getHours()).padStart(2, '0') + const minutes = String(date.getMinutes()).padStart(2, '0') + const seconds = String(date.getSeconds()).padStart(2, '0') + return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}` +} diff --git a/frontend-vue/src/shared/utils/upload-to-java.ts b/frontend-vue/src/shared/utils/upload-to-java.ts new file mode 100644 index 00000000..6290db2f --- /dev/null +++ b/frontend-vue/src/shared/utils/upload-to-java.ts @@ -0,0 +1,47 @@ +import type { PywebviewApi, UploadedJavaFile } from '../bridges/pywebview.ts' + +/** 待上传条目:本机绝对路径字符串,或展开文件夹得到的 { absolutePath, relativePath }。 */ +export interface UploadPathItem { + absolutePath: string + relativePath?: string +} + +export interface UploadPathsToJavaOptions { + /** 透传给桥的 uploadOss 参数;需要把文件落到 OSS 的模块传 true。 */ + uploadOss?: boolean + /** 缺少上传桥时返回空数组而非抛错(个别模块的降级语义)。 */ + returnEmptyWhenUnavailable?: boolean +} + +/** + * 逐个把本机文件上传给 Java,返回上传结果列表。 + * + * 任一个失败即整体抛错(不返回半成品),由调用方统一提示并清空已选文件, + * 避免残留上一批让用户误以为新文件已就绪。 + * + * api 由调用方传入(通常是 `getPywebviewApi()`),便于脱离桌面桥单测。 + */ +export async function uploadPathsToJava( + api: PywebviewApi | undefined, + paths: ReadonlyArray, + options: UploadPathsToJavaOptions = {}, +): Promise { + const upload = api?.upload_file_to_java + if (!upload) { + if (options.returnEmptyWhenUnavailable) { + return [] + } + throw new Error('当前桌面端未提供文件上传能力') + } + const uploaded: UploadedJavaFile[] = [] + for (const item of paths) { + const filePath = typeof item === 'string' ? item : item.absolutePath + const relativePath = typeof item === 'string' ? undefined : item.relativePath + const result = await upload(filePath, relativePath, options.uploadOss) + if (!result?.success || !result.data) { + throw new Error(result?.error || result?.message || `上传失败:${filePath}`) + } + uploaded.push(result.data) + } + return uploaded +} diff --git a/frontend-vue/tests/datetime.test.ts b/frontend-vue/tests/datetime.test.ts new file mode 100644 index 00000000..10bef776 --- /dev/null +++ b/frontend-vue/tests/datetime.test.ts @@ -0,0 +1,23 @@ +import { test } from 'node:test' +import assert from 'node:assert/strict' +import { formatDateTime } from '../src/shared/utils/datetime.ts' + +// 用本地时间构造再往返 ISO,期望值由同一本地时区还原,避免依赖运行环境的时区。 +test('formatDateTime 空值返回 -', () => { + assert.equal(formatDateTime(undefined), '-') + assert.equal(formatDateTime(''), '-') +}) + +test('formatDateTime 无法解析时原样返回', () => { + assert.equal(formatDateTime('not-a-date'), 'not-a-date') +}) + +test('formatDateTime 按 YYYY-MM-DD HH:mm:ss 补零(个位月/日/时/分/秒)', () => { + const local = new Date(2026, 0, 5, 9, 7, 3) + assert.equal(formatDateTime(local.toISOString()), '2026-01-05 09:07:03') +}) + +test('formatDateTime 年末边界', () => { + const local = new Date(2026, 11, 31, 23, 59, 59) + assert.equal(formatDateTime(local.toISOString()), '2026-12-31 23:59:59') +}) diff --git a/frontend-vue/tests/upload-to-java.test.ts b/frontend-vue/tests/upload-to-java.test.ts new file mode 100644 index 00000000..7e75faa9 --- /dev/null +++ b/frontend-vue/tests/upload-to-java.test.ts @@ -0,0 +1,70 @@ +import { test } from 'node:test' +import assert from 'node:assert/strict' +import { uploadPathsToJava } from '../src/shared/utils/upload-to-java.ts' +import type { PywebviewApi } from '../src/shared/bridges/pywebview.ts' + +function fakeApi( + upload: ( + filePath: string, + relativePath?: string, + uploadOss?: boolean, + ) => Promise<{ success: boolean; data?: never; error?: string; message?: string }>, +) { + return { upload_file_to_java: upload } as unknown as PywebviewApi +} + +function okData(filePath: string) { + return { fileKey: `k:${filePath}`, originalFilename: filePath, localPath: '', size: 1 } +} + +test('uploadPathsToJava 逐个上传并返回结果列表', async () => { + const calls: Array<[string, string | undefined, boolean | undefined]> = [] + const api = fakeApi(async (filePath, relativePath, uploadOss) => { + calls.push([filePath, relativePath, uploadOss]) + return { success: true, data: okData(filePath) as never } + }) + + const result = await uploadPathsToJava(api, ['/a.xlsx', { absolutePath: '/b.xlsx', relativePath: 'sub/b.xlsx' }]) + + assert.equal(result.length, 2) + assert.deepEqual(calls, [ + ['/a.xlsx', undefined, undefined], + ['/b.xlsx', 'sub/b.xlsx', undefined], + ]) +}) + +test('uploadPathsToJava 透传 uploadOss 参数', async () => { + let seen: boolean | undefined + const api = fakeApi(async (filePath, _relativePath, uploadOss) => { + seen = uploadOss + return { success: true, data: okData(filePath) as never } + }) + + await uploadPathsToJava(api, ['/a.xlsx'], { uploadOss: true }) + + assert.equal(seen, true) +}) + +test('uploadPathsToJava 任一失败即整体抛错并带上桥返回的文案', async () => { + const api = fakeApi(async (filePath) => + filePath === '/bad.xlsx' + ? { success: false, error: '磁盘读取失败' } + : { success: true, data: okData(filePath) as never }, + ) + + await assert.rejects(() => uploadPathsToJava(api, ['/ok.xlsx', '/bad.xlsx']), /磁盘读取失败/) +}) + +test('uploadPathsToJava 失败无文案时回落到「上传失败:路径」', async () => { + const api = fakeApi(async () => ({ success: false })) + + await assert.rejects(() => uploadPathsToJava(api, ['/x.xlsx']), /上传失败:\/x\.xlsx/) +}) + +test('uploadPathsToJava 缺上传桥默认抛错', async () => { + await assert.rejects(() => uploadPathsToJava(undefined, ['/a.xlsx']), /未提供文件上传能力/) +}) + +test('uploadPathsToJava 缺上传桥时 returnEmptyWhenUnavailable 返回空数组', async () => { + assert.deepEqual(await uploadPathsToJava(undefined, ['/a.xlsx'], { returnEmptyWhenUnavailable: true }), []) +})