提交一些更改
This commit is contained in:
@@ -19,7 +19,7 @@
|
||||
|
||||
<div class="prompt-card">
|
||||
<div class="section-title">新增条件要求</div>
|
||||
<textarea v-model="aiPrompt" class="prompt-input" rows="8" placeholder="可选,填写后会追加到默认检测提示词中" />
|
||||
<textarea v-model="aiPrompt" class="prompt-input" rows="8" placeholder="可选,仅在激活任务时传递这里填写的 prompt" />
|
||||
</div>
|
||||
|
||||
<div class="run-row">
|
||||
@@ -72,6 +72,8 @@
|
||||
<div class="left">
|
||||
<span class="id">{{ item.sourceFilename || '外观专利检测' }}</span>
|
||||
<div class="files">任务 ID:{{ item.taskId }}</div>
|
||||
<div class="files">开始时间:{{ formatDateTime(item.startedAt || item.createdAt) }}</div>
|
||||
<div class="files">结束时间:{{ formatDateTime(item.finishedAt) }}</div>
|
||||
<div class="files">行数:{{ item.rowCount ?? '-' }}</div>
|
||||
<div v-if="pendingResultHint(item)" class="files result-hint">{{ pendingResultHint(item) }}</div>
|
||||
<div v-if="showFileProgress(item)" class="file-progress">
|
||||
@@ -102,6 +104,8 @@
|
||||
<div class="left">
|
||||
<span class="id">{{ item.sourceFilename || '外观专利检测' }}</span>
|
||||
<div class="files">任务 ID:{{ item.taskId ?? '-' }}</div>
|
||||
<div class="files">开始时间:{{ formatDateTime(item.startedAt || item.createdAt) }}</div>
|
||||
<div class="files">结束时间:{{ formatDateTime(item.finishedAt) }}</div>
|
||||
<div v-if="item.resultFilename" class="files">
|
||||
{{ item.resultFilename || '下载结果' }}
|
||||
</div>
|
||||
@@ -254,6 +258,10 @@ function effectiveAiPrompt() {
|
||||
return `${defaultAiPrompt}\n\n新增条件要求:\n${extraPrompt}`
|
||||
}
|
||||
|
||||
function queueAiPrompt() {
|
||||
return aiPrompt.value.trim()
|
||||
}
|
||||
|
||||
function effectiveCozeApiKey() {
|
||||
return getStoredApiSecret().trim()
|
||||
}
|
||||
@@ -314,6 +322,19 @@ 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) {
|
||||
@@ -427,7 +448,7 @@ async function pushToPythonQueue() {
|
||||
ts: Date.now(),
|
||||
data: {
|
||||
taskId,
|
||||
prompt: parsedPayload.aiPrompt || currentParseResult.aiPrompt || effectiveAiPrompt(),
|
||||
prompt: queueAiPrompt(),
|
||||
api_key: effectiveCozeApiKey(),
|
||||
sourceFileCount: currentParseResult.sourceFileCount || 0,
|
||||
totalRows: currentParseResult.totalRows || 0,
|
||||
|
||||
@@ -209,6 +209,9 @@
|
||||
<div v-if="item.platform" class="files">
|
||||
平台: {{ item.platform }}
|
||||
</div>
|
||||
<div class="files">
|
||||
开始时间: {{ formatDateTime(taskStartTime(item.taskId)) }}
|
||||
</div>
|
||||
<div class="files">
|
||||
创建时间: {{ formatDateTime(item.createdAt) }}
|
||||
</div>
|
||||
@@ -260,6 +263,9 @@
|
||||
<div v-if="item.platform" class="files">
|
||||
平台: {{ item.platform }}
|
||||
</div>
|
||||
<div class="files">
|
||||
开始时间: {{ formatDateTime(taskStartTime(item.taskId)) }}
|
||||
</div>
|
||||
<div class="files">
|
||||
创建时间: {{ formatDateTime(item.createdAt) }}
|
||||
</div>
|
||||
@@ -358,6 +364,7 @@ const activeTaskId = ref<number | null>(null);
|
||||
const activeQueueItem = ref<QueryAsinShopQueueItem | null>(null);
|
||||
const queueWorkerRunning = ref(false);
|
||||
const autoQueueEnabled = ref(false);
|
||||
const taskStartTimes = ref<Record<number, string>>({});
|
||||
let historyPollTimer: number | null = null;
|
||||
const timers = createCategorizedTimers("query-asin");
|
||||
|
||||
@@ -392,6 +399,10 @@ function queueStateStorageKey() {
|
||||
return `query-asin:queue-state:${uidForStorage()}`;
|
||||
}
|
||||
|
||||
function taskStartTimeStorageKey() {
|
||||
return `query-asin:start-times:${uidForStorage()}`;
|
||||
}
|
||||
|
||||
function rowKeyForMatch(row: QueryAsinShopQueueItem) {
|
||||
return `${(row.shopName || "").trim()}::${row.shopId || ""}`;
|
||||
}
|
||||
@@ -600,6 +611,15 @@ function saveQueueState() {
|
||||
window.localStorage.setItem(queueStateStorageKey(), JSON.stringify(payload));
|
||||
}
|
||||
|
||||
function saveTaskStartTimes() {
|
||||
if (typeof window === "undefined") return;
|
||||
if (!Object.keys(taskStartTimes.value).length) {
|
||||
window.localStorage.removeItem(taskStartTimeStorageKey());
|
||||
return;
|
||||
}
|
||||
window.localStorage.setItem(taskStartTimeStorageKey(), JSON.stringify(taskStartTimes.value));
|
||||
}
|
||||
|
||||
function loadQueueState() {
|
||||
try {
|
||||
const raw =
|
||||
@@ -622,6 +642,48 @@ function loadQueueState() {
|
||||
}
|
||||
}
|
||||
|
||||
function loadTaskStartTimes() {
|
||||
try {
|
||||
const raw =
|
||||
typeof window !== "undefined"
|
||||
? window.localStorage.getItem(taskStartTimeStorageKey())
|
||||
: null;
|
||||
const parsed = raw ? JSON.parse(raw) : {};
|
||||
const next: Record<number, string> = {};
|
||||
for (const [taskId, value] of Object.entries(parsed || {})) {
|
||||
const numericTaskId = Number(taskId);
|
||||
if (!Number.isFinite(numericTaskId) || numericTaskId <= 0) continue;
|
||||
if (typeof value !== "string" || !value.trim()) continue;
|
||||
next[numericTaskId] = value;
|
||||
}
|
||||
taskStartTimes.value = next;
|
||||
} catch {
|
||||
taskStartTimes.value = {};
|
||||
}
|
||||
}
|
||||
|
||||
function setTaskStartTime(taskId: number, startedAt = new Date().toISOString()) {
|
||||
if (!Number.isFinite(taskId) || taskId <= 0) return;
|
||||
taskStartTimes.value = {
|
||||
...taskStartTimes.value,
|
||||
[taskId]: startedAt,
|
||||
};
|
||||
saveTaskStartTimes();
|
||||
}
|
||||
|
||||
function clearTaskStartTime(taskId?: number | null) {
|
||||
if (!taskId || !(taskId in taskStartTimes.value)) return;
|
||||
const next = { ...taskStartTimes.value };
|
||||
delete next[taskId];
|
||||
taskStartTimes.value = next;
|
||||
saveTaskStartTimes();
|
||||
}
|
||||
|
||||
function taskStartTime(taskId?: number | null) {
|
||||
if (!taskId) return "";
|
||||
return taskStartTimes.value[taskId] || "";
|
||||
}
|
||||
|
||||
function clearActiveQueueTask() {
|
||||
activeTaskId.value = null;
|
||||
activeQueueItem.value = null;
|
||||
@@ -1003,6 +1065,7 @@ async function processQueue() {
|
||||
continue;
|
||||
}
|
||||
|
||||
setTaskStartTime(created.taskId);
|
||||
queuePushResult.value =
|
||||
pendingQueue.value.length > 0
|
||||
? `任务 ${created.taskId} 已入队,等待完成后自动继续下一个(剩余 ${pendingQueue.value.length} 条)`
|
||||
@@ -1064,8 +1127,10 @@ async function deleteTaskRecord(item: QueryAsinHistoryItem) {
|
||||
if (activeTaskId.value === item.taskId) {
|
||||
clearActiveQueueTask();
|
||||
}
|
||||
clearTaskStartTime(item.taskId);
|
||||
} else if (item.resultId) {
|
||||
await deleteQueryAsinHistory(item.resultId);
|
||||
clearTaskStartTime(item.taskId);
|
||||
} else {
|
||||
throw new Error("缺少可删除的任务标识");
|
||||
}
|
||||
@@ -1077,6 +1142,7 @@ async function deleteTaskRecord(item: QueryAsinHistoryItem) {
|
||||
if (item.taskId && activeTaskId.value === item.taskId) {
|
||||
clearActiveQueueTask();
|
||||
}
|
||||
clearTaskStartTime(item.taskId);
|
||||
removeHistoryItemLocally(item);
|
||||
await loadCandidates();
|
||||
resetQueueWorkerIfIdle();
|
||||
@@ -1090,6 +1156,7 @@ async function deleteTaskRecord(item: QueryAsinHistoryItem) {
|
||||
onMounted(async () => {
|
||||
loadMatchedItems();
|
||||
loadQueueState();
|
||||
loadTaskStartTimes();
|
||||
await Promise.all([loadCandidates(), loadDashboard(), loadHistory()]);
|
||||
reconcileActiveQueueTaskWithHistory();
|
||||
|
||||
|
||||
@@ -17,33 +17,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="prompt-card">
|
||||
<div class="section-title">筛选条件</div>
|
||||
<textarea v-model="filterConditionInput" class="prompt-input" rows="4" placeholder="请输入筛选条件" />
|
||||
<div class="condition-actions">
|
||||
<button type="button" class="opt-btn" :disabled="savingCondition" @click="saveFilterCondition">
|
||||
{{ savingCondition ? '保存中...' : '保存' }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="section-title condition-list-title">备选区</div>
|
||||
<div v-if="!filterConditions.length" class="empty-conditions">暂无筛选条件,输入后保存即可加入备选区</div>
|
||||
<ul v-else class="condition-list">
|
||||
<li v-for="condition in filterConditions" :key="condition.id" class="condition-item">
|
||||
<label class="condition-check">
|
||||
<input
|
||||
v-model="selectedFilterConditionId"
|
||||
type="radio"
|
||||
name="similar-asin-filter-condition"
|
||||
:value="condition.id"
|
||||
@change="applySelectedFilterCondition(condition)"
|
||||
/>
|
||||
<span :title="condition.conditionText">{{ condition.conditionText }}</span>
|
||||
</label>
|
||||
<button type="button" class="link-danger" @click="removeFilterCondition(condition.id)">删除</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="run-row">
|
||||
<button type="button" class="btn-run" :disabled="parsing || !uploadedFiles.length" @click="parseFiles">
|
||||
{{ parsing ? '解析中...' : '解析并创建任务' }}
|
||||
@@ -94,7 +67,22 @@
|
||||
<div class="left">
|
||||
<span class="id">{{ item.sourceFilename || '货源查询' }}</span>
|
||||
<div class="files">任务 ID:{{ item.taskId }}</div>
|
||||
<div class="files">开始时间:{{ formatDateTime(item.startedAt || item.createdAt) }}</div>
|
||||
<div class="files">结束时间:{{ formatDateTime(item.finishedAt) }}</div>
|
||||
<div class="files">行数:{{ item.rowCount ?? '-' }}</div>
|
||||
<div v-if="pendingResultHint(item)" class="files result-hint">{{ pendingResultHint(item) }}</div>
|
||||
<div v-if="showFileProgress(item)" class="file-progress">
|
||||
<div class="file-progress-meta">
|
||||
<span>{{ displayFileProgressStage(item, '处理中') }}</span>
|
||||
<span>总进度 {{ fileProgressPercent(item) }}%</span>
|
||||
</div>
|
||||
<div class="file-progress-track">
|
||||
<div class="file-progress-bar" :style="{ width: `${fileProgressPercent(item)}%` }"></div>
|
||||
</div>
|
||||
<div v-if="hasFileProgressCount(item)" class="file-progress-count">
|
||||
{{ fileProgressCountLabel(item) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="task-right">
|
||||
<span class="status running">{{ statusText(item) }}</span>
|
||||
@@ -114,20 +102,22 @@
|
||||
<div class="left">
|
||||
<span class="id">{{ item.sourceFilename || '货源查询' }}</span>
|
||||
<div class="files">任务 ID:{{ item.taskId ?? '-' }}</div>
|
||||
<div class="files">开始时间:{{ formatDateTime(item.startedAt || item.createdAt) }}</div>
|
||||
<div class="files">结束时间:{{ formatDateTime(item.finishedAt) }}</div>
|
||||
<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="showFileProgress(item)" class="file-progress">
|
||||
<div class="file-progress-meta">
|
||||
<span>{{ item.fileProgressMessage || '结果生成中' }}</span>
|
||||
<span>{{ fileProgressPercent(item) }}%</span>
|
||||
<span>{{ displayFileProgressStage(item, '结果生成中') }}</span>
|
||||
<span>总进度 {{ fileProgressPercent(item) }}%</span>
|
||||
</div>
|
||||
<div class="file-progress-track">
|
||||
<div class="file-progress-bar" :style="{ width: `${fileProgressPercent(item)}%` }"></div>
|
||||
</div>
|
||||
<div class="file-progress-count">
|
||||
{{ item.fileProgressCurrent || 0 }}/{{ item.fileProgressTotal || 0 }}
|
||||
<div v-if="hasFileProgressCount(item)" class="file-progress-count">
|
||||
{{ fileProgressCountLabel(item) }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="item.error" class="files">错误:{{ item.error }}</div>
|
||||
@@ -153,17 +143,13 @@ import { ElMessage } from 'element-plus'
|
||||
import BrandTopBar from '@/pages/brand/components/BrandTopBar.vue'
|
||||
import {
|
||||
activateSimilarAsinTask,
|
||||
addSimilarAsinFilterCondition,
|
||||
deleteSimilarAsinFilterCondition,
|
||||
deleteSimilarAsinHistory,
|
||||
deleteSimilarAsinTask,
|
||||
getSimilarAsinDashboard,
|
||||
getSimilarAsinHistory,
|
||||
getSimilarAsinResultDownloadUrl,
|
||||
getSimilarAsinTaskProgressBatch,
|
||||
listSimilarAsinFilterConditions,
|
||||
parseSimilarAsin,
|
||||
type SimilarAsinFilterConditionVo,
|
||||
type SimilarAsinDashboardVo,
|
||||
type SimilarAsinHistoryItem,
|
||||
type SimilarAsinParseVo,
|
||||
@@ -182,12 +168,8 @@ const uploadedFiles = ref<UploadFileVo[]>([])
|
||||
const parseResult = ref<SimilarAsinParseVo | null>(null)
|
||||
type TaskSummary = Pick<SimilarAsinParseVo, 'taskId' | 'totalRows' | 'acceptedRows' | 'groupCount' | 'droppedRows'>
|
||||
const queuedTaskSummary = ref<TaskSummary | null>(null)
|
||||
const filterConditionInput = ref('')
|
||||
const filterConditions = ref<SimilarAsinFilterConditionVo[]>([])
|
||||
const selectedFilterConditionId = ref<number | null>(null)
|
||||
const parsing = ref(false)
|
||||
const pushing = ref(false)
|
||||
const savingCondition = ref(false)
|
||||
const queuePayloadText = ref('')
|
||||
const pollingTaskIds = ref<number[]>([])
|
||||
const pendingFileTaskIds = ref<number[]>([])
|
||||
@@ -238,20 +220,23 @@ const historyOnlyItems = computed(() =>
|
||||
}),
|
||||
)
|
||||
|
||||
function selectedFilterConditionText() {
|
||||
const selectedId = selectedFilterConditionId.value
|
||||
if (selectedId == null) return ''
|
||||
return filterConditions.value.find((item) => item.id === selectedId)?.conditionText?.trim() || ''
|
||||
}
|
||||
|
||||
function effectiveFilterCondition() {
|
||||
return filterConditionInput.value.trim() || selectedFilterConditionText()
|
||||
}
|
||||
|
||||
function effectiveCozeApiKey() {
|
||||
return getStoredApiSecret().trim()
|
||||
}
|
||||
|
||||
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 '***'
|
||||
@@ -274,6 +259,10 @@ function uidForStorage() {
|
||||
return typeof window !== 'undefined' ? window.localStorage.getItem('uid') || '0' : '0'
|
||||
}
|
||||
|
||||
function currentUserId() {
|
||||
return Number(uidForStorage()) || 0
|
||||
}
|
||||
|
||||
function pollingKey() {
|
||||
return `similar-asin:tasks:${uidForStorage()}`
|
||||
}
|
||||
@@ -368,62 +357,11 @@ async function selectFolder() {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadFilterConditions() {
|
||||
try {
|
||||
filterConditions.value = await listSimilarAsinFilterConditions()
|
||||
const ids = new Set(filterConditions.value.map((item) => item.id))
|
||||
if (selectedFilterConditionId.value != null && !ids.has(selectedFilterConditionId.value)) {
|
||||
selectedFilterConditionId.value = null
|
||||
}
|
||||
} catch (e) {
|
||||
ElMessage.error(e instanceof Error ? e.message : '加载筛选条件失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function saveFilterCondition() {
|
||||
const text = filterConditionInput.value.trim()
|
||||
if (!text) {
|
||||
ElMessage.warning('请输入筛选条件')
|
||||
return
|
||||
}
|
||||
savingCondition.value = true
|
||||
try {
|
||||
const saved = await addSimilarAsinFilterCondition(text)
|
||||
await loadFilterConditions()
|
||||
if (saved?.id) selectedFilterConditionId.value = saved.id
|
||||
ElMessage.success('筛选条件已保存')
|
||||
} catch (e) {
|
||||
ElMessage.error(e instanceof Error ? e.message : '保存筛选条件失败')
|
||||
} finally {
|
||||
savingCondition.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function applySelectedFilterCondition(condition: SimilarAsinFilterConditionVo) {
|
||||
filterConditionInput.value = condition.conditionText || ''
|
||||
}
|
||||
|
||||
async function removeFilterCondition(id: number) {
|
||||
try {
|
||||
await deleteSimilarAsinFilterCondition(id)
|
||||
if (selectedFilterConditionId.value === id) selectedFilterConditionId.value = null
|
||||
await loadFilterConditions()
|
||||
ElMessage.success('筛选条件已删除')
|
||||
} catch (e) {
|
||||
ElMessage.error(e instanceof Error ? e.message : '删除筛选条件失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function parseFiles() {
|
||||
if (!uploadedFiles.value.length) {
|
||||
ElMessage.warning('请先选择 Excel')
|
||||
return
|
||||
}
|
||||
const filterCondition = effectiveFilterCondition()
|
||||
if (!filterCondition) {
|
||||
ElMessage.warning('请输入筛选条件,或在备选区选择一个筛选条件')
|
||||
return
|
||||
}
|
||||
if (!effectiveCozeApiKey()) {
|
||||
ElMessage.warning('请先在左上角设置中填写货源查询密钥')
|
||||
return
|
||||
@@ -435,7 +373,7 @@ async function parseFiles() {
|
||||
originalFilename: f.originalFilename,
|
||||
relativePath: f.relativePath,
|
||||
}))
|
||||
const res = await parseSimilarAsin(files, filterCondition, effectiveCozeApiKey())
|
||||
const res = await parseSimilarAsin(files, effectiveCozeApiKey())
|
||||
parseResult.value = res
|
||||
queuedTaskSummary.value = null
|
||||
queuePayloadText.value = ''
|
||||
@@ -470,7 +408,7 @@ async function pushToPythonQueue() {
|
||||
ts: Date.now(),
|
||||
data: {
|
||||
taskId,
|
||||
prompt: currentParseResult.aiPrompt || effectiveFilterCondition(),
|
||||
user_id: currentUserId(),
|
||||
api_key: effectiveCozeApiKey(),
|
||||
sourceFileCount: currentParseResult.sourceFileCount || 0,
|
||||
totalRows: currentParseResult.totalRows || 0,
|
||||
@@ -658,6 +596,16 @@ function seedPendingFileTasksFromHistory() {
|
||||
if (pendingFileTaskIds.value.length) ensurePolling()
|
||||
}
|
||||
|
||||
function seedRunningTasksFromHistory() {
|
||||
const ids = historyItems.value
|
||||
.filter((item) => item.taskId != null && normalizeTaskStatus(item) === 'RUNNING')
|
||||
.map((item) => item.taskId as number)
|
||||
if (!ids.length) return
|
||||
pollingTaskIds.value = Array.from(new Set([...pollingTaskIds.value, ...ids]))
|
||||
savePollingIds()
|
||||
ensurePolling()
|
||||
}
|
||||
|
||||
async function loadDashboard() {
|
||||
dashboard.value = await getSimilarAsinDashboard()
|
||||
}
|
||||
@@ -668,7 +616,7 @@ async function loadHistory(options: { force?: boolean } = {}) {
|
||||
if (historyInFlight) return historyInFlight
|
||||
historyInFlight = getSimilarAsinHistory()
|
||||
.then((res) => {
|
||||
historyItems.value = res.items || []
|
||||
historyItems.value = mergeHistoryItemsPreservingLiveProgress(historyItems.value, res.items || [])
|
||||
lastHistoryLoadedAt = Date.now()
|
||||
})
|
||||
.finally(() => {
|
||||
@@ -677,6 +625,66 @@ async function loadHistory(options: { force?: boolean } = {}) {
|
||||
return historyInFlight
|
||||
}
|
||||
|
||||
function mergeHistoryItemsPreservingLiveProgress(
|
||||
previousItems: SimilarAsinHistoryItem[],
|
||||
incomingItems: SimilarAsinHistoryItem[],
|
||||
) {
|
||||
const previousByTaskId = new Map<number, SimilarAsinHistoryItem>()
|
||||
const previousByResultId = new Map<number, SimilarAsinHistoryItem>()
|
||||
for (const item of previousItems || []) {
|
||||
if (item.taskId != null) previousByTaskId.set(item.taskId, item)
|
||||
if (item.resultId != null) previousByResultId.set(item.resultId, item)
|
||||
}
|
||||
return (incomingItems || []).map((incoming) => {
|
||||
const previous = incoming.resultId != null
|
||||
? previousByResultId.get(incoming.resultId)
|
||||
: incoming.taskId != null
|
||||
? previousByTaskId.get(incoming.taskId)
|
||||
: undefined
|
||||
return mergeHistoryItemPreservingLiveProgress(previous, incoming)
|
||||
})
|
||||
}
|
||||
|
||||
function mergeHistoryItemPreservingLiveProgress(
|
||||
previous: SimilarAsinHistoryItem | undefined,
|
||||
incoming: SimilarAsinHistoryItem,
|
||||
) {
|
||||
if (!previous) return incoming
|
||||
const shouldPreserveLiveProgress =
|
||||
(normalizeTaskStatus(previous) === 'RUNNING' || isResultPreparing(previous) || showFileProgress(previous))
|
||||
&& !showFileProgress(incoming)
|
||||
&& !canDownload(incoming)
|
||||
|
||||
if (!shouldPreserveLiveProgress) {
|
||||
return incoming
|
||||
}
|
||||
|
||||
return {
|
||||
...incoming,
|
||||
fileJobId: incoming.fileJobId ?? previous.fileJobId,
|
||||
fileStatus: incoming.fileStatus || previous.fileStatus,
|
||||
fileError: incoming.fileError || previous.fileError,
|
||||
fileReady: incoming.fileReady || previous.fileReady,
|
||||
fileProgressPercent: hasMeaningfulProgressValue(incoming.fileProgressPercent)
|
||||
? incoming.fileProgressPercent
|
||||
: previous.fileProgressPercent,
|
||||
fileProgressCurrent: hasMeaningfulProgressValue(incoming.fileProgressCurrent)
|
||||
? incoming.fileProgressCurrent
|
||||
: previous.fileProgressCurrent,
|
||||
fileProgressTotal: hasMeaningfulProgressValue(incoming.fileProgressTotal)
|
||||
? incoming.fileProgressTotal
|
||||
: previous.fileProgressTotal,
|
||||
fileProgressMessage: (incoming.fileProgressMessage || '').trim()
|
||||
? incoming.fileProgressMessage
|
||||
: previous.fileProgressMessage,
|
||||
}
|
||||
}
|
||||
|
||||
function hasMeaningfulProgressValue(value: unknown) {
|
||||
const num = Number(value)
|
||||
return Number.isFinite(num) && num > 0
|
||||
}
|
||||
|
||||
function statusText(item: SimilarAsinHistoryItem) {
|
||||
const status = normalizeTaskStatus(item)
|
||||
if (status === 'RUNNING') return '执行中'
|
||||
@@ -717,6 +725,9 @@ function pendingResultHint(item: SimilarAsinHistoryItem) {
|
||||
if (isResultBuildFailed(item)) {
|
||||
return item.fileError || '结果文件生成失败,请稍后重试或检查后端日志'
|
||||
}
|
||||
if (showFileProgress(item)) {
|
||||
return ''
|
||||
}
|
||||
if (!isResultPreparing(item)) return ''
|
||||
const fileStatus = (item.fileStatus || '').toUpperCase()
|
||||
if (fileStatus === 'RUNNING') {
|
||||
@@ -734,12 +745,79 @@ function canDownload(item: SimilarAsinHistoryItem) {
|
||||
|
||||
function fileProgressPercent(item: SimilarAsinHistoryItem) {
|
||||
const percent = Number(item.fileProgressPercent || 0)
|
||||
if (!Number.isFinite(percent)) return 0
|
||||
return Math.max(0, Math.min(100, Math.round(percent)))
|
||||
if (Number.isFinite(percent) && percent > 0) {
|
||||
return Math.max(0, Math.min(100, Math.round(percent)))
|
||||
}
|
||||
if (shouldShowFallbackJobProgress(item)) {
|
||||
return activeFileJobStatus(item) === 'PENDING' ? 6 : 10
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
function displayFileProgressStage(item: SimilarAsinHistoryItem, fallback: string) {
|
||||
const message = (item.fileProgressMessage || '').trim()
|
||||
if (!message) return fallback
|
||||
if (/coze/i.test(message) || /Coze|回流/.test(message)) {
|
||||
if (/submitting/i.test(message) || /提交/.test(message)) return '正在提交 Coze 批次'
|
||||
if (/生成结果|结果文件|组装/i.test(message)) return 'Coze 已回流,正在生成结果文件'
|
||||
if (/waiting/i.test(message) || /等待/.test(message) || /\d+\s*\/\s*\d+/.test(message)) {
|
||||
return 'Coze 回流中,等待结果回传'
|
||||
}
|
||||
return 'Coze 处理中'
|
||||
}
|
||||
if (/assembling|xlsx|组装|生成结果/i.test(message)) return '正在组装结果文件'
|
||||
return message
|
||||
}
|
||||
|
||||
function fileProgressCountLabel(item: SimilarAsinHistoryItem) {
|
||||
const message = (item.fileProgressMessage || '').trim()
|
||||
const parsed = parseProgressCountFromMessage(message)
|
||||
const current = parsed?.current ?? item.fileProgressCurrent ?? 0
|
||||
const total = parsed?.total ?? item.fileProgressTotal ?? 0
|
||||
if (/coze/i.test(message) || /Coze|回流/.test(message)) {
|
||||
if (/submitting/i.test(message) || /提交/.test(message)) {
|
||||
return `Coze 提交批次 ${current}/${total}`
|
||||
}
|
||||
return `Coze 回流批次 ${current}/${total}`
|
||||
}
|
||||
return `当前阶段 ${current}/${total}`
|
||||
}
|
||||
|
||||
function parseProgressCountFromMessage(message: string) {
|
||||
const match = message.match(/(\d+)\s*\/\s*(\d+)/)
|
||||
if (!match) return null
|
||||
const current = Number(match[1])
|
||||
const total = Number(match[2])
|
||||
if (!Number.isFinite(current) || !Number.isFinite(total)) return null
|
||||
return { current, total }
|
||||
}
|
||||
|
||||
function showFileProgress(item: SimilarAsinHistoryItem) {
|
||||
return isResultPreparing(item) && (item.fileProgressTotal || 0) > 0
|
||||
const status = normalizeTaskStatus(item)
|
||||
return (status === 'RUNNING' || isResultPreparing(item)) && (hasExplicitFileProgress(item) || shouldShowFallbackJobProgress(item))
|
||||
}
|
||||
|
||||
function hasExplicitFileProgress(item: SimilarAsinHistoryItem) {
|
||||
return hasFileProgressCount(item)
|
||||
|| Number(item.fileProgressPercent || 0) > 0
|
||||
|| Boolean((item.fileProgressMessage || '').trim())
|
||||
}
|
||||
|
||||
function hasFileProgressCount(item: SimilarAsinHistoryItem) {
|
||||
return (item.fileProgressTotal || 0) > 0
|
||||
}
|
||||
|
||||
function activeFileJobStatus(item: SimilarAsinHistoryItem) {
|
||||
return (item.fileStatus || '').toUpperCase()
|
||||
}
|
||||
|
||||
function hasActiveFileJob(item: SimilarAsinHistoryItem) {
|
||||
const fileStatus = activeFileJobStatus(item)
|
||||
return item.fileJobId != null || fileStatus === 'PENDING' || fileStatus === 'RUNNING'
|
||||
}
|
||||
|
||||
function shouldShowFallbackJobProgress(item: SimilarAsinHistoryItem) {
|
||||
return hasActiveFileJob(item) && !hasExplicitFileProgress(item)
|
||||
}
|
||||
|
||||
async function downloadResult(item: SimilarAsinHistoryItem) {
|
||||
@@ -778,11 +856,11 @@ async function deleteTaskRecord(item: SimilarAsinHistoryItem) {
|
||||
onMounted(async () => {
|
||||
loadPollingIds()
|
||||
await Promise.all([
|
||||
loadFilterConditions().catch(() => undefined),
|
||||
loadDashboard().catch(() => undefined),
|
||||
loadHistory().catch(() => undefined),
|
||||
])
|
||||
seedPendingFileTasksFromHistory()
|
||||
seedRunningTasksFromHistory()
|
||||
if (pollingTaskIds.value.length || pendingFileTaskIds.value.length) ensurePolling()
|
||||
})
|
||||
|
||||
@@ -811,20 +889,6 @@ onUnmounted(() => {
|
||||
.btn-run:disabled { opacity: .55; cursor: not-allowed; }
|
||||
.selected-files { margin-top: 14px; color: #999; font-size: 12px; word-break: break-all; }
|
||||
.selected-files span { display: block; margin: 4px 0; }
|
||||
.prompt-card { margin-bottom: 14px; }
|
||||
.prompt-input { width: 100%; box-sizing: border-box; resize: vertical; min-height: 96px; max-height: 140px; padding: 8px 12px; border: 1px solid #333; border-radius: 8px; background: #202020; color: #d8d8d8; font-size: 12px; line-height: 1.5; outline: none; }
|
||||
.prompt-input:focus { border-color: #3498db; }
|
||||
.condition-actions { display: flex; justify-content: flex-end; margin-top: 8px; }
|
||||
.condition-actions .opt-btn { padding: 6px 14px; }
|
||||
.condition-list-title { margin-top: 10px; }
|
||||
.empty-conditions { color: #666; font-size: 12px; padding: 10px 4px; }
|
||||
.condition-list { list-style: none; margin: 0; padding: 0; max-height: 96px; overflow: auto; display: flex; flex-direction: column; gap: 6px; }
|
||||
.condition-item { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 6px; border-bottom: 1px solid #303030; }
|
||||
.condition-item:last-child { border-bottom: none; }
|
||||
.condition-check { display: flex; align-items: center; gap: 8px; min-width: 0; color: #cfd6df; font-size: 12px; cursor: pointer; }
|
||||
.condition-check input { width: 16px; height: 16px; margin: 0; flex: 0 0 auto; }
|
||||
.condition-check span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.link-danger { border: none; background: transparent; color: #ff8f8f; cursor: pointer; font-size: 12px; padding: 2px 0; white-space: nowrap; }
|
||||
.parse-card, .queue-payload { margin-top: 14px; padding: 12px; border: 1px solid #2a2a2a; border-radius: 8px; background: #202020; color: #b8c1cc; font-size: 12px; }
|
||||
.queue-payload { max-height: 220px; overflow: auto; color: #8fd3ff; white-space: pre-wrap; }
|
||||
.panel-header { padding: 16px 20px; border-bottom: 1px solid #2a2a2a; font-size: 15px; font-weight: 600; color: #ddd; }
|
||||
|
||||
@@ -1561,6 +1561,8 @@ export interface AppearancePatentHistoryItem {
|
||||
error?: string;
|
||||
rowCount?: number;
|
||||
createdAt?: string;
|
||||
startedAt?: string;
|
||||
finishedAt?: string;
|
||||
}
|
||||
|
||||
export interface AppearancePatentHistoryVo {
|
||||
@@ -1680,12 +1682,6 @@ export function getAppearancePatentResultDownloadUrl(resultId: number) {
|
||||
|
||||
// ========== 货源查询 ==========
|
||||
|
||||
export interface SimilarAsinFilterConditionVo {
|
||||
id: number;
|
||||
conditionText: string;
|
||||
createdAt?: string;
|
||||
}
|
||||
|
||||
export interface SimilarAsinParsedRow {
|
||||
rowIndex: number;
|
||||
sourceFileKey?: string;
|
||||
@@ -1751,6 +1747,8 @@ export interface SimilarAsinHistoryItem {
|
||||
error?: string;
|
||||
rowCount?: number;
|
||||
createdAt?: string;
|
||||
startedAt?: string;
|
||||
finishedAt?: string;
|
||||
}
|
||||
|
||||
export interface SimilarAsinHistoryVo {
|
||||
@@ -1777,49 +1775,19 @@ export interface SimilarAsinTaskBatchVo {
|
||||
missingTaskIds?: number[];
|
||||
}
|
||||
|
||||
export function parseSimilarAsin(files: UploadedFileRef[], filterCondition: string, apiKey?: string) {
|
||||
export function parseSimilarAsin(files: UploadedFileRef[], apiKey?: string) {
|
||||
return unwrapJavaResponse(
|
||||
post<
|
||||
JavaApiResponse<SimilarAsinParseVo>,
|
||||
{ user_id: number; files: UploadedFileRef[]; ai_prompt: string; api_key?: string }
|
||||
{ user_id: number; files: UploadedFileRef[]; api_key?: string }
|
||||
>(`${JAVA_API_PREFIX}/similar-asin/parse`, {
|
||||
user_id: getCurrentUserId(),
|
||||
files,
|
||||
ai_prompt: filterCondition,
|
||||
api_key: apiKey,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export function listSimilarAsinFilterConditions() {
|
||||
return unwrapJavaResponse(
|
||||
get<JavaApiResponse<SimilarAsinFilterConditionVo[]>>(
|
||||
`${JAVA_API_PREFIX}/similar-asin/filter-conditions`,
|
||||
{ params: { user_id: getCurrentUserId() } },
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
export function addSimilarAsinFilterCondition(conditionText: string) {
|
||||
return unwrapJavaResponse(
|
||||
post<
|
||||
JavaApiResponse<SimilarAsinFilterConditionVo>,
|
||||
{ user_id: number; condition_text: string }
|
||||
>(`${JAVA_API_PREFIX}/similar-asin/filter-conditions`, {
|
||||
user_id: getCurrentUserId(),
|
||||
condition_text: conditionText,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export function deleteSimilarAsinFilterCondition(id: number) {
|
||||
return unwrapJavaResponse(
|
||||
del<JavaApiResponse<null>>(`${JAVA_API_PREFIX}/similar-asin/filter-conditions/${id}`, {
|
||||
params: { user_id: getCurrentUserId() },
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export function getSimilarAsinDashboard() {
|
||||
return unwrapJavaResponse(
|
||||
get<JavaApiResponse<SimilarAsinDashboardVo>>(
|
||||
|
||||
Reference in New Issue
Block a user