合并错误信息

This commit is contained in:
super
2026-04-28 15:25:34 +08:00
parent e4bf104ae2
commit f21934e55b
26 changed files with 1272 additions and 2465 deletions

View File

@@ -9,6 +9,7 @@
<div class="hint">选择 Excel 后点击解析后端会提取 idASIN国家并保留整数 id 与子数据的第一条</div>
<div class="btns">
<button type="button" class="opt-btn" @click="selectFiles">选择 Excel</button>
<button type="button" class="opt-btn" @click="selectFolder">选择文件夹</button>
</div>
<div class="selected-files">
<span v-if="!selectedFileNames.length">暂未选择文件</span>
@@ -35,7 +36,7 @@
<div v-if="parseResult" class="parse-card">
<div>任务 ID{{ parseResult.taskId }}</div>
<div>总行数{{ parseResult.totalRows }}保留{{ parseResult.acceptedRows }}过滤{{ parseResult.droppedRows }}</div>
<div>总行数{{ parseResult.totalRows }}有效{{ parseResult.acceptedRows }}分组{{ parseResult.groupCount || 0 }}过滤{{ parseResult.droppedRows }}</div>
</div>
<pre v-if="queuePayloadText" class="queue-payload">{{ queuePayloadText }}</pre>
</aside>
@@ -66,7 +67,7 @@
<div class="result-list-wrap preview-wrap">
<div class="result-list-header">
<span>解析结果</span>
<span v-if="parsedRows.length" class="muted">显示 1 样例 / {{ parsedRows.length }} </span>
<span v-if="parsedRows.length" class="muted">显示首组 {{ previewRows.length }} / {{ parseResult?.groupCount || parsedGroups.length }} {{ parsedRows.length }} </span>
</div>
<div v-if="!parsedRows.length" class="empty-tasks">暂无解析结果</div>
<el-table v-else :data="previewRows" height="120" class="result-table">
@@ -111,10 +112,11 @@
<div v-if="item.resultFilename" class="files">
{{ item.resultFilename || '下载结果' }}
</div>
<div v-if="pendingResultHint(item)" class="files result-hint">{{ pendingResultHint(item) }}</div>
<div v-if="item.error" class="files">错误{{ item.error }}</div>
</div>
<div class="task-right">
<span class="status" :class="item.success ? 'success' : 'failed'">{{ statusText(item) }}</span>
<span class="status" :class="statusClass(item)">{{ statusText(item) }}</span>
<button v-if="canDownload(item)" type="button" class="download" @click="downloadResult(item)">下载</button>
<button v-if="item.resultId" type="button" class="btn-delete" @click="deleteTaskRecord(item)">删除</button>
</div>
@@ -140,6 +142,7 @@ import {
getAppearancePatentResultDownloadUrl,
getAppearancePatentTaskProgressBatch,
parseAppearancePatent,
type AppearancePatentParsedGroup,
type AppearancePatentDashboardVo,
type AppearancePatentHistoryItem,
type AppearancePatentParsedRow,
@@ -147,13 +150,14 @@ import {
type UploadedFileRef,
type UploadFileVo,
} from '@/shared/api/java-modules'
import { expandBrandFolderRecursive, type BrandExpandFolderItem } from '@/shared/api/brand'
import { getPywebviewApi } from '@/shared/bridges/pywebview'
import { getTaskPollIntervalMs } from '@/shared/task-progress-config'
const selectedFileNames = ref<string[]>([])
const uploadedFiles = ref<UploadFileVo[]>([])
const parseResult = ref<AppearancePatentParseVo | null>(null)
const parsedRows = ref<AppearancePatentParsedRow[]>([])
const parsedGroups = ref<AppearancePatentParsedGroup[]>([])
const defaultAiPrompt = `请帮我排查以下亚马逊商品在英国及欧洲地区是否存在知识产权侵权风险。
请直接给出明确结论(有侵权风险 或 未发现明显侵权风险),并严格按照以下三个维度提供精简的排查理由:
外观维度: 评估产品外形、图案设计是否与欧洲/英国常见外观专利雷同。
@@ -176,7 +180,10 @@ const dashboard = ref<AppearancePatentDashboardVo>({
})
const historyItems = ref<AppearancePatentHistoryItem[]>([])
const previewRows = computed(() => parsedRows.value.slice(0, 1))
const parsedRows = computed<AppearancePatentParsedRow[]>(() =>
parsedGroups.value.flatMap((group) => group.items || []),
)
const previewRows = computed(() => parsedGroups.value[0]?.items || [])
const currentItems = computed(() => historyItems.value.filter((i) => i.taskId != null && pollingTaskIds.value.includes(i.taskId)))
const historyOnlyItems = computed(() => historyItems.value.filter((i) => (i.taskStatus || '') !== 'RUNNING'))
@@ -204,6 +211,24 @@ function clearPollingIds() {
stopPolling()
}
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) {
@@ -212,21 +237,40 @@ async function selectFiles() {
}
const paths = await api.select_brand_xlsx_files()
if (!paths?.length) return
const files: UploadFileVo[] = []
for (const path of paths) {
const uploaded = await api.upload_file_to_java(path)
if (!uploaded?.success || !uploaded.data) {
throw new Error(uploaded?.error || `上传失败:${path}`)
}
files.push(uploaded.data)
}
const files = await uploadAppearancePathsToJava(paths)
uploadedFiles.value = files
selectedFileNames.value = files.map((f) => f.originalFilename || f.fileKey)
selectedFileNames.value = files.map((f) => f.relativePath || f.originalFilename || f.fileKey)
parseResult.value = null
parsedRows.value = []
parsedGroups.value = []
queuePayloadText.value = ''
}
async function selectFolder() {
const api = getPywebviewApi()
if (!api?.select_brand_folder) {
ElMessage.warning('当前环境不支持文件夹选择,请在本地客户端中打开')
return
}
try {
const folder = await api.select_brand_folder()
if (!folder) return
const result = await expandBrandFolderRecursive(folder)
if (!result.success || !result.items?.length) {
ElMessage.warning(result.error || '该文件夹下没有可用的 xlsx 文件')
return
}
const files = await uploadAppearancePathsToJava(result.items)
uploadedFiles.value = files
selectedFileNames.value = result.items.map((item) => item.relativePath || item.absolutePath)
parseResult.value = null
parsedGroups.value = []
queuePayloadText.value = ''
ElMessage.success(`已选择文件夹内 ${result.items.length} 个 Excel 文件`)
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : '选择失败')
}
}
async function parseFiles() {
if (!uploadedFiles.value.length) {
ElMessage.warning('请先选择 Excel')
@@ -241,11 +285,23 @@ async function parseFiles() {
}))
const res = await parseAppearancePatent(files, effectiveAiPrompt())
parseResult.value = res
parsedRows.value = res.items || []
parsedGroups.value = res.groups?.length
? res.groups
: (res.items?.length
? [{
sourceFileKey: res.items[0]?.sourceFileKey,
sourceFilename: res.items[0]?.sourceFilename,
groupKey: res.items[0]?.groupKey,
baseId: '',
displayId: res.items[0]?.displayId,
itemCount: res.items.length,
items: res.items,
}]
: [])
queuePayloadText.value = ''
await loadDashboard()
await loadHistory()
ElMessage.success(`解析完成,保留 ${res.acceptedRows}`)
ElMessage.success(`解析完成,${res.groupCount || 0} 组 / ${res.acceptedRows}`)
} catch (e) {
ElMessage.error(e instanceof Error ? e.message : '解析失败')
} finally {
@@ -272,7 +328,29 @@ async function pushToPythonQueue() {
data: {
taskId,
prompt: parseResult.value?.aiPrompt || effectiveAiPrompt(),
groups: parsedGroups.value.map((group) => ({
sourceFileKey: group.sourceFileKey || '',
sourceFilename: group.sourceFilename || '',
groupKey: group.groupKey || '',
baseId: group.baseId || '',
displayId: group.displayId || '',
items: (group.items || []).map((row) => ({
sourceFileKey: row.sourceFileKey || '',
sourceFilename: row.sourceFilename || '',
rowToken: row.rowToken || '',
groupKey: row.groupKey || '',
id: row.displayId,
asin: row.asin,
country: row.country,
url: row.url || '',
title: row.title || '',
})),
})),
rows: parsedRows.value.map((row) => ({
sourceFileKey: row.sourceFileKey || '',
sourceFilename: row.sourceFilename || '',
rowToken: row.rowToken || '',
groupKey: row.groupKey || '',
id: row.displayId,
asin: row.asin,
country: row.country,
@@ -381,11 +459,49 @@ async function loadHistory() {
function statusText(item: AppearancePatentHistoryItem) {
const status = item.taskStatus || ''
if (status === 'RUNNING') return '执行中'
if (isResultPreparing(item)) return '结果生成中'
if (isResultBuildFailed(item)) return '结果生成失败'
if (status === 'SUCCESS' || item.success) return '已完成'
if (status === 'FAILED') return '失败'
return '等待中'
}
function statusClass(item: AppearancePatentHistoryItem) {
const status = item.taskStatus || ''
if (status === 'RUNNING' || isResultPreparing(item)) return 'running'
if (isResultBuildFailed(item) || status === 'FAILED') return 'failed'
if (status === 'SUCCESS' || item.success) return 'success'
return 'running'
}
function isResultPreparing(item: AppearancePatentHistoryItem) {
const status = item.taskStatus || ''
const fileStatus = (item.fileStatus || '').toUpperCase()
if (!(status === 'SUCCESS' || item.success)) return false
if (canDownload(item)) return false
if (fileStatus === 'FAILED' || fileStatus === 'SUCCESS') return false
return true
}
function isResultBuildFailed(item: AppearancePatentHistoryItem) {
return (item.fileStatus || '').toUpperCase() === 'FAILED'
}
function pendingResultHint(item: AppearancePatentHistoryItem) {
if (isResultBuildFailed(item)) {
return item.fileError || '结果文件生成失败,请稍后重试或检查后端日志'
}
if (!isResultPreparing(item)) return ''
const fileStatus = (item.fileStatus || '').toUpperCase()
if (fileStatus === 'RUNNING') {
return '任务已完成,正在请求 Coze 或组装结果文件,下载按钮稍后出现'
}
if (fileStatus === 'PENDING') {
return '任务已完成,结果文件已入队,正在等待后端生成'
}
return '任务已完成,正在生成结果文件,下载按钮稍后出现'
}
function canDownload(item: AppearancePatentHistoryItem) {
return Boolean(item.resultId && (item.fileReady || item.downloadUrl))
}
@@ -445,7 +561,7 @@ onUnmounted(() => stopPolling())
.link { color: #6ea8fe; text-decoration: none; }
.link:hover { color: #9fc5ff; }
.btns, .run-row { display: flex; gap: 10px; flex-wrap: wrap; }
.opt-btn, .btn-run, .btn-delete { border: none; cursor: pointer; border-radius: 7px; }
.opt-btn, .btn-run, .btn-delete, .download { border: none; cursor: pointer; border-radius: 7px; }
.opt-btn { padding: 8px 14px; color: #ccc; background: #2a2a2a; border: 1px solid #3a3a3a; }
.btn-run { padding: 10px 18px; color: #fff; background: #3498db; font-weight: 600; }
.btn-queue { background: #27ae60; }
@@ -478,6 +594,8 @@ onUnmounted(() => stopPolling())
.status.success { background: rgba(46, 204, 113, .18); color: #2ecc71; }
.status.failed { background: rgba(231, 76, 60, .18); color: #ff6b6b; }
.status.running { background: rgba(52, 152, 219, .18); color: #3498db; }
.result-hint { margin-top: 6px; color: #e0b96d; }
.download { padding: 6px 10px; color: #d6ecff; background: rgba(52, 152, 219, .18); }
.btn-delete { padding: 6px 10px; color: #ff8f8f; background: rgba(231, 76, 60, .12); }
@media (max-width: 1100px) {
.main-content { flex-direction: column; height: auto; }

View File

@@ -1390,22 +1390,39 @@ export function deleteQueryAsinHistory(resultId: number) {
export interface AppearancePatentParsedRow {
rowIndex: number;
sourceFileKey?: string;
sourceFilename?: string;
sourceId: string;
displayId: string;
rowToken?: string;
groupKey?: string;
asin: string;
country: string;
url?: string;
title?: string;
}
export interface AppearancePatentParsedGroup {
sourceFileKey?: string;
sourceFilename?: string;
groupKey?: string;
baseId?: string;
displayId?: string;
itemCount?: number;
items: AppearancePatentParsedRow[];
}
export interface AppearancePatentParseVo {
taskId: number;
sourceFilename?: string;
sourceFileCount?: number;
totalRows: number;
acceptedRows: number;
droppedRows: number;
groupCount?: number;
aiPrompt?: string;
items: AppearancePatentParsedRow[];
groups?: AppearancePatentParsedGroup[];
}
export interface AppearancePatentDashboardVo {