refactor(品牌工具页): 抽取 formatDateTime 与 uploadPathsToJava 公共工具

- 新增 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 个前端单测全通过
This commit is contained in:
2026-09-13 23:16:59 +08:00
parent b70557a077
commit 882ccdac12
18 changed files with 210 additions and 377 deletions
@@ -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<string[]>([])
const uploadedFiles = ref<UploadFileVo[]>([])
@@ -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<string | BrandExpandFolderItem>) {
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
@@ -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<UploadFileVo[]>([])
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<string | BrandExpandFolderItem>) {
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) {
@@ -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<string | BrandExpandFolderItem>) {
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()
}
@@ -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<string[]>([])
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<string | BrandExpandFolderItem>) {
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
}
@@ -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<string[]>([])
const cleanSelectedColumns = ref<string[]>([])
@@ -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<string | BrandExpandFolderItem>) {
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)
}
@@ -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<string | BrandExpandFolderItem>) {
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 = []
@@ -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<string, string> = {
@@ -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<number | null>(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<string | { absolutePath: string; relativePath?: string }>) {
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 绝对路径形态——安全加固后服务端按拼接解析绝对路径会失败。
@@ -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<ProductRiskCandidateVo[]>([])
@@ -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<number | null>(null)
const pollingInFlight = ref(false)
let disposed = false
@@ -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<string | BrandExpandFolderItem>) {
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 文件`)
@@ -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<string, string> = {
@@ -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<string[]>([])
const uploadedFiles = ref<UploadFileVo[]>([])
@@ -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<string | BrandExpandFolderItem>) {
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
@@ -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<string[]>([])
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<string | BrandExpandFolderItem>) {
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)
}
@@ -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<string, string> = {
+20
View File
@@ -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}`
}
@@ -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<string | UploadPathItem>,
options: UploadPathsToJavaOptions = {},
): Promise<UploadedJavaFile[]> {
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
}
+23
View File
@@ -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')
})
+70
View File
@@ -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 }), [])
})