提交优化更新
This commit is contained in:
@@ -25,28 +25,32 @@
|
||||
<div class="section-title">筛选条件</div>
|
||||
<div class="filter-zone">
|
||||
<div class="filter-row">
|
||||
<label class="filter-label">金额(手动输入)</label>
|
||||
<input
|
||||
v-model.number="filters.amount"
|
||||
type="number"
|
||||
step="0.01"
|
||||
min="0"
|
||||
class="filter-input"
|
||||
placeholder="例如:59.99"
|
||||
/>
|
||||
</div>
|
||||
<div class="filter-row">
|
||||
<label class="filter-label">排名</label>
|
||||
<input
|
||||
v-model.number="filters.rank"
|
||||
type="number"
|
||||
step="1"
|
||||
min="0"
|
||||
class="filter-input"
|
||||
placeholder="例如:100000"
|
||||
/>
|
||||
<label class="filter-label">金额区间</label>
|
||||
<div class="range-inputs">
|
||||
<input
|
||||
v-model.number="filters.minAmount"
|
||||
type="number"
|
||||
step="0.01"
|
||||
min="0"
|
||||
class="filter-input"
|
||||
placeholder="最低"
|
||||
/>
|
||||
<span class="range-separator">至</span>
|
||||
<input
|
||||
v-model.number="filters.maxAmount"
|
||||
type="number"
|
||||
step="0.01"
|
||||
min="0"
|
||||
class="filter-input"
|
||||
placeholder="最高"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="filter-checks">
|
||||
<label class="filter-check-row">
|
||||
<input type="checkbox" class="filter-check-input" v-model="filters.rank" />
|
||||
<span class="filter-check-text">排名</span>
|
||||
</label>
|
||||
<label class="filter-check-row">
|
||||
<input type="checkbox" class="filter-check-input" v-model="filters.fba" />
|
||||
<span class="filter-check-text">FBA</span>
|
||||
@@ -59,43 +63,29 @@
|
||||
</div>
|
||||
|
||||
<div class="section-title country-section-title">
|
||||
<span>国家与顺序</span>
|
||||
<span>采集国家</span>
|
||||
<span v-if="countryPrefSaving" class="country-pref-saving">保存中…</span>
|
||||
</div>
|
||||
<p class="hint country-pref-hint">
|
||||
勾选需要采集的国家(暂定 5 国,至少保留 1 个);上方勾选区<strong>先按已选顺序</strong>排列,其余未选国家排在后面。拖拽下方列表可调整顺序。
|
||||
每次任务只采集一个国家,选择后会自动保存,下次进入默认使用该国家。
|
||||
</p>
|
||||
<div class="country-pref-checks">
|
||||
<label v-for="row in countryCheckboxRows" :key="row.code" class="country-check-row">
|
||||
<div class="country-pref-radios">
|
||||
<label
|
||||
v-for="row in COUNTRY_OPTIONS"
|
||||
:key="row.code"
|
||||
class="country-check-row"
|
||||
:class="{ selected: selectedCountryCode === row.code }"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
type="radio"
|
||||
name="collect-data-country"
|
||||
class="country-check-input"
|
||||
:checked="isCountrySelected(row.code)"
|
||||
:disabled="isCountrySelectionLocked(row.code)"
|
||||
@change="onCountryNativeChange(row.code, $event)"
|
||||
:checked="selectedCountryCode === row.code"
|
||||
@change="selectCountry(row.code)"
|
||||
/>
|
||||
<span class="country-check-text">{{ row.label }}({{ row.code }})</span>
|
||||
</label>
|
||||
</div>
|
||||
<div v-if="orderedCountryCodes.length" class="country-order-panel">
|
||||
<div class="country-order-caption">已选顺序(拖拽 ⋮⋮ 调整)</div>
|
||||
<div class="country-order-list">
|
||||
<div
|
||||
v-for="(code, idx) in orderedCountryCodes"
|
||||
:key="code"
|
||||
class="country-drag-row"
|
||||
:class="{ dragging: dragCountryIndex === idx }"
|
||||
draggable="true"
|
||||
@dragstart="onCountryDragStart(idx)"
|
||||
@dragend="onCountryDragEnd"
|
||||
@dragover.prevent
|
||||
@drop.prevent="onCountryDrop(idx)"
|
||||
>
|
||||
<span class="drag-handle" title="拖动排序">⋮⋮</span>
|
||||
<span class="country-drag-label">{{ countryLabel(code) }}({{ code }})</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="run-row">
|
||||
<button
|
||||
@@ -235,19 +225,21 @@ const submitting = ref(false)
|
||||
const pushing = ref(false)
|
||||
|
||||
const filters = reactive<{
|
||||
amount: number | null
|
||||
rank: number | null
|
||||
minAmount: number | null
|
||||
maxAmount: number | null
|
||||
rank: boolean
|
||||
fba: boolean
|
||||
fbm: boolean
|
||||
}>({
|
||||
amount: null,
|
||||
rank: null,
|
||||
minAmount: null,
|
||||
maxAmount: null,
|
||||
rank: false,
|
||||
fba: false,
|
||||
fbm: false,
|
||||
})
|
||||
|
||||
const orderedCountryCodes = ref<string[]>(['DE', 'UK', 'FR', 'IT', 'ES'])
|
||||
const dragCountryIndex = ref<number | null>(null)
|
||||
const selectedCountryCode = ref<string>('DE')
|
||||
const selectedCountryCodes = computed(() => selectedCountryCode.value ? [selectedCountryCode.value] : [])
|
||||
/** 防止接口慢回包覆盖用户已经做出的修改 */
|
||||
const countryPrefUserTouched = ref(false)
|
||||
const countryPrefSaving = ref(false)
|
||||
@@ -304,65 +296,11 @@ function countryLabel(code: string) {
|
||||
return row?.label ?? code
|
||||
}
|
||||
|
||||
const countryCheckboxRows = computed(() => {
|
||||
const sel = orderedCountryCodes.value
|
||||
const selectedSet = new Set(sel)
|
||||
const selectedPart = sel.map((code) => ({ code, label: countryLabel(code) }))
|
||||
const rest = COUNTRY_OPTIONS.filter((o) => !selectedSet.has(o.code)).map((o) => ({
|
||||
code: o.code,
|
||||
label: o.label,
|
||||
}))
|
||||
return [...selectedPart, ...rest]
|
||||
})
|
||||
|
||||
function isCountrySelected(code: string) {
|
||||
return orderedCountryCodes.value.includes(code)
|
||||
}
|
||||
|
||||
function isCountrySelectionLocked(code: string) {
|
||||
return orderedCountryCodes.value.length === 1 && orderedCountryCodes.value[0] === code
|
||||
}
|
||||
|
||||
function onCountryNativeChange(code: string, e: Event) {
|
||||
const el = e.target as HTMLInputElement | null
|
||||
if (!el) return
|
||||
if (!el.checked && isCountrySelectionLocked(code)) {
|
||||
el.checked = true
|
||||
ElMessage.warning('至少保留 1 个国家')
|
||||
return
|
||||
}
|
||||
function selectCountry(code: string) {
|
||||
if (!COUNTRY_OPTIONS.some((row) => row.code === code)) return
|
||||
if (selectedCountryCode.value === code) return
|
||||
countryPrefUserTouched.value = true
|
||||
if (el.checked) {
|
||||
if (!orderedCountryCodes.value.includes(code)) {
|
||||
orderedCountryCodes.value = [...orderedCountryCodes.value, code]
|
||||
}
|
||||
} else {
|
||||
if (orderedCountryCodes.value.length <= 1) {
|
||||
ElMessage.warning('至少保留 1 个国家')
|
||||
return
|
||||
}
|
||||
orderedCountryCodes.value = orderedCountryCodes.value.filter((c) => c !== code)
|
||||
}
|
||||
scheduleSaveCountryPref()
|
||||
}
|
||||
|
||||
function onCountryDragStart(index: number) {
|
||||
dragCountryIndex.value = index
|
||||
}
|
||||
|
||||
function onCountryDragEnd() {
|
||||
dragCountryIndex.value = null
|
||||
}
|
||||
|
||||
function onCountryDrop(toIndex: number) {
|
||||
const from = dragCountryIndex.value
|
||||
dragCountryIndex.value = null
|
||||
if (from == null || from === toIndex) return
|
||||
countryPrefUserTouched.value = true
|
||||
const arr = [...orderedCountryCodes.value]
|
||||
const [item] = arr.splice(from, 1)
|
||||
arr.splice(toIndex, 0, item)
|
||||
orderedCountryCodes.value = arr
|
||||
selectedCountryCode.value = code
|
||||
scheduleSaveCountryPref()
|
||||
}
|
||||
|
||||
@@ -377,10 +315,10 @@ function scheduleSaveCountryPref() {
|
||||
}
|
||||
|
||||
async function persistCountryPref() {
|
||||
if (!orderedCountryCodes.value.length) return
|
||||
if (!selectedCountryCode.value) return
|
||||
countryPrefSaving.value = true
|
||||
try {
|
||||
await putCollectDataCountryPreference([...orderedCountryCodes.value])
|
||||
await putCollectDataCountryPreference([selectedCountryCode.value])
|
||||
} catch (error) {
|
||||
ElMessage.error(error instanceof Error ? error.message : '保存国家顺序失败')
|
||||
} finally {
|
||||
@@ -394,7 +332,7 @@ async function loadCountryPreference() {
|
||||
if (countryPrefUserTouched.value) return
|
||||
const codes = vo?.country_codes
|
||||
if (Array.isArray(codes) && codes.length) {
|
||||
orderedCountryCodes.value = [...codes]
|
||||
selectedCountryCode.value = String(codes[0] || 'DE')
|
||||
}
|
||||
} catch {
|
||||
/* 接口失败时保留本地默认 */
|
||||
@@ -464,16 +402,24 @@ async function selectFolder() {
|
||||
}
|
||||
|
||||
function buildFiltersPayload() {
|
||||
const minAmount = filters.minAmount === null || filters.minAmount === undefined || Number.isNaN(filters.minAmount)
|
||||
? null
|
||||
: filters.minAmount
|
||||
const maxAmount = filters.maxAmount === null || filters.maxAmount === undefined || Number.isNaN(filters.maxAmount)
|
||||
? null
|
||||
: filters.maxAmount
|
||||
return {
|
||||
amount: filters.amount === null || filters.amount === undefined || Number.isNaN(filters.amount)
|
||||
? null
|
||||
: filters.amount,
|
||||
rank: filters.rank === null || filters.rank === undefined || Number.isNaN(filters.rank)
|
||||
? null
|
||||
: filters.rank,
|
||||
amount: maxAmount ?? minAmount,
|
||||
minAmount,
|
||||
maxAmount,
|
||||
min_amount: minAmount,
|
||||
max_amount: maxAmount,
|
||||
rank: !!filters.rank,
|
||||
fba: !!filters.fba,
|
||||
fbm: !!filters.fbm,
|
||||
countryCodes: [...orderedCountryCodes.value],
|
||||
countryCodes: [...selectedCountryCodes.value],
|
||||
countryCode: selectedCountryCode.value,
|
||||
country_code: selectedCountryCode.value,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -482,8 +428,8 @@ async function submitCollect() {
|
||||
ElMessage.warning('请先选择 Excel 文件')
|
||||
return
|
||||
}
|
||||
if (!orderedCountryCodes.value.length) {
|
||||
ElMessage.warning('至少保留 1 个国家')
|
||||
if (!selectedCountryCode.value) {
|
||||
ElMessage.warning('请选择采集国家')
|
||||
return
|
||||
}
|
||||
submitting.value = true
|
||||
@@ -722,6 +668,7 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
.filter-input {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding: 6px 10px;
|
||||
background: #1e1e1e;
|
||||
border: 1px solid #3a3a3a;
|
||||
@@ -731,6 +678,8 @@ onBeforeUnmount(() => {
|
||||
outline: none;
|
||||
}
|
||||
.filter-input:focus { border-color: #3498db; }
|
||||
.range-inputs { flex: 1; min-width: 0; display: flex; align-items: center; gap: 8px; }
|
||||
.range-separator { color: #777; font-size: 12px; white-space: nowrap; }
|
||||
.filter-checks { display: flex; gap: 16px; }
|
||||
.filter-check-row { display: inline-flex; align-items: center; gap: 6px; cursor: pointer; user-select: none; color: #ccc; font-size: 12px; }
|
||||
.filter-check-input { width: 16px; height: 16px; accent-color: #409eff; cursor: pointer; }
|
||||
@@ -738,17 +687,11 @@ onBeforeUnmount(() => {
|
||||
.country-pref-hint { margin-top: -4px; }
|
||||
.country-section-title { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
|
||||
.country-pref-saving { color: #3498db; font-size: 11px; }
|
||||
.country-pref-checks { display: flex; flex-wrap: wrap; gap: 10px 16px; margin-bottom: 12px; }
|
||||
.country-check-row { display: inline-flex; align-items: center; gap: 8px; cursor: pointer; user-select: none; font-size: 12px; color: #ccc; }
|
||||
.country-pref-radios { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; margin-bottom: 14px; }
|
||||
.country-check-row { display: inline-flex; align-items: center; gap: 8px; cursor: pointer; user-select: none; font-size: 12px; color: #ccc; padding: 8px 10px; border: 1px solid #333; border-radius: 8px; background: #252525; }
|
||||
.country-check-row.selected { border-color: #409eff; background: rgba(64, 158, 255, .12); color: #e6f2ff; }
|
||||
.country-check-input { width: 16px; height: 16px; accent-color: #409eff; cursor: pointer; flex-shrink: 0; }
|
||||
.country-check-text { line-height: 1.3; }
|
||||
.country-order-panel { border: 1px solid #333; border-radius: 8px; padding: 10px 12px; background: #252525; margin-bottom: 14px; }
|
||||
.country-order-caption { font-size: 11px; color: #777; margin-bottom: 8px; }
|
||||
.country-order-list { display: flex; flex-direction: column; gap: 6px; }
|
||||
.country-drag-row { display: flex; align-items: center; gap: 10px; padding: 8px 10px; background: #2c2c2c; border-radius: 6px; border: 1px solid #3a3a3a; cursor: grab; user-select: none; }
|
||||
.country-drag-row.dragging { opacity: 0.55; border-color: #3498db; }
|
||||
.drag-handle { color: #666; font-size: 12px; letter-spacing: -2px; }
|
||||
.country-drag-label { font-size: 12px; color: #ccc; }
|
||||
|
||||
.queue-debug-card { margin-top: 14px; border: 1px solid #2a2a2a; border-radius: 8px; padding: 10px 12px; background: #1f1f1f; }
|
||||
.queue-debug-title { margin-top: 0; }
|
||||
|
||||
@@ -17,6 +17,18 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="condition-card">
|
||||
<div class="section-title">检测条件</div>
|
||||
<label class="switch-row">
|
||||
<span>
|
||||
<strong>图片检测</strong>
|
||||
<em>开启后请求 Coze 时携带 img_switch=true</em>
|
||||
</span>
|
||||
<input v-model="imgSwitch" type="checkbox" />
|
||||
<i></i>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="run-row">
|
||||
<button type="button" class="btn-run" :disabled="parsing || !uploadedFiles.length" @click="parseFiles">
|
||||
{{ parsing ? '解析中...' : '解析并创建任务' }}
|
||||
@@ -171,6 +183,7 @@ type TaskSummary = Pick<SimilarAsinParseVo, 'taskId' | 'totalRows' | 'acceptedRo
|
||||
const queuedTaskSummary = ref<TaskSummary | null>(null)
|
||||
const parsing = ref(false)
|
||||
const pushing = ref(false)
|
||||
const imgSwitch = ref(false)
|
||||
const queuePayloadText = ref('')
|
||||
const pollingTaskIds = ref<number[]>([])
|
||||
const pendingFileTaskIds = ref<number[]>([])
|
||||
@@ -374,7 +387,7 @@ async function parseFiles() {
|
||||
originalFilename: f.originalFilename,
|
||||
relativePath: f.relativePath,
|
||||
}))
|
||||
const res = await parseSimilarAsin(files, effectiveCozeApiKey())
|
||||
const res = await parseSimilarAsin(files, effectiveCozeApiKey(), imgSwitch.value)
|
||||
parseResult.value = res
|
||||
queuedTaskSummary.value = null
|
||||
queuePayloadText.value = ''
|
||||
@@ -874,6 +887,16 @@ onUnmounted(() => {
|
||||
.right-panel { flex: 1; min-width: 0; background: #1a1a1a; display: flex; flex-direction: column; }
|
||||
.section-title, .subsection-title { font-size: 13px; color: #bbb; margin-bottom: 10px; }
|
||||
.upload-zone { border: 1px dashed #3a3a3a; border-radius: 10px; padding: 18px; background: #252525; margin-bottom: 18px; }
|
||||
.condition-card { margin-bottom: 18px; padding: 14px 16px; border: 1px solid #2f3a34; border-radius: 10px; background: linear-gradient(135deg, #222a25, #202020); }
|
||||
.switch-row { display: flex; align-items: center; justify-content: space-between; gap: 14px; color: #d8e4dc; cursor: pointer; user-select: none; }
|
||||
.switch-row span { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
|
||||
.switch-row strong { font-size: 13px; font-weight: 600; }
|
||||
.switch-row em { color: #8d9a91; font-size: 12px; font-style: normal; line-height: 1.4; }
|
||||
.switch-row input { position: absolute; opacity: 0; pointer-events: none; }
|
||||
.switch-row i { position: relative; width: 46px; height: 24px; flex: 0 0 auto; border-radius: 999px; background: #3a3a3a; box-shadow: inset 0 0 0 1px #4b4b4b; transition: background .2s ease, box-shadow .2s ease; }
|
||||
.switch-row i::after { content: ''; position: absolute; top: 3px; left: 3px; width: 18px; height: 18px; border-radius: 50%; background: #c7c7c7; transition: transform .2s ease, background .2s ease; }
|
||||
.switch-row input:checked + i { background: #27ae60; box-shadow: inset 0 0 0 1px #42d17a; }
|
||||
.switch-row input:checked + i::after { transform: translateX(22px); background: #fff; }
|
||||
.hint, .loading-msg, .files, .muted { color: #888; font-size: 12px; line-height: 1.5; }
|
||||
.link { color: #6ea8fe; text-decoration: none; }
|
||||
.link:hover { color: #9fc5ff; }
|
||||
|
||||
@@ -1718,6 +1718,7 @@ export interface SimilarAsinParseVo {
|
||||
droppedRows: number;
|
||||
groupCount?: number;
|
||||
aiPrompt?: string;
|
||||
imgSwitch?: boolean;
|
||||
items: SimilarAsinParsedRow[];
|
||||
groups?: SimilarAsinParsedGroup[];
|
||||
}
|
||||
@@ -1776,15 +1777,16 @@ export interface SimilarAsinTaskBatchVo {
|
||||
missingTaskIds?: number[];
|
||||
}
|
||||
|
||||
export function parseSimilarAsin(files: UploadedFileRef[], apiKey?: string) {
|
||||
export function parseSimilarAsin(files: UploadedFileRef[], apiKey?: string, imgSwitch = false) {
|
||||
return unwrapJavaResponse(
|
||||
post<
|
||||
JavaApiResponse<SimilarAsinParseVo>,
|
||||
{ user_id: number; files: UploadedFileRef[]; api_key?: string }
|
||||
{ user_id: number; files: UploadedFileRef[]; api_key?: string; img_switch: boolean }
|
||||
>(`${JAVA_API_PREFIX}/similar-asin/parse`, {
|
||||
user_id: getCurrentUserId(),
|
||||
files,
|
||||
api_key: apiKey,
|
||||
img_switch: imgSwitch,
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -2231,10 +2233,16 @@ export interface CollectDataSourceFile {
|
||||
|
||||
export interface CollectDataFilters {
|
||||
amount?: number | string | null;
|
||||
rank?: number | null;
|
||||
minAmount?: number | string | null;
|
||||
maxAmount?: number | string | null;
|
||||
min_amount?: number | string | null;
|
||||
max_amount?: number | string | null;
|
||||
rank?: boolean | null;
|
||||
fba?: boolean | null;
|
||||
fbm?: boolean | null;
|
||||
countryCodes?: string[];
|
||||
countryCode?: string | null;
|
||||
country_code?: string | null;
|
||||
}
|
||||
|
||||
export interface CollectDataParseRequest {
|
||||
|
||||
Reference in New Issue
Block a user