提交货源采集更新

This commit is contained in:
super
2026-05-10 01:00:08 +08:00
parent a4e4745921
commit 5c990e651e
23 changed files with 308 additions and 37 deletions

View File

@@ -44,6 +44,29 @@
</ul>
</div>
<div class="category-card">
<div class="category-card-head">
<div class="section-title">商品类目</div>
<div class="category-actions">
<button type="button" class="opt-btn" @click="openCategoryDialog">选择类目</button>
<button
v-if="selectedProductCategoryIds.length"
type="button"
class="link-danger"
@click="clearSelectedProductCategories"
>
清空
</button>
</div>
</div>
<div v-if="!selectedProductCategories.length" class="empty-conditions">暂未选择商品类目</div>
<div v-else class="selected-category-list">
<span v-for="category in selectedProductCategories" :key="category.id" class="category-chip">
{{ category.path || category.name }}
</span>
</div>
</div>
<div class="run-row">
<button type="button" class="btn-run" :disabled="parsing || !uploadedFiles.length" @click="parseFiles">
{{ parsing ? '解析中...' : '解析并创建任务' }}
@@ -159,6 +182,41 @@
</div>
</section>
</div>
<el-dialog
v-model="categoryDialogVisible"
title="选择商品类目"
width="560px"
class="category-dialog"
>
<div class="category-dialog-toolbar">
<span class="muted">可多选确认后会追加到筛选条件 prompt 后面</span>
<button type="button" class="link-danger" @click="clearCategoryTreeSelection">清空选择</button>
</div>
<div v-if="loadingProductCategories" class="empty-tasks">正在加载商品类目...</div>
<div v-else-if="!productCategoryTree.length" class="empty-tasks">暂无商品类目</div>
<el-tree
v-else
ref="categoryTreeRef"
class="category-tree"
:data="productCategoryTree"
node-key="id"
show-checkbox
default-expand-all
:props="{ label: 'name', children: 'children' }"
>
<template #default="{ data }">
<span class="category-tree-node">
<span>{{ data.name }}</span>
<span v-if="data.description" class="category-description">{{ data.description }}</span>
</span>
</template>
</el-tree>
<template #footer>
<button type="button" class="opt-btn" @click="categoryDialogVisible = false">取消</button>
<button type="button" class="btn-run category-confirm" @click="confirmCategorySelection">确定</button>
</template>
</el-dialog>
</div>
</template>
@@ -176,8 +234,10 @@ import {
getSimilarAsinHistory,
getSimilarAsinResultDownloadUrl,
getSimilarAsinTaskProgressBatch,
listProductCategories,
listSimilarAsinFilterConditions,
parseSimilarAsin,
type ProductCategoryItemVo,
type SimilarAsinFilterConditionVo,
type SimilarAsinParsedGroup,
type SimilarAsinDashboardVo,
@@ -203,6 +263,12 @@ const queuedTaskSummary = ref<TaskSummary | null>(null)
const filterConditionInput = ref('')
const filterConditions = ref<SimilarAsinFilterConditionVo[]>([])
const selectedFilterConditionId = ref<number | null>(null)
const productCategoryTree = ref<ProductCategoryItemVo[]>([])
const productCategoryItems = ref<ProductCategoryItemVo[]>([])
const selectedProductCategoryIds = ref<number[]>([])
const categoryDialogVisible = ref(false)
const loadingProductCategories = ref(false)
const categoryTreeRef = ref<any>(null)
const parsing = ref(false)
const pushing = ref(false)
const savingCondition = ref(false)
@@ -243,13 +309,80 @@ function selectedFilterConditionText() {
}
function effectiveFilterCondition() {
return filterConditionInput.value.trim() || selectedFilterConditionText()
return appendProductCategoriesToPrompt(filterConditionInput.value.trim() || selectedFilterConditionText())
}
function effectiveCozeApiKey() {
return getStoredApiSecret('similar-asin').trim()
}
const selectedProductCategories = computed(() => {
const selected = new Set(selectedProductCategoryIds.value)
return productCategoryItems.value.filter((item) => selected.has(item.id))
})
function appendProductCategoriesToPrompt(prompt: string) {
const lines = selectedProductCategories.value
.map((item) => {
const path = item.path || item.name
const description = item.description?.trim()
return description ? `${path}${description}` : path
})
.filter(Boolean)
if (!lines.length) return prompt
const categoryBlock = [
'商品类目要求:',
...lines.map((line) => `- ${line}`),
].join('\n')
return prompt ? `${prompt}\n\n${categoryBlock}` : categoryBlock
}
async function loadProductCategories() {
if (loadingProductCategories.value) return
loadingProductCategories.value = true
try {
const res = await listProductCategories()
productCategoryTree.value = res.tree || []
productCategoryItems.value = res.items || []
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : '加载商品类目失败')
} finally {
loadingProductCategories.value = false
}
}
async function openCategoryDialog() {
categoryDialogVisible.value = true
if (!productCategoryTree.value.length) {
await loadProductCategories()
}
requestAnimationFrame(() => {
categoryTreeRef.value?.setCheckedKeys?.(selectedProductCategoryIds.value)
})
}
function hasCheckedDescendant(item: ProductCategoryItemVo, checkedIds: Set<number>): boolean {
return Boolean(item.children?.some((child) => checkedIds.has(child.id) || hasCheckedDescendant(child, checkedIds)))
}
function confirmCategorySelection() {
const checkedNodes = (categoryTreeRef.value?.getCheckedNodes?.(false, false) || []) as ProductCategoryItemVo[]
const checkedIds = new Set(checkedNodes.map((item) => item.id))
selectedProductCategoryIds.value = checkedNodes
.filter((item) => !hasCheckedDescendant(item, checkedIds))
.map((item) => item.id)
categoryDialogVisible.value = false
}
function clearCategoryTreeSelection() {
categoryTreeRef.value?.setCheckedKeys?.([])
}
function clearSelectedProductCategories() {
selectedProductCategoryIds.value = []
categoryTreeRef.value?.setCheckedKeys?.([])
}
function maskSecret(secret: string) {
if (!secret) return ''
if (secret.length <= 10) return '***'
@@ -781,6 +914,7 @@ onMounted(async () => {
loadPollingIds()
await Promise.all([
loadFilterConditions().catch(() => undefined),
loadProductCategories().catch(() => undefined),
loadDashboard().catch(() => undefined),
loadHistory().catch(() => undefined),
])
@@ -827,6 +961,18 @@ onUnmounted(() => {
.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; }
.category-card { margin: 0 0 14px; padding: 12px; border: 1px solid #2a2a2a; border-radius: 8px; background: #202020; }
.category-card-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.category-card-head .section-title { margin-bottom: 0; }
.category-actions { display: flex; align-items: center; gap: 10px; }
.category-actions .opt-btn { padding: 6px 12px; }
.selected-category-list { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 10px; max-height: 96px; overflow: auto; }
.category-chip { max-width: 100%; padding: 4px 8px; border-radius: 6px; background: rgba(52, 152, 219, .16); color: #cfe7ff; font-size: 12px; line-height: 1.4; }
.category-dialog-toolbar { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 10px; }
.category-tree { max-height: 420px; overflow: auto; padding: 8px; border: 1px solid #2f2f2f; border-radius: 8px; background: #202020; --el-tree-bg-color: #202020; --el-tree-text-color: #d8d8d8; --el-tree-node-hover-bg-color: #2a2a2a; }
.category-tree-node { display: inline-flex; align-items: center; gap: 8px; min-width: 0; }
.category-description { color: #888; font-size: 12px; }
.category-confirm { padding: 8px 16px; }
.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; }

View File

@@ -1680,6 +1680,31 @@ export function getAppearancePatentResultDownloadUrl(resultId: number) {
// ========== 货源查询 ==========
export interface ProductCategoryItemVo {
id: number;
parentId?: number | null;
name: string;
categoryKey?: string;
sortOrder?: number;
description?: string;
isBuiltin?: boolean;
childCount?: number;
level?: number;
path?: string;
children?: ProductCategoryItemVo[];
}
export interface ProductCategoryListVo {
tree: ProductCategoryItemVo[];
items: ProductCategoryItemVo[];
}
export function listProductCategories() {
return unwrapJavaResponse(
get<JavaApiResponse<ProductCategoryListVo>>(`${JAVA_API_PREFIX}/admin/product-categories`),
);
}
export interface SimilarAsinFilterConditionVo {
id: number;
conditionText: string;