菜单修改优化

This commit is contained in:
supernijia
2026-07-28 13:40:19 +08:00
parent 4ddb8b47b0
commit 7c3c9e53a0
90 changed files with 9388 additions and 1545 deletions
+12
View File
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>店铺数据抓取 - 数富AI</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/shop-data-crawl-main.ts"></script>
</body>
</html>
@@ -128,8 +128,7 @@
>
<div class="task-section-header">
<div class="task-title-wrap">
<strong>{{ detail.task.taskNo || `任务 ${detail.task.id}` }}</strong>
<span class="task-meta">任务 ID{{ detail.task.id }}</span>
<strong>上架任务 #{{ detail.task.id }}</strong>
<span class="task-meta">创建时间{{ formatDateTime(detail.task.createdAt) }}</span>
</div>
<div class="task-actions">
@@ -144,6 +143,14 @@
>
下载结果
</button>
<button
type="button"
class="btn-delete"
:disabled="isDeletingTask(detail.task.id)"
@click="deleteTaskRecord(detail)"
>
{{ isDeletingTask(detail.task.id) ? '删除中...' : '删除' }}
</button>
</div>
</div>
@@ -163,17 +170,19 @@
<span v-if="file.platform">平台{{ file.platform }}</span>
<span>数据{{ fileProgressCurrent(file) }}/{{ fileProgressTotal(file) }}</span>
</div>
<div class="file-progress-header">
<span>{{ file.progressMessage || statusText(file.status) }}</span>
<span>{{ fileProgressPercent(file) }}%</span>
</div>
<div class="file-progress-track">
<div
class="file-progress-fill"
:class="statusClass(file.status)"
:style="{ width: `${fileProgressPercent(file)}%` }"
/>
</div>
<template v-if="shouldShowFileProgress(file)">
<div class="file-progress-header">
<span>{{ file.progressMessage || statusText(file.status) }}</span>
<span>{{ fileProgressPercent(file) }}%</span>
</div>
<div class="file-progress-track">
<div
class="file-progress-fill"
:class="statusClass(file.status)"
:style="{ width: `${fileProgressPercent(file)}%` }"
/>
</div>
</template>
<div v-if="fileErrorText(file)" class="file-error">{{ fileErrorText(file) }}</div>
</div>
<span class="status file-status" :class="statusClass(file.status)">
@@ -191,7 +200,7 @@
<script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { ElMessage } from 'element-plus'
import { ElMessage, ElMessageBox } from 'element-plus'
import BrandTopBar from './BrandTopBar.vue'
import ZiniaoVersionSetting from '@/shared/components/ZiniaoVersionSetting.vue'
@@ -199,6 +208,7 @@ import { expandBrandFolderRecursive, type BrandExpandFolderItem } from '@/shared
import {
activatePublishFile,
activatePublishTask,
deletePublishTask,
getPublishDashboard,
getPublishHistory,
getPublishItemsPageUrl,
@@ -261,6 +271,7 @@ const queuedBatches = ref<PublishQueueBatch[]>([])
const taskSnapshots = ref<Record<number, PublishTaskDetailVo>>({})
const historyItems = ref<PublishTaskDetailVo[]>([])
const missingTaskIds = ref<number[]>([])
const deletingTaskIds = ref<number[]>([])
const dashboard = ref<PublishDashboardVo>({
pendingCount: 0,
runningCount: 0,
@@ -990,6 +1001,10 @@ function fileErrorText(file: PublishFileItem) {
return file.errorMessage || file.error || ''
}
function shouldShowFileProgress(file: PublishFileItem) {
return ['RUNNING', 'SUCCESS', 'COMPLETED', 'FAILED', 'CANCELLED'].includes(normalizeStatus(file.status))
}
function fileProgressCurrent(file: PublishFileItem) {
return Math.max(0, Number(file.progressCurrent ?? file.processedRows ?? 0))
}
@@ -1038,6 +1053,58 @@ async function downloadResult(detail: PublishTaskDetailVo) {
else if (saved.error && saved.error !== '用户取消') ElMessage.error(saved.error)
}
function isDeletingTask(taskId: number) {
return deletingTaskIds.value.includes(taskId)
}
function removeTaskLocally(taskId: number) {
progressLoop.remove(taskId)
historyItems.value = historyItems.value.filter((detail) => detail.task?.id !== taskId)
const snapshots = { ...taskSnapshots.value }
delete snapshots[taskId]
taskSnapshots.value = snapshots
missingTaskIds.value = Array.from(new Set([...missingTaskIds.value, taskId]))
queuedBatches.value = queuedBatches.value.filter((batch) => batch.taskId !== taskId)
if (currentTaskId.value === taskId) {
currentTaskId.value = null
currentFiles.value = []
pendingFileIds.value = []
activeFileId.value = null
dispatchOptions.value = null
}
saveQueueState()
}
async function deleteTaskRecord(detail: PublishTaskDetailVo) {
const taskId = detail.task?.id
if (!taskId || isDeletingTask(taskId)) return
try {
await ElMessageBox.confirm(
`确定删除上架任务 #${taskId}?任务明细和结果文件也会一并删除。`,
'删除任务',
{
confirmButtonText: '删除',
cancelButtonText: '取消',
type: 'warning',
},
)
} catch {
return
}
deletingTaskIds.value = [...deletingTaskIds.value, taskId]
try {
await deletePublishTask(taskId)
removeTaskLocally(taskId)
ElMessage.success('任务已删除')
await Promise.all([loadDashboard(), loadHistory()])
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : '删除任务失败')
} finally {
deletingTaskIds.value = deletingTaskIds.value.filter((id) => id !== taskId)
}
}
onMounted(async () => {
loadQueueState()
if (currentTaskId.value) progressLoop.add(currentTaskId.value)
@@ -1069,7 +1136,7 @@ onBeforeUnmount(() => {
.hint, .loading-msg, .task-meta, .file-info, .history-count { color: #888; font-size: 12px; line-height: 1.5; }
.btns, .run-row, .task-actions { display: flex; align-items: center; flex-wrap: wrap; gap: 10px; }
.btns { justify-content: center; }
.opt-btn, .btn-run, .download { border: 0; border-radius: 6px; cursor: pointer; transition: background-color .15s ease, color .15s ease, opacity .15s ease; }
.opt-btn, .btn-run, .download, .btn-delete { border: 0; border-radius: 6px; cursor: pointer; transition: background-color .15s ease, color .15s ease, opacity .15s ease; }
.opt-btn { padding: 8px 16px; border: 1px solid #3a3a3a; background: #2a2a2a; color: #ccc; font-size: 13px; }
.opt-btn:hover:not(:disabled) { border-color: #3498db; color: #67b7ef; }
.opt-btn:disabled, .btn-run:disabled { opacity: .55; cursor: not-allowed; }
@@ -1105,6 +1172,9 @@ onBeforeUnmount(() => {
.task-error { padding: 10px 16px 0; }
.download { padding: 7px 12px; background: #2d6b46; color: #dff7e8; font-size: 12px; white-space: nowrap; }
.download:hover { background: #367d54; }
.btn-delete { padding: 7px 12px; background: #472929; color: #efaaaa; font-size: 12px; white-space: nowrap; }
.btn-delete:hover:not(:disabled) { background: #5a3030; color: #ffd0d0; }
.btn-delete:disabled { cursor: wait; opacity: .55; }
.status { display: inline-flex; align-items: center; justify-content: center; min-width: 58px; min-height: 26px; padding: 0 8px; border-radius: 4px; font-size: 12px; white-space: nowrap; }
.status.pending { background: #3a3424; color: #e4c56a; }
.status.running { background: #24394a; color: #75bff1; }
@@ -0,0 +1,604 @@
<template>
<div class="page-shell module-page">
<BrandTopBar active="shop-data-crawl" />
<div class="main-content">
<aside class="left-panel">
<div class="section-title">店铺输入</div>
<div class="input-zone">
<div class="input-row">
<el-input v-model="shopInput" clearable placeholder="请输入店铺名" @keyup.enter="confirmAdd" />
<button type="button" class="opt-btn" :disabled="adding" @click="confirmAdd">
{{ adding ? '添加中...' : '添加' }}
</button>
</div>
</div>
<div class="section-title">备选区</div>
<div v-if="!candidates.length" class="empty-candidates">暂无备选店铺</div>
<div v-else class="candidate-table-scroll">
<el-table :data="candidates" row-key="id" height="250" class="candidate-table"
@selection-change="onSelectionChange">
<el-table-column type="selection" width="42" />
<el-table-column prop="shop_name" label="店铺名" min-width="140" show-overflow-tooltip />
<el-table-column label="操作" width="64" align="center">
<template #default="{ row }">
<button type="button" class="link-danger" @click="removeCandidate(row.id)">删除</button>
</template>
</el-table-column>
</el-table>
</div>
<div class="section-title">抓取国家与顺序</div>
<div class="country-pref-checks">
<label v-for="row in countryCheckboxRows" :key="row.code" class="country-check-row">
<input type="checkbox" class="country-check-input" :checked="isCountrySelected(row.code)"
:disabled="isCountrySelectionLocked(row.code)" @change="onCountryNativeChange(row.code, $event)" />
<span>{{ row.label }}{{ row.code }}</span>
</label>
</div>
<div class="country-order-panel">
<div class="country-order-caption">已选顺序</div>
<div class="country-order-list">
<div v-for="(code, index) in orderedCountryCodes" :key="code" class="country-drag-row"
:class="{ dragging: dragCountryIndex === index }" draggable="true"
@dragstart="dragCountryIndex = index" @dragend="dragCountryIndex = null"
@dragover.prevent @drop.prevent="onCountryDrop(index)">
<span class="drag-handle" title="拖动排序"></span>
<span>{{ countryLabel(code) }}{{ code }}</span>
</div>
</div>
</div>
<div v-if="countryPrefSaving" class="country-pref-status">保存中...</div>
<ZiniaoVersionSetting v-model="ziniaoVersion" />
<div class="run-row">
<button type="button" class="btn-run" :disabled="matching" @click="runMatch">
{{ matching ? '匹配中...' : '匹配店铺' }}
</button>
<button type="button" class="btn-run btn-queue" :disabled="isQueueBusy || !matchedRunnableItems.length"
@click="startQueue">
{{ isQueueBusy ? '串行抓取中...' : '开始串行抓取' }}
</button>
</div>
<p v-if="queueStatus" class="queue-status">{{ queueStatus }}</p>
</aside>
<section class="right-panel">
<div class="panel-header">匹配与任务</div>
<div class="task-list-wrap">
<div class="clean-result-summary">
<div class="summary-card"><span class="summary-label">备选店铺</span><strong>{{ dashboard.candidateCount }}</strong></div>
<div class="summary-card"><span class="summary-label">已处理任务</span><strong>{{ dashboard.processedTaskCount }}</strong></div>
<div class="summary-card"><span class="summary-label">成功任务</span><strong>{{ dashboard.successTaskCount }}</strong></div>
<div class="summary-card"><span class="summary-label">失败任务</span><strong>{{ dashboard.failedTaskCount }}</strong></div>
</div>
<div class="subsection-title">匹配结果</div>
<div v-if="!matchedItems.length" class="empty-tasks narrow">匹配后将在这里显示结果</div>
<el-table v-else :data="matchedItems" :row-key="rowKey" :highlight-current-row="false"
class="result-table match-table">
<el-table-column prop="shopName" label="店铺名" min-width="120" />
<el-table-column label="匹配" width="64" align="center">
<template #default="{ row }"><span :class="row.matched ? 'ok' : 'fail'">{{ row.matched ? '是' : '否' }}</span></template>
</el-table-column>
<el-table-column prop="shopId" label="店铺 ID" min-width="120" show-overflow-tooltip />
<el-table-column prop="platform" label="平台" width="88" show-overflow-tooltip />
<el-table-column prop="companyName" label="公司" min-width="120" show-overflow-tooltip />
<el-table-column label="状态" width="100"><template #default="{ row }">{{ formatMatchStatus(row.matchStatus) }}</template></el-table-column>
<el-table-column label="说明" min-width="150" show-overflow-tooltip><template #default="{ row }">{{ row.matchMessage || '-' }}</template></el-table-column>
<el-table-column label="操作" width="64" align="center"><template #default="{ row }">
<button type="button" class="link-danger" @click="removeMatchedRow(row)">删除</button>
</template></el-table-column>
</el-table>
<div class="result-list-wrap">
<div class="result-list-header"><span>任务记录</span></div>
<div v-if="!currentItems.length && !historySectionItems.length" class="empty-tasks">暂无任务记录</div>
<div v-if="currentItems.length" class="result-subsection">
<div class="result-subsection-title">当前任务</div>
<ul class="task-list">
<TaskRow v-for="item in currentItems" :key="historyKey(item)" :item="item"
@download="downloadResult" @delete="deleteTaskRecord" />
</ul>
</div>
<div v-if="historySectionItems.length" class="result-subsection">
<div class="result-subsection-title">历史记录</div>
<ul class="task-list">
<TaskRow v-for="item in historySectionItems" :key="historyKey(item)" :item="item"
@download="downloadResult" @delete="deleteTaskRecord" />
</ul>
</div>
</div>
</div>
</section>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, defineComponent, h, onMounted, onUnmounted, ref } from 'vue'
import { ElMessage } from 'element-plus'
import BrandTopBar from '@/pages/brand/components/BrandTopBar.vue'
import ZiniaoVersionSetting from '@/shared/components/ZiniaoVersionSetting.vue'
import { useZiniaoVersion } from '@/shared/utils/ziniao-version'
import { getPywebviewApi } from '@/shared/bridges/pywebview'
import { getTaskPollIntervalMs } from '@/shared/task-progress-config'
import { createCategorizedTimers } from '@/shared/utils/categorized-timers'
import { saveUrlWithProgress } from '@/shared/utils/download-progress'
import {
addShopDataCrawlCandidate,
createShopDataCrawlTask,
deleteShopDataCrawlCandidate,
deleteShopDataCrawlHistory,
deleteShopDataCrawlTask,
getShopDataCrawlCountryPreference,
getShopDataCrawlDashboard,
getShopDataCrawlHistory,
getShopDataCrawlResultDownloadUrl,
getShopDataCrawlTaskProgressBatch,
listShopDataCrawlCandidates,
matchShopDataCrawlShops,
putShopDataCrawlCountryPreference,
type ShopDataCrawlCandidateVo,
type ShopDataCrawlDashboardVo,
type ShopDataCrawlHistoryItem,
type ShopDataCrawlShopItem,
type ShopDataCrawlTaskDetailVo,
} from '@/shared/api/java-modules'
const COUNTRY_OPTIONS = [
{ code: 'UK', label: '英国' },
{ code: 'DE', label: '德国' },
{ code: 'FR', label: '法国' },
{ code: 'ES', label: '西班牙' },
{ code: 'IT', label: '意大利' },
] as const
const TaskRow = defineComponent({
props: { item: { type: Object as () => ShopDataCrawlHistoryItem, required: true } },
emits: ['download', 'delete'],
setup(props, { emit }) {
return () => h('li', { class: 'task-item' }, [
h('div', { class: 'left split-result-main' }, [
h('span', { class: 'id', title: props.item.shopName || '' }, props.item.shopName || '-'),
h('div', { class: 'files' }, `任务 ID${props.item.taskId ?? '-'}`),
props.item.platform ? h('div', { class: 'files' }, `平台:${props.item.platform}`) : null,
props.item.outputFilename ? h('div', { class: 'files' }, `文件:${props.item.outputFilename}`) : null,
props.item.createdAt ? h('div', { class: 'files' }, `创建时间:${formatDateTime(props.item.createdAt)}`) : null,
props.item.error ? h('div', { class: 'files error-text' }, `错误:${props.item.error}`) : null,
]),
h('div', { class: 'task-right' }, [
h('span', { class: ['status', statusClass(props.item.taskStatus)] }, statusText(props.item.taskStatus)),
canDownload(props.item) ? h('button', { type: 'button', class: 'download', onClick: () => emit('download', props.item) }, '下载') : null,
h('button', { type: 'button', class: 'btn-delete', onClick: () => emit('delete', props.item) }, '删除'),
]),
])
},
})
const timers = createCategorizedTimers('shop-data-crawl')
const ziniaoVersion = useZiniaoVersion()
const shopInput = ref('')
const candidates = ref<ShopDataCrawlCandidateVo[]>([])
const selectedCandidates = ref<ShopDataCrawlCandidateVo[]>([])
const matchedItems = ref<ShopDataCrawlShopItem[]>([])
const historyItems = ref<ShopDataCrawlHistoryItem[]>([])
const dashboard = ref<ShopDataCrawlDashboardVo>({ candidateCount: 0, processedTaskCount: 0, successTaskCount: 0, failedTaskCount: 0 })
const orderedCountryCodes = ref<string[]>(COUNTRY_OPTIONS.map((row) => row.code))
const dragCountryIndex = ref<number | null>(null)
const countryPrefSaving = ref(false)
const countryPrefUserTouched = ref(false)
const adding = ref(false)
const matching = ref(false)
const queueWorkerRunning = ref(false)
const queueStatus = ref('')
const pendingQueue = ref<ShopDataCrawlShopItem[]>([])
const activeTaskId = ref<number | null>(null)
const activeQueueItem = ref<ShopDataCrawlShopItem | null>(null)
const activeDispatched = ref(false)
const activeCountryCodes = ref<string[]>([])
const activeZiniaoVersion = ref<'new' | 'old'>('new')
const pollingTaskIds = ref<number[]>([])
const taskSnapshots = ref<Record<number, ShopDataCrawlTaskDetailVo>>({})
const autoQueueEnabled = ref(false)
let countryPrefSaveTimer: number | null = null
let disposed = false
const matchedRunnableItems = computed(() => matchedItems.value.filter((item) => item.matched))
const currentItems = computed(() => historyItems.value.filter((item) => !isTerminal(item.taskStatus)))
const historySectionItems = computed(() => historyItems.value.filter((item) => isTerminal(item.taskStatus)))
const isQueueBusy = computed(() => queueWorkerRunning.value || !!activeTaskId.value || pendingQueue.value.length > 0)
const countryCheckboxRows = computed(() => {
const selected = new Set(orderedCountryCodes.value)
return [
...orderedCountryCodes.value.map((code) => ({ code, label: countryLabel(code) })),
...COUNTRY_OPTIONS.filter((row) => !selected.has(row.code)),
]
})
function uid() { return typeof window === 'undefined' ? '0' : window.localStorage.getItem('uid') || '0' }
function storageKey(name: string) { return `shop-data-crawl:${name}:${uid()}` }
function rowKey(item: ShopDataCrawlShopItem) { return `${(item.shopName || '').trim()}::${item.shopId || ''}` }
function historyKey(item: ShopDataCrawlHistoryItem) { return `${item.taskId || 0}:${item.resultId || 0}:${rowKey(item)}` }
function countryLabel(code: string) { return COUNTRY_OPTIONS.find((row) => row.code === code)?.label || code }
function isCountrySelected(code: string) { return orderedCountryCodes.value.includes(code) }
function isCountrySelectionLocked(code: string) { return orderedCountryCodes.value.length === 1 && orderedCountryCodes.value[0] === code }
function isTerminal(status?: string) { return status === 'SUCCESS' || status === 'FAILED' || status === 'COMPLETED' }
function statusText(status?: string) { return status === 'SUCCESS' || status === 'COMPLETED' ? '已完成' : status === 'FAILED' ? '失败' : '执行中' }
function statusClass(status?: string) { return status === 'SUCCESS' || status === 'COMPLETED' ? 'success' : status === 'FAILED' ? 'failed' : 'running' }
function canDownload(item: ShopDataCrawlHistoryItem) { return Boolean(item.resultId && (item.fileReady || item.downloadUrl)) }
function formatDateTime(value?: string) { if (!value) return '-'; const date = new Date(value); return Number.isNaN(date.getTime()) ? value : date.toLocaleString('zh-CN', { hour12: false }) }
function formatMatchStatus(status?: string) { return ({ MATCHED: '已匹配', PENDING: '待匹配', CONFLICT: '需确认', INDEX_STALE: '索引过期' } as Record<string, string>)[status || ''] || status || '-' }
function setStorage(name: string, value: unknown) {
if (typeof window !== 'undefined') window.localStorage.setItem(storageKey(name), JSON.stringify(value))
}
function loadLocalState() {
try { matchedItems.value = JSON.parse(window.localStorage.getItem(storageKey('matched')) || '[]') } catch { matchedItems.value = [] }
try {
const state = JSON.parse(window.localStorage.getItem(storageKey('queue')) || '{}') as { pendingQueue?: ShopDataCrawlShopItem[]; activeTaskId?: number | null; activeQueueItem?: ShopDataCrawlShopItem | null; activeDispatched?: boolean; activeCountryCodes?: string[]; activeZiniaoVersion?: 'new' | 'old'; autoQueueEnabled?: boolean }
pendingQueue.value = state.pendingQueue || []
activeTaskId.value = state.activeTaskId || null
activeQueueItem.value = state.activeQueueItem || null
activeDispatched.value = state.activeDispatched || false
activeCountryCodes.value = state.activeCountryCodes || []
activeZiniaoVersion.value = state.activeZiniaoVersion || 'new'
autoQueueEnabled.value = state.autoQueueEnabled || false
} catch { pendingQueue.value = []; activeTaskId.value = null; activeQueueItem.value = null }
try { pollingTaskIds.value = JSON.parse(window.localStorage.getItem(storageKey('polling-task-ids')) || '[]') } catch { pollingTaskIds.value = [] }
try { taskSnapshots.value = JSON.parse(window.localStorage.getItem(storageKey('task-snapshots')) || '{}') } catch { taskSnapshots.value = {} }
if (activeDispatched.value && activeQueueItem.value) removeMatchedRow(activeQueueItem.value)
}
function saveQueueState() {
setStorage('queue', { pendingQueue: pendingQueue.value, activeTaskId: activeTaskId.value, activeQueueItem: activeQueueItem.value, activeDispatched: activeDispatched.value, activeCountryCodes: activeCountryCodes.value, activeZiniaoVersion: activeZiniaoVersion.value, autoQueueEnabled: autoQueueEnabled.value })
setStorage('polling-task-ids', pollingTaskIds.value)
setStorage('task-snapshots', taskSnapshots.value)
}
function mergeItems(base: ShopDataCrawlShopItem[], incoming: ShopDataCrawlShopItem[]) {
const map = new Map(base.map((item) => [rowKey(item), item]))
for (const item of incoming) map.set(rowKey(item), item)
return [...map.values()]
}
function saveMatched() { setStorage('matched', matchedItems.value) }
function onSelectionChange(rows: ShopDataCrawlCandidateVo[]) { selectedCandidates.value = rows || [] }
function onCountryNativeChange(code: string, event: Event) {
const input = event.target as HTMLInputElement
if (!input.checked && isCountrySelectionLocked(code)) { input.checked = true; ElMessage.warning('至少保留 1 个国家'); return }
countryPrefUserTouched.value = true
orderedCountryCodes.value = input.checked ? [...orderedCountryCodes.value, code] : orderedCountryCodes.value.filter((item) => item !== code)
scheduleCountryPreferenceSave()
}
function onCountryDrop(toIndex: number) {
const from = dragCountryIndex.value
dragCountryIndex.value = null
if (from == null || from === toIndex) return
const next = [...orderedCountryCodes.value]
const [item] = next.splice(from, 1)
next.splice(toIndex, 0, item)
orderedCountryCodes.value = next
countryPrefUserTouched.value = true
scheduleCountryPreferenceSave()
}
function scheduleCountryPreferenceSave() {
if (countryPrefSaveTimer) timers.clearTimer('preference-save', countryPrefSaveTimer)
countryPrefSaveTimer = timers.setTimeout('preference-save', () => { countryPrefSaveTimer = null; void persistCountryPreference() }, 450)
}
async function persistCountryPreference() {
countryPrefSaving.value = true
try { await putShopDataCrawlCountryPreference(orderedCountryCodes.value) }
catch (error) { ElMessage.error(error instanceof Error ? error.message : '保存国家顺序失败') }
finally { countryPrefSaving.value = false }
}
async function loadCandidates() { candidates.value = await listShopDataCrawlCandidates() }
async function loadDashboard() { dashboard.value = await getShopDataCrawlDashboard() }
async function loadHistory() {
const localFailures = historyItems.value.filter((item) => !item.taskId && !item.resultId && item.taskStatus === 'FAILED')
historyItems.value = [...localFailures, ...((await getShopDataCrawlHistory()).items || [])]
}
async function refreshViews() { await Promise.all([loadDashboard(), loadHistory()]) }
async function confirmAdd() {
const name = shopInput.value.trim()
if (!name) { ElMessage.warning('请输入店铺名'); return }
adding.value = true
try { await addShopDataCrawlCandidate(name); shopInput.value = ''; await Promise.all([loadCandidates(), loadDashboard()]); ElMessage.success('已加入备选区') }
catch (error) { ElMessage.error(error instanceof Error ? error.message : '添加失败') }
finally { adding.value = false }
}
async function removeCandidate(id: number) {
try { await deleteShopDataCrawlCandidate(id); selectedCandidates.value = selectedCandidates.value.filter((item) => item.id !== id); await Promise.all([loadCandidates(), loadDashboard()]); ElMessage.success('已删除') }
catch (error) { ElMessage.error(error instanceof Error ? error.message : '删除失败') }
}
async function runMatch() {
const names = selectedCandidates.value.map((item) => item.shop_name).filter(Boolean)
if (!names.length) { ElMessage.warning('请先勾选备选店铺'); return }
matching.value = true
try {
const incoming = (await matchShopDataCrawlShops(names)).items || []
matchedItems.value = mergeItems(matchedItems.value, incoming)
saveMatched()
if (autoQueueEnabled.value) {
const activeKey = activeQueueItem.value ? rowKey(activeQueueItem.value) : ''
pendingQueue.value = mergeItems(
pendingQueue.value,
incoming.filter((item) => item.matched && rowKey(item) !== activeKey),
)
saveQueueState()
void processQueue()
}
ElMessage.success(`匹配完成,共 ${incoming.length}`)
} catch (error) { ElMessage.error(error instanceof Error ? error.message : '匹配失败') }
finally { matching.value = false }
}
function removeMatchedRow(item: ShopDataCrawlShopItem) {
const key = rowKey(item)
matchedItems.value = matchedItems.value.filter((row) => rowKey(row) !== key)
pendingQueue.value = pendingQueue.value.filter((row) => rowKey(row) !== key)
saveMatched(); saveQueueState()
}
function buildTaskItem(item: ShopDataCrawlShopItem): ShopDataCrawlShopItem { return { ...item } }
function mergeProgress(detail: ShopDataCrawlTaskDetailVo) {
const taskId = detail.task?.id || detail.items?.[0]?.taskId
if (!taskId) return
taskSnapshots.value = { ...taskSnapshots.value, [taskId]: detail }
const incoming = (detail.items || []).map((item) => ({ ...item, taskId: item.taskId || taskId, taskStatus: item.taskStatus || detail.task?.status, error: item.error || detail.task?.errorMessage }))
const map = new Map(historyItems.value.map((item) => [historyKey(item), item]))
for (const item of incoming) {
const existing = [...map.values()].find((row) => row.taskId === item.taskId && (row.resultId === item.resultId || !item.resultId))
if (existing) map.set(historyKey(existing), { ...existing, ...item })
else map.set(historyKey(item), item)
}
historyItems.value = [...map.values()]
saveQueueState()
}
function isTaskDetail(row: ShopDataCrawlTaskDetailVo | ShopDataCrawlHistoryItem): row is ShopDataCrawlTaskDetailVo {
return 'task' in row || 'items' in row
}
async function refreshTaskProgress(taskId: number) {
const batch = await getShopDataCrawlTaskProgressBatch([taskId])
for (const row of batch.items || []) {
if (isTaskDetail(row)) {
mergeProgress(row)
} else {
mergeProgress({ task: { id: row.taskId, status: row.taskStatus }, items: [row] })
}
}
return !(batch.missingTaskIds || []).includes(taskId)
}
function activeTaskStatus(taskId: number) {
return taskSnapshots.value[taskId]?.task?.status || historyItems.value.find((item) => item.taskId === taskId)?.taskStatus || ''
}
function sleep(ms: number) { return disposed ? Promise.resolve() : timers.sleep('queue-wait', ms) }
function isTransientError(error: unknown) { return /network|fetch|timeout|50[0234]|load failed|connection refused|econnrefused|无法连接.*后端|后端服务.*(?:不可用|连接)|网络错误/i.test(error instanceof Error ? error.message : String(error || '')) }
async function retry<T>(action: () => Promise<T>) {
let attempt = 0
while (!disposed) {
try { return await action() } catch (error) {
if (!isTransientError(error)) throw error
attempt += 1; queueStatus.value = `服务暂时不可用,正在重试(${attempt}`; await sleep(getTaskPollIntervalMs())
}
}
throw new Error('页面已关闭')
}
async function waitForTerminal(taskId: number) {
while (!disposed) {
try {
const exists = await refreshTaskProgress(taskId)
if (!exists) return 'FAILED'
const status = activeTaskStatus(taskId)
if (isTerminal(status)) return status
} catch (error) {
if (!isTransientError(error)) throw error
queueStatus.value = `任务 ${taskId} 执行中,等待服务恢复...`
}
await sleep(getTaskPollIntervalMs())
}
return 'STOPPED'
}
function clearActiveTask() {
const taskId = activeTaskId.value
activeTaskId.value = null; activeQueueItem.value = null; activeDispatched.value = false
activeCountryCodes.value = []; activeZiniaoVersion.value = 'new'
if (taskId) pollingTaskIds.value = pollingTaskIds.value.filter((id) => id !== taskId)
saveQueueState()
}
function recordLocalFailure(item: ShopDataCrawlShopItem, error: unknown) {
historyItems.value = [{ ...item, taskStatus: 'FAILED', success: false, error: error instanceof Error ? error.message : String(error || '任务失败'), createdAt: new Date().toISOString() }, ...historyItems.value]
}
async function dispatchActiveTask(api: NonNullable<ReturnType<typeof getPywebviewApi>>) {
if (!activeTaskId.value || !activeQueueItem.value || !api.enqueue_json) return false
const payload = {
type: 'shop-data-crawl-run',
ts: Date.now(),
data: {
taskId: activeTaskId.value,
ziniao_version: activeZiniaoVersion.value,
items: [buildTaskItem(activeQueueItem.value)],
country_codes: [...activeCountryCodes.value],
},
}
const pushed = await api.enqueue_json(payload)
if (!pushed?.success) throw new Error(pushed?.error || `任务 ${activeTaskId.value} 入队失败`)
activeDispatched.value = true
saveQueueState()
return true
}
async function processQueue() {
if (disposed || queueWorkerRunning.value) return
queueWorkerRunning.value = true
try {
const api = getPywebviewApi()
if (!api?.enqueue_json) throw new Error('当前客户端不支持任务队列')
while (!disposed && (activeTaskId.value || pendingQueue.value.length)) {
if (activeTaskId.value) {
if (!activeDispatched.value) {
const taskId = activeTaskId.value
const item = activeQueueItem.value
try {
await dispatchActiveTask(api)
if (item) removeMatchedRow(item)
queueStatus.value = `任务 ${taskId} 执行中,剩余 ${pendingQueue.value.length}`
} catch (error) {
await deleteShopDataCrawlTask(taskId).catch(() => undefined)
if (item) recordLocalFailure(item, error)
clearActiveTask()
queueStatus.value = `任务 ${taskId} 入队失败,继续下一店`
continue
}
}
const taskId = activeTaskId.value
const status = await waitForTerminal(taskId)
clearActiveTask()
queueStatus.value = `任务 ${taskId} ${status === 'SUCCESS' || status === 'COMPLETED' ? '已完成' : '失败'},剩余 ${pendingQueue.value.length}`
continue
}
const next = pendingQueue.value.shift()
saveQueueState()
if (!next) break
const countries = [...orderedCountryCodes.value]
let created
try {
created = await retry(() => createShopDataCrawlTask([buildTaskItem(next)], countries))
if (!created.taskId) throw new Error('后端未返回任务 ID')
} catch (error) {
if (disposed) return
recordLocalFailure(next, error)
queueStatus.value = `${next.shopName} 创建任务失败,继续下一店`
continue
}
activeTaskId.value = created.taskId
activeQueueItem.value = next
activeDispatched.value = false
activeCountryCodes.value = countries
activeZiniaoVersion.value = ziniaoVersion.value
pollingTaskIds.value = [...new Set([...pollingTaskIds.value, created.taskId])]
const createdItems = (created.items || []).map((item) => ({ ...item, taskId: item.taskId || created.taskId }))
taskSnapshots.value = { ...taskSnapshots.value, [created.taskId]: { task: { id: created.taskId, status: createdItems[0]?.taskStatus }, items: createdItems } }
historyItems.value = [...createdItems, ...historyItems.value.filter((item) => item.taskId !== created.taskId)]
saveQueueState()
continue
}
if (!disposed) {
autoQueueEnabled.value = false
saveQueueState()
queueStatus.value = '串行抓取已完成'
await refreshViews()
ElMessage.success('店铺数据抓取队列已完成')
}
} catch (error) {
if (!disposed) { queueStatus.value = error instanceof Error ? error.message : '队列执行失败'; ElMessage.error(queueStatus.value) }
} finally { queueWorkerRunning.value = false; saveQueueState() }
}
function startQueue() {
const runnable = matchedRunnableItems.value
if (!runnable.length) { ElMessage.warning('请先匹配可用店铺'); return }
autoQueueEnabled.value = true
pendingQueue.value = mergeItems(pendingQueue.value, runnable)
saveQueueState()
queueStatus.value = `已加入 ${runnable.length} 条店铺`
void processQueue()
}
async function downloadResult(item: ShopDataCrawlHistoryItem) {
if (!item.resultId) return
const filename = item.outputFilename || `${item.shopName || 'shop-data'}.xlsx`
const result = await saveUrlWithProgress(getShopDataCrawlResultDownloadUrl(item.resultId), filename, `shop-data-crawl:${item.resultId}`)
if (result.success) ElMessage.success(`已保存:${result.path || filename}`)
else if (result.error && result.error !== '用户取消') ElMessage.error(result.error)
}
async function deleteTaskRecord(item: ShopDataCrawlHistoryItem) {
try {
if (!item.taskId && !item.resultId) {
historyItems.value = historyItems.value.filter((row) => row !== item)
ElMessage.success('已删除')
return
}
if (item.taskId && !isTerminal(item.taskStatus)) await deleteShopDataCrawlTask(item.taskId)
else if (item.resultId) await deleteShopDataCrawlHistory(item.resultId)
else if (item.taskId) await deleteShopDataCrawlTask(item.taskId)
else throw new Error('缺少记录标识')
if (item.taskId === activeTaskId.value) clearActiveTask()
historyItems.value = historyItems.value.filter((row) => historyKey(row) !== historyKey(item))
await loadDashboard(); ElMessage.success('已删除')
} catch (error) { ElMessage.error(error instanceof Error ? error.message : '删除失败') }
}
onMounted(async () => {
loadLocalState()
await Promise.allSettled([
loadCandidates(), loadDashboard(), loadHistory(),
getShopDataCrawlCountryPreference().then((preference) => {
if (!countryPrefUserTouched.value && preference.country_codes?.length) orderedCountryCodes.value = preference.country_codes
}).catch(() => undefined),
])
if (activeTaskId.value || pendingQueue.value.length) {
autoQueueEnabled.value = true
queueStatus.value = '检测到未完成队列,正在恢复...'
void processQueue()
}
})
onUnmounted(() => {
disposed = true
if (countryPrefSaveTimer) timers.clearTimer('preference-save', countryPrefSaveTimer)
timers.clearScope()
})
</script>
<style scoped>
.module-page { min-height: 100vh; background: #1a1a1a; }
.main-content { display: flex; height: calc(100vh - 56px); min-height: calc(100vh - 56px); }
.left-panel { width: 400px; padding: 20px; overflow-y: auto; border-right: 1px solid #2a2a2a; background: #1e1e1e; }
.right-panel { flex: 1; min-width: 0; background: #1a1a1a; }
.section-title { margin-bottom: 10px; color: #bbb; font-size: 13px; }
.input-zone { margin-bottom: 18px; padding: 14px; border: 1px dashed #3a3a3a; border-radius: 8px; background: #252525; }
.input-row { display: flex; gap: 10px; }
.opt-btn, .btn-run { min-height: 36px; padding: 0 16px; border: 0; border-radius: 5px; background: #409eff; color: #fff; white-space: nowrap; cursor: pointer; }
.opt-btn { flex: 0 0 auto; }
.opt-btn:disabled, .btn-run:disabled { opacity: .5; cursor: not-allowed; }
.empty-candidates, .empty-tasks { padding: 16px; border: 1px dashed #333; border-radius: 6px; color: #777; font-size: 13px; }
.candidate-table-scroll { margin-bottom: 18px; border: 1px solid #2a2a2a; border-radius: 6px; overflow: hidden; }
.candidate-table { --el-table-bg-color: #252525; --el-table-tr-bg-color: #252525; --el-table-header-bg-color: #2a2a2a; --el-table-text-color: #ccc; --el-table-border-color: #333; }
.link-danger { border: 0; background: transparent; color: #f56c6c; cursor: pointer; }
.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-order-panel { margin-bottom: 8px; padding: 10px; border: 1px solid #333; border-radius: 6px; background: #242424; }
.country-order-caption { margin-bottom: 8px; color: #888; 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; }
.country-pref-status, .queue-status { color: #8dc4ff; font-size: 12px; line-height: 1.5; }
.run-row { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-top: 16px; }
.btn-queue { background: #67c23a; }
.panel-header { height: 52px; padding: 16px 22px; border-bottom: 1px solid #2a2a2a; color: #eee; font-size: 15px; font-weight: 700; }
.task-list-wrap { height: calc(100% - 52px); padding: 18px 22px 28px; overflow-y: auto; }
.clean-result-summary { display: grid; grid-template-columns: repeat(4, minmax(100px, 1fr)); gap: 12px; margin-bottom: 20px; }
.summary-card { display: flex; min-height: 70px; flex-direction: column; justify-content: center; padding: 12px 16px; border: 1px solid #303030; border-radius: 6px; background: #222; }
.summary-label { margin-bottom: 4px; color: #888; font-size: 12px; }
.summary-card strong { color: #fff; font-size: 22px; }
.subsection-title, .result-subsection-title { margin: 18px 0 10px; color: #bbb; font-size: 13px; font-weight: 700; }
.match-table { margin-bottom: 20px; --el-table-bg-color: #222; --el-table-tr-bg-color: #222; --el-table-header-bg-color: #292929; --el-table-text-color: #ccc; --el-table-border-color: #333; }
.ok { color: #67c23a; }.fail, .error-text { color: #f56c6c; }
.result-list-header { padding: 13px 0; border-bottom: 1px solid #303030; color: #eee; font-weight: 700; }
.task-list { margin: 0; padding: 0; list-style: none; }
:deep(.task-item) { display: flex; align-items: center; justify-content: space-between; gap: 18px; min-height: 84px; padding: 14px 0; border-bottom: 1px solid #2b2b2b; }
:deep(.split-result-main) { min-width: 0; }
:deep(.id) { display: block; margin-bottom: 5px; color: #eee; font-weight: 700; }
:deep(.files) { margin-top: 3px; color: #888; font-size: 12px; }
:deep(.task-right) { display: flex; align-items: center; gap: 10px; }
:deep(.status) { min-width: 52px; font-size: 12px; text-align: center; }
:deep(.status.success) { color: #67c23a; }:deep(.status.failed) { color: #f56c6c; }:deep(.status.running) { color: #e6a23c; }
:deep(.download), :deep(.btn-delete) { padding: 5px 10px; border: 1px solid #444; border-radius: 4px; background: transparent; color: #ccc; cursor: pointer; }
:deep(.download) { border-color: #409eff; color: #8dc4ff; }:deep(.btn-delete) { color: #f56c6c; }
@media (max-width: 900px) { .main-content { height: auto; flex-direction: column; }.left-panel { width: 100%; border-right: 0; }.clean-result-summary { grid-template-columns: repeat(2, 1fr); } }
</style>
@@ -792,7 +792,9 @@ function isResultPreparing(item: SimilarAsinHistoryItem) {
}
function normalizeTaskStatus(item: SimilarAsinHistoryItem) {
return (item.taskStatus || '').toUpperCase()
const taskStatus = (item.taskStatus || '').toUpperCase()
if (taskStatus !== 'FAILED' && isResultFileComplete(item)) return 'SUCCESS'
return taskStatus
}
function isResultBuildFailed(item: SimilarAsinHistoryItem) {
@@ -821,7 +823,16 @@ function canDownload(item: SimilarAsinHistoryItem) {
return Boolean(item.resultId && (item.fileReady || item.downloadUrl))
}
function isResultFileComplete(item: SimilarAsinHistoryItem) {
return Boolean(
item.fileReady
|| item.downloadUrl
|| (item.fileStatus || '').toUpperCase() === 'SUCCESS',
)
}
function fileProgressPercent(item: SimilarAsinHistoryItem) {
if (isResultFileComplete(item)) return 100
const percent = Number(item.fileProgressPercent || 0)
if (Number.isFinite(percent) && percent > 0) {
return Math.max(0, Math.min(100, Math.round(percent)))
@@ -52,6 +52,7 @@ type ActiveNavKey =
| 'pricing'
| 'patrol-delete'
| 'query-asin'
| 'shop-data-crawl'
| 'withdraw'
| 'collect-data'
| 'image-video'
@@ -85,7 +86,7 @@ const active = props.active
const showNav = props.showNav
const showHomeLink = props.showHomeLink
const showActions = props.showActions
const allowedColumnKeys = ref<string[] | null>(null)
const allowedColumnKeys = ref<string[]>([])
const navGroups: ReadonlyArray<NavGroup> = [
{
@@ -114,6 +115,7 @@ const navGroups: ReadonlyArray<NavGroup> = [
{ key: 'pricing', label: '跟价', href: '/new_web_source/price-track.html', aliases: ['price-track'] },
{ key: 'patrol-delete', label: '巡店删除', href: '/new_web_source/patrol-delete.html' },
{ key: 'query-asin', label: '查询ASIN', href: '/new_web_source/query-asin.html' },
{ key: 'shop-data-crawl', label: '店铺数据抓取', href: '/new_web_source/shop-data-crawl.html', columnKey: 'shop_data_crawl', aliases: ['shop-data-crawl'] },
{ key: 'withdraw', label: '取款', href: '/new_web_source/withdraw.html' },
{ key: 'shop-status', label: '店铺状态查询' },
],
@@ -135,9 +137,6 @@ function getItemPermissionKeys(item: NavItem) {
}
const visibleNavGroups = computed(() => {
if (allowedColumnKeys.value === null) {
return navGroups
}
const allowedSet = new Set(allowedColumnKeys.value)
const groups = navGroups.flatMap((group) => {
@@ -150,14 +149,14 @@ const visibleNavGroups = computed(() => {
return items.length ? [{ ...group, items }] : []
})
return groups.length ? groups : navGroups
return groups
})
onMounted(async () => {
try {
allowedColumnKeys.value = await getCurrentUserAppColumnKeys()
} catch (_error) {
allowedColumnKeys.value = null
allowedColumnKeys.value = []
}
})
</script>
@@ -5,8 +5,9 @@
<a class="back-link" href="/home">返回首页</a>
</header>
<main class="entrances" aria-label="视频入口">
<main class="entrances" :class="{ 'is-loading': permissionsLoading }" aria-label="视频与图片入口">
<button
v-if="hasMenuPermission('digital-human')"
type="button"
class="entrance-btn"
:disabled="launching"
@@ -14,12 +15,15 @@
>
数字人
</button>
<button type="button" class="entrance-btn" @click="openDeliveryWorkspace">
<button v-if="hasMenuPermission('delivery-video')" type="button" class="entrance-btn" @click="openDeliveryWorkspace">
带货视频
</button>
<button type="button" class="entrance-btn" @click="showSoon('混剪')">
<button v-if="hasMenuPermission('mix-video')" type="button" class="entrance-btn" @click="showSoon('混剪')">
混剪
</button>
<a v-if="hasMenuPermission('image')" class="entrance-btn" href="/image">
图片
</a>
</main>
<div class="toast" :class="{ show: Boolean(statusText), error: statusType === 'error' }">
@@ -64,17 +68,20 @@
</template>
<script setup lang="ts">
import { onBeforeUnmount, ref } from 'vue'
import { onBeforeUnmount, onMounted, ref } from 'vue'
import PageShell from '@/components/layout/PageShell.vue'
import BrandTopBar from '@/pages/brand/components/BrandTopBar.vue'
import DeliveryVideoWorkspace from '@/pages/image-video/components/DeliveryVideoWorkspace.vue'
import { getCurrentUserAppColumnKeys } from '@/shared/api/permission'
import { getPywebviewApi } from '@/shared/bridges/pywebview'
import DownloadProgressPanel from '@/shared/components/DownloadProgressPanel.vue'
type ViewMode = 'menu' | 'delivery'
const currentView = ref<ViewMode>('menu')
const allowedColumnKeys = ref(new Set<string>())
const permissionsLoading = ref(true)
const launching = ref(false)
const statusText = ref('')
const statusType = ref<'normal' | 'error'>('normal')
@@ -103,6 +110,20 @@ function showSoon(name: string) {
showStatus(`${name}暂未开通,敬请期待`)
}
function hasMenuPermission(columnKey: string) {
return allowedColumnKeys.value.has('wb') || allowedColumnKeys.value.has(columnKey)
}
onMounted(async () => {
try {
allowedColumnKeys.value = new Set(await getCurrentUserAppColumnKeys())
} catch (_error) {
showStatus('菜单权限加载失败', 'error')
} finally {
permissionsLoading.value = false
}
})
function openDeliveryWorkspace() {
currentView.value = 'delivery'
}
@@ -265,6 +286,10 @@ async function launchDesktop() {
justify-content: center;
}
.entrances.is-loading {
visibility: hidden;
}
.entrance-btn {
width: 160px;
height: 100px;
+193
View File
@@ -1569,6 +1569,191 @@ export function deleteQueryAsinHistory(resultId: number) {
);
}
// ========== 店铺数据抓取 ==========
export interface ShopDataCrawlCandidateVo {
id: number;
shop_name: string;
created_at?: string;
}
export interface ShopDataCrawlCountryPreferenceVo {
country_codes: string[];
}
export interface ShopDataCrawlShopItem {
shopName: string;
matched: boolean;
shopId?: string;
platform?: string;
companyName?: string;
openStoreUrl?: string;
matchedUserId?: number;
matchStatus?: string;
matchMessage?: string;
}
export interface ShopDataCrawlMatchVo {
items: ShopDataCrawlShopItem[];
}
export interface ShopDataCrawlDashboardVo {
candidateCount: number;
processedTaskCount: number;
successTaskCount: number;
failedTaskCount: number;
}
export interface ShopDataCrawlHistoryItem extends ShopDataCrawlShopItem {
resultId?: number;
taskId?: number;
taskStatus?: string;
success?: boolean;
error?: string;
fileReady?: boolean;
fileStatus?: string;
downloadUrl?: string;
outputFilename?: string;
createdAt?: string;
finishedAt?: string;
}
export interface ShopDataCrawlHistoryVo {
items: ShopDataCrawlHistoryItem[];
}
export interface ShopDataCrawlTaskSummary {
id?: number;
status?: string;
errorMessage?: string;
createdAt?: string;
finishedAt?: string;
countryCodes?: string[];
}
export interface ShopDataCrawlTaskDetailVo {
task?: ShopDataCrawlTaskSummary;
items?: ShopDataCrawlHistoryItem[];
}
export interface ShopDataCrawlTaskBatchVo {
items: Array<ShopDataCrawlTaskDetailVo | ShopDataCrawlHistoryItem>;
missingTaskIds?: number[];
}
export interface ShopDataCrawlCreateTaskVo {
taskId: number;
items: ShopDataCrawlHistoryItem[];
}
export function listShopDataCrawlCandidates() {
return unwrapJavaResponse(
get<JavaApiResponse<ShopDataCrawlCandidateVo[]>>(`${JAVA_API_PREFIX}/shop-data-crawl/candidates`, {
params: { user_id: getCurrentUserId() },
}),
);
}
export function addShopDataCrawlCandidate(shopName: string) {
return unwrapJavaResponse(
post<JavaApiResponse<ShopDataCrawlCandidateVo>, { user_id: number; shop_name: string }>(
`${JAVA_API_PREFIX}/shop-data-crawl/candidates`,
{ user_id: getCurrentUserId(), shop_name: shopName },
),
);
}
export function deleteShopDataCrawlCandidate(id: number) {
return unwrapJavaResponse(
del<JavaApiResponse<null>>(`${JAVA_API_PREFIX}/shop-data-crawl/candidates/${id}`, {
params: { user_id: getCurrentUserId() },
}),
);
}
export function getShopDataCrawlCountryPreference() {
return unwrapJavaResponse(
get<JavaApiResponse<ShopDataCrawlCountryPreferenceVo>>(
`${JAVA_API_PREFIX}/shop-data-crawl/country-preference`,
{ params: { user_id: getCurrentUserId() } },
),
);
}
export function putShopDataCrawlCountryPreference(countryCodes: string[]) {
return unwrapJavaResponse(
put<JavaApiResponse<ShopDataCrawlCountryPreferenceVo>, { user_id: number; country_codes: string[] }>(
`${JAVA_API_PREFIX}/shop-data-crawl/country-preference`,
{ user_id: getCurrentUserId(), country_codes: countryCodes },
),
);
}
export function matchShopDataCrawlShops(shopNames: string[]) {
return unwrapJavaResponse(
post<JavaApiResponse<ShopDataCrawlMatchVo>, { user_id: number; shop_names: string[] }>(
`${JAVA_API_PREFIX}/shop-data-crawl/match-shops`,
{ user_id: getCurrentUserId(), shop_names: shopNames },
),
);
}
export function getShopDataCrawlDashboard() {
return unwrapJavaResponse(
get<JavaApiResponse<ShopDataCrawlDashboardVo>>(`${JAVA_API_PREFIX}/shop-data-crawl/dashboard`, {
params: { user_id: getCurrentUserId() },
}),
);
}
export function getShopDataCrawlHistory() {
return unwrapJavaResponse(
get<JavaApiResponse<ShopDataCrawlHistoryVo>>(`${JAVA_API_PREFIX}/shop-data-crawl/history`, {
params: { user_id: getCurrentUserId() },
}),
);
}
export function createShopDataCrawlTask(items: ShopDataCrawlShopItem[], countryCodes: string[]) {
return unwrapJavaResponse(
post<
JavaApiResponse<ShopDataCrawlCreateTaskVo>,
{ user_id: number; items: ShopDataCrawlShopItem[]; country_codes: string[] }
>(`${JAVA_API_PREFIX}/shop-data-crawl/tasks`, {
user_id: getCurrentUserId(),
items,
country_codes: countryCodes,
}),
);
}
export function getShopDataCrawlTaskProgressBatch(taskIds: number[]) {
return postTaskProgressBatch<ShopDataCrawlTaskBatchVo>(
`${JAVA_API_PREFIX}/shop-data-crawl/tasks/progress/batch`,
taskIds,
);
}
export function getShopDataCrawlResultDownloadUrl(resultId: number) {
return getJavaDownloadUrl(`/shop-data-crawl/results/${resultId}/download`);
}
export function deleteShopDataCrawlTask(taskId: number) {
return unwrapJavaResponse(
del<JavaApiResponse<null>>(`${JAVA_API_PREFIX}/shop-data-crawl/tasks/${taskId}`, {
params: { user_id: getCurrentUserId() },
}),
);
}
export function deleteShopDataCrawlHistory(resultId: number) {
return unwrapJavaResponse(
del<JavaApiResponse<null>>(`${JAVA_API_PREFIX}/shop-data-crawl/history/${resultId}`, {
params: { user_id: getCurrentUserId() },
}),
);
}
// ========== 取款 ==========
export type WithdrawCandidateVo = QueryAsinCandidateVo;
@@ -3324,6 +3509,14 @@ export function getPublishHistory() {
);
}
export function deletePublishTask(taskId: number) {
return unwrapJavaResponse(
del<JavaApiResponse<null>>(`${JAVA_API_PREFIX}/publish/tasks/${taskId}`, {
params: { user_id: getCurrentUserId() },
}),
);
}
export function getJavaDownloadUrl(path: string) {
let raw =
path.startsWith("http://") || path.startsWith("https://")
+4 -10
View File
@@ -8,6 +8,10 @@ export interface PermissionMenuItem {
route_path?: string
routePath?: string
menu_type?: string
parent_id?: number | string | null
parentId?: number | string | null
root_column_key?: string
rootColumnKey?: string
sort_order?: number
created_at?: string
}
@@ -54,16 +58,6 @@ export async function getCurrentUserAppColumnKeys() {
const uid = getCurrentUserId()
const cacheKey = getAppPermissionCacheKey(uid)
try {
const cachedItems = JSON.parse(window.localStorage.getItem(cacheKey) || 'null') as PermissionMenuItem[] | null
if (Array.isArray(cachedItems)) {
const cachedKeys = normalizeColumnKeys(cachedItems)
if (cachedKeys.length) {
return cachedKeys
}
}
} catch (_error) {}
const headers: Record<string, string> = {}
const token = getAuthToken()
if (token) {
+12
View File
@@ -0,0 +1,12 @@
import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import zhCn from 'element-plus/es/locale/lang/zh-cn'
import dayjs from 'dayjs'
import 'dayjs/locale/zh-cn'
import 'element-plus/dist/index.css'
import '@/styles/main.css'
import BrandShopDataCrawlTab from '@/pages/brand/components/BrandShopDataCrawlTab.vue'
dayjs.locale('zh-cn')
createApp(BrandShopDataCrawlTab).use(ElementPlus, { locale: zhCn }).mount('#app')
+1
View File
@@ -54,6 +54,7 @@ export default defineConfig({
'price-track': resolve(__dirname, 'price-track.html'),
'patrol-delete': resolve(__dirname, 'patrol-delete.html'),
'query-asin': resolve(__dirname, 'query-asin.html'),
'shop-data-crawl': resolve(__dirname, 'shop-data-crawl.html'),
withdraw: resolve(__dirname, 'withdraw.html'),
'collect-data': resolve(__dirname, 'collect-data.html'),
'image-video': resolve(__dirname, 'image-video.html'),