style(frontend): 全站背景图 webp 化(bg.jpg 4MB→251KB 压缩 + bg.webp)+品牌工具页默认策略调优(品牌检测默认改精确匹配 Simple、Terms 后置)+任务面板/登录页/亚马逊控制台观感微调

This commit is contained in:
2026-09-09 18:32:48 +08:00
parent 9a487ae9a7
commit 83cb3ff84e
29 changed files with 565 additions and 570 deletions
@@ -7,7 +7,7 @@
<div class="section-title">店铺输入</div>
<div class="input-zone">
<div class="hint">
左侧负责录入店铺并加入备选区确认命中后启动任务每次推送会把本次选中的所有店铺合并为一条取款任务任务之间串行执行
左侧负责录入店铺并加入备选区确认匹配结果后启动任务每次启动会把本次选中的所有店铺合并为一条取款任务任务按顺序逐个执行
</div>
<div class="input-row">
<el-input
@@ -88,16 +88,16 @@
:disabled="isQueueBusy || !matchedRunnableItems.length"
@click="pushToPythonQueue"
>
{{ isQueueBusy ? "串行执行中..." : "启动任务" }}
{{ isQueueBusy ? "按顺序执行中..." : "启动任务" }}
</button>
</div>
<p class="loading-msg">
当前页面会按任务串行推送一个任务可包含多个店铺并在任务成功或失败后自动接续下一个任务服务端临时重启时会自动重试查询状态恢复继续后续任务
任务会按顺序逐个执行一个任务完成无论成功或失败都会自动开始下一个如果中途网络异常系统会自动恢复继续处理
</p>
<div v-if="queuePushResult" class="queue-debug-card">
<div class="section-title queue-debug-title">队列状态</div>
<div class="section-title queue-debug-title">任务状态</div>
<div class="queue-debug-line">{{ queuePushResult }}</div>
<pre v-if="queuePayloadText" class="queue-debug-payload">{{
queuePayloadText
@@ -525,7 +525,7 @@ function formatMatchStatus(status?: string) {
MATCHED: "已匹配",
PENDING: "待匹配",
CONFLICT: "需人工确认",
INDEX_STALE: "索引过期",
INDEX_STALE: "匹配已过期",
};
return map[value] || value || "-";
}
@@ -534,12 +534,12 @@ function formatMatchRemark(row: WithdrawShopQueueItem) {
const message = (row.matchMessage || "").trim();
if (message) return message;
if (row.matched && row.matchStatus === "MATCHED") {
return "索引已命中,可启动任务";
return "已匹配成功,可启动任务";
}
if (row.matched) {
return "已命中索引,请结合状态确认";
return "已匹配成功,请查看状态确认";
}
return "未命中或未就绪,请检查店铺名";
return "未匹配成功,请检查店铺名";
}
function withWithdrawFallback<T extends WithdrawHistoryItem | WithdrawShopQueueItem>(
@@ -612,7 +612,7 @@ function formatTemplateSummary(record: Pick<WithdrawTaskGroupItem, "shops" | "ro
: record.rows?.length || 0;
const amount = record.reservedAmount ?? reservedAmount.value;
const shopCount = shops.length || 1;
return `保留金额 ${amount || 0},共 ${shopCount} 个店铺,已回 ${rowCount}`;
return `保留金额 ${amount || 0},共 ${shopCount} 个店铺,已${rowCount}`;
}
function isTaskTerminal(status?: string) {
@@ -799,8 +799,8 @@ function reconcileActiveQueueTaskWithHistory() {
const missingTaskId = activeTaskId.value;
clearActiveQueueTask();
queuePushResult.value = pendingQueue.value.length
? `任务 ${missingTaskId} 已不存在,已跳过并继续剩余队列`
: "当前任务已不存在,已释放本地队列状态";
? `任务 ${missingTaskId} 已不存在,已跳过并继续剩余任务`
: "当前任务已不存在,已重置本地状态";
resetQueueWorkerIfIdle();
}
@@ -940,7 +940,7 @@ async function runMatch() {
function removeMatchedRow(row: WithdrawShopQueueItem) {
if (hasQueueWork.value) {
ElMessage.warning("队列执行中,请等待当前串行任务结束后再调整");
ElMessage.warning("任务执行中,请等待当前任务结束后再调整");
return;
}
matchedItems.value = matchedItems.value.filter(
@@ -1017,7 +1017,7 @@ async function waitForTaskTerminal(taskId: number) {
return "FAILED";
}
if (transientErrorCount > 0) {
queuePushResult.value = `任务 ${taskId} 后端已恢复,继续等待执行结果...`;
queuePushResult.value = `任务 ${taskId} 服务已恢复,继续等待执行结果...`;
}
transientErrorCount = 0;
const item = findHistoryItemByTaskId(taskId);
@@ -1032,10 +1032,10 @@ async function waitForTaskTerminal(taskId: number) {
transientErrorCount += 1;
if (transientErrorCount >= MAX_TRANSIENT_ERRORS) {
throw new Error(
`任务 ${taskId} 等待后端恢复超时,请稍后刷新查看状态`,
`任务 ${taskId} 等待服务恢复超时,请稍后刷新查看状态`,
);
}
queuePushResult.value = `任务 ${taskId} 执行中,正在等待后端服务恢复(${transientErrorCount}/${MAX_TRANSIENT_ERRORS}...`;
queuePushResult.value = `任务 ${taskId} 执行中,正在等待服务恢复(${transientErrorCount}/${MAX_TRANSIENT_ERRORS}...`;
}
await sleep(getTaskPollIntervalMs());
}
@@ -1071,7 +1071,7 @@ async function processQueue() {
const created = await withTransientRetry(
() => createWithdrawTask(nextBatch.items.map(buildTaskItem), nextBatch.reservedAmount),
(attempt, maxAttempts) => {
queuePushResult.value = `后端服务暂时不可用,正在重试创建任务(${attempt}/${maxAttempts}...`;
queuePushResult.value = `服务暂时不可用,正在重试创建任务(${attempt}/${maxAttempts}...`;
},
);
const createdItem = created.items?.[0];
@@ -1096,14 +1096,14 @@ async function processQueue() {
await submitWithdrawTaskResult(created.taskId, {
shops: nextBatch.items.map((item) => ({
shopName: item.shopName || "",
error: `任务 ${created.taskId} 数据校验未通过,已阻止推送`,
error: `任务 ${created.taskId} 数据校验未通过,已阻止启动`,
shopDone: true,
submissionId: createSubmissionId(created.taskId, item.shopName),
rows: [],
})),
});
await refreshTaskViews();
queuePushResult.value = `任务 ${created.taskId} 数据校验未通过,已阻止推送并继续下一个任务`;
queuePushResult.value = `任务 ${created.taskId} 数据校验未通过,已阻止启动并继续下一个任务`;
clearActiveQueueTask();
continue;
}
@@ -1113,14 +1113,14 @@ async function processQueue() {
await submitWithdrawTaskResult(created.taskId, {
shops: nextBatch.items.map((item) => ({
shopName: item.shopName || "",
error: pushResult?.error || `任务 ${created.taskId} 推送失败`,
error: pushResult?.error || `任务 ${created.taskId} 启动失败`,
shopDone: true,
submissionId: createSubmissionId(created.taskId, item.shopName),
rows: [],
})),
});
await refreshTaskViews();
queuePushResult.value = `任务 ${created.taskId} 推送失败,已自动继续下一个任务`;
queuePushResult.value = `任务 ${created.taskId} 启动失败,已自动继续下一个任务`;
clearActiveQueueTask();
continue;
}
@@ -1132,11 +1132,11 @@ async function processQueue() {
await Promise.all([loadCandidates(), loadDashboard()]);
queuePushResult.value =
pendingQueue.value.length > 0
? `任务 ${created.taskId}入队,包含 ${nextBatch.items.length} 个店铺,等待完成后自动继续下一个任务(剩余 ${pendingQueue.value.length} 个任务)`
: `任务 ${created.taskId}入队,包含 ${nextBatch.items.length} 个店铺,等待执行完成`;
? `任务 ${created.taskId}提交,包含 ${nextBatch.items.length} 个店铺,等待完成后自动继续下一个任务(剩余 ${pendingQueue.value.length} 个任务)`
: `任务 ${created.taskId}提交,包含 ${nextBatch.items.length} 个店铺,等待执行完成`;
}
queuePushResult.value = "串行队列已执行完成";
queuePushResult.value = "任务已按顺序执行完成";
await refreshTaskViews();
ElMessage.success("取款队列已按顺序执行完成");
} catch (error) {
@@ -1164,7 +1164,7 @@ async function pushToPythonQueue() {
const batch = createQueueBatch(runnable);
pendingQueue.value = mergeQueueBatches(pendingQueue.value, [batch]);
saveQueueState();
queuePushResult.value = `已加入 1 个取款任务,包含 ${batch.items.length} 个店铺,开始串行执行`;
queuePushResult.value = `已加入 1 个取款任务,包含 ${batch.items.length} 个店铺,开始按顺序执行`;
await processQueue();
}
@@ -1206,7 +1206,7 @@ async function deleteTaskRecord(item: WithdrawTaskGroupItem) {
removeHistoryItemLocally(item);
await loadCandidates();
resetQueueWorkerIfIdle();
ElMessage.success("后端记录已不存在,已同步移除本地记录");
ElMessage.success("记录已不存在,已同步移除本地记录");
return;
}
ElMessage.error(error instanceof Error ? error.message : "删除失败");
@@ -1259,24 +1259,24 @@ async function batchDeleteHistory(views: TaskItemView[]) {
</script>
<style scoped>
.module-page { min-height: 100vh; background: #151a25; }
.module-page { min-height: 100vh; background: #242424; }
.main-content { display: flex; min-height: calc(100vh - 56px); height: calc(100vh - 56px); }
.left-panel { width: 400px; background: #1c2333; padding: 20px; overflow-y: auto; border-right: 1px solid #2e3a52; }
.right-panel { flex: 1; display: flex; flex-direction: column; min-width: 0; background: #151a25; }
.left-panel { width: 400px; background: #242424; padding: 20px; overflow-y: auto; border-right: 1px solid #2e3a52; }
.right-panel { flex: 1; display: flex; flex-direction: column; min-width: 0; background: #242424; }
.section-title { font-size: 13px; color: #a0acbe; margin-bottom: 10px; }
.input-zone { border: 1px dashed #3e4a62; border-radius: 10px; padding: 16px; background: #2e3a52; margin-bottom: 20px; }
.input-zone { border: 1px dashed #3e4a62; border-radius: 10px; padding: 16px; background: #242424; margin-bottom: 20px; }
.hint { color: #5e6878; font-size: 12px; margin-bottom: 12px; line-height: 1.5; text-align: left; }
.input-row { display: flex; gap: 10px; align-items: center; }
.input-row :deep(.el-input) { flex: 1; }
.filter-row { display: flex; align-items: center; gap: 10px; margin-top: 12px; }
.filter-label { color: #a0acbe; font-size: 12px; white-space: nowrap; }
.reserved-input { width: 180px; }
.opt-btn { padding: 8px 12px; font-size: 12px; color: #c8d2e2; background: #2e3a52; border: 1px solid #3e4a62; border-radius: 6px; cursor: pointer; white-space: nowrap; }
.opt-btn { padding: 8px 12px; font-size: 12px; color: #c8d2e2; background: #242424; border: 1px solid #3e4a62; border-radius: 6px; cursor: pointer; white-space: nowrap; }
.opt-btn:hover:not(:disabled) { color: #3498db; border-color: #3498db; }
.opt-btn:disabled { opacity: 0.6; cursor: not-allowed; }
.empty-candidates { color: #5e6878; font-size: 13px; padding: 16px; border: 1px dashed #333; border-radius: 8px; margin-bottom: 16px; }
.candidate-table-scroll { max-height: 320px; overflow: auto; margin-bottom: 18px; border-radius: 8px; border: 1px solid #2e3a52; }
.candidate-table { --el-table-bg-color: #2e3a52; --el-table-tr-bg-color: #2e3a52; --el-table-header-bg-color: #2e3a52; --el-table-text-color: #c8d2e2; --el-table-border-color: #333; }
.candidate-table { --el-table-bg-color: #242424; --el-table-tr-bg-color: #242424; --el-table-header-bg-color: #2e3a52; --el-table-text-color: #c8d2e2; --el-table-border-color: #333; }
.link-danger { background: none; border: none; color: #e74c3c; cursor: pointer; font-size: 12px; padding: 0; }
.link-danger:hover { text-decoration: underline; }
.run-row { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 8px; }
@@ -1295,7 +1295,7 @@ async function batchDeleteHistory(views: TaskItemView[]) {
margin: 16px 20px 0;
border: 1px solid #2e3a52;
border-radius: 10px;
background: #1c2333;
background: #242424;
overflow: hidden;
}
@@ -1318,8 +1318,8 @@ async function batchDeleteHistory(views: TaskItemView[]) {
overflow: auto;
}
.match-table :deep(.el-table__body tr:hover > td.el-table__cell), .match-table :deep(.el-table__body tr.hover-row > td.el-table__cell), .match-table :deep(.el-table__body tr.current-row > td.el-table__cell) { background-color: var(--el-table-tr-bg-color, #222) !important; }
.result-table { --el-table-bg-color: #222; --el-table-tr-bg-color: #222; --el-table-header-bg-color: #2e3a52; --el-table-text-color: #c8d2e2; --el-table-border-color: #333; }
.match-table :deep(.el-table__body tr:hover > td.el-table__cell), .match-table :deep(.el-table__body tr.hover-row > td.el-table__cell), .match-table :deep(.el-table__body tr.current-row > td.el-table__cell) { background-color: var(--el-table-tr-bg-color, #242424) !important; }
.result-table { --el-table-bg-color: #242424; --el-table-tr-bg-color: #242424; --el-table-header-bg-color: #2e3a52; --el-table-text-color: #c8d2e2; --el-table-border-color: #333; }
.ok { color: #27ae60; }
.fail { color: #e67e22; }