改造后台权限和APP权限
This commit is contained in:
@@ -20,6 +20,24 @@
|
||||
</div>
|
||||
|
||||
<!-- 按指定ASIN文档:上传 -->
|
||||
<div class="section-title">执行配置</div>
|
||||
<div class="mode-zone">
|
||||
<label class="mode-check-row">
|
||||
<input v-model="executionMode" type="radio" class="mode-check-input" value="FINITE" />
|
||||
<span class="mode-check-text">执行固定轮次</span>
|
||||
<span class="mode-hint-inline">按整批店铺重复执行指定次数</span>
|
||||
</label>
|
||||
<label class="mode-check-row">
|
||||
<input v-model="executionMode" type="radio" class="mode-check-input" value="INFINITE" />
|
||||
<span class="mode-check-text">无限执行</span>
|
||||
<span class="mode-hint-inline">上一轮全部成功后立即开始下一轮</span>
|
||||
</label>
|
||||
<div v-if="executionMode === 'FINITE'" class="loop-round-input">
|
||||
<span class="hint">轮次</span>
|
||||
<el-input-number v-model="roundCount" :min="1" :max="9999" :disabled="hasRunningLoop || pushing" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-if="asinModeEnabled">
|
||||
<div class="section-title">ASIN文档 <span class="tag-badge">自定义模式</span></div>
|
||||
<div class="upload-zone">
|
||||
@@ -101,12 +119,23 @@
|
||||
<button type="button" class="btn-run" :disabled="matching" @click="runMatch">
|
||||
{{ matching ? '匹配中…' : '匹配店铺' }}
|
||||
</button>
|
||||
<button type="button" class="btn-run btn-queue" :disabled="pushing || !hasValidMode" @click="pushToPythonQueue">
|
||||
<button type="button" class="btn-run btn-queue" :disabled="pushing || !hasValidMode" @click="pushToPythonLoopQueue">
|
||||
{{ pushing ? '推送中…' : '推送到 Python 队列' }}
|
||||
</button>
|
||||
</div>
|
||||
<p class="loading-msg">上传ASIN文档和多选店铺<strong>可同时生效</strong>,跟价结果合并输出。全量和自定义的区别仅在于ASIN来源不同。</p>
|
||||
|
||||
<div v-if="activeLoopRun" class="loop-status-card">
|
||||
<div class="loop-status-line">循环任务 ID:{{ activeLoopRun.id }}</div>
|
||||
<div class="loop-status-line">状态:{{ activeLoopRun.status || '-' }}</div>
|
||||
<div class="loop-status-line">进度:{{ loopProgressText }}</div>
|
||||
<div class="loop-status-line">当前店铺:{{ ((activeLoopRun.currentShopIndex || 0) + 1) }}/{{ activeLoopRun.totalShopCount || 0 }}</div>
|
||||
<div v-if="activeLoopRun.errorMessage" class="loop-status-line loop-error">{{ activeLoopRun.errorMessage }}</div>
|
||||
<button type="button" class="opt-btn loop-stop-btn" :disabled="!hasRunningLoop || stopLoopPending" @click="stopLoopExecution">
|
||||
{{ stopLoopPending ? '停止中...' : '停止循环' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="queuePushResult" class="queue-debug-card">
|
||||
<div class="section-title queue-debug-title">推送结果</div>
|
||||
<div class="queue-debug-line">{{ queuePushResult }}</div>
|
||||
@@ -176,6 +205,7 @@
|
||||
<li v-for="item in currentSectionItems" :key="`cur-${item.resultId}-${item.taskId}`"
|
||||
class="task-item split-result-item">
|
||||
<div class="left split-result-main">
|
||||
<div v-if="item.roundIndex" class="files">轮次:第 {{ item.roundIndex }} 轮</div>
|
||||
<span class="id" :title="item.shopName || ''">{{ item.shopName || '-' }}</span>
|
||||
<div class="files">任务 ID:{{ item.taskId ?? '-' }}</div>
|
||||
<div v-if="item.platform" class="files">平台:{{ item.platform }}</div>
|
||||
@@ -198,6 +228,7 @@
|
||||
<li v-for="item in historySectionItems" :key="`his-${item.resultId}-${item.taskId}`"
|
||||
class="task-item split-result-item">
|
||||
<div class="left split-result-main">
|
||||
<div v-if="item.roundIndex" class="files">轮次:第 {{ item.roundIndex }} 轮</div>
|
||||
<span class="id" :title="item.shopName || ''">{{ item.shopName || '-' }}</span>
|
||||
<div class="files">任务 ID:{{ item.taskId ?? '-' }}</div>
|
||||
<div v-if="item.outputFilename" class="files">文件:{{ item.outputFilename }}</div>
|
||||
@@ -230,7 +261,10 @@ import { expandBrandFolderRecursive } from '@/shared/api/brand'
|
||||
import { getPywebviewApi } from '@/shared/bridges/pywebview'
|
||||
import {
|
||||
addPriceTrackCandidate,
|
||||
completePriceTrackLoopChild,
|
||||
createPriceTrackTask,
|
||||
createPriceTrackLoopRun,
|
||||
dispatchNextPriceTrackLoopRun,
|
||||
deletePendingPriceTrackShopResult,
|
||||
deletePriceTrackCandidate,
|
||||
deletePriceTrackHistory,
|
||||
@@ -238,17 +272,21 @@ import {
|
||||
getPriceTrackCountryPreference,
|
||||
getPriceTrackDashboard,
|
||||
getPriceTrackHistory,
|
||||
getPriceTrackLoopRun,
|
||||
getPriceTrackResultDownloadUrl,
|
||||
getPriceTrackTaskProgressBatch,
|
||||
getPriceTrackTasksBatch,
|
||||
listPriceTrackCandidates,
|
||||
matchPriceTrackShops,
|
||||
putPriceTrackCountryPreference,
|
||||
stopPriceTrackLoopRun,
|
||||
type PriceTrackAsinParsedRow,
|
||||
type PriceTrackCandidateVo,
|
||||
type PriceTrackCreateTaskVo,
|
||||
type PriceTrackDashboardVo,
|
||||
type PriceTrackExecutionMode,
|
||||
type PriceTrackHistoryItem,
|
||||
type PriceTrackLoopRunVo,
|
||||
type PriceTrackShopQueueItem,
|
||||
type PriceTrackTaskDetailVo,
|
||||
} from '@/shared/api/java-modules'
|
||||
@@ -273,9 +311,15 @@ const pushing = ref(false)
|
||||
const queuePushResult = ref('')
|
||||
const queuePayloadText = ref('')
|
||||
const matchAsinRowsByCountry = ref<Record<string, PriceTrackAsinParsedRow[]>>({})
|
||||
const executionMode = ref<PriceTrackExecutionMode>('FINITE')
|
||||
const roundCount = ref(1)
|
||||
const activeLoopRunId = ref<number | null>(null)
|
||||
const activeLoopRun = ref<PriceTrackLoopRunVo | null>(null)
|
||||
const loopDispatching = ref(false)
|
||||
const stopLoopPending = ref(false)
|
||||
|
||||
// 跟价模式(可同时启用)
|
||||
const statusModeEnabled = ref(false) // 按商品状态(全量)
|
||||
const statusModeEnabled = ref(true) // 按商品状态(全量)
|
||||
const asinModeEnabled = ref(false) // 按指定ASIN文档(自定义)
|
||||
|
||||
// 上传文件
|
||||
@@ -283,6 +327,19 @@ const asinFiles = ref<string[]>([]) // ASIN文档
|
||||
|
||||
// 是否有有效模式
|
||||
const hasValidMode = computed(() => statusModeEnabled.value || asinModeEnabled.value)
|
||||
const loopIsTerminal = computed(() => {
|
||||
const status = activeLoopRun.value?.status || ''
|
||||
return status === 'SUCCESS' || status === 'FAILED' || status === 'STOPPED'
|
||||
})
|
||||
const hasRunningLoop = computed(() => !!activeLoopRunId.value && !loopIsTerminal.value)
|
||||
const loopProgressText = computed(() => {
|
||||
const loop = activeLoopRun.value
|
||||
if (!loop) return ''
|
||||
const round = loop.currentRound || 1
|
||||
if (loop.executionMode === 'INFINITE') return `第 ${round} 轮(无限)`
|
||||
const total = Math.max(1, loop.targetRounds || roundCount.value || 1)
|
||||
return `第 ${round}/${total} 轮`
|
||||
})
|
||||
|
||||
const orderedCountryCodes = ref<string[]>(['DE', 'UK', 'FR', 'IT', 'ES'])
|
||||
const dragCountryIndex = ref<number | null>(null)
|
||||
@@ -324,6 +381,10 @@ function matchedItemsStorageKey() {
|
||||
return `price-track:matched-items:${uidForStorage()}`
|
||||
}
|
||||
|
||||
function activeLoopStorageKey() {
|
||||
return `price-track:active-loop:${uidForStorage()}`
|
||||
}
|
||||
|
||||
function setStorageJson(key: string, value: unknown, shouldRemove: boolean) {
|
||||
if (typeof window === 'undefined') return
|
||||
try {
|
||||
@@ -407,6 +468,24 @@ function saveMatchedItemsToStorage() {
|
||||
setStorageJson(matchedItemsStorageKey(), matchedItems.value, matchedItems.value.length === 0)
|
||||
}
|
||||
|
||||
function loadActiveLoopRunIdFromStorage() {
|
||||
try {
|
||||
const raw = typeof window !== 'undefined' ? window.localStorage.getItem(activeLoopStorageKey()) : null
|
||||
if (!raw) {
|
||||
activeLoopRunId.value = null
|
||||
return
|
||||
}
|
||||
const parsed = Number(JSON.parse(raw))
|
||||
activeLoopRunId.value = Number.isFinite(parsed) && parsed > 0 ? parsed : null
|
||||
} catch {
|
||||
activeLoopRunId.value = null
|
||||
}
|
||||
}
|
||||
|
||||
function saveActiveLoopRunId() {
|
||||
setStorageJson(activeLoopStorageKey(), activeLoopRunId.value, !activeLoopRunId.value)
|
||||
}
|
||||
|
||||
// ========== 文件上传 ==========
|
||||
async function selectAsinFile() {
|
||||
const api = getPywebviewApi()
|
||||
@@ -704,15 +783,18 @@ function rowKeyForMatch(row: { shopName?: string; shopId?: number | string | nul
|
||||
function removeMatchedRowsLocally(rows: Array<{ shopName?: string; shopId?: number | string | null }>) {
|
||||
if (!rows.length) return
|
||||
const keys = new Set(rows.map((row) => rowKeyForMatch(row)))
|
||||
matchedItems.value = matchedItems.value.filter((row) => !keys.has(rowKeyForMatch(row)))
|
||||
const names = new Set(rows.map((row) => (row.shopName || '').trim()).filter((name) => !!name))
|
||||
matchedItems.value = matchedItems.value.filter((row) => {
|
||||
const key = rowKeyForMatch(row)
|
||||
const name = (row.shopName || '').trim()
|
||||
return !keys.has(key) && (!name || !names.has(name))
|
||||
})
|
||||
saveMatchedItemsToStorage()
|
||||
}
|
||||
|
||||
async function removeMatchedRow(row: PriceTrackShopQueueItem) {
|
||||
const key = rowKeyForMatch(row)
|
||||
const backup = [...matchedItems.value]
|
||||
matchedItems.value = matchedItems.value.filter((r) => rowKeyForMatch(r) !== key)
|
||||
saveMatchedItemsToStorage()
|
||||
removeMatchedRowsLocally([row])
|
||||
const name = (row.shopName || '').trim()
|
||||
if (!name) {
|
||||
ElMessage.success('已从匹配结果中移除')
|
||||
@@ -942,6 +1024,185 @@ async function pushToPythonQueue() {
|
||||
}
|
||||
}
|
||||
|
||||
function setLoopRun(next: PriceTrackLoopRunVo | null) {
|
||||
activeLoopRun.value = next
|
||||
activeLoopRunId.value = next?.id ?? null
|
||||
saveActiveLoopRunId()
|
||||
}
|
||||
|
||||
function clearLoopRunIfTerminal() {
|
||||
if (!activeLoopRun.value) return
|
||||
const status = activeLoopRun.value.status || ''
|
||||
if (status === 'SUCCESS' || status === 'FAILED' || status === 'STOPPED') {
|
||||
activeLoopRun.value = null
|
||||
activeLoopRunId.value = null
|
||||
saveActiveLoopRunId()
|
||||
}
|
||||
}
|
||||
|
||||
async function syncActiveLoopRun() {
|
||||
if (!activeLoopRunId.value) return null
|
||||
try {
|
||||
const loop = await getPriceTrackLoopRun(activeLoopRunId.value)
|
||||
setLoopRun(loop)
|
||||
clearLoopRunIfTerminal()
|
||||
return activeLoopRun.value
|
||||
} catch {
|
||||
activeLoopRun.value = null
|
||||
activeLoopRunId.value = null
|
||||
saveActiveLoopRunId()
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function recordCreatedTask(taskVo: PriceTrackCreateTaskVo) {
|
||||
taskSnapshots.value = {
|
||||
...taskSnapshots.value,
|
||||
[taskVo.taskId]: {
|
||||
task: { id: taskVo.taskId, status: 'RUNNING' },
|
||||
items: taskVo.items,
|
||||
},
|
||||
}
|
||||
taskDetails.value = {
|
||||
...taskDetails.value,
|
||||
[taskVo.taskId]: 'RUNNING',
|
||||
}
|
||||
saveTaskSnapshotsToStorage()
|
||||
saveTaskDetailsToStorage()
|
||||
}
|
||||
|
||||
async function stopLoopExecution() {
|
||||
if (!activeLoopRunId.value || stopLoopPending.value) return
|
||||
stopLoopPending.value = true
|
||||
try {
|
||||
const loop = await stopPriceTrackLoopRun(activeLoopRunId.value)
|
||||
setLoopRun(loop)
|
||||
clearLoopRunIfTerminal()
|
||||
ElMessage.success(loop.status === 'STOPPED' ? '循环已停止' : '已请求停止,当前子任务完成后将收尾')
|
||||
} catch (error) {
|
||||
ElMessage.error(error instanceof Error ? error.message : '停止循环失败')
|
||||
} finally {
|
||||
stopLoopPending.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function runLoopExecution(loopId?: number) {
|
||||
if (loopDispatching.value) return
|
||||
if (loopId && activeLoopRunId.value !== loopId) {
|
||||
activeLoopRunId.value = loopId
|
||||
saveActiveLoopRunId()
|
||||
}
|
||||
if (!activeLoopRunId.value) return
|
||||
const api = getPywebviewApi()
|
||||
if (!api?.enqueue_json) {
|
||||
throw new Error('当前环境未启用 pywebview enqueue_json')
|
||||
}
|
||||
loopDispatching.value = true
|
||||
try {
|
||||
while (activeLoopRunId.value) {
|
||||
const loop = await syncActiveLoopRun()
|
||||
if (!loop || loop.status === 'SUCCESS' || loop.status === 'FAILED' || loop.status === 'STOPPED') break
|
||||
|
||||
if (loop.activeTaskId) {
|
||||
addPollingTask(loop.activeTaskId)
|
||||
scheduleNextPoll(true)
|
||||
const finalStatus = await waitForTaskTerminal(loop.activeTaskId)
|
||||
const updatedLoop = await completePriceTrackLoopChild(loop.id, loop.activeTaskId)
|
||||
setLoopRun(updatedLoop)
|
||||
await loadHistory()
|
||||
await loadDashboard()
|
||||
syncPollingIdsWithHistory()
|
||||
clearLoopRunIfTerminal()
|
||||
if (finalStatus !== 'SUCCESS') {
|
||||
throw new Error(updatedLoop.errorMessage || `任务 ${loop.activeTaskId} 执行失败`)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
const dispatch = await dispatchNextPriceTrackLoopRun(loop.id)
|
||||
setLoopRun(dispatch.loopRun || loop)
|
||||
if (!dispatch.childTaskRequest) {
|
||||
clearLoopRunIfTerminal()
|
||||
break
|
||||
}
|
||||
const row = dispatch.childTaskRequest.items?.[0]
|
||||
if (!row) throw new Error('循环任务缺少待执行店铺')
|
||||
const taskVo = await createPriceTrackTask(dispatch.childTaskRequest)
|
||||
recordCreatedTask(taskVo)
|
||||
const queuePayload = buildQueuePayload(taskVo, row)
|
||||
queuePayloadText.value = JSON.stringify(queuePayload, null, 2)
|
||||
const pushResult = await api.enqueue_json(queuePayload)
|
||||
if (!pushResult?.success) {
|
||||
throw new Error(pushResult?.error || `任务 ${taskVo.taskId} 推送失败`)
|
||||
}
|
||||
addPollingTask(taskVo.taskId)
|
||||
scheduleNextPoll(true)
|
||||
removeMatchedRowsLocally([row])
|
||||
queuePushResult.value = `循环任务 ${loop.id} 第 ${dispatch.childTaskRequest.roundIndex || 1} 轮已推送:${row.shopName || taskVo.taskId}`
|
||||
}
|
||||
} finally {
|
||||
loopDispatching.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function pushToPythonLoopQueue() {
|
||||
if (hasRunningLoop.value) {
|
||||
try {
|
||||
await runLoopExecution(activeLoopRunId.value || undefined)
|
||||
} catch (error) {
|
||||
if (error instanceof Error && /子任务不存在|循环任务不存在|404/.test(error.message)) {
|
||||
activeLoopRun.value = null
|
||||
activeLoopRunId.value = null
|
||||
saveActiveLoopRunId()
|
||||
}
|
||||
const message = error instanceof Error ? error.message : '循环执行失败'
|
||||
queuePushResult.value = message
|
||||
ElMessage.error(message)
|
||||
}
|
||||
return
|
||||
}
|
||||
if (!hasValidMode.value) {
|
||||
ElMessage.warning('请至少选择一个跟价模式')
|
||||
return
|
||||
}
|
||||
if (asinModeEnabled.value && !asinFiles.value.length) {
|
||||
ElMessage.warning('请先选择 ASIN 文件')
|
||||
return
|
||||
}
|
||||
const matchedRows = matchedItems.value.filter((item) => item.matched)
|
||||
if (!matchedRows.length) {
|
||||
ElMessage.warning('请先匹配可用店铺')
|
||||
return
|
||||
}
|
||||
pushing.value = true
|
||||
queuePayloadText.value = ''
|
||||
try {
|
||||
const loop = await createPriceTrackLoopRun({
|
||||
userId: 0,
|
||||
statusMode: statusModeEnabled.value,
|
||||
asinMode: asinModeEnabled.value,
|
||||
items: matchedRows,
|
||||
asinFiles: asinFiles.value,
|
||||
countryCodes: [...orderedCountryCodes.value],
|
||||
executionMode: executionMode.value,
|
||||
targetRounds: executionMode.value === 'FINITE' ? Math.max(1, Number(roundCount.value) || 1) : undefined,
|
||||
})
|
||||
setLoopRun(loop)
|
||||
queuePushResult.value = `循环任务 ${loop.id} 已创建`
|
||||
await runLoopExecution(loop.id)
|
||||
clearLoopRunIfTerminal()
|
||||
if (!activeLoopRunId.value) {
|
||||
ElMessage.success('跟价循环已启动')
|
||||
}
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : '启动循环失败'
|
||||
queuePushResult.value = message
|
||||
ElMessage.error(message)
|
||||
} finally {
|
||||
pushing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function getPollIntervalMs() {
|
||||
return document.visibilityState === 'visible' ? 6000 : 20000
|
||||
}
|
||||
@@ -1103,6 +1364,9 @@ async function deleteTaskRecord(item: PriceTrackHistoryItem) {
|
||||
} else if (item.taskId != null) {
|
||||
await deletePriceTrackTask(item.taskId)
|
||||
}
|
||||
if (activeLoopRun.value && ((item.loopRunId != null && item.loopRunId === activeLoopRun.value.id) || (item.taskId != null && item.taskId === activeLoopRun.value.activeTaskId))) {
|
||||
await syncActiveLoopRun()
|
||||
}
|
||||
if (item.taskId != null) removePollingTask(item.taskId)
|
||||
removeMatchedRowsLocally([item])
|
||||
await loadHistory()
|
||||
@@ -1116,15 +1380,33 @@ async function deleteTaskRecord(item: PriceTrackHistoryItem) {
|
||||
|
||||
// ========== 生命周期 ==========
|
||||
onMounted(() => {
|
||||
if (!statusModeEnabled.value && !asinModeEnabled.value) {
|
||||
selectMode('status')
|
||||
}
|
||||
loadPollingIdsFromStorage()
|
||||
loadTaskDetailsFromStorage()
|
||||
loadTaskSnapshotsFromStorage()
|
||||
loadMatchedItemsFromStorage()
|
||||
loadActiveLoopRunIdFromStorage()
|
||||
loadCandidates().catch(() => undefined)
|
||||
loadCountryPreference().catch(() => undefined)
|
||||
loadHistory().catch(() => undefined)
|
||||
loadDashboard().catch(() => undefined)
|
||||
if (pollingTaskIds.value.length) scheduleNextPoll(true)
|
||||
if (activeLoopRunId.value) {
|
||||
void syncActiveLoopRun().then((loop) => {
|
||||
if (loop?.activeTaskId) {
|
||||
addPollingTask(loop.activeTaskId)
|
||||
scheduleNextPoll(true)
|
||||
}
|
||||
if (loop && loop.status !== 'SUCCESS' && loop.status !== 'FAILED' && loop.status !== 'STOPPED') {
|
||||
void runLoopExecution(loop.id)
|
||||
}
|
||||
}).catch(() => {
|
||||
activeLoopRunId.value = null
|
||||
saveActiveLoopRunId()
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
@@ -1258,6 +1540,22 @@ onUnmounted(() => {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.loop-round-input {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.loop-round-input :deep(.el-input-number) {
|
||||
width: 160px;
|
||||
}
|
||||
|
||||
.loop-round-input :deep(.el-input__wrapper) {
|
||||
background-color: #2a2a2a;
|
||||
box-shadow: 0 0 0 1px #3a3a3a inset;
|
||||
}
|
||||
|
||||
.tag-badge {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
@@ -1533,6 +1831,28 @@ onUnmounted(() => {
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.loop-status-card {
|
||||
margin-top: 14px;
|
||||
padding: 12px 14px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid #2f3d2f;
|
||||
background: rgba(39, 174, 96, 0.08);
|
||||
}
|
||||
|
||||
.loop-status-line {
|
||||
font-size: 12px;
|
||||
color: #d6e6d8;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.loop-error {
|
||||
color: #ff8a80;
|
||||
}
|
||||
|
||||
.loop-stop-btn {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.loading-msg code {
|
||||
font-size: 11px;
|
||||
color: #8fd3ff;
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
|
||||
<nav class="nav-sections" aria-label="顶部功能导航">
|
||||
<section
|
||||
v-for="group in navGroups"
|
||||
:key="group.label"
|
||||
v-for="group in visibleNavGroups"
|
||||
:key="group.columnKey"
|
||||
class="nav-section"
|
||||
>
|
||||
<div class="nav-section-title">{{ group.label }}</div>
|
||||
@@ -39,6 +39,10 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
|
||||
import { getCurrentUserAppColumnKeys } from '@/shared/api/permission'
|
||||
|
||||
type ActiveNavKey =
|
||||
| 'brand'
|
||||
| 'dedupe'
|
||||
@@ -55,14 +59,22 @@ type NavItem = {
|
||||
href?: string
|
||||
}
|
||||
|
||||
type NavGroup = {
|
||||
columnKey: string
|
||||
label: string
|
||||
items: ReadonlyArray<NavItem>
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
active: ActiveNavKey
|
||||
}>()
|
||||
|
||||
const active = props.active
|
||||
const allowedColumnKeys = ref<string[] | null>(null)
|
||||
|
||||
const navGroups: ReadonlyArray<{ label: string; items: ReadonlyArray<NavItem> }> = [
|
||||
const navGroups: ReadonlyArray<NavGroup> = [
|
||||
{
|
||||
columnKey: 'brand_front_tools',
|
||||
label: '前端工具',
|
||||
items: [
|
||||
{ key: 'collect', label: '采集数据' },
|
||||
@@ -74,6 +86,7 @@ const navGroups: ReadonlyArray<{ label: string; items: ReadonlyArray<NavItem> }>
|
||||
],
|
||||
},
|
||||
{
|
||||
columnKey: 'brand_operation_tools',
|
||||
label: '运营工具',
|
||||
items: [
|
||||
{ key: 'delete-brand', label: '删除ASIN', href: '/new_web_source/delete-brand.html' },
|
||||
@@ -86,6 +99,7 @@ const navGroups: ReadonlyArray<{ label: string; items: ReadonlyArray<NavItem> }>
|
||||
],
|
||||
},
|
||||
{
|
||||
columnKey: 'brand_logistics_tools',
|
||||
label: '后勤工具',
|
||||
items: [
|
||||
{ key: 'purchase', label: '采购' },
|
||||
@@ -93,6 +107,22 @@ const navGroups: ReadonlyArray<{ label: string; items: ReadonlyArray<NavItem> }>
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const visibleNavGroups = computed(() => {
|
||||
if (allowedColumnKeys.value === null) {
|
||||
return navGroups
|
||||
}
|
||||
const allowedSet = new Set(allowedColumnKeys.value)
|
||||
return navGroups.filter((group) => allowedSet.has(group.columnKey))
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
allowedColumnKeys.value = await getCurrentUserAppColumnKeys()
|
||||
} catch (_error) {
|
||||
allowedColumnKeys.value = null
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -955,6 +955,8 @@ export interface PriceTrackDashboardVo {
|
||||
export interface PriceTrackHistoryItem {
|
||||
resultId?: number;
|
||||
taskId?: number;
|
||||
loopRunId?: number;
|
||||
roundIndex?: number;
|
||||
shopName?: string;
|
||||
shopId?: number | string | null;
|
||||
platform?: string;
|
||||
@@ -998,6 +1000,8 @@ export interface PriceTrackTaskSummary {
|
||||
id?: number;
|
||||
taskNo?: string;
|
||||
status?: string;
|
||||
loopRunId?: number;
|
||||
roundIndex?: number;
|
||||
errorMessage?: string;
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
@@ -1146,6 +1150,45 @@ export interface PriceTrackCreateTaskRequest {
|
||||
items: PriceTrackShopQueueItem[];
|
||||
asinFiles: string[];
|
||||
countryCodes: string[];
|
||||
loopRunId?: number;
|
||||
roundIndex?: number;
|
||||
shopIndex?: number;
|
||||
}
|
||||
|
||||
export type PriceTrackExecutionMode = "FINITE" | "INFINITE";
|
||||
|
||||
export interface PriceTrackLoopRunCreateRequest {
|
||||
userId: number;
|
||||
statusMode: boolean;
|
||||
asinMode: boolean;
|
||||
items: PriceTrackShopQueueItem[];
|
||||
asinFiles: string[];
|
||||
countryCodes: string[];
|
||||
executionMode: PriceTrackExecutionMode;
|
||||
targetRounds?: number;
|
||||
}
|
||||
|
||||
export interface PriceTrackLoopRunVo {
|
||||
id: number;
|
||||
status?: string;
|
||||
executionMode?: PriceTrackExecutionMode;
|
||||
targetRounds?: number;
|
||||
currentRound?: number;
|
||||
currentShopIndex?: number;
|
||||
totalShopCount?: number;
|
||||
activeTaskId?: number;
|
||||
activeTaskStatus?: string;
|
||||
stopRequested?: boolean;
|
||||
errorMessage?: string;
|
||||
statusMode?: boolean;
|
||||
asinMode?: boolean;
|
||||
asinFiles?: string[];
|
||||
countryCodes?: string[];
|
||||
}
|
||||
|
||||
export interface PriceTrackLoopRunDispatchVo {
|
||||
loopRun?: PriceTrackLoopRunVo;
|
||||
childTaskRequest?: PriceTrackCreateTaskRequest;
|
||||
}
|
||||
|
||||
export function createPriceTrackTask(
|
||||
@@ -1176,6 +1219,52 @@ export function getPriceTrackTasksBatch(taskIds: number[]) {
|
||||
);
|
||||
}
|
||||
|
||||
export function createPriceTrackLoopRun(
|
||||
request: Omit<PriceTrackLoopRunCreateRequest, "userId"> | PriceTrackLoopRunCreateRequest,
|
||||
) {
|
||||
return unwrapJavaResponse(
|
||||
post<JavaApiResponse<PriceTrackLoopRunVo>, PriceTrackLoopRunCreateRequest>(
|
||||
`${JAVA_API_PREFIX}/price-track/loop-runs`,
|
||||
{ ...request, userId: getCurrentUserId() },
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
export function getPriceTrackLoopRun(loopRunId: number) {
|
||||
return unwrapJavaResponse(
|
||||
get<JavaApiResponse<PriceTrackLoopRunVo>>(
|
||||
`${JAVA_API_PREFIX}/price-track/loop-runs/${loopRunId}?user_id=${encodeURIComponent(String(getCurrentUserId()))}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
export function dispatchNextPriceTrackLoopRun(loopRunId: number) {
|
||||
return unwrapJavaResponse(
|
||||
post<JavaApiResponse<PriceTrackLoopRunDispatchVo>, undefined>(
|
||||
`${JAVA_API_PREFIX}/price-track/loop-runs/${loopRunId}/dispatch-next?user_id=${encodeURIComponent(String(getCurrentUserId()))}`,
|
||||
undefined,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
export function completePriceTrackLoopChild(loopRunId: number, childTaskId: number) {
|
||||
return unwrapJavaResponse(
|
||||
post<JavaApiResponse<PriceTrackLoopRunVo>, { childTaskId: number }>(
|
||||
`${JAVA_API_PREFIX}/price-track/loop-runs/${loopRunId}/child-finished?user_id=${encodeURIComponent(String(getCurrentUserId()))}`,
|
||||
{ childTaskId },
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
export function stopPriceTrackLoopRun(loopRunId: number) {
|
||||
return unwrapJavaResponse(
|
||||
post<JavaApiResponse<PriceTrackLoopRunVo>, undefined>(
|
||||
`${JAVA_API_PREFIX}/price-track/loop-runs/${loopRunId}/stop?user_id=${encodeURIComponent(String(getCurrentUserId()))}`,
|
||||
undefined,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
export function getPriceTrackTaskProgressBatch(taskIds: number[]) {
|
||||
return unwrapJavaResponse(
|
||||
post<JavaApiResponse<PriceTrackTaskBatchVo>, { taskIds: number[] }>(
|
||||
|
||||
63
frontend-vue/src/shared/api/permission.ts
Normal file
63
frontend-vue/src/shared/api/permission.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { requestGetJson } from '@/shared/api/http'
|
||||
|
||||
export interface PermissionMenuItem {
|
||||
id: number | string
|
||||
name?: string
|
||||
column_key?: string
|
||||
route_path?: string
|
||||
menu_type?: string
|
||||
sort_order?: number
|
||||
created_at?: string
|
||||
}
|
||||
|
||||
interface PermissionMenuResponse {
|
||||
success: boolean
|
||||
items?: PermissionMenuItem[]
|
||||
error?: string
|
||||
}
|
||||
|
||||
function getCurrentUserId() {
|
||||
const raw = typeof window === 'undefined' ? '' : window.localStorage.getItem('uid') || ''
|
||||
const value = Number(raw)
|
||||
if (!Number.isFinite(value) || value <= 0) {
|
||||
throw new Error('未获取到用户ID')
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
function getAppPermissionCacheKey(uid: number) {
|
||||
return `app_column_permissions:${String(uid)}`
|
||||
}
|
||||
|
||||
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)) {
|
||||
return cachedItems
|
||||
.map((item) => String(item.column_key || '').trim().toLowerCase())
|
||||
.filter(Boolean)
|
||||
}
|
||||
} catch (_error) {}
|
||||
|
||||
const res = await requestGetJson<PermissionMenuResponse>(
|
||||
`/api/admin/user/${encodeURIComponent(String(uid))}/column-permissions`,
|
||||
{
|
||||
params: { menu_type: 'app' },
|
||||
},
|
||||
)
|
||||
|
||||
if (!res.success) {
|
||||
throw new Error(res.error || '获取菜单权限失败')
|
||||
}
|
||||
|
||||
try {
|
||||
window.localStorage.setItem(cacheKey, JSON.stringify(res.items || []))
|
||||
} catch (_error) {}
|
||||
|
||||
return (res.items || [])
|
||||
.map((item) => String(item.column_key || '').trim().toLowerCase())
|
||||
.filter(Boolean)
|
||||
}
|
||||
Reference in New Issue
Block a user