diff --git a/app/main.py b/app/main.py index 1c561fb..3cc2e65 100644 --- a/app/main.py +++ b/app/main.py @@ -154,7 +154,7 @@ class WindowAPI: ) return result[0] if result else '' - def save_file_from_url(self, url, default_filename='download.bin'): + def save_file_from_url_new(self, url, default_filename='download.bin'): """弹窗选择保存位置,从 url 下载文件并保存。根据文件后缀动态设置保存类型。""" if not url or not url.strip(): return {'success': False, 'error': '下载地址为空'} @@ -360,7 +360,7 @@ def main(): ) api = WindowAPI(window) window.expose(api.close, api.minimize, api.maximize, api.toggle_maximize, generate_images, api.save_image, api.save_image_to_folder, api.select_folder, api.select_brand_xlsx_files, api.select_brand_folder, - api.save_file_from_url, api.save_template_xlsx,api.save_template_zip,api.upload_file_to_java,api.open_external_url,api.enqueue_json,api.get_detail_del) + api.save_file_from_url_new, api.save_template_xlsx,api.save_template_zip,api.upload_file_to_java,api.open_external_url,api.enqueue_json,api.get_detail_del) webview.start( debug=True, storage_path=cache_path, diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/convert/controller/ConvertRunController.java b/backend-java/src/main/java/com/nanri/aiimage/modules/convert/controller/ConvertRunController.java index 2ffc659..0fff6e1 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/convert/controller/ConvertRunController.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/convert/controller/ConvertRunController.java @@ -95,7 +95,7 @@ public class ConvertRunController { } @GetMapping("/results/{resultId}/download") - @Operation(summary = "下载格式转换结果", description = "根据结果记录 ID 下载当前用户的转换结果文件。桌面端前端可继续通过 save_file_from_url 选择保存位置。") + @Operation(summary = "下载格式转换结果", description = "根据结果记录 ID 下载当前用户的转换结果文件。桌面端前端可继续通过 save_file_from_url_new 选择保存位置。") @ApiResponses({ @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "返回结果文件流"), @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "结果文件不存在") diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/deletebrand/controller/DeleteBrandRunController.java b/backend-java/src/main/java/com/nanri/aiimage/modules/deletebrand/controller/DeleteBrandRunController.java index 157ef90..54e0a83 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/deletebrand/controller/DeleteBrandRunController.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/deletebrand/controller/DeleteBrandRunController.java @@ -103,7 +103,7 @@ public class DeleteBrandRunController { } @GetMapping("/tasks/{taskId}/download") - @Operation(summary = "下载删除品牌任务结果", description = "返回任务结果文件流,便于桌面端通过 save_file_from_url 直接保存。") + @Operation(summary = "下载删除品牌任务结果", description = "返回任务结果文件流,便于桌面端通过 save_file_from_url_new 直接保存。") public void download(@PathVariable Long taskId, jakarta.servlet.http.HttpServletResponse response) { String url = deleteBrandRunService.resolveTaskDownloadUrl(taskId); String filename = deleteBrandRunService.resolveTaskDownloadFilename(taskId); diff --git a/frontend-vue/src/pages/brand/components/BrandConvertTab.vue b/frontend-vue/src/pages/brand/components/BrandConvertTab.vue index 5172082..cedd88a 100644 --- a/frontend-vue/src/pages/brand/components/BrandConvertTab.vue +++ b/frontend-vue/src/pages/brand/components/BrandConvertTab.vue @@ -391,8 +391,8 @@ async function saveTemplateFromUrl(url: string, fallbackFilename: string, bridge return } - if (api?.save_file_from_url) { - const result = await api.save_file_from_url(url, fallbackFilename) + if (api?.save_file_from_url_new) { + const result = await api.save_file_from_url_new(url, fallbackFilename) if (result.success) { ElMessage.success(`已保存:${result.path || fallbackFilename}`) } else if (result.error && result.error !== '用户取消') { @@ -480,8 +480,8 @@ async function downloadConvertResult(item: ConvertResultItem) { } const filename = item.outputFilename || `${new Date().toISOString().slice(0, 10).replace(/-/g, '')}.txt` - if (api?.save_file_from_url) { - const result = await api.save_file_from_url(item.downloadUrl, filename) + if (api?.save_file_from_url_new) { + const result = await api.save_file_from_url_new(item.downloadUrl, filename) if (result.success) { ElMessage.success(`已保存:${result.path || filename}`) } else if (result.error && result.error !== '用户取消') { diff --git a/frontend-vue/src/pages/brand/components/BrandDedupeTab.vue b/frontend-vue/src/pages/brand/components/BrandDedupeTab.vue index a6cc2cd..2c7e4ec 100644 --- a/frontend-vue/src/pages/brand/components/BrandDedupeTab.vue +++ b/frontend-vue/src/pages/brand/components/BrandDedupeTab.vue @@ -335,8 +335,8 @@ async function downloadCleanResult(item: DedupeResultItem) { } const filename = item.outputFilename || `${new Date().toISOString().slice(0, 10).replace(/-/g, '')}_cleaned.xlsx` - if (api?.save_file_from_url) { - const result = await api.save_file_from_url(item.downloadUrl, filename) + if (api?.save_file_from_url_new) { + const result = await api.save_file_from_url_new(item.downloadUrl, filename) if (result.success) { ElMessage.success(`已保存:${result.path || filename}`) } else if (result.error && result.error !== '用户取消') { diff --git a/frontend-vue/src/pages/brand/components/BrandDeleteBrandTab.vue b/frontend-vue/src/pages/brand/components/BrandDeleteBrandTab.vue index 07f3cb5..d3d0c91 100644 --- a/frontend-vue/src/pages/brand/components/BrandDeleteBrandTab.vue +++ b/frontend-vue/src/pages/brand/components/BrandDeleteBrandTab.vue @@ -286,22 +286,52 @@ function loadSessionTasksFromStorage() { } function saveSessionTask(taskId: number, items: DeleteBrandResultItem[], pushResult?: string, payloadText?: string) { - const validItems = items.filter(i => i.matchStatus === 'MATCHED' && i.success !== false) - if (validItems.length === 0) return + if (!items?.length) return + + const existingIndex = sessionTasks.value.findIndex(t => t.taskId === taskId) + const existingTask = existingIndex >= 0 ? sessionTasks.value[existingIndex] : null + + // 全量保留任务项(不在存储层按 matched/success 过滤),避免单任务多店铺时出现“未处理项丢失”。 + // 同时尽量继承已有的 _pushed/_completed 会话态。 + const mergedItems = items.map((item) => { + const prev = existingTask?.items.find((i) => isSameDeleteBrandItem(i, item)) + + // 保护大字段:当新值为空数组时,保留本地已有的非空数据,避免被轮询详情的精简 payload 覆盖。 + const nextCountries = Array.isArray(item.countries) ? item.countries : undefined + const prevCountries = Array.isArray(prev?.countries) ? prev?.countries : undefined + const countries = (nextCountries && nextCountries.length > 0) + ? nextCountries + : (prevCountries && prevCountries.length > 0 ? prevCountries : (nextCountries ?? prevCountries)) + + const nextPreviewRows = Array.isArray(item.previewRows) ? item.previewRows : undefined + const prevPreviewRows = Array.isArray(prev?.previewRows) ? prev?.previewRows : undefined + const previewRows = (nextPreviewRows && nextPreviewRows.length > 0) + ? nextPreviewRows + : (prevPreviewRows && prevPreviewRows.length > 0 ? prevPreviewRows : (nextPreviewRows ?? prevPreviewRows)) + + return { + ...item, + countries, + previewRows, + _pushed: prev?._pushed ?? (item as SessionDeleteBrandItem)._pushed ?? false, + _completed: prev?._completed ?? (item as SessionDeleteBrandItem)._completed ?? false, + } as SessionDeleteBrandItem + }) const newTask: StoredCurrentTask = { taskId, - items: validItems, - createdAt: Date.now(), - queuePushResult: pushResult, - queuePayloadText: payloadText + items: mergedItems, + createdAt: existingTask?.createdAt || Date.now(), + queuePushResult: pushResult ?? existingTask?.queuePushResult, + queuePayloadText: payloadText ?? existingTask?.queuePayloadText, } - const existingIndex = sessionTasks.value.findIndex(t => t.taskId === taskId) + if (existingIndex >= 0) { sessionTasks.value[existingIndex] = newTask } else { sessionTasks.value.push(newTask) } + if (typeof window !== 'undefined') { window.localStorage.setItem(getStorageKey(), JSON.stringify(sessionTasks.value)) } @@ -318,7 +348,7 @@ function removeSessionTaskFromStorage(taskId: number) { const historySectionItems = computed(() => historyItems.value.filter( - (item) => !currentSectionItems.value.some(c => c.taskId === item.taskId || (c.resultId && c.resultId === item.resultId)) + (item) => !currentSectionItems.value.some(c => isSameDeleteBrandItem(c, item)) ), ); const hasVisibleItems = computed(() => currentSectionItems.value.length > 0 || historySectionItems.value.length > 0) @@ -366,30 +396,28 @@ function updateCompletedItemsFromProgress(taskId: number | undefined) { let changed = false const info = taskDetails.value[taskId]?.line_progress?.info + const multiFileTask = isMultiFileTask(taskId) - // 多文件任务优先依据后端 finished_files 同步已完成文件数量。 - // 规则:按 sessionTask.items 的顺序,标记「已推送」中的前 N 个为 completed。 - if (isMultiFileTask(taskId) && info?.finished_files != null && info.finished_files >= 0) { - const pushedItems = sessionTask.items.filter(i => i._pushed) - const shouldCompleted = Math.max(0, Math.min(info.finished_files, pushedItems.length)) - - for (let i = 0; i < shouldCompleted; i += 1) { - if (!pushedItems[i]._completed) { - pushedItems[i]._completed = true + // 关键修复:多文件任务下不再用 finished_files 按顺序回填 completed。 + // 否则当第 1 个文件完成后,推入第 2 个文件时会因为 finished_files=1 被误判为“本文件已完成(100%)”。 + // 改为仅当后端进度明确指向该文件且 current_line >= total_lines 时,才标记该文件 completed。 + if (info?.file_name && info.current_line != null && info.total_lines != null && info.total_lines > 0) { + if (info.current_line >= info.total_lines) { + const sessionItem = sessionTask.items.find(i => i.sourceFilename === info.file_name) + if (sessionItem && !sessionItem._completed) { + sessionItem._completed = true changed = true } } } - // 兼容单文件/兜底:当当前文件行进度到达末行时,标记活动文件完成。 - if (info?.file_name && info.current_line != null && info.total_lines != null && info.total_lines > 0) { - if (info.current_line >= info.total_lines) { - const activeItem = currentSectionItems.value.find(item => getItemKey(item) === activeItemKey.value) - const sessionItem = activeItem && activeItem.taskId === taskId - ? findSessionItem(taskId, activeItem) - : sessionTask.items.find(i => i.sourceFilename === info.file_name) - if (sessionItem && !sessionItem._completed) { - sessionItem._completed = true + // 单文件任务兜底:任务终态时标记完成,避免偶发进度丢包导致状态卡住。 + if (!multiFileTask) { + const status = taskDetails.value[taskId]?.task?.status + if ((status === 'SUCCESS' || status === 'FAILED') && sessionTask.items.length === 1) { + const onlyItem = sessionTask.items[0] + if (onlyItem && !onlyItem._completed) { + onlyItem._completed = true changed = true } } @@ -531,6 +559,48 @@ function formatProgress(item: DeleteBrandResultItem) { const detail = taskDetails.value[taskId] const info = detail?.line_progress?.info const fp = getFileProgress(item) + const multiFileTask = isMultiFileTask(taskId) + + // 多文件任务优先按“当前运行文件 + 本地会话态”展示,避免后端 finished_files 按顺序分配导致某个文件被误判 COMPLETED。 + if (multiFileTask) { + const sessionItem = findSessionItem(taskId, item) + const status = detail?.task?.status + const isRunningTarget = Boolean(info?.file_name && info.file_name === item.sourceFilename) + + let displayStatus = sessionItem?._completed ? 'COMPLETED' : (sessionItem?._pushed ? 'PENDING' : 'PENDING') + let processedRows = 0 + let totalRows = Math.max(0, item.totalRows ?? fp?.totalRows ?? 0) + + if (status === 'SUCCESS') { + displayStatus = 'COMPLETED' + processedRows = totalRows + } else if (status === 'FAILED' && sessionItem?._completed) { + displayStatus = 'COMPLETED' + processedRows = totalRows + } else if (isRunningTarget) { + displayStatus = 'RUNNING' + processedRows = Math.max(0, Math.min(totalRows || Number.MAX_SAFE_INTEGER, info?.current_line ?? 0)) + if (!totalRows && info?.total_lines && info.total_lines > 0) { + totalRows = info.total_lines + } + } else if (sessionItem?._completed) { + displayStatus = 'COMPLETED' + processedRows = totalRows + } else if (sessionItem?._pushed) { + displayStatus = 'PENDING' + processedRows = 0 + } + + const parts: string[] = [] + parts.push(`文件状态:${displayStatus}`) + parts.push(`文件进度:${processedRows}/${totalRows}`) + if (info?.finished_files != null) { + const total = info.file_total && info.file_total > 0 ? `/${info.file_total}` : '' + parts.push(`已完成文件:${info.finished_files}${total}`) + } + if (info?.phase) parts.push(`阶段:${info.phase}`) + return parts.join('|') + } if (fp) { const parts: string[] = [] @@ -571,12 +641,32 @@ function formatProgressPercent(item: DeleteBrandResultItem) { const taskId = item.taskId if (!taskId) return 0 + const info = taskDetails.value[taskId]?.line_progress?.info + const multiFileTask = isMultiFileTask(taskId) + + if (multiFileTask) { + const sessionItem = findSessionItem(taskId, item) + const taskStatus = taskDetails.value[taskId]?.task?.status + if (taskStatus === 'SUCCESS' || sessionItem?._completed) return 100 + + const totalRows = Math.max(0, item.totalRows ?? getFileProgress(item)?.totalRows ?? 0) + const isRunningTarget = Boolean(info?.file_name && info.file_name === item.sourceFilename) + if (isRunningTarget) { + if (info?.current_line != null && info?.total_lines && info.total_lines > 0) { + return Math.max(0, Math.min(100, Math.round((info.current_line / info.total_lines) * 100))) + } + if (info?.current_line != null && totalRows > 0) { + return Math.max(0, Math.min(100, Math.round((info.current_line / totalRows) * 100))) + } + } + return 0 + } + const fp = getFileProgress(item) if (fp) { return Math.max(0, Math.min(100, fp.percent ?? 0)) } - const info = taskDetails.value[taskId]?.line_progress?.info if (!info) return 0 if (info.current_line != null && info.total_lines && info.total_lines > 0) { @@ -667,30 +757,58 @@ function canDownloadTaskResult(item: DeleteBrandResultItem) { function mergeTaskDetailItemsIntoSession(detail: DeleteBrandTaskDetailVo) { const taskId = detail?.task?.id - if (!taskId || !detail?.items?.length) return false + if (!taskId) return false const sessionTask = sessionTasks.value.find(t => t.taskId === taskId) if (!sessionTask) return false + const detailItems = detail?.items || [] let changed = false + + // 以本地会话项为主做增量合并,保留 _pushed/_completed; + // 对后端新增返回的项追加到末尾,避免单任务多店铺/后续补齐时丢项。 const mergedItems = sessionTask.items.map((item) => { - const latest = detail.items?.find((candidate) => isSameDeleteBrandItem(candidate, item)) + const latest = detailItems.find((candidate) => isSameDeleteBrandItem(candidate, item)) if (!latest) return item + + const latestCountries = Array.isArray(latest.countries) ? latest.countries : undefined + const itemCountries = Array.isArray(item.countries) ? item.countries : undefined + const countries = (latestCountries && latestCountries.length > 0) + ? latestCountries + : (itemCountries && itemCountries.length > 0 ? itemCountries : (latestCountries ?? itemCountries)) + + const latestPreviewRows = Array.isArray(latest.previewRows) ? latest.previewRows : undefined + const itemPreviewRows = Array.isArray(item.previewRows) ? item.previewRows : undefined + const previewRows = (latestPreviewRows && latestPreviewRows.length > 0) + ? latestPreviewRows + : (itemPreviewRows && itemPreviewRows.length > 0 ? itemPreviewRows : (latestPreviewRows ?? itemPreviewRows)) + const merged = { ...item, ...latest, - success: item.success ?? latest.success, + countries, + previewRows, _pushed: item._pushed, _completed: item._completed, - } + } as SessionDeleteBrandItem if (JSON.stringify(merged) !== JSON.stringify(item)) { changed = true } return merged }) - const validItems = mergedItems.filter(i => i.matchStatus === 'MATCHED' && i.success !== false) - if (JSON.stringify(validItems) !== JSON.stringify(sessionTask.items)) { - sessionTask.items = validItems + for (const latest of detailItems) { + if (!mergedItems.some((item) => isSameDeleteBrandItem(item, latest))) { + mergedItems.push({ + ...latest, + _pushed: false, + _completed: false, + } as SessionDeleteBrandItem) + changed = true + } + } + + if (JSON.stringify(mergedItems) !== JSON.stringify(sessionTask.items)) { + sessionTask.items = mergedItems changed = true } @@ -750,21 +868,26 @@ function scheduleNextPoll(immediate = false) { pollingInFlight.value = true let stateChanged = false + let shouldRefreshHistory = false try { await refreshTaskDetails(taskIds) await maybeAutoAdvance() for (const taskId of taskIds) { if (isTaskTerminal(taskId)) { + // 以后端任务终态为准:进入 SUCCESS/FAILED 后立即清理本地当前任务并刷新 history, + // 避免 _completed 会话态偶发不同步导致持续轮询 batch。 removeSessionTaskFromStorage(taskId) stateChanged = true + shouldRefreshHistory = true } } } finally { pollingInFlight.value = false } - if (stateChanged) { - loadHistory() + if (shouldRefreshHistory) { + await loadHistory() + } else if (stateChanged) { syncResultState() } @@ -923,8 +1046,8 @@ async function downloadTaskResult(item: DeleteBrandResultItem) { item.sourceFilename || `delete-brand_${taskId}.xlsx` - if (api?.save_file_from_url) { - const result = await api.save_file_from_url(url, filename) + if (api?.save_file_from_url_new) { + const result = await api.save_file_from_url_new(url, filename) if (result.success) { ElMessage.success(`已保存:${result.path || filename}`) } else if (result.error && result.error !== '用户取消') { @@ -1174,15 +1297,20 @@ onMounted(() => { loadSessionTasksFromStorage() const taskIds = getPollingTaskIds() if (taskIds.length > 0) { - refreshTaskDetails(taskIds).then(() => { + refreshTaskDetails(taskIds).then(async () => { let stateChanged = false + let shouldRefreshHistory = false for (const taskId of taskIds) { if (isTaskTerminal(taskId)) { + // 首屏恢复也以后端终态为准,避免依赖 _completed 造成卡轮询。 removeSessionTaskFromStorage(taskId) stateChanged = true + shouldRefreshHistory = true } } - if (stateChanged) { + if (shouldRefreshHistory) { + await loadHistory() + } else if (stateChanged) { syncResultState() } ensurePolling(true) diff --git a/frontend-vue/src/pages/brand/components/BrandProductRiskTab.vue b/frontend-vue/src/pages/brand/components/BrandProductRiskTab.vue index 2fca269..124ac85 100644 --- a/frontend-vue/src/pages/brand/components/BrandProductRiskTab.vue +++ b/frontend-vue/src/pages/brand/components/BrandProductRiskTab.vue @@ -18,13 +18,8 @@
备选区域
暂无备选店铺,请先输入并确认
- + @@ -40,34 +35,18 @@ 勾选需要处理的站点(可只选 2~3 国);上方勾选区先按已选顺序排列,其余未选国家排在后面。拖拽下方列表调整顺序后会与上方同步。设置会按当前登录用户保存。

-
已选顺序(拖拽 ⋮⋮ 调整)
-
+
⋮⋮ {{ countryLabel(code) }}({{ code }})
@@ -78,18 +57,9 @@
商品列表筛选

与「推送到 Python 队列」一并下发,供 Python 区分处理场景。

- - + +
@@ -97,16 +67,15 @@ -
-

与删除 ASIN 品牌相同:先创建任务,再按店铺逐条入队(每条 data.items 仅含一个店铺)。队列串行执行;Python 每处理完一店可 POST /api/product-risk-resolve/tasks/{taskId}/result(可多次,每次可只含已完成的店铺);全部完成后任务结束。前端轮询任务状态。

+

与删除 ASIN 品牌相同:先创建任务,再按店铺逐条入队(每条 data.items + 仅含一个店铺)。队列串行执行;Python + 每处理完一店可 POST /api/product-risk-resolve/tasks/{taskId}/result(可多次,每次可只含已完成的店铺);全部完成后任务结束。前端轮询任务状态。 +

推送结果
@@ -139,13 +108,8 @@
匹配结果
完成「匹配店铺」后,在此展示紫鸟索引匹配结果,供核对后再推送队列。
- + @@ -360,8 +354,8 @@ async function downloadSplitResult(item: SplitResultItem) { } const filename = item.outputFilename || `${new Date().toISOString().slice(0, 10).replace(/-/g, '')}.zip` - if (api?.save_file_from_url) { - const result = await api.save_file_from_url(item.downloadUrl, filename) + if (api?.save_file_from_url_new) { + const result = await api.save_file_from_url_new(item.downloadUrl, filename) if (result.success) { ElMessage.success(`已保存:${result.path || filename}`) } else if (result.error && result.error !== '用户取消') { @@ -379,75 +373,516 @@ onMounted(() => { diff --git a/frontend-vue/src/shared/api/java-modules.ts b/frontend-vue/src/shared/api/java-modules.ts index 2cffca2..a4a4833 100644 --- a/frontend-vue/src/shared/api/java-modules.ts +++ b/frontend-vue/src/shared/api/java-modules.ts @@ -1,461 +1,581 @@ -import { del, get, http, post, put, type JavaApiResponse, unwrapJavaResponse } from '@/shared/api/http' +import { + del, + get, + http, + post, + put, + type JavaApiResponse, + unwrapJavaResponse, +} from "@/shared/api/http"; -const JAVA_API_PREFIX = '/newApi/api' +const JAVA_API_PREFIX = "/newApi/api"; function getCurrentUserId() { - const raw = typeof window === 'undefined' ? '' : window.localStorage.getItem('uid') || '' - const value = Number(raw) + const raw = + typeof window === "undefined" + ? "" + : window.localStorage.getItem("uid") || ""; + const value = Number(raw); if (!Number.isFinite(value) || value <= 0) { - throw new Error('未获取到用户ID') + throw new Error("未获取到用户ID"); } - return value + return value; } export interface UploadedFileRef { - fileKey: string - originalFilename?: string - relativePath?: string + fileKey: string; + originalFilename?: string; + relativePath?: string; } export interface UploadFileVo { - fileKey: string - originalFilename: string - localPath: string - size: number - relativePath?: string + fileKey: string; + originalFilename: string; + localPath: string; + size: number; + relativePath?: string; } export interface DedupeResultItem { - resultId?: number - sourceFilename: string - outputFilename?: string - success: boolean - error?: string - downloadUrl?: string + resultId?: number; + sourceFilename: string; + outputFilename?: string; + success: boolean; + error?: string; + downloadUrl?: string; } export interface DedupeRunVo { - total: number - successCount: number - failedCount: number - items: DedupeResultItem[] + total: number; + successCount: number; + failedCount: number; + items: DedupeResultItem[]; } export interface DedupeHistoryVo { - items: DedupeResultItem[] + items: DedupeResultItem[]; } export interface DedupeRunRequest { - files: UploadedFileRef[] - selectedColumns: string[] - keepIntegerIds: boolean - keepUnderscoreIds: boolean - keepIntegerMainIdsWhenNoSubIds: boolean - archiveName?: string - user_id: number + files: UploadedFileRef[]; + selectedColumns: string[]; + keepIntegerIds: boolean; + keepUnderscoreIds: boolean; + keepIntegerMainIdsWhenNoSubIds: boolean; + archiveName?: string; + user_id: number; } export interface SplitArchiveEntry { - filename: string - rowCount?: number + filename: string; + rowCount?: number; } export interface SplitResultItem { - resultId?: number - sourceFilename: string - outputFilename?: string - success: boolean - error?: string - rowCount?: number - downloadUrl?: string - entryCount?: number - entries?: SplitArchiveEntry[] + resultId?: number; + sourceFilename: string; + outputFilename?: string; + success: boolean; + error?: string; + rowCount?: number; + downloadUrl?: string; + entryCount?: number; + entries?: SplitArchiveEntry[]; } export interface SplitRunVo { - total: number - successCount: number - failedCount: number - items: SplitResultItem[] + total: number; + successCount: number; + failedCount: number; + items: SplitResultItem[]; } export interface SplitHistoryVo { - items: SplitResultItem[] + items: SplitResultItem[]; } export interface SplitRunRequest { - files: UploadedFileRef[] - selectedColumns: string[] - splitMode: 'rows_per_file' | 'parts' - rowsPerFile?: number - parts?: number - archiveName?: string - user_id: number + files: UploadedFileRef[]; + selectedColumns: string[]; + splitMode: "rows_per_file" | "parts"; + rowsPerFile?: number; + parts?: number; + archiveName?: string; + user_id: number; } export interface ConvertResultItem { - resultId?: number - sourceFilename: string - outputFilename?: string - success: boolean - error?: string - downloadUrl?: string + resultId?: number; + sourceFilename: string; + outputFilename?: string; + success: boolean; + error?: string; + downloadUrl?: string; } export interface ConvertRunVo { - total: number - successCount: number - failedCount: number - items: ConvertResultItem[] + total: number; + successCount: number; + failedCount: number; + items: ConvertResultItem[]; } export interface ConvertHistoryVo { - items: ConvertResultItem[] + items: ConvertResultItem[]; } export interface ConvertRunRequest { - files: UploadedFileRef[] - templateId: string - archiveName?: string - user_id: number + files: UploadedFileRef[]; + templateId: string; + archiveName?: string; + user_id: number; } export interface DeleteBrandPreviewRow { - rowIndex: number - country: string - asin: string - status: string + rowIndex: number; + country: string; + asin: string; + status: string; } export interface DeleteBrandCountryAsinItem { - rowIndex: number - asin: string - status: string + rowIndex: number; + asin: string; + status: string; } export interface DeleteBrandCountryGroup { - country: string - asinCount: number - items: DeleteBrandCountryAsinItem[] + country: string; + asinCount: number; + items: DeleteBrandCountryAsinItem[]; } export interface DeleteBrandCountryResultItem { - asin: string - status?: string + asin: string; + status?: string; } export interface DeleteBrandProcessedCountry { - country: string - items: DeleteBrandCountryResultItem[] + country: string; + items: DeleteBrandCountryResultItem[]; } export interface DeleteBrandResultFile { - fileKey?: string - sourceFilename: string - fileIndex?: number - fileTotal?: number - chunkIndex?: number - chunkTotal?: number - processedRows?: number - totalRows?: number - currentCountry?: string - currentAsin?: string - countries: DeleteBrandProcessedCountry[] + fileKey?: string; + sourceFilename: string; + fileIndex?: number; + fileTotal?: number; + chunkIndex?: number; + chunkTotal?: number; + processedRows?: number; + totalRows?: number; + currentCountry?: string; + currentAsin?: string; + countries: DeleteBrandProcessedCountry[]; } export interface DeleteBrandSubmitResultRequest { - submissionId?: string - files: DeleteBrandResultFile[] + submissionId?: string; + files: DeleteBrandResultFile[]; } export interface DeleteBrandLineProgressInfo { - file_index?: number - file_total?: number - file_name?: string - current_line?: number - total_lines?: number - current_country?: string - current_asin?: string - finished_files?: number - phase?: string + file_index?: number; + file_total?: number; + file_name?: string; + current_line?: number; + total_lines?: number; + current_country?: string; + current_asin?: string; + finished_files?: number; + phase?: string; } export interface DeleteBrandLineProgress { - has_progress: boolean - info?: DeleteBrandLineProgressInfo + has_progress: boolean; + info?: DeleteBrandLineProgressInfo; } export interface DeleteBrandTaskItem { - id: number - taskNo?: string - status?: string - sourceFileCount?: number - successFileCount?: number - failedFileCount?: number - errorMessage?: string - createdAt?: string - updatedAt?: string - finishedAt?: string - downloadUrl?: string - downloadFilename?: string + id: number; + taskNo?: string; + status?: string; + sourceFileCount?: number; + successFileCount?: number; + failedFileCount?: number; + errorMessage?: string; + createdAt?: string; + updatedAt?: string; + finishedAt?: string; + downloadUrl?: string; + downloadFilename?: string; } export interface DeleteBrandTaskFileProgress { - fileKey?: string - sourceFilename?: string - processedRows?: number - totalRows?: number - percent?: number - status?: 'PENDING' | 'RUNNING' | 'COMPLETED' | 'FAILED' + fileKey?: string; + sourceFilename?: string; + processedRows?: number; + totalRows?: number; + percent?: number; + status?: "PENDING" | "RUNNING" | "COMPLETED" | "FAILED"; } export interface DeleteBrandTaskDetailVo { - task: DeleteBrandTaskItem - line_progress: DeleteBrandLineProgress - items?: DeleteBrandResultItem[] - fileProgress?: DeleteBrandTaskFileProgress[] + task: DeleteBrandTaskItem; + line_progress: DeleteBrandLineProgress; + items?: DeleteBrandResultItem[]; + fileProgress?: DeleteBrandTaskFileProgress[]; } export interface DeleteBrandTaskBatchVo { - items: DeleteBrandTaskDetailVo[] - missingTaskIds: number[] + items: DeleteBrandTaskDetailVo[]; + missingTaskIds: number[]; } -export type DeleteBrandMatchStatus = 'MATCHED' | 'PENDING' | 'CONFLICT' | 'INDEX_STALE' +export type DeleteBrandMatchStatus = + | "MATCHED" + | "PENDING" + | "CONFLICT" + | "INDEX_STALE"; export interface DeleteBrandResultItem { - resultId?: number - fileKey?: string - sourceFilename: string - shopName?: string - matched?: boolean - matchStatus?: DeleteBrandMatchStatus - matchMessage?: string - shopId?: string - platform?: string - openStoreUrl?: string - outputFilename?: string - downloadUrl?: string - taskId?: number - countryCount?: number - countries?: DeleteBrandCountryGroup[] - totalRows?: number - truncated?: boolean - previewRows?: DeleteBrandPreviewRow[] - success: boolean - error?: string - taskStatus?: string + resultId?: number; + fileKey?: string; + sourceFilename: string; + shopName?: string; + matched?: boolean; + matchStatus?: DeleteBrandMatchStatus; + matchMessage?: string; + shopId?: string; + platform?: string; + openStoreUrl?: string; + outputFilename?: string; + downloadUrl?: string; + taskId?: number; + countryCount?: number; + countries?: DeleteBrandCountryGroup[]; + totalRows?: number; + truncated?: boolean; + previewRows?: DeleteBrandPreviewRow[]; + success: boolean; + error?: string; + taskStatus?: string; } export interface DeleteBrandRunVo { - total: number - successCount: number - failedCount: number - items: DeleteBrandResultItem[] + total: number; + successCount: number; + failedCount: number; + items: DeleteBrandResultItem[]; } export interface DeleteBrandHistoryVo { - items: DeleteBrandResultItem[] + items: DeleteBrandResultItem[]; } export interface DeleteBrandRunRequest { - files: UploadedFileRef[] - user_id: number + files: UploadedFileRef[]; + user_id: number; } export interface ConvertTemplateVo { - id: string - templateCode: string - templateName: string - outputFilename: string - isDefault?: boolean - builtIn?: boolean + id: string; + templateCode: string; + templateName: string; + outputFilename: string; + isDefault?: boolean; + builtIn?: boolean; } export interface ConvertTemplateImportRequest { - templateName: string - templateContent: string + templateName: string; + templateContent: string; } export interface ExcelInfoVo { - headers: string[] - totalRows: number + headers: string[]; + totalRows: number; } export function getExcelInfo(fileKey: string) { - return unwrapJavaResponse(get>(`${JAVA_API_PREFIX}/files/excel-info`, { - params: { fileKey }, - })) + return unwrapJavaResponse( + get>(`${JAVA_API_PREFIX}/files/excel-info`, { + params: { fileKey }, + }), + ); } -export function runDedupe(request: Omit | DedupeRunRequest) { - return unwrapJavaResponse(post, DedupeRunRequest>(`${JAVA_API_PREFIX}/dedupe/run`, { - ...request, - user_id: getCurrentUserId(), - })) +export function runDedupe( + request: Omit | DedupeRunRequest, +) { + return unwrapJavaResponse( + post, DedupeRunRequest>( + `${JAVA_API_PREFIX}/dedupe/run`, + { + ...request, + user_id: getCurrentUserId(), + }, + ), + ); } export function getDedupeHistory() { - return unwrapJavaResponse(get>(`${JAVA_API_PREFIX}/dedupe/history`, { - params: { user_id: getCurrentUserId() }, - })) + return unwrapJavaResponse( + get>(`${JAVA_API_PREFIX}/dedupe/history`, { + params: { user_id: getCurrentUserId() }, + }), + ); } export function deleteDedupeHistory(resultId: number) { - return unwrapJavaResponse(del>(`${JAVA_API_PREFIX}/dedupe/history/${resultId}`, { - params: { user_id: getCurrentUserId() }, - })) + return unwrapJavaResponse( + del>( + `${JAVA_API_PREFIX}/dedupe/history/${resultId}`, + { + params: { user_id: getCurrentUserId() }, + }, + ), + ); } -export function runSplit(request: Omit | SplitRunRequest) { - return unwrapJavaResponse(post, SplitRunRequest>(`${JAVA_API_PREFIX}/split/run`, { - ...request, - user_id: getCurrentUserId(), - })) +export function runSplit( + request: Omit | SplitRunRequest, +) { + return unwrapJavaResponse( + post, SplitRunRequest>( + `${JAVA_API_PREFIX}/split/run`, + { + ...request, + user_id: getCurrentUserId(), + }, + ), + ); } export function getSplitHistory() { - return unwrapJavaResponse(get>(`${JAVA_API_PREFIX}/split/history`, { - params: { user_id: getCurrentUserId() }, - })) + return unwrapJavaResponse( + get>(`${JAVA_API_PREFIX}/split/history`, { + params: { user_id: getCurrentUserId() }, + }), + ); } export function deleteSplitHistory(resultId: number) { - return unwrapJavaResponse(del>(`${JAVA_API_PREFIX}/split/history/${resultId}`, { - params: { user_id: getCurrentUserId() }, - })) + return unwrapJavaResponse( + del>(`${JAVA_API_PREFIX}/split/history/${resultId}`, { + params: { user_id: getCurrentUserId() }, + }), + ); } export function getConvertTemplates() { - return unwrapJavaResponse(get>(`${JAVA_API_PREFIX}/convert/templates`)) + return unwrapJavaResponse( + get>( + `${JAVA_API_PREFIX}/convert/templates`, + ), + ); } export function importConvertTemplate(request: ConvertTemplateImportRequest) { return unwrapJavaResponse( - post, ConvertTemplateImportRequest>(`${JAVA_API_PREFIX}/convert/templates/import`, request), - ) + post, ConvertTemplateImportRequest>( + `${JAVA_API_PREFIX}/convert/templates/import`, + request, + ), + ); } export function setDefaultConvertTemplate(templateCode: string) { - return unwrapJavaResponse(post>(`${JAVA_API_PREFIX}/convert/templates/${templateCode}/default`)) + return unwrapJavaResponse( + post>( + `${JAVA_API_PREFIX}/convert/templates/${templateCode}/default`, + ), + ); } export function deleteConvertTemplate(templateCode: string) { - return unwrapJavaResponse(del>(`${JAVA_API_PREFIX}/convert/templates/${templateCode}`)) + return unwrapJavaResponse( + del>( + `${JAVA_API_PREFIX}/convert/templates/${templateCode}`, + ), + ); } -export function runConvert(request: Omit | ConvertRunRequest) { - return unwrapJavaResponse(post, ConvertRunRequest>(`${JAVA_API_PREFIX}/convert/run`, { - ...request, - user_id: getCurrentUserId(), - })) +export function runConvert( + request: Omit | ConvertRunRequest, +) { + return unwrapJavaResponse( + post, ConvertRunRequest>( + `${JAVA_API_PREFIX}/convert/run`, + { + ...request, + user_id: getCurrentUserId(), + }, + ), + ); } export function getConvertHistory() { - return unwrapJavaResponse(get>(`${JAVA_API_PREFIX}/convert/history`, { - params: { user_id: getCurrentUserId() }, - })) + return unwrapJavaResponse( + get>( + `${JAVA_API_PREFIX}/convert/history`, + { + params: { user_id: getCurrentUserId() }, + }, + ), + ); } export function deleteConvertHistory(resultId: number) { - return unwrapJavaResponse(del>(`${JAVA_API_PREFIX}/convert/history/${resultId}`, { - params: { user_id: getCurrentUserId() }, - })) + return unwrapJavaResponse( + del>( + `${JAVA_API_PREFIX}/convert/history/${resultId}`, + { + params: { user_id: getCurrentUserId() }, + }, + ), + ); } -export function runDeleteBrand(request: Omit | DeleteBrandRunRequest) { - return unwrapJavaResponse(post, DeleteBrandRunRequest>(`${JAVA_API_PREFIX}/delete-brand/run`, { - ...request, - user_id: getCurrentUserId(), - })) +export function runDeleteBrand( + request: Omit | DeleteBrandRunRequest, +) { + return unwrapJavaResponse( + post, DeleteBrandRunRequest>( + `${JAVA_API_PREFIX}/delete-brand/run`, + { + ...request, + user_id: getCurrentUserId(), + }, + ), + ); } export function getDeleteBrandHistory() { - return unwrapJavaResponse(get>(`${JAVA_API_PREFIX}/delete-brand/history`, { - params: { user_id: getCurrentUserId() }, - })) + return unwrapJavaResponse( + get>( + `${JAVA_API_PREFIX}/delete-brand/history`, + { + params: { user_id: getCurrentUserId() }, + }, + ), + ); } export function deleteDeleteBrandHistory(resultId: number) { - return unwrapJavaResponse(del>(`${JAVA_API_PREFIX}/delete-brand/history/${resultId}`, { - params: { user_id: getCurrentUserId() }, - })) + return unwrapJavaResponse( + del>( + `${JAVA_API_PREFIX}/delete-brand/history/${resultId}`, + { + params: { user_id: getCurrentUserId() }, + }, + ), + ); } export function getDeleteBrandTaskDetail(taskId: number) { - return unwrapJavaResponse(get>(`${JAVA_API_PREFIX}/delete-brand/tasks/${taskId}`)) + return unwrapJavaResponse( + get>( + `${JAVA_API_PREFIX}/delete-brand/tasks/${taskId}`, + ), + ); } export function getDeleteBrandTaskDetails(taskIds: number[]) { - return unwrapJavaResponse(post, { taskIds: number[] }>(`${JAVA_API_PREFIX}/delete-brand/tasks/batch`, { taskIds })) + return unwrapJavaResponse( + post, { taskIds: number[] }>( + `${JAVA_API_PREFIX}/delete-brand/tasks/batch`, + { taskIds }, + ), + ); } export function getDeleteBrandTaskDownloadUrl(taskId: number) { - return getJavaDownloadUrl(`/delete-brand/tasks/${taskId}/download`) + return getJavaDownloadUrl(`/delete-brand/tasks/${taskId}/download`); } -export function submitDeleteBrandResult(taskId: number, request: DeleteBrandSubmitResultRequest) { - return unwrapJavaResponse(post, DeleteBrandSubmitResultRequest>(`${JAVA_API_PREFIX}/delete-brand/tasks/${taskId}/result`, request)) +export function submitDeleteBrandResult( + taskId: number, + request: DeleteBrandSubmitResultRequest, +) { + return unwrapJavaResponse( + post, DeleteBrandSubmitResultRequest>( + `${JAVA_API_PREFIX}/delete-brand/tasks/${taskId}/result`, + request, + ), + ); } /** 商品风险解决:备选店铺与匹配(推队列由前端 pywebview 完成) */ export interface ProductRiskCandidateVo { - id: number - shop_name: string - created_at?: string + id: number; + shop_name: string; + created_at?: string; } export interface ProductRiskShopQueueItem { - shopName: string - matched: boolean - shopId?: string - platform?: string - companyName?: string - openStoreUrl?: string - matchedUserId?: number - matchStatus?: string - matchMessage?: string + shopName: string; + matched: boolean; + shopId?: string; + platform?: string; + companyName?: string; + openStoreUrl?: string; + matchedUserId?: number; + matchStatus?: string; + matchMessage?: string; } export interface ProductRiskMatchShopsVo { - items: ProductRiskShopQueueItem[] + items: ProductRiskShopQueueItem[]; } export function listProductRiskCandidates() { return unwrapJavaResponse( - get>(`${JAVA_API_PREFIX}/product-risk-resolve/candidates`, { - params: { user_id: getCurrentUserId() }, - }), - ) + get>( + `${JAVA_API_PREFIX}/product-risk-resolve/candidates`, + { + params: { user_id: getCurrentUserId() }, + }, + ), + ); } export function addProductRiskCandidate(shopName: string) { return unwrapJavaResponse( - post, { user_id: number; shop_name: string }>( - `${JAVA_API_PREFIX}/product-risk-resolve/candidates`, - { user_id: getCurrentUserId(), shop_name: shopName }, - ), - ) + post< + JavaApiResponse, + { user_id: number; shop_name: string } + >(`${JAVA_API_PREFIX}/product-risk-resolve/candidates`, { + user_id: getCurrentUserId(), + shop_name: shopName, + }), + ); } export function deleteProductRiskCandidate(id: number) { return unwrapJavaResponse( - del>(`${JAVA_API_PREFIX}/product-risk-resolve/candidates/${id}`, { - params: { user_id: getCurrentUserId() }, - }), - ) + del>( + `${JAVA_API_PREFIX}/product-risk-resolve/candidates/${id}`, + { + params: { user_id: getCurrentUserId() }, + }, + ), + ); } export interface ProductRiskCountryPreferenceVo { - country_codes: string[] + country_codes: string[]; } export function getProductRiskCountryPreference() { return unwrapJavaResponse( - get>(`${JAVA_API_PREFIX}/product-risk-resolve/country-preference`, { - params: { user_id: getCurrentUserId() }, - }), - ) + get>( + `${JAVA_API_PREFIX}/product-risk-resolve/country-preference`, + { + params: { user_id: getCurrentUserId() }, + }, + ), + ); } export function putProductRiskCountryPreference(countryCodes: string[]) { @@ -467,122 +587,143 @@ export function putProductRiskCountryPreference(countryCodes: string[]) { user_id: getCurrentUserId(), country_codes: countryCodes, }), - ) + ); } export function matchProductRiskShops(shopNames: string[]) { return unwrapJavaResponse( - post, { user_id: number; shop_names: string[] }>( - `${JAVA_API_PREFIX}/product-risk-resolve/match-shops`, - { user_id: getCurrentUserId(), shop_names: shopNames }, - ), - ) + post< + JavaApiResponse, + { user_id: number; shop_names: string[] } + >(`${JAVA_API_PREFIX}/product-risk-resolve/match-shops`, { + user_id: getCurrentUserId(), + shop_names: shopNames, + }), + ); } export interface ProductRiskDashboardVo { - candidateCount: number - processedTaskCount: number - successTaskCount: number - failedTaskCount: number + candidateCount: number; + processedTaskCount: number; + successTaskCount: number; + failedTaskCount: number; } export interface ProductRiskHistoryItem { - resultId?: number - taskId?: number - shopName?: string - shopId?: string - platform?: string - companyName?: string - matched?: boolean - matchStatus?: string - matchMessage?: string - taskStatus?: string - success?: boolean - error?: string - outputFilename?: string - downloadUrl?: string + resultId?: number; + taskId?: number; + shopName?: string; + shopId?: string; + platform?: string; + companyName?: string; + matched?: boolean; + matchStatus?: string; + matchMessage?: string; + taskStatus?: string; + success?: boolean; + error?: string; + outputFilename?: string; + downloadUrl?: string; } export interface ProductRiskHistoryVo { - items: ProductRiskHistoryItem[] + items: ProductRiskHistoryItem[]; } export interface ProductRiskTaskSummary { - id?: number - taskNo?: string - status?: string - errorMessage?: string - createdAt?: string - updatedAt?: string - finishedAt?: string + id?: number; + taskNo?: string; + status?: string; + errorMessage?: string; + createdAt?: string; + updatedAt?: string; + finishedAt?: string; } export interface ProductRiskTaskDetailVo { - task?: ProductRiskTaskSummary - items?: ProductRiskHistoryItem[] + task?: ProductRiskTaskSummary; + items?: ProductRiskHistoryItem[]; } export interface ProductRiskTaskBatchVo { - items: ProductRiskTaskDetailVo[] - missingTaskIds?: number[] + items: ProductRiskTaskDetailVo[]; + missingTaskIds?: number[]; } export interface ProductRiskPendingDeleteVo { - removed: boolean + removed: boolean; } export interface ProductRiskCreateTaskVo { - taskId: number - items: ProductRiskHistoryItem[] + taskId: number; + items: ProductRiskHistoryItem[]; } export function getProductRiskDashboard() { return unwrapJavaResponse( - get>(`${JAVA_API_PREFIX}/product-risk-resolve/dashboard`, { - params: { user_id: getCurrentUserId() }, - }), - ) + get>( + `${JAVA_API_PREFIX}/product-risk-resolve/dashboard`, + { + params: { user_id: getCurrentUserId() }, + }, + ), + ); } export function getProductRiskHistory() { return unwrapJavaResponse( - get>(`${JAVA_API_PREFIX}/product-risk-resolve/history`, { - params: { user_id: getCurrentUserId() }, - }), - ) + get>( + `${JAVA_API_PREFIX}/product-risk-resolve/history`, + { + params: { user_id: getCurrentUserId() }, + }, + ), + ); } export function deleteProductRiskHistory(resultId: number) { return unwrapJavaResponse( - del>(`${JAVA_API_PREFIX}/product-risk-resolve/history/${resultId}`, { - params: { user_id: getCurrentUserId() }, - }), - ) + del>( + `${JAVA_API_PREFIX}/product-risk-resolve/history/${resultId}`, + { + params: { user_id: getCurrentUserId() }, + }, + ), + ); } export function createProductRiskTask(items: ProductRiskShopQueueItem[]) { return unwrapJavaResponse( - post, { user_id: number; items: ProductRiskShopQueueItem[] }>( - `${JAVA_API_PREFIX}/product-risk-resolve/tasks`, - { user_id: getCurrentUserId(), items }, - ), - ) + post< + JavaApiResponse, + { user_id: number; items: ProductRiskShopQueueItem[] } + >(`${JAVA_API_PREFIX}/product-risk-resolve/tasks`, { + user_id: getCurrentUserId(), + items, + }), + ); } export function deleteProductRiskTask(taskId: number) { return unwrapJavaResponse( - del>(`${JAVA_API_PREFIX}/product-risk-resolve/tasks/${taskId}`, { - params: { user_id: getCurrentUserId() }, - }), - ) + del>( + `${JAVA_API_PREFIX}/product-risk-resolve/tasks/${taskId}`, + { + params: { user_id: getCurrentUserId() }, + }, + ), + ); } export function deletePendingProductRiskShopResult(shopName: string) { return unwrapJavaResponse( - del>(`${JAVA_API_PREFIX}/product-risk-resolve/pending-shop-result`, { - params: { user_id: getCurrentUserId(), shop_name: shopName }, - }), - ) + del>( + `${JAVA_API_PREFIX}/product-risk-resolve/pending-shop-result`, + { + params: { user_id: getCurrentUserId(), shop_name: shopName }, + }, + ), + ); } export function getProductRiskTasksBatch(taskIds: number[]) { @@ -591,21 +732,30 @@ export function getProductRiskTasksBatch(taskIds: number[]) { `${JAVA_API_PREFIX}/product-risk-resolve/tasks/batch`, { taskIds }, ), - ) + ); } export function getProductRiskResultDownloadUrl(resultId: number) { - return getJavaDownloadUrl(`/product-risk-resolve/results/${resultId}/download`) + return getJavaDownloadUrl( + `/product-risk-resolve/results/${resultId}/download`, + ); } export function getJavaDownloadUrl(path: string) { - let raw = path.startsWith('http://') || path.startsWith('https://') ? path : `${JAVA_API_PREFIX}${path}` - // 核心修复:pywebview 的 save_file_from_url 需要完整带 schema 的 URL - if (!raw.startsWith('http://') && !raw.startsWith('https://') && typeof window !== 'undefined') { - raw = `${window.location.origin}${raw}` + let raw = + path.startsWith("http://") || path.startsWith("https://") + ? path + : `${JAVA_API_PREFIX}${path}`; + // 核心修复:pywebview 的 save_file_from_url_new 需要完整带 schema 的 URL + if ( + !raw.startsWith("http://") && + !raw.startsWith("https://") && + typeof window !== "undefined" + ) { + raw = `${window.location.origin}${raw}`; } - const separator = raw.includes('?') ? '&' : '?' - return `${raw}${separator}user_id=${encodeURIComponent(String(getCurrentUserId()))}` + const separator = raw.includes("?") ? "&" : "?"; + return `${raw}${separator}user_id=${encodeURIComponent(String(getCurrentUserId()))}`; } -export { JAVA_API_PREFIX, http } +export { JAVA_API_PREFIX, http }; diff --git a/frontend-vue/src/shared/bridges/pywebview.ts b/frontend-vue/src/shared/bridges/pywebview.ts index 2a08292..4bc2ffe 100644 --- a/frontend-vue/src/shared/bridges/pywebview.ts +++ b/frontend-vue/src/shared/bridges/pywebview.ts @@ -1,58 +1,91 @@ export interface UploadedJavaFile { - fileKey: string - originalFilename: string - localPath: string - size: number - relativePath?: string + fileKey: string; + originalFilename: string; + localPath: string; + size: number; + relativePath?: string; } export interface PywebviewApi { - close?: () => Promise - minimize?: () => Promise - maximize?: () => Promise - toggle_maximize?: () => Promise - save_image?: (urlOrData: string, filename?: string) => Promise<{ success: boolean; path?: string; error?: string }> - save_image_to_folder?: (urlOrData: string, dirPath: string, filename?: string) => Promise<{ success: boolean; path?: string; error?: string }> - select_folder?: () => Promise - select_brand_xlsx_files?: () => Promise - select_brand_folder?: () => Promise - upload_file_to_java?: (filePath: string, relativePath?: string) => Promise<{ success: boolean; message?: string; data?: UploadedJavaFile; error?: string }> - save_file_from_url?: (url: string, filename: string) => Promise<{ success: boolean; path?: string; error?: string }> - save_template_xlsx?: () => Promise<{ success: boolean; path?: string; error?: string }> - save_template_zip?: () => Promise<{ success: boolean; path?: string; error?: string }> - enqueue_json?: (data: unknown) => Promise<{ success: boolean; queue_size?: number; error?: string }> + close?: () => Promise; + minimize?: () => Promise; + maximize?: () => Promise; + toggle_maximize?: () => Promise; + save_image?: ( + urlOrData: string, + filename?: string, + ) => Promise<{ success: boolean; path?: string; error?: string }>; + save_image_to_folder?: ( + urlOrData: string, + dirPath: string, + filename?: string, + ) => Promise<{ success: boolean; path?: string; error?: string }>; + select_folder?: () => Promise; + select_brand_xlsx_files?: () => Promise; + select_brand_folder?: () => Promise; + upload_file_to_java?: ( + filePath: string, + relativePath?: string, + ) => Promise<{ + success: boolean; + message?: string; + data?: UploadedJavaFile; + error?: string; + }>; + save_file_from_url_new?: ( + url: string, + filename: string, + ) => Promise<{ success: boolean; path?: string; error?: string }>; + save_template_xlsx?: () => Promise<{ + success: boolean; + path?: string; + error?: string; + }>; + save_template_zip?: () => Promise<{ + success: boolean; + path?: string; + error?: string; + }>; + enqueue_json?: ( + data: unknown, + ) => Promise<{ success: boolean; queue_size?: number; error?: string }>; } declare global { interface Window { pywebview?: { - api?: PywebviewApi - } + api?: PywebviewApi; + }; } } -let cachedPywebviewApi: PywebviewApi | undefined -let pywebviewReadyBound = false +let cachedPywebviewApi: PywebviewApi | undefined; +let pywebviewReadyBound = false; function syncPywebviewApi() { - cachedPywebviewApi = window.pywebview?.api - return cachedPywebviewApi + cachedPywebviewApi = window.pywebview?.api; + return cachedPywebviewApi; } function bindPywebviewReady() { - if (pywebviewReadyBound || typeof window === 'undefined' || !window.addEventListener) return - pywebviewReadyBound = true - window.addEventListener('pywebviewready', () => { - syncPywebviewApi() - }) + if ( + pywebviewReadyBound || + typeof window === "undefined" || + !window.addEventListener + ) + return; + pywebviewReadyBound = true; + window.addEventListener("pywebviewready", () => { + syncPywebviewApi(); + }); } -bindPywebviewReady() +bindPywebviewReady(); export function getPywebviewApi() { - return syncPywebviewApi() || cachedPywebviewApi + return syncPywebviewApi() || cachedPywebviewApi; } export function hasPywebview() { - return Boolean(getPywebviewApi()) + return Boolean(getPywebviewApi()); } diff --git a/source_code/main.py b/source_code/main.py index eb64fdf..634a142 100644 --- a/source_code/main.py +++ b/source_code/main.py @@ -143,7 +143,7 @@ class WindowAPI: ) return result[0] if result else '' - def save_file_from_url(self, url, default_filename='download.zip'): + def save_file_from_url_new(self, url, default_filename='download.zip'): """弹窗选择保存位置,从 url 下载文件并保存。用于品牌任务结果 zip 等。""" if not url or not url.strip(): return {'success': False, 'error': '下载地址为空'} @@ -332,7 +332,7 @@ def main(): easy_drag=True ) api = WindowAPI(window) - window.expose(api.close, api.minimize, api.maximize, api.toggle_maximize, generate_images, api.save_image, api.save_image_to_folder, api.select_folder, api.select_brand_xlsx_files, api.select_brand_folder, api.save_file_from_url, api.save_template_xlsx,api.save_template_zip) + window.expose(api.close, api.minimize, api.maximize, api.toggle_maximize, generate_images, api.save_image, api.save_image_to_folder, api.select_folder, api.select_brand_xlsx_files, api.select_brand_folder, api.save_file_from_url_new, api.save_template_xlsx,api.save_template_zip) webview.start( debug=True, storage_path=cache_path, diff --git a/web_source/templates_backup/brand.html b/web_source/templates_backup/brand.html index 81aa193..842827a 100644 --- a/web_source/templates_backup/brand.html +++ b/web_source/templates_backup/brand.html @@ -806,7 +806,7 @@ } var zipUrl = task.result_paths.zip_url; var filename = 'brand_task_' + taskId + '.zip'; - if (api && api.save_file_from_url) { + if (api && api.save_file_from_url_new) { a.style.pointerEvents = 'none'; var done = function(ret) { if (ret && ret.success) { @@ -817,7 +817,7 @@ a.style.pointerEvents = ''; }; try { - var ret = api.save_file_from_url(zipUrl, filename); + var ret = api.save_file_from_url_new(zipUrl, filename); if (ret && typeof ret.then === 'function') { ret.then(done).catch(function(err) { showToast('保存失败:' + (err && err.message || err)); a.style.pointerEvents = ''; }); } else {