后台店铺数据记录页新增 Tab 切换与重复 ASIN 分析;task-169/170 HTTP 客户端配置接入;dedupe/brand 相关改造
Build Backend JAR / build (push) Has been cancelled
Build Backend JAR / build (push) Has been cancelled
- 管理后台:店铺数据记录改为列表分页展示(去任务号/文件列/累计标题,'最新'改'更新时间'),新增'重复 ASIN' Tab 按跨店聚合展示 ASIN/店铺/分组/国家/日期/价格;修复失败/进行中任务被过滤不显示的问题 - task-169: BrandCheckHttpConfigResolver 从 aiimage.http-client.* 读取品牌检查超时/重试(默认同现状、钳制),附 8 条测试 - task-170: surefire 内存调整为 1536m - dedupe: DedupeRunService 重构(事务边界/进度)、新增 DedupeRunProgressVo、Controller 与结果 VO 调整,PermissionMenuService 相应适配 - brand/dedupe 前端:BrandDedupeTab/BrandPatrolDeleteTab 改造、新增 CountrySelector 组件与 country-options、类型与接口端对齐、测试更新 - 移除无引用文件:backend/static/logo.jpg、prompts/
This commit is contained in:
@@ -7,6 +7,7 @@ export const API_ENDPOINTS = {
|
||||
},
|
||||
dedupe: {
|
||||
run: '/api/dedupe/run',
|
||||
runProgress: '/api/dedupe/run/{runId}/progress',
|
||||
history: '/api/dedupe/history',
|
||||
historyDelete: '/api/dedupe/history/{resultId}',
|
||||
resultDownload: '/api/dedupe/results/{resultId}/download',
|
||||
|
||||
@@ -35,11 +35,25 @@ export interface DedupeRunRequest {
|
||||
user_id: number;
|
||||
}
|
||||
|
||||
export interface DedupeRunProgressVo {
|
||||
runId: string;
|
||||
/** running / success / failed / not_found */
|
||||
status: string;
|
||||
total: number;
|
||||
processedCount: number;
|
||||
successCount: number;
|
||||
failedCount: number;
|
||||
finished: boolean;
|
||||
error?: string;
|
||||
/** 任务完成后才有 */
|
||||
result?: DedupeRunVo | null;
|
||||
}
|
||||
|
||||
export function runDedupe(
|
||||
request: Omit<DedupeRunRequest, "user_id"> | DedupeRunRequest,
|
||||
) {
|
||||
return unwrapJavaResponse(
|
||||
post<JavaApiResponse<DedupeRunVo>, DedupeRunRequest>(
|
||||
post<JavaApiResponse<DedupeRunProgressVo>, DedupeRunRequest>(
|
||||
buildJavaUrl(API_ENDPOINTS.dedupe.run),
|
||||
{
|
||||
...request,
|
||||
@@ -49,6 +63,14 @@ export function runDedupe(
|
||||
);
|
||||
}
|
||||
|
||||
export function getDedupeRunProgress(runId: string) {
|
||||
return unwrapJavaResponse(
|
||||
get<JavaApiResponse<DedupeRunProgressVo>>(
|
||||
buildJavaUrl(API_ENDPOINTS.dedupe.runProgress.replace('{runId}', encodeURIComponent(runId))),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
export function getDedupeHistory() {
|
||||
return unwrapJavaResponse(
|
||||
get<JavaApiResponse<DedupeHistoryVo>>(
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
<template>
|
||||
<section class="country-selector">
|
||||
<div v-if="title" class="selector-title">{{ title }}</div>
|
||||
<div class="country-pref-checks">
|
||||
<label v-for="row in checkboxRows" :key="row.code" class="country-check-row">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="country-check-input"
|
||||
:checked="isSelected(row.code)"
|
||||
:disabled="disabled || isLastSelected(row.code)"
|
||||
@change="onNativeChange(row.code, $event)"
|
||||
/>
|
||||
<span>{{ row.text }}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="country-order-panel">
|
||||
<div class="country-order-caption">{{ orderCaption }}</div>
|
||||
<div v-if="!modelValue.length" class="country-order-empty">尚未选择国家</div>
|
||||
<div v-else class="country-order-list">
|
||||
<div
|
||||
v-for="(code, index) in modelValue"
|
||||
:key="code"
|
||||
class="country-drag-row"
|
||||
:class="{ dragging: dragIndex === index }"
|
||||
:draggable="!disabled"
|
||||
@dragstart="dragIndex = index"
|
||||
@dragend="dragIndex = null"
|
||||
@dragover.prevent
|
||||
@drop.prevent="onDrop(index)"
|
||||
>
|
||||
<span class="drag-handle" title="拖动排序">⋮⋮</span>
|
||||
<span>{{ textOf(code) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { EU_COUNTRY_OPTIONS, type CountryOption } from '@/shared/country-options'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
/** 已选国家代码,数组顺序即执行顺序 */
|
||||
modelValue: string[]
|
||||
options?: readonly CountryOption[]
|
||||
title?: string
|
||||
orderCaption?: string
|
||||
minSelectedWarning?: string
|
||||
disabled?: boolean
|
||||
}>(),
|
||||
{
|
||||
options: () => EU_COUNTRY_OPTIONS,
|
||||
title: '国家与顺序',
|
||||
orderCaption: '已选顺序(拖动可调整执行先后)',
|
||||
minSelectedWarning: '至少保留 1 个国家',
|
||||
disabled: false,
|
||||
},
|
||||
)
|
||||
|
||||
const emit = defineEmits<{ 'update:modelValue': [value: string[]] }>()
|
||||
|
||||
const dragIndex = ref<number | null>(null)
|
||||
|
||||
/** 国家代码和展示名相同时(例如直接用中文名作 code)不再重复追加括号 */
|
||||
function textOf(code: string) {
|
||||
const label = props.options.find((row) => row.code === code)?.label || code
|
||||
return label === code ? label : `${label}(${code})`
|
||||
}
|
||||
|
||||
// 已勾选的按当前顺序排在前面,未勾选的按选项原始顺序补在后面
|
||||
const checkboxRows = computed(() => {
|
||||
const selected = new Set(props.modelValue)
|
||||
return [
|
||||
...props.modelValue.map((code) => ({ code, text: textOf(code) })),
|
||||
...props.options
|
||||
.filter((row) => !selected.has(row.code))
|
||||
.map((row) => ({ code: row.code, text: textOf(row.code) })),
|
||||
]
|
||||
})
|
||||
|
||||
function isSelected(code: string) {
|
||||
return props.modelValue.includes(code)
|
||||
}
|
||||
|
||||
function isLastSelected(code: string) {
|
||||
return props.modelValue.length === 1 && props.modelValue[0] === code
|
||||
}
|
||||
|
||||
function onNativeChange(code: string, event: Event) {
|
||||
const input = event.target as HTMLInputElement
|
||||
if (!input.checked && isLastSelected(code)) {
|
||||
input.checked = true
|
||||
ElMessage.warning(props.minSelectedWarning)
|
||||
return
|
||||
}
|
||||
emit(
|
||||
'update:modelValue',
|
||||
input.checked
|
||||
? [...props.modelValue, code]
|
||||
: props.modelValue.filter((item) => item !== code),
|
||||
)
|
||||
}
|
||||
|
||||
function onDrop(toIndex: number) {
|
||||
const from = dragIndex.value
|
||||
dragIndex.value = null
|
||||
if (props.disabled || from == null || from === toIndex) return
|
||||
const next = [...props.modelValue]
|
||||
const [moved] = next.splice(from, 1)
|
||||
next.splice(toIndex, 0, moved)
|
||||
emit('update:modelValue', next)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.country-selector { margin: 16px 0; }
|
||||
.selector-title { margin-bottom: 10px; color: #bbb; font-size: 13px; }
|
||||
.country-pref-checks { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; margin-bottom: 10px; }
|
||||
.country-check-row { display: flex; align-items: center; gap: 7px; min-height: 32px; color: #ccc; font-size: 13px; }
|
||||
.country-check-input { width: 15px; height: 15px; }
|
||||
.country-check-input:disabled { cursor: not-allowed; }
|
||||
.country-order-panel { padding: 10px; border: 1px solid #333; border-radius: 6px; background: #242424; }
|
||||
.country-order-caption { margin-bottom: 8px; color: #888; font-size: 12px; }
|
||||
.country-order-empty { color: #666; font-size: 12px; }
|
||||
.country-order-list { display: flex; flex-direction: column; gap: 6px; }
|
||||
.country-drag-row { display: flex; align-items: center; gap: 8px; min-height: 30px; padding: 0 9px; border: 1px solid #383838; border-radius: 4px; color: #ccc; font-size: 13px; cursor: grab; }
|
||||
.country-drag-row.dragging { opacity: .5; }
|
||||
.drag-handle { color: #777; }
|
||||
</style>
|
||||
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* 各功能页共用的欧洲五国选项。
|
||||
*
|
||||
* code 用于接口参数与本地存储(和店铺数据抓取 / 跟价 / 商品风险等页面的
|
||||
* country_codes 保持一致),label 用于界面展示;巡店删除的模板结构直接以
|
||||
* 中文国家名作为 key,取 label 即可。
|
||||
*/
|
||||
export interface CountryOption {
|
||||
code: string
|
||||
label: string
|
||||
}
|
||||
|
||||
export const EU_COUNTRY_OPTIONS: readonly CountryOption[] = [
|
||||
{ code: 'DE', label: '德国' },
|
||||
{ code: 'UK', label: '英国' },
|
||||
{ code: 'FR', label: '法国' },
|
||||
{ code: 'IT', label: '意大利' },
|
||||
{ code: 'ES', label: '西班牙' },
|
||||
]
|
||||
|
||||
export const EU_COUNTRY_CODES: readonly string[] = EU_COUNTRY_OPTIONS.map((row) => row.code)
|
||||
|
||||
export function countryLabel(code: string) {
|
||||
return EU_COUNTRY_OPTIONS.find((row) => row.code === code)?.label || code
|
||||
}
|
||||
|
||||
/**
|
||||
* 过滤出合法国家代码并去重,用于校验本地缓存或接口返回的顺序。
|
||||
* 结果为空时回落到 fallback,避免出现「一个国家都没有」导致任务空转。
|
||||
*/
|
||||
export function sanitizeCountryCodes(
|
||||
raw: unknown,
|
||||
fallback: readonly string[] = EU_COUNTRY_CODES,
|
||||
): string[] {
|
||||
if (!Array.isArray(raw)) return [...fallback]
|
||||
const valid = new Set(EU_COUNTRY_CODES)
|
||||
const result: string[] = []
|
||||
for (const item of raw) {
|
||||
const code = String(item ?? '').trim().toUpperCase()
|
||||
if (valid.has(code) && !result.includes(code)) result.push(code)
|
||||
}
|
||||
return result.length ? result : [...fallback]
|
||||
}
|
||||
Reference in New Issue
Block a user