feat(frontend-vue): 亚马逊运营工具台与新功能页接入——新版工具台(amazon-console+tool-catalog)、品牌检测/ASIN变体采集/查询ASIN 三个工具页 Vue 化并对接既有后端与桌面桥接;桌面首页/登录页 Vue 化(沿用旧视觉);补充 vc_*/get_device_id/save_file_from_url 桥类型、dev 代理;品牌默认勾选排名+FBM;修正 query-asin 标题

含未提交新版改造累积:Brand*Tab 套入工具台壳(AmazonToolPageShell/AmazonTopBar)与目录/权限联动。
This commit is contained in:
2026-09-07 11:06:05 +08:00
parent f6c44d1b92
commit d2301db292
43 changed files with 4513 additions and 614 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/amazon-console-main.ts"></script>
</body>
</html>
+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/brand-main.ts"></script>
</body>
</html>
+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/home-main.ts"></script>
</body>
</html>
+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/login-main.ts"></script>
</body>
</html>
+1 -1
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>巡店删除 - 数富AI</title> <title>查询ASIN - 数富AI</title>
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>
+7
View File
@@ -0,0 +1,7 @@
import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import '@/styles/main.css'
import AmazonConsolePage from '@/pages/amazon/AmazonConsolePage.vue'
createApp(AmazonConsolePage).use(ElementPlus).mount('#app')
+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 BrandBrandTab from '@/pages/brand/components/BrandBrandTab.vue'
dayjs.locale('zh-cn')
createApp(BrandBrandTab).use(ElementPlus, { locale: zhCn }).mount('#app')
+8
View File
@@ -0,0 +1,8 @@
import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import zhCn from 'element-plus/es/locale/lang/zh-cn'
import 'element-plus/dist/index.css'
import '@/styles/main.css'
import DesktopHomePage from '@/pages/home/DesktopHomePage.vue'
createApp(DesktopHomePage).use(ElementPlus, { locale: zhCn }).mount('#app')
+8
View File
@@ -0,0 +1,8 @@
import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import zhCn from 'element-plus/es/locale/lang/zh-cn'
import 'element-plus/dist/index.css'
import '@/styles/main.css'
import DesktopLoginPage from '@/pages/login/DesktopLoginPage.vue'
createApp(DesktopLoginPage).use(ElementPlus, { locale: zhCn }).mount('#app')
@@ -0,0 +1,643 @@
<template>
<div class="page-shell module-page amazon-console">
<AmazonTopBar />
<main class="console-body">
<!-- Hero 宣传区 -->
<section class="hero">
<div class="hero-inner">
<div class="hero-left">
<span class="hero-pill">运营辅助 · 数据安全 · 高效跟价</span>
<h1 class="hero-title">让亚马逊运营更清晰高效</h1>
<p class="hero-sub">
从采集去重查品到上架跟价20 个工具按前端 · 运营 · 后勤三线组织每条工具一句话讲清用途标准流程一步不缺
</p>
<div class="hero-stats">
<div class="stat">
<strong style="color: #2dd4bf">20</strong>
<span>个工具</span>
</div>
<div class="stat">
<strong style="color: #60a5fa">3</strong>
<span>大分类</span>
</div>
<div class="stat">
<strong style="color: #f59e0b">8</strong>
<span>前端标准流程步骤</span>
</div>
</div>
</div>
<div class="hero-card">
<div class="hero-card-head">
<div class="hero-card-dots">
<i class="dot dot--teal"></i>
<i class="dot dot--blue"></i>
<i class="dot dot--brand"></i>
<span class="dots-line"></span>
</div>
<span class="hero-card-caption">数富AI · 运营工作台</span>
</div>
<h2 class="hero-card-title">让每一次运营动作<br />都有清晰的工作流</h2>
<div class="hero-card-divider"></div>
<p class="hero-card-sub">内置完整使用教程从软件配置到运营上架每一步流程清晰可查</p>
<button type="button" class="hero-card-btn" @click="scrollToTools">查看工具</button>
</div>
</div>
</section>
<!-- 三组工具标题 + 标准工作流 + 卡片墙 -->
<section v-for="group in visibleGroups" :key="group.key" class="tool-group" :id="`group-${group.key}`">
<header class="group-head">
<i class="group-dot" :style="{ background: group.color }"></i>
<span class="group-name">{{ group.name }}</span>
<span class="group-desc">{{ group.desc }}</span>
<span class="group-count">{{ group.tools.length }} 个工具</span>
</header>
<!-- 标准工作流时间线 -->
<section v-if="visibleWorkflow(group)?.length" class="workflow-panel">
<div class="workflow-head">
<span class="workflow-title">{{ group.workflowTitle }}</span>
<span class="workflow-badge" :style="{ color: group.color }">{{ group.workflowBadge }}</span>
<span class="workflow-hint">{{ group.workflowHint }}</span>
</div>
<ol class="workflow-steps">
<li
v-for="(step, index) in visibleWorkflow(group)"
:key="`${group.key}-${index}`"
class="workflow-step"
:class="{ 'workflow-step--disabled': !step.toolId || !stepHref(step.toolId) }"
@click="onWorkflowStepClick(step)"
>
<span class="step-circle" :style="{ background: group.weak, color: group.color }">{{ index + 1 }}</span>
<span class="step-name">{{ step.name }}</span>
<span class="step-desc">{{ step.desc }}</span>
<span v-if="index < visibleWorkflow(group).length - 1" class="step-line" :style="{ background: group.weak }"></span>
</li>
</ol>
</section>
<!-- 卡片墙 -->
<div class="card-grid">
<template v-for="tool in group.tools" :key="tool.id">
<a v-if="resolveHref(tool.href)" :href="resolveHref(tool.href)!" class="tool-card">
<div class="tool-card-top">
<ToolIcon :name="tool.id" />
<span v-if="tool.tag" class="tool-tag" :style="{ color: group.color }">{{ tool.tag }}</span>
</div>
<div class="tool-card-name">{{ tool.name }}</div>
<div class="tool-card-desc">{{ tool.desc }}</div>
<div class="tool-card-go">打开工具 </div>
</a>
<button v-else type="button" class="tool-card tool-card--disabled" @click="onPendingClick(tool)">
<div class="tool-card-top">
<ToolIcon :name="tool.id" />
<span v-if="tool.tag" class="tool-tag" :style="{ color: group.color }">{{ tool.tag }}</span>
</div>
<div class="tool-card-name">{{ tool.name }}</div>
<div class="tool-card-desc">{{ tool.desc }}</div>
<div class="tool-card-go">{{ tool.href ? '仅桌面客户端可用' : '暂未开通' }}</div>
</button>
</template>
</div>
</section>
</main>
<AmazonFooterBar />
<div class="toast" :class="{ show: Boolean(statusText), error: statusType === 'error' }">{{ statusText }}</div>
</div>
</template>
<script setup lang="ts">
import { computed, nextTick, onMounted, ref } from 'vue'
import AmazonTopBar from '@/pages/amazon/components/AmazonTopBar.vue'
import AmazonFooterBar from '@/pages/amazon/components/AmazonFooterBar.vue'
import ToolIcon from '@/pages/amazon/components/ToolIcon.vue'
import { filterGroupsByPermission, filterWorkflowByPermission, TOOL_GROUPS, TOOL_MAP } from '@/pages/amazon/tool-catalog'
import type { ToolInfo, VisibleGroup, WorkflowStep } from '@/pages/amazon/tool-catalog'
import { resolvePageHref } from '@/shared/page-prefix'
import { getCurrentUserAppColumnKeys } from '@/shared/api/permission'
const allowedKeys = ref<string[]>([])
const visibleGroups = ref<VisibleGroup[]>(TOOL_GROUPS.map((group) => ({ ...group, tools: [...group.tools] })))
/** 权限接口异常时为 true:展示全部工具与时间线,避免页面空白 */
const permissionUnavailable = ref(false)
const statusText = ref('')
const statusType = ref<'normal' | 'error'>('normal')
let statusTimer: number | undefined
/** 链接本地化:dev 预览去掉 /new_web_source 前缀;/brand、/web_source/* 等旧静态页仅桌面端存在 */
function resolveHref(rawHref?: string): string {
return resolvePageHref(rawHref)
}
function visibleWorkflow(group: VisibleGroup) {
if (permissionUnavailable.value) return group.workflow ?? []
return group.workflow ? filterWorkflowByPermission(group.workflow, allowedKeys.value) : []
}
function stepHref(toolId?: string) {
if (!toolId) return ''
return resolveHref(TOOL_MAP.get(toolId)?.href ?? '')
}
function onWorkflowStepClick(step: WorkflowStep) {
if (!step.toolId) {
return
}
const target = TOOL_MAP.get(step.toolId)
const href = resolveHref(target?.href ?? '')
if (href) {
window.location.href = href
return
}
showSoon(target?.href ? `${step.name}仅桌面客户端可用` : `${step.name}暂未开通,敬请期待`)
}
function onPendingClick(tool: ToolInfo) {
if (!tool.href) {
showSoon(`${tool.name}暂未开通,敬请期待`)
return
}
showSoon(`${tool.name}仅桌面客户端可用`)
}
function showSoon(name: string) {
statusText.value = `${name}暂未开通,敬请期待`
statusType.value = 'normal'
if (statusTimer) {
window.clearTimeout(statusTimer)
}
statusTimer = window.setTimeout(() => {
statusText.value = ''
}, 2200)
}
function scrollToTools() {
void nextTick(() => {
document.querySelector('.card-grid')?.scrollIntoView({ behavior: 'smooth', block: 'start' })
})
}
function scrollToHashGroup() {
const hash = window.location.hash.replace('#', '')
if (!hash) return
const target = document.getElementById(hash)
if (target) {
void nextTick(() => {
target.scrollIntoView({ behavior: 'smooth', block: 'start' })
})
}
}
onMounted(async () => {
try {
allowedKeys.value = await getCurrentUserAppColumnKeys()
} catch (_error) {
// 权限接口异常时展示全部工具,避免页面空白;正常时按菜单权限过滤
permissionUnavailable.value = true
}
visibleGroups.value = permissionUnavailable.value
? TOOL_GROUPS.map((group) => ({ ...group, tools: [...group.tools] }))
: filterGroupsByPermission(TOOL_GROUPS, allowedKeys.value)
scrollToHashGroup()
})
</script>
<style scoped>
.amazon-console {
display: flex;
flex-direction: column;
min-height: 0;
overflow: hidden;
background: #151a25;
}
.console-body {
flex: 1 1 auto;
min-height: 0;
overflow-y: auto;
overscroll-behavior: contain;
padding: 22px 34px 44px;
background: #151a25;
}
/* ===== Hero ===== */
.hero {
max-width: 1180px;
margin: 0 auto 26px;
border: 1px solid #2e3a52;
border-radius: 16px;
background: linear-gradient(135deg, #1b2233 0%, #151a24 100%);
}
.hero-inner {
display: flex;
gap: 24px;
padding: 26px 28px;
}
.hero-left {
flex: 1 1 auto;
min-width: 0;
}
.hero-pill {
display: inline-block;
padding: 5px 12px;
border-radius: 999px;
background: rgba(45, 212, 191, 0.14);
color: #2dd4bf;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.04em;
}
.hero-title {
margin: 14px 0 0;
color: #f5f8fc;
font-size: 28px;
font-weight: 800;
line-height: 1.3;
}
.hero-sub {
max-width: 620px;
margin: 10px 0 0;
color: #a0acbe;
font-size: 12px;
line-height: 1.7;
}
.hero-stats {
display: flex;
gap: 44px;
margin-top: 22px;
}
.stat strong {
display: block;
font-size: 22px;
font-weight: 800;
line-height: 1.2;
}
.stat span {
display: block;
margin-top: 3px;
color: #5e6878;
font-size: 10px;
}
/* Hero 右侧工作台卡 */
.hero-card {
width: 300px;
flex-shrink: 0;
padding: 18px 20px;
border: 1px solid #2e3a52;
border-radius: 12px;
background: #202a3e;
}
.hero-card-head {
display: flex;
align-items: center;
gap: 12px;
}
.hero-card-dots {
display: flex;
align-items: center;
}
.dot {
width: 10px;
height: 10px;
border-radius: 50%;
}
.dot--teal {
background: #2dd4bf;
}
.dot--blue {
background: #60a5fa;
margin-left: 6px;
}
.dot--brand {
background: #f59e0b;
margin-left: 6px;
}
.dots-line {
display: block;
width: 42px;
height: 2px;
margin-top: 14px;
background: #3e4a62;
}
.hero-card-caption {
color: #5e6878;
font-size: 10px;
}
.hero-card-title {
margin: 16px 0 0;
color: #f5f8fc;
font-size: 17px;
font-weight: 800;
line-height: 1.6;
}
.hero-card-divider {
height: 1px;
margin: 14px 0;
background: #3e4a62;
}
.hero-card-sub {
margin: 0;
color: #a0acbe;
font-size: 11px;
line-height: 1.7;
}
.hero-card-btn {
margin-top: 14px;
padding: 9px 22px;
border: 0;
border-radius: 8px;
background: #f59e0b;
color: #fff;
font: inherit;
font-size: 13px;
font-weight: 700;
cursor: pointer;
transition: background 0.18s ease;
}
.hero-card-btn:hover {
background: #f7b13c;
}
/* ===== 工具分组 ===== */
.tool-group {
max-width: 1180px;
margin: 0 auto 34px;
}
.group-head {
display: flex;
align-items: center;
gap: 10px;
margin: 0 0 14px;
}
.group-dot {
width: 10px;
height: 10px;
border-radius: 2px;
}
.group-name {
color: #f5f8fc;
font-size: 17px;
font-weight: 800;
}
.group-desc {
color: #5e6878;
font-size: 11px;
}
.group-count {
margin-left: auto;
color: #5e6878;
font-size: 11px;
}
/* ===== 工作流时间线 ===== */
.workflow-panel {
margin-bottom: 18px;
padding: 16px 22px 20px;
border: 1px solid #2e3a52;
border-radius: 12px;
background: #1c2333;
}
.workflow-head {
display: flex;
align-items: center;
gap: 12px;
}
.workflow-title {
color: #f5f8fc;
font-size: 14px;
font-weight: 700;
}
.workflow-badge {
padding: 3px 10px;
border-radius: 999px;
background: rgba(245, 158, 11, 0.14);
font-size: 10px;
font-weight: 700;
}
.workflow-hint {
margin-left: auto;
color: #5e6878;
font-size: 10px;
}
.workflow-steps {
display: flex;
align-items: flex-start;
margin: 16px 0 0;
padding: 0;
list-style: none;
}
.workflow-step {
display: flex;
flex-direction: column;
align-items: center;
min-width: 0;
cursor: pointer;
}
.workflow-step--disabled {
cursor: default;
opacity: 0.7;
}
.step-circle {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border-radius: 50%;
font-size: 12px;
font-weight: 800;
}
.step-name {
margin-top: 8px;
color: #c8d2e2;
font-size: 11px;
font-weight: 700;
text-align: center;
}
.step-desc {
margin-top: 2px;
color: #5e6878;
font-size: 9px;
text-align: center;
}
.step-line {
flex: 1 1 auto;
min-width: 12px;
height: 2px;
margin-top: 15px;
align-self: flex-start;
}
/* ===== 卡片墙 ===== */
.card-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 12px;
}
.tool-card {
display: flex;
flex-direction: column;
min-width: 0;
padding: 16px 18px 14px;
border: 2px solid #2e3a52;
border-radius: 12px;
background: #1c2333;
text-align: left;
text-decoration: none;
font: inherit;
cursor: pointer;
transition: border-color 0.18s ease, background 0.18s ease;
}
.tool-card:hover {
border-color: #2dd4bf;
background: #262f42;
}
.tool-card-top {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 8px;
}
.tool-tag {
padding: 3px 9px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.06);
font-size: 9px;
font-weight: 700;
white-space: nowrap;
}
.tool-card-name {
margin-top: 12px;
color: #f5f8fc;
font-size: 15px;
font-weight: 700;
}
.tool-card-desc {
margin-top: 6px;
color: #5e6878;
font-size: 11px;
line-height: 1.6;
}
.tool-card-go {
margin-top: 14px;
color: #5e6878;
font-size: 11px;
transition: color 0.18s ease;
}
.tool-card:hover .tool-card-go {
color: #2dd4bf;
font-weight: 700;
}
.tool-card--disabled {
cursor: default;
opacity: 0.55;
}
.tool-card--disabled:hover {
border-color: #2e3a52;
background: #1c2333;
}
.tool-card--disabled:hover .tool-card-go {
color: #5e6878;
font-weight: 400;
}
/* ===== toast ===== */
.toast {
position: fixed;
bottom: 40px;
left: 50%;
z-index: 1000;
padding: 12px 24px;
max-width: min(420px, calc(100vw - 48px));
border-radius: 8px;
background: rgba(0, 0, 0, 0.75);
color: #fff;
font-size: 14px;
text-align: center;
opacity: 0;
pointer-events: none;
transform: translateX(-50%);
transition: opacity 0.3s;
}
.toast.show {
opacity: 1;
}
.toast.error {
background: rgba(176, 42, 55, 0.92);
}
@media (max-width: 1100px) {
.card-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.hero-inner {
flex-direction: column;
}
.hero-card {
width: 100%;
}
.console-body {
padding: 18px 20px 36px;
}
}
</style>
@@ -0,0 +1,85 @@
<template>
<footer class="amazon-footer">
<div class="amazon-footer__gradient"></div>
<div class="amazon-footer__inner">
<a class="bug-banner" href="https://bug.aishufu.top/bug-tracker/" target="_blank" rel="noopener">
<span class="bug-banner__title">数富AI-BUG登记系统</span>
<span class="bug-banner__url">bug.aishufu.top/bug-tracker/</span>
</a>
<span class="amazon-footer__status">前端 · 运营 · 后勤 三线 20 工具一眼看清用途</span>
</div>
</footer>
</template>
<style scoped>
.amazon-footer {
flex: 0 0 auto;
background: #151a25;
}
.amazon-footer__gradient {
height: 4px;
background: linear-gradient(90deg, #2dd4bf 0%, #60a5fa 55%, #a78bfa 72%, #f59e0b 87%, #f59e0b 100%);
}
.amazon-footer__inner {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 8px 12px;
}
.bug-banner {
flex: 1;
min-width: 0;
display: flex;
align-items: center;
gap: 48px;
padding: 9px 24px;
border: 1px solid #2dd4bf;
border-radius: 8px;
background: #1e293b;
text-decoration: none;
transition: background 0.18s ease, border-color 0.18s ease;
}
.bug-banner:hover {
background: #0f172a;
border-color: #60a5fa;
}
.bug-banner__title {
color: #2dd4bf;
font-size: 13px;
font-weight: 800;
white-space: nowrap;
}
.bug-banner__url {
min-width: 0;
overflow: hidden;
color: #94a3b8;
font-size: 11px;
text-overflow: ellipsis;
white-space: nowrap;
}
.amazon-footer__status {
flex-shrink: 0;
color: #c8d2e2;
font-size: 12px;
white-space: nowrap;
}
@media (max-width: 900px) {
.bug-banner {
gap: 12px;
padding: 8px 14px;
}
.amazon-footer__status {
display: none;
}
}
</style>
@@ -0,0 +1,112 @@
<template>
<div class="amazon-tool-shell">
<AmazonTopBar :active-cat="groupKey" />
<div class="tool-head">
<a class="tool-head__back" :href="backHref"> 返回{{ group ? group.name : '工具' }}列表</a>
<span class="tool-head__sep"></span>
<span class="tool-head__title">{{ tool?.name ?? '' }}</span>
<span class="tool-head__desc">{{ detailText }}</span>
</div>
<div class="tool-shell-body">
<slot />
</div>
<AmazonFooterBar />
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import AmazonTopBar from '@/pages/amazon/components/AmazonTopBar.vue'
import AmazonFooterBar from '@/pages/amazon/components/AmazonFooterBar.vue'
import { TOOL_DETAILS, TOOL_GROUPS, TOOL_MAP } from '@/pages/amazon/tool-catalog'
import { resolvePageHref } from '@/shared/page-prefix'
const props = defineProps<{
/** 工具 id(见 tool-catalog TOOL_MAP */
toolId: string
}>()
const tool = computed(() => TOOL_MAP.get(props.toolId))
const group = computed(() => TOOL_GROUPS.find((g) => g.tools.some((t) => t.id === props.toolId)))
const groupKey = computed(() => group.value?.key ?? 'front')
const detailText = computed(() => {
if (!tool.value) return ''
return TOOL_DETAILS[tool.value.id] ?? tool.value.desc
})
/** 返回工具台列表对应分组(带锚点) */
const backHref = computed(() => `${resolvePageHref('/new_web_source/amazon-console.html')}#group-${groupKey.value}`)
</script>
<style scoped>
.amazon-tool-shell {
display: flex;
flex-direction: column;
flex: 1 1 auto;
min-height: 0;
}
.tool-head {
flex: 0 0 auto;
display: flex;
align-items: center;
gap: 14px;
min-height: 62px;
padding: 10px 34px;
background: #151a25;
border-bottom: 1px solid #2a3344;
}
.tool-head__back {
flex-shrink: 0;
color: #c8d2e2;
font-size: 13px;
text-decoration: none;
white-space: nowrap;
}
.tool-head__back:hover {
color: #fff;
}
.tool-head__sep {
flex-shrink: 0;
width: 1px;
height: 18px;
background: #3e4a62;
}
.tool-head__title {
flex-shrink: 0;
color: #f5f8fc;
font-size: 20px;
font-weight: 800;
white-space: nowrap;
}
.tool-head__desc {
min-width: 0;
overflow: hidden;
color: #a0acbe;
font-size: 12px;
text-overflow: ellipsis;
white-space: nowrap;
}
.tool-shell-body {
flex: 1 1 auto;
min-height: 0;
overflow: hidden;
}
@media (max-width: 1100px) {
.tool-head {
padding: 10px 16px;
flex-wrap: wrap;
gap: 8px;
}
}
</style>
@@ -0,0 +1,249 @@
<template>
<header class="amazon-top">
<div class="brand">
<span class="brand-logo"></span>
<div class="brand-text">
<span class="brand-name">数富AI</span>
<span class="brand-sub">亚马逊运营工具台</span>
</div>
</div>
<nav class="cat-tabs" aria-label="分类导航">
<template v-for="cat in visibleCats" :key="cat.key">
<a
class="cat-tab"
:class="{ 'cat-tab--active': cat.key === activeCat }"
:href="`${toolbarHref}#group-${cat.key}`"
>
<span class="cat-dot" :style="{ background: cat.color }"></span>
<span class="cat-name">{{ cat.name }}</span>
<span class="cat-badge" :style="badgeStyle(cat)">{{ cat.tools.length }}</span>
<span class="cat-underline" :style="{ background: cat.color }"></span>
</a>
</template>
</nav>
<div class="top-right">
<a href="/home" class="btn-home">返回首页</a>
<BrandApiSecretSettingsButton />
<span class="help-mark" title="帮助">?</span>
</div>
</header>
</template>
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue'
import BrandApiSecretSettingsButton from '@/pages/brand/components/BrandApiSecretSettingsButton.vue'
import { filterGroupsByPermission, TOOL_GROUPS } from '@/pages/amazon/tool-catalog'
import type { VisibleGroup } from '@/pages/amazon/tool-catalog'
import { resolvePageHref } from '@/shared/page-prefix'
import { getCurrentUserAppColumnKeys } from '@/shared/api/permission'
const props = withDefaults(
defineProps<{
/** 当前工具所属分类(功能页高亮对应 tab;工具台首页不传) */
activeCat?: 'front' | 'ops' | 'logi'
}>(),
{
activeCat: undefined,
},
)
const activeCat = computed(() => props.activeCat)
const visibleCats = ref<VisibleGroup[]>(TOOL_GROUPS.map((group) => ({ ...group, tools: [...group.tools] })))
/** 分类 tab 点击回到工具台对应分组 */
const toolbarHref = resolvePageHref('/new_web_source/amazon-console.html')
function badgeStyle(cat: VisibleGroup) {
return activeCat.value === cat.key ? { background: cat.color } : {}
}
onMounted(async () => {
try {
const keys = await getCurrentUserAppColumnKeys()
visibleCats.value = filterGroupsByPermission(TOOL_GROUPS, keys)
} catch (_error) {
// 权限接口异常时展示全部分类,避免顶栏空白
visibleCats.value = TOOL_GROUPS.map((group) => ({ ...group, tools: [...group.tools] }))
}
})
</script>
<style scoped>
.amazon-top {
flex: 0 0 auto;
min-height: 64px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 20px;
padding: 0 24px;
background: #151a25;
border-bottom: 1px solid #2a3344;
}
.brand {
display: flex;
align-items: center;
gap: 12px;
flex-shrink: 0;
}
.brand-logo {
display: flex;
align-items: center;
justify-content: center;
width: 38px;
height: 38px;
border-radius: 9px;
background: #f59e0b;
color: #fff;
font-size: 15px;
font-weight: 800;
}
.brand-text {
display: flex;
flex-direction: column;
line-height: 1.35;
}
.brand-name {
color: #f5f8fc;
font-size: 16px;
font-weight: 800;
letter-spacing: 0.02em;
white-space: nowrap;
}
.brand-sub {
color: #5e6878;
font-size: 10px;
white-space: nowrap;
}
.cat-tabs {
flex: 1;
min-width: 0;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
}
.cat-tab {
position: relative;
display: inline-flex;
align-items: center;
gap: 8px;
min-width: 132px;
height: 38px;
padding: 0 14px;
border-radius: 10px;
background: transparent;
color: #c8d2e2;
font-size: 13px;
font-weight: 700;
text-decoration: none;
transition: background 0.18s ease, color 0.18s ease;
}
.cat-tab:hover {
background: #232a3b;
color: #f5f8fc;
}
.cat-tab--active {
background: #3d4a63;
color: #f5f8fc;
}
.cat-dot {
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
}
.cat-badge {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 22px;
height: 18px;
padding: 0 4px;
border-radius: 999px;
background: #2b3447;
color: #5e6878;
font-size: 10px;
font-weight: 800;
}
.cat-tab--active .cat-badge {
color: #fff;
}
.cat-underline {
position: absolute;
left: 10px;
right: 10px;
bottom: -4px;
height: 3px;
border-radius: 2px;
background: transparent;
opacity: 0;
}
.cat-tab--active .cat-underline {
opacity: 1;
}
.top-right {
display: flex;
align-items: center;
gap: 14px;
flex-shrink: 0;
}
.btn-home {
color: #a0acbe;
font-size: 13px;
text-decoration: none;
white-space: nowrap;
}
.btn-home:hover {
color: #fff;
}
.help-mark {
display: inline-flex;
align-items: center;
justify-content: center;
width: 22px;
height: 22px;
border-radius: 50%;
background: #232a3b;
color: #a0acbe;
font-size: 13px;
font-weight: 800;
cursor: help;
}
@media (max-width: 1100px) {
.amazon-top {
flex-wrap: wrap;
height: auto;
padding: 10px 16px;
}
.cat-tabs {
order: 3;
width: 100%;
justify-content: flex-start;
overflow-x: auto;
}
}
</style>
@@ -0,0 +1,59 @@
<template>
<span class="tool-icon" :style="{ background: gradient }" aria-hidden="true">
<svg viewBox="0 0 48 48" fill="none" stroke="#fff" stroke-width="3" stroke-linecap="butt" stroke-linejoin="miter" v-html="shape"></svg>
</span>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { TOOL_ICON_GRADIENTS, TOOL_ICON_SHAPES } from '@/pages/amazon/tool-catalog'
const props = withDefaults(
defineProps<{
name: string
size?: number
}>(),
{
size: 48,
},
)
const gradient = computed(() => TOOL_ICON_GRADIENTS[props.name] ?? TOOL_ICON_GRADIENTS.collect)
const shape = computed(() => TOOL_ICON_SHAPES[props.name] ?? TOOL_ICON_SHAPES.collect)
</script>
<style scoped>
.tool-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 48px;
height: 48px;
flex-shrink: 0;
border-radius: 10px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.35);
}
.tool-icon svg {
width: 40px;
height: 40px;
}
.tool-icon :deep(path:not(.fill)),
.tool-icon :deep(rect),
.tool-icon :deep(circle:not(.fill)),
.tool-icon :deep(ellipse),
.tool-icon :deep(line) {
fill: none;
stroke: #fff;
stroke-width: 3;
stroke-linecap: butt;
stroke-linejoin: miter;
}
.tool-icon :deep(.fill) {
fill: #fff;
stroke: none;
}
</style>
@@ -0,0 +1,392 @@
/**
* 亚马逊运营工具台 · 工具目录
*
* 来源:reference/数富AI工具台_含源代码/数富AI工具台_主程序.py 的 CATS 数据,
* 页面链接按现有 BrandTopBar 导航映射到既有功能页(表单 + 右侧任务全部保留现有)。
*/
export type ToolInfo = {
/** 工具 id(与参考桌面板一致) */
id: string
/** 工具名称 */
name: string
/** 卡片一句话描述 */
desc: string
/** 角标文字(如 流程·1 / 重点 / 核心),无则不显示 */
tag?: string
/** 是否核心工具(角标走核心样式) */
core?: boolean
/** 现有功能页面地址;未提供表示暂未开通 */
href?: string
/** 菜单权限键(小写),与 BrandTopBar 导航一致 */
permissionKeys: string[]
}
export type WorkflowStep = {
name: string
desc: string
/** 指向的工具 id,用于点击跳转 */
toolId?: string
}
export type ToolGroup = {
key: 'front' | 'ops' | 'logi'
/** 菜单权限组键(与 BrandTopBar 一致) */
columnKey: string
name: string
desc: string
/** 分类主色 */
color: string
/** 分类弱色(角标底、编号圆底) */
weak: string
tools: ToolInfo[]
/** 标准工作流时间线(后勤无) */
workflowTitle?: string
workflowBadge?: string
workflowHint?: string
workflow?: WorkflowStep[]
}
/** 现有功能页地址(全部为 new_web_source 的 Vue 构建页) */
const VM = '/new_web_source'
export const TOOL_GROUPS: ReadonlyArray<ToolGroup> = [
{
key: 'front',
columnKey: 'brand_front_tools',
name: '前端工具',
desc: '采集 · 清洗 · 查品,产出安全可上架的数据',
color: '#2DD4BF',
weak: '#0F2E28',
workflowTitle: '前端工具 · 标准工作流',
workflowBadge: '产出完整安全数据',
workflowHint: '8步工作流 · 从采集到上架,一步不缺',
workflow: [
{ name: '采集数据', desc: '站点批量采集', toolId: 'collect' },
{ name: 'ASIN变体采集', desc: '按ASIN拉变体', toolId: 'variant' },
{ name: '数据去重', desc: '谁先采集归谁', toolId: 'dedupe' },
{ name: '货源查询·第一遍', desc: '粗略不扣费', toolId: 'source' },
{ name: '品牌检测', desc: '精确+嵌入', toolId: 'brand' },
{ name: '外观专利检测', desc: '规避专利风险', toolId: 'patent' },
{ name: '货源查询·第二遍', desc: '精细扣费', toolId: 'source' },
{ name: '上传后台', desc: '可上架数据', toolId: 'convert' },
],
tools: [
{
id: 'collect',
name: '采集数据',
desc: '批量采集站点商品数据,按照自己要求数据',
tag: '流程·1',
href: `${VM}/collect-data.html`,
permissionKeys: ['collect-data'],
},
{
id: 'variant',
name: 'ASIN变体采集',
desc: '支持粘贴和文档,按照采集国家选择导出',
tag: '流程·2',
href: `${VM}/variant-collection.html`,
permissionKeys: ['variant-collection'],
},
{
id: 'dedupe',
name: '数据去重',
desc: '全站唯一 ASIN 归属:谁先采集归谁,自动过滤撞款',
tag: '重点',
core: true,
href: `${VM}/dedupe.html`,
permissionKeys: ['dedupe'],
},
{
id: 'source',
name: '货源查询',
desc: '关闭图片检查和类目检测不扣费,打开需要算力费用',
tag: '流程·4',
href: `${VM}/similar-asin.html`,
permissionKeys: ['similar-asin'],
},
{
id: 'brand',
name: '品牌检测',
desc: '核查上架数据品牌风险,精确 / 嵌入两种策略',
tag: '流程·5',
href: `${VM}/brand.html`,
permissionKeys: ['brand'],
},
{
id: 'patent',
name: '外观专利检测',
desc: '检测商品外观专利,版权,标题,图片。详细页风险,规避下架',
tag: '流程·6',
href: `${VM}/appearance-patent.html`,
permissionKeys: ['appearance-patent'],
},
{
id: 'split',
name: '数据拆分',
desc: '大文件按规则拆分,便于分批处理与上传',
href: `${VM}/split.html`,
permissionKeys: ['split'],
},
{
id: 'convert',
name: '格式转换',
desc: '数据格式一键转换,适配各工具导入',
href: `${VM}/convert.html`,
permissionKeys: ['convert'],
},
],
},
{
key: 'ops',
columnKey: 'brand_operation_tools',
name: '运营工具',
desc: '上架 · 跟价 · 查品 · 巡店,日常店铺运营与维护',
color: '#60A5FA',
weak: '#12243D',
workflowTitle: '运营工具 · 标准工作流',
workflowBadge: '日常运营与维护',
workflowHint: '10步工作流 · 从上架到复盘,环环相扣',
workflow: [
{ name: '上架', desc: '安全数据批量上架', toolId: 'list' },
{ name: '指定删除ASIN', desc: '批量清理无效ASIN', toolId: 'delasin' },
{ name: '商品风险解决', desc: '4类风险一键处理', toolId: 'risk' },
{ name: '定时匹配', desc: '定时价格匹配', toolId: 'time' },
{ name: '跟价', desc: '跟价规则配置', toolId: 'follow' },
{ name: '巡店删除', desc: '巡检店铺状态', toolId: 'patrol' },
{ name: '查询ASIN', desc: '排查ASIN在售状态', toolId: 'qasin' },
{ name: '店铺数据抓取', desc: '抓取运营数据', toolId: 'scrape' },
{ name: '取款', desc: '留足平台扣款费用', toolId: 'cash' },
{ name: '店铺状态查询', desc: '查询店铺账号状态', toolId: 'shop' },
],
tools: [
{
id: 'list',
name: '上架',
desc: '把安全数据批量上架到指定店铺',
href: `${VM}/publish.html`,
permissionKeys: ['publish'],
},
{
id: 'delasin',
name: '指定删除ASIN',
desc: '批量删除需清理的 ASIN(风险 / 优化 / 邮箱汇总)',
href: `${VM}/delete-brand.html`,
permissionKeys: ['delete-brand'],
},
{
id: 'risk',
name: '商品风险解决',
desc: '4 类风险一键处理:禁止显示 / 需要批准 / 详情已删 / 账户状态',
tag: '高频',
core: true,
href: `${VM}/product-risk.html`,
permissionKeys: ['product-risk'],
},
{
id: 'time',
name: '定时匹配',
desc: '定时价格匹配,更快更及时抢回购物车',
href: `${VM}/shop-match.html`,
permissionKeys: ['shop-match'],
},
{
id: 'follow',
name: '跟价',
desc: '核心工具 · 时时跟价抢购物车,支持指定 ASIN 快速跟价',
tag: '核心',
core: true,
href: `${VM}/price-track.html`,
permissionKeys: ['pricing', 'price-track'],
},
{
id: 'patrol',
name: '巡店删除',
desc: '按商品状态巡店清理,摸清店铺商品全貌',
href: `${VM}/patrol-delete.html`,
permissionKeys: ['patrol-delete'],
},
{
id: 'qasin',
name: '查询ASIN',
desc: '排查出单 ASIN 是否仍在售,保住优质数据',
href: `${VM}/query-asin.html`,
permissionKeys: ['query-asin'],
},
{
id: 'scrape',
name: '店铺数据抓取',
desc: '核心 · 抓取店铺排名 / 品牌 / 销售,支撑复查与优化',
tag: '核心',
core: true,
href: `${VM}/shop-data-crawl.html`,
permissionKeys: ['shop-data-crawl', 'shop_data_crawl'],
},
{
id: 'cash',
name: '取款',
desc: '留足平台扣款费用,其余全部提现',
href: `${VM}/withdraw.html`,
permissionKeys: ['withdraw'],
},
{
id: 'shop',
name: '店铺状态查询',
desc: '查询店铺账号当前状态,及时发现异常',
permissionKeys: ['shop-status', 'shop'],
},
],
},
{
key: 'logi',
columnKey: 'brand_logistics_tools',
name: '后勤工具',
desc: '采购 · ERP,供应链与系统对接',
color: '#A78BFA',
weak: '#221E38',
tools: [
{
id: 'purchase',
name: '采购',
desc: '供应链采购与订单管理',
permissionKeys: ['purchase'],
},
{
id: 'erp',
name: 'ERP',
desc: '对接 ERP 系统,同步库存与订单',
permissionKeys: ['erp'],
},
],
},
]
/** 图标渐变背景(照 icons.py ICON_COLORS 转 CSS */
export const TOOL_ICON_GRADIENTS: Record<string, string> = {
collect: 'linear-gradient(180deg, #5EEAD4, #14B8A6)',
variant: 'linear-gradient(180deg, #93C5FD, #3B82F6)',
dedupe: 'linear-gradient(180deg, #C4B5FD, #8B5CF6)',
source: 'linear-gradient(180deg, #F9A8D4, #EC4899)',
brand: 'linear-gradient(180deg, #FDBA74, #F97316)',
patent: 'linear-gradient(180deg, #FDE047, #EAB308)',
split: 'linear-gradient(180deg, #86EFAC, #22C55E)',
convert: 'linear-gradient(180deg, #FCA5A5, #EF4444)',
list: 'linear-gradient(180deg, #5EEAD4, #14B8A6)',
delasin: 'linear-gradient(180deg, #93C5FD, #3B82F6)',
risk: 'linear-gradient(180deg, #C4B5FD, #8B5CF6)',
time: 'linear-gradient(180deg, #F9A8D4, #EC4899)',
follow: 'linear-gradient(180deg, #FDBA74, #F97316)',
patrol: 'linear-gradient(180deg, #FDE047, #EAB308)',
qasin: 'linear-gradient(180deg, #86EFAC, #22C55E)',
scrape: 'linear-gradient(180deg, #FCA5A5, #EF4444)',
cash: 'linear-gradient(180deg, #5EEAD4, #14B8A6)',
shop: 'linear-gradient(180deg, #93C5FD, #3B82F6)',
purchase: 'linear-gradient(180deg, #C4B5FD, #8B5CF6)',
erp: 'linear-gradient(180deg, #F9A8D4, #EC4899)',
}
/**
* 图标白色图形(48x48,照 icons.py 各 icon_* 坐标转 SVG 路径)
* 默认描边 stroke:#fff width:3(方头 butt),实心填充 shape 用 .fill 标记
*/
export const TOOL_ICON_SHAPES: Record<string, string> = {
// 采集数据:圆角方框+向下箭头
collect: '<rect x="10" y="8" width="28" height="24"/><line x1="24" y1="14" x2="24" y2="36"/><path class="fill" d="M19 33 L29 33 L24 38 Z"/>',
// ASIN变体采集:两个重叠方框
variant: '<rect x="8" y="10" width="20" height="20"/><rect x="20" y="18" width="20" height="20"/>',
// 数据去重:左右双向箭头
dedupe: '<line x1="10" y1="24" x2="38" y2="24"/><path class="fill" d="M15 19 L15 29 L10 24 Z"/><path class="fill" d="M33 19 L33 29 L38 24 Z"/>',
// 货源查询:地球(圆+经纬线)
source: '<circle cx="24" cy="24" r="14"/><line x1="24" y1="10" x2="24" y2="38"/><line x1="10" y1="24" x2="38" y2="24"/><path d="M24 38 A 10 14 0 0 1 24 10"/><path d="M24 10 A 10 14 0 0 1 24 38"/>',
// 品牌检测:盾牌+对勾
brand: '<path d="M14 10 L34 10 L34 28 L24 38 L14 28 Z"/><path d="M19 22 L23 25 L29 17"/>',
// 外观专利检测:灯泡+对勾
patent: '<circle cx="24" cy="20" r="12"/><rect x="18" y="32" width="12" height="6"/><path d="M19 20 L23 23 L29 15"/>',
// 数据拆分:Y形分叉箭头
split: '<line x1="24" y1="12" x2="24" y2="24"/><line x1="24" y1="24" x2="14" y2="36"/><line x1="24" y1="24" x2="34" y2="36"/><path class="fill" d="M10 32 L18 32 L14 36 Z"/><path class="fill" d="M30 32 L38 32 L34 36 Z"/>',
// 格式转换:循环箭头
convert: '<path d="M11.88 31 A 14 14 0 0 1 36.12 17"/><path d="M36.12 17 A 14 14 0 0 1 11.88 31"/><path class="fill" d="M32 18 L40 18 L36 14 Z"/><path class="fill" d="M8 30 L16 30 L12 34 Z"/>',
// 上架:方框+向上箭头
list: '<rect x="10" y="16" width="28" height="24"/><line x1="24" y1="34" x2="24" y2="12"/><path class="fill" d="M19 15 L29 15 L24 10 Z"/>',
// 删除ASIN:垃圾桶
delasin: '<rect x="10" y="10" width="28" height="6"/><path d="M13 18 L35 18 L33 40 L15 40 Z"/><line x1="20" y1="22" x2="20" y2="36"/><line x1="28" y1="22" x2="28" y2="36"/>',
// 商品风险解决:三角+感叹号
risk: '<path d="M24 8 L40 38 L8 38 Z"/><line x1="24" y1="18" x2="24" y2="28" stroke-width="3"/><circle class="fill" cx="24" cy="33" r="2"/>',
// 定时匹配:时钟
time: '<circle cx="24" cy="24" r="16"/><line x1="24" y1="14" x2="24" y2="24"/><line x1="24" y1="24" x2="32" y2="28"/>',
// 跟价:标签
follow: '<path d="M10 12 L32 12 L40 24 L32 36 L10 36 Z"/><circle cx="32" cy="23" r="2" stroke-width="2"/>',
// 巡店删除:眼睛
patrol: '<ellipse cx="24" cy="24" rx="16" ry="8"/><circle class="fill" cx="24" cy="24" r="4"/>',
// 查询ASIN:放大镜
qasin: '<circle cx="20" cy="20" r="12"/><line x1="28" y1="28" x2="40" y2="40" stroke-width="4"/>',
// 店铺数据抓取:立方体
scrape: '<path d="M24 8 L40 16 L24 24 L8 16 Z"/><path d="M8 16 L24 24 L24 40 L8 32 Z"/><path d="M40 16 L24 24 L24 40 L40 32 Z"/>',
// 取款:钱包
cash: '<rect x="8" y="14" width="32" height="22"/><rect x="8" y="10" width="20" height="8"/><circle class="fill" cx="35" cy="25" r="3"/>',
// 店铺状态查询:店铺
shop: '<path d="M8 20 L24 8 L40 20 Z"/><rect x="10" y="20" width="28" height="20"/><rect x="20" y="26" width="8" height="14"/>',
// 采购:购物车
purchase: '<path d="M10 14 L34 14 L30 34 L14 34 Z"/><line x1="34" y1="14" x2="40" y2="14"/><line x1="40" y1="14" x2="38" y2="20"/><circle class="fill" cx="19" cy="39" r="3"/><circle class="fill" cx="33" cy="39" r="3"/>',
// ERP:网格
erp: '<rect x="8" y="8" width="32" height="32"/><line x1="24" y1="8" x2="24" y2="40"/><line x1="8" y1="24" x2="40" y2="24"/>',
}
export const TOOL_MAP: ReadonlyMap<string, ToolInfo> = new Map(
TOOL_GROUPS.flatMap((group) => group.tools.map((tool) => [tool.id, tool] as const)),
)
/**
* 工具页页头长描述(参考桌面板 detail_desc,优先于卡片 desc 显示)
*/
export const TOOL_DETAILS: Readonly<Record<string, string>> = {
collect: '默认勾选有排名和FBM这两个选择',
dedupe: '去重之后,切记一定上传到数富后台去重数据库',
brand: '核查上架数据品牌风险,先跑精确匹配,后跑嵌入式',
delasin: '收集统一要删除的ASIN,批量删除清理',
risk:
'A 搜索结果中禁止显示;B 需要批准(最通用、使用频率最高);C 详情页面已删除;D 账户状态处理(需审批时用后台处理)。',
follow:
'一个虚拟桌面跟一个国家,错开跑即可时时跟价抢购物车。指定 ASIN 文档跟价:用后台有利润数据快速跟价,下班前跑一下,确保出单数据及时是我们的购物车。',
patrol: '巡店删除是按商品状态删除,如审核多次不通过的商品。不放任何状态跑一遍,可清楚掌握店铺数据情况。',
qasin: '通过店小蜜导出出单 ASIN,上传后台,时不时排查是否还在店铺售卖,确保优质数据时刻在店。',
scrape:
'没排名、久未出单数据可优化删除;获取店铺所有数据的排名、品牌和销售情况。半个月到一个月复查一次品牌,确保数据没有被注册。',
}
export type VisibleGroup = ToolGroup & { tools: ToolInfo[] }
/**
* 按菜单权限过滤分组(逻辑与 BrandTopBar.visibleNavGroups 一致):
* 组键(brand_front_tools 等)命中则整组展示;否则只展示有权限的工具;
* 无任何可见工具的分组整体隐藏。
*/
export function filterGroupsByPermission(
groups: ReadonlyArray<ToolGroup>,
allowedKeys: ReadonlyArray<string>,
): VisibleGroup[] {
const allowedSet = new Set(allowedKeys.map((key) => String(key || '').trim().toLowerCase()).filter(Boolean))
return groups.flatMap((group) => {
if (allowedSet.has(group.columnKey)) {
return [{ ...group, tools: [...group.tools] }]
}
const tools = group.tools.filter((tool) =>
tool.permissionKeys.some((key) => allowedSet.has(key.toLowerCase())),
)
return tools.length ? [{ ...group, tools }] : []
})
}
/** 从工作流时间线中截取有权限的步骤(与分组逻辑共用权限集合) */
export function filterWorkflowByPermission(
steps: ReadonlyArray<WorkflowStep>,
allowedKeys: ReadonlyArray<string>,
): WorkflowStep[] {
const allowedSet = new Set(allowedKeys.map((key) => String(key || '').trim().toLowerCase()).filter(Boolean))
return steps.filter((step) => {
if (!step.toolId) return true
const tool = TOOL_MAP.get(step.toolId)
if (!tool) return true
return tool.permissionKeys.some((key) => allowedSet.has(key.toLowerCase()))
})
}
@@ -1,6 +1,6 @@
<template> <template>
<div class="page-shell module-page"> <div class="page-shell module-page">
<BrandTopBar active="appearance-patent" /> <AmazonToolPageShell tool-id="patent">
<div class="main-content"> <div class="main-content">
<aside class="left-panel left-panel--with-template"> <aside class="left-panel left-panel--with-template">
@@ -133,13 +133,14 @@
</div> </div>
</section> </section>
</div> </div>
</AmazonToolPageShell>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, onMounted, onUnmounted, ref } from 'vue' import { computed, onMounted, onUnmounted, ref } from 'vue'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import BrandTopBar from '@/pages/brand/components/BrandTopBar.vue' import AmazonToolPageShell from '@/pages/amazon/components/AmazonToolPageShell.vue';
import ModuleTemplateDownload from '@/shared/components/ModuleTemplateDownload.vue' import ModuleTemplateDownload from '@/shared/components/ModuleTemplateDownload.vue'
import { import {
activateAppearancePatentTask, activateAppearancePatentTask,
@@ -981,60 +982,60 @@ onUnmounted(() => {
</script> </script>
<style scoped> <style scoped>
.module-page { min-height: 100vh; background: #1a1a1a; } .module-page { min-height: 100vh; background: #151a25; }
.main-content { display: flex; height: calc(100vh - 56px); min-height: calc(100vh - 56px); } .main-content { display: flex; height: calc(100vh - 56px); min-height: calc(100vh - 56px); }
.left-panel { width: 400px; background: #1e1e1e; padding: 20px; overflow-y: auto; border-right: 1px solid #2a2a2a; } .left-panel { width: 400px; background: #1c2333; padding: 20px; overflow-y: auto; border-right: 1px solid #2e3a52; }
.right-panel { flex: 1; min-width: 0; background: #1a1a1a; display: flex; flex-direction: column; } .right-panel { flex: 1; min-width: 0; background: #151a25; display: flex; flex-direction: column; }
.section-title, .subsection-title { font-size: 13px; color: #bbb; margin-bottom: 10px; } .section-title, .subsection-title { font-size: 13px; color: #a0acbe; margin-bottom: 10px; }
.upload-zone { border: 1px dashed #3a3a3a; border-radius: 10px; padding: 18px; background: #252525; margin-bottom: 18px; } .upload-zone { border: 1px dashed #3e4a62; border-radius: 10px; padding: 18px; background: #2e3a52; margin-bottom: 18px; }
.hint, .loading-msg, .files, .muted { color: #888; font-size: 12px; line-height: 1.5; } .hint, .loading-msg, .files, .muted { color: #5e6878; font-size: 12px; line-height: 1.5; }
.link { color: #6ea8fe; text-decoration: none; } .link { color: #6ea8fe; text-decoration: none; }
.link:hover { color: #9fc5ff; } .link:hover { color: #9fc5ff; }
.btns, .run-row { display: flex; gap: 10px; flex-wrap: wrap; } .btns, .run-row { display: flex; gap: 10px; flex-wrap: wrap; }
.opt-btn, .btn-run, .btn-delete, .download { border: none; cursor: pointer; border-radius: 7px; } .opt-btn, .btn-run, .btn-delete, .download { border: none; cursor: pointer; border-radius: 7px; }
.opt-btn { padding: 8px 14px; color: #ccc; background: #2a2a2a; border: 1px solid #3a3a3a; } .opt-btn { padding: 8px 14px; color: #c8d2e2; background: #2e3a52; border: 1px solid #3e4a62; }
.btn-run { padding: 10px 18px; color: #fff; background: #3498db; font-weight: 600; } .btn-run { padding: 10px 18px; color: #f5f8fc; background: #3498db; font-weight: 600; }
.btn-queue { background: #27ae60; } .btn-queue { background: #27ae60; }
.btn-run:disabled { opacity: .55; cursor: not-allowed; } .btn-run:disabled { opacity: .55; cursor: not-allowed; }
.selected-files { margin-top: 14px; color: #999; font-size: 12px; word-break: break-all; } .selected-files { margin-top: 14px; color: #a0acbe; font-size: 12px; word-break: break-all; }
.selected-files span { display: block; margin: 4px 0; } .selected-files span { display: block; margin: 4px 0; }
.prompt-card, .secret-card { margin-bottom: 18px; } .prompt-card, .secret-card { margin-bottom: 18px; }
.secret-card { display: none; } .secret-card { display: none; }
.prompt-input { width: 100%; box-sizing: border-box; resize: vertical; min-height: 180px; padding: 10px 12px; border: 1px solid #333; border-radius: 8px; background: #202020; color: #d8d8d8; font-size: 12px; line-height: 1.6; outline: none; } .prompt-input { width: 100%; box-sizing: border-box; resize: vertical; min-height: 180px; padding: 10px 12px; border: 1px solid #333; border-radius: 8px; background: #222b3d; color: #c8d2e2; font-size: 12px; line-height: 1.6; outline: none; }
.secret-input { width: 100%; box-sizing: border-box; height: 38px; padding: 0 12px; border: 1px solid #333; border-radius: 8px; background: #202020; color: #d8d8d8; font-size: 12px; outline: none; } .secret-input { width: 100%; box-sizing: border-box; height: 38px; padding: 0 12px; border: 1px solid #333; border-radius: 8px; background: #222b3d; color: #c8d2e2; font-size: 12px; outline: none; }
.prompt-input:focus, .secret-input:focus { border-color: #3498db; } .prompt-input:focus, .secret-input:focus { border-color: #3498db; }
.parse-card, .queue-payload { margin-top: 14px; padding: 12px; border: 1px solid #2a2a2a; border-radius: 8px; background: #202020; color: #b8c1cc; font-size: 12px; } .parse-card, .queue-payload { margin-top: 14px; padding: 12px; border: 1px solid #2e3a52; border-radius: 8px; background: #222b3d; color: #a0acbe; font-size: 12px; }
.queue-payload { max-height: 220px; overflow: auto; color: #8fd3ff; white-space: pre-wrap; } .queue-payload { max-height: 220px; overflow: auto; color: #8fd3ff; white-space: pre-wrap; }
.panel-header { padding: 16px 20px; border-bottom: 1px solid #2a2a2a; font-size: 15px; font-weight: 600; color: #ddd; } .panel-header { padding: 16px 20px; border-bottom: 1px solid #2e3a52; font-size: 15px; font-weight: 600; color: #c8d2e2; }
.task-list-wrap { flex: 1; padding: 16px 20px; overflow: auto; } .task-list-wrap { flex: 1; padding: 16px 20px; overflow: auto; }
.clean-result-summary { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 12px; margin-bottom: 16px; } .clean-result-summary { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 12px; margin-bottom: 16px; }
.summary-card { padding: 14px 16px; border: 1px solid #2a2a2a; border-radius: 8px; background: #1e1e1e; } .summary-card { padding: 14px 16px; border: 1px solid #2e3a52; border-radius: 8px; background: #1c2333; }
.summary-card strong { display: block; margin-top: 8px; color: #eaf4ff; font-size: 22px; } .summary-card strong { display: block; margin-top: 8px; color: #f5f8fc; font-size: 22px; }
.summary-label { color: #8d8d8d; font-size: 12px; } .summary-label { color: #5e6878; font-size: 12px; }
.result-list-wrap { border: 1px solid #2a2a2a; border-radius: 8px; background: #1e1e1e; min-height: 180px; margin: 0 0 16px; } .result-list-wrap { border: 1px solid #2e3a52; border-radius: 8px; background: #1c2333; min-height: 180px; margin: 0 0 16px; }
.result-list-header { display: flex; justify-content: space-between; padding: 12px 16px; border-bottom: 1px solid #2a2a2a; color: #ddd; font-size: 14px; } .result-list-header { display: flex; justify-content: space-between; padding: 12px 16px; border-bottom: 1px solid #2e3a52; color: #c8d2e2; font-size: 14px; }
.empty-tasks { color: #666; font-size: 13px; padding: 18px; text-align: center; } .empty-tasks { color: #5e6878; font-size: 13px; padding: 18px; text-align: center; }
.result-table { --el-table-bg-color: #222; --el-table-tr-bg-color: #222; --el-table-header-bg-color: #2a2a2a; --el-table-text-color: #ccc; --el-table-border-color: #333; } .result-table { --el-table-bg-color: #222; --el-table-tr-bg-color: #222; --el-table-header-bg-color: #2e3a52; --el-table-text-color: #c8d2e2; --el-table-border-color: #333; }
.task-list { list-style: none; margin: 0; padding: 12px; } .task-list { list-style: none; margin: 0; padding: 12px; }
.task-item { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; padding: 12px 14px; border: 1px solid #2a2a2a; border-radius: 8px; margin-bottom: 8px; background: #222; } .task-item { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; padding: 12px 14px; border: 1px solid #2e3a52; border-radius: 8px; margin-bottom: 8px; background: #222; }
.left { flex: 1; min-width: 0; } .left { flex: 1; min-width: 0; }
.id { color: #e0e0e0; font-size: 13px; font-weight: 600; } .id { color: #f5f8fc; font-size: 13px; font-weight: 600; }
.task-right { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; } .task-right { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
.status { padding: 4px 10px; border-radius: 6px; font-size: 12px; } .status { padding: 4px 10px; border-radius: 6px; font-size: 12px; }
.status.success { background: rgba(46, 204, 113, .18); color: #2ecc71; } .status.success { background: rgba(46, 204, 113, .18); color: #2ecc71; }
.status.failed { background: rgba(231, 76, 60, .18); color: #ff6b6b; } .status.failed { background: rgba(231, 76, 60, .18); color: #ff6b6b; }
.status.running { background: rgba(52, 152, 219, .18); color: #3498db; } .status.running { background: rgba(52, 152, 219, .18); color: #3498db; }
.status.pending { background: rgba(149, 165, 166, .18); color: #bdc3c7; } .status.pending { background: rgba(149, 165, 166, .18); color: #a0acbe; }
.result-hint { margin-top: 6px; color: #e0b96d; } .result-hint { margin-top: 6px; color: #e0b96d; }
.file-progress { margin-top: 8px; max-width: 520px; } .file-progress { margin-top: 8px; max-width: 520px; }
.file-progress-meta { display: flex; justify-content: space-between; gap: 12px; color: #d8c278; font-size: 12px; } .file-progress-meta { display: flex; justify-content: space-between; gap: 12px; color: #d8c278; font-size: 12px; }
.file-progress-track { margin-top: 5px; height: 8px; border-radius: 999px; overflow: hidden; background: #303030; border: 1px solid #3b3b3b; } .file-progress-track { margin-top: 5px; height: 8px; border-radius: 999px; overflow: hidden; background: #333f55; border: 1px solid #3b3b3b; }
.file-progress-bar { height: 100%; border-radius: inherit; background: linear-gradient(90deg, #4aa3ff, #f0c75e); transition: width .25s ease; } .file-progress-bar { height: 100%; border-radius: inherit; background: linear-gradient(90deg, #4aa3ff, #f0c75e); transition: width .25s ease; }
.download { padding: 6px 10px; color: #d6ecff; background: rgba(52, 152, 219, .18); } .download { padding: 6px 10px; color: #c8d2e2; background: rgba(52, 152, 219, .18); }
.btn-delete { padding: 6px 10px; color: #ff8f8f; background: rgba(231, 76, 60, .12); } .btn-delete { padding: 6px 10px; color: #ff8f8f; background: rgba(231, 76, 60, .12); }
@media (max-width: 1100px) { @media (max-width: 1100px) {
.main-content { flex-direction: column; height: auto; } .main-content { flex-direction: column; height: auto; }
.left-panel { width: 100%; border-right: none; border-bottom: 1px solid #2a2a2a; } .left-panel { width: 100%; border-right: none; border-bottom: 1px solid #2e3a52; }
.clean-result-summary { grid-template-columns: repeat(2, minmax(0, 1fr)); } .clean-result-summary { grid-template-columns: repeat(2, minmax(0, 1fr)); }
} }
</style> </style>
@@ -0,0 +1,494 @@
<template>
<div class="page-shell module-page">
<AmazonToolPageShell tool-id="brand">
<div class="main-content">
<aside class="left-panel">
<div class="section-title">待检品牌文件</div>
<div class="upload-zone">
<div class="hint">
Excel 每行含品牌列即会加入检测可多选文件或选择整个文件夹.xlsx检测由本机客户端执行请在桌面客户端中打开本页面
</div>
<div class="btns">
<button type="button" class="opt-btn" :disabled="!hasBridge" @click="selectFiles">选择 Excel</button>
<button type="button" class="opt-btn" :disabled="!hasBridge" @click="selectFolder">选择文件夹</button>
</div>
<div class="selected-files">
<template v-if="selectedPaths.length">
<span v-for="name in displayPaths" :key="name" :title="name">{{ baseName(name) }}</span>
<span v-if="selectedPaths.length > displayPaths.length" class="more-line">
还有 {{ selectedPaths.length - displayPaths.length }} 个文件未展开显示
</span>
</template>
<span v-else>暂未选择文件</span>
</div>
<div v-if="!hasBridge" class="bridge-tip">当前浏览器环境无桌面客户端桥接仅可预览页面请用本机客户端打开</div>
</div>
<div class="section-title">匹配策略</div>
<div class="radio-cards">
<label class="radio-card" :class="{ selected: strategy === 'Terms' }">
<input type="radio" name="brand-strategy" value="Terms" v-model="strategy" />
<span class="radio-card-name">词条 / 嵌入匹配Terms</span>
<span class="radio-card-desc">按品牌词条做宽范围比对误杀率低推荐第一遍使用</span>
</label>
<label class="radio-card" :class="{ selected: strategy === 'Simple' }">
<input type="radio" name="brand-strategy" value="Simple" v-model="strategy" />
<span class="radio-card-name">精确匹配Simple</span>
<span class="radio-card-desc">仅精确命中才判为疑似误杀最低</span>
</label>
</div>
<div class="section-title">运行方式</div>
<div class="run-mode-zone">
<label class="run-mode-row">
<input type="radio" name="brand-run-mode" value="immediate" v-model="runMode" />
<span class="run-mode-text">立即执行前台运行可实时取消</span>
</label>
<label class="run-mode-row">
<input type="radio" name="brand-run-mode" value="queue" v-model="runMode" />
<span class="run-mode-text">添加到任务队列后台排队执行</span>
</label>
</div>
<div class="run-row">
<button
type="button"
class="btn-run"
:disabled="!selectedPaths.length || submitting || !hasBridge || !hasUid"
@click="submitRun"
>
{{ submitting ? '提交中...' : runMode === 'immediate' ? '立即运行品牌检测' : '添加到任务队列' }}
</button>
</div>
<p v-if="!hasBridge || !hasUid" class="env-tip">
品牌检测在桌面客户端内执行请在本机客户端登录后打开本页面使用浏览器仅可预览界面
</p>
<p v-else class="loading-msg">
检测完成后自动生成结果 Excel含原始数据不符合品牌查询失败品牌到右侧任务列表下载
</p>
<div class="section-title">文档模板</div>
<div class="btns">
<button type="button" class="opt-btn" :disabled="!hasBridge" @click="downloadTemplate('xlsx')">下载 Excel 模板</button>
<button type="button" class="opt-btn" :disabled="!hasBridge" @click="downloadTemplate('zip')">下载文件夹模板</button>
</div>
</aside>
<section class="right-panel">
<div class="panel-header">
<span>品牌检测任务</span>
<button type="button" class="btn-refresh" @click="loadTasks">刷新任务状态</button>
</div>
<div class="task-list-wrap">
<div v-if="!tasks.length" class="empty-tasks">
暂无品牌检测任务选择待检文件后点击运行添加到任务队列
</div>
<ul v-else class="task-list">
<li v-for="item in tasks" :key="`brand-${item.id}`" class="task-item">
<div class="left">
<span class="id" :title="taskDesc(item)">{{ taskDesc(item) }}</span>
<div class="files">任务 ID{{ item.id }}</div>
<div v-if="item.file_paths?.length" class="files">文件{{ item.file_paths.length }} </div>
<div class="files">创建时间{{ formatDateTime(item.created_at) }}</div>
<template v-if="isRunning(item)">
<div class="task-progress">
<div class="task-progress-meta">
<span>任务进度</span>
<span>{{ taskProgress(item) }}%</span>
</div>
<div class="task-progress-track">
<div class="task-progress-fill" :style="{ width: `${taskProgress(item)}%` }"></div>
</div>
</div>
<div v-if="errorMessage(item)" class="files error">{{ errorMessage(item) }}</div>
</template>
<div v-else-if="errorMessage(item)" class="files error">错误{{ errorMessage(item) }}</div>
</div>
<div class="task-right">
<span class="status" :class="statusClass(item.status)">{{ statusText(item.status) }}</span>
<button v-if="isRunning(item)" type="button" class="act-btn danger" @click="cancelTask(item)">取消</button>
<button v-if="canDownload(item)" type="button" class="act-btn ok" @click="downloadResult(item)">下载结果</button>
<button v-if="canDelete(item)" type="button" class="act-btn" @click="deleteTask(item)">删除</button>
</div>
</li>
</ul>
</div>
</section>
</div>
</AmazonToolPageShell>
</div>
</template>
<script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import AmazonToolPageShell from '@/pages/amazon/components/AmazonToolPageShell.vue'
import { getPywebviewApi } from '@/shared/bridges/pywebview'
import { requestPostJson } from '@/shared/api/http'
import {
cancelBrandTask,
createBrandTask,
deleteBrandTask,
getBrandTaskDownloadUrl,
getBrandTasks,
runBrandNow,
type BrandTaskItem,
} from '@/shared/api/brand'
interface BrandExpandFolderPathsResponse {
success: boolean
paths?: string[]
error?: string
}
const selectedPaths = ref<string[]>([])
const strategy = ref<'Terms' | 'Simple'>('Terms')
const runMode = ref<'immediate' | 'queue'>('immediate')
const submitting = ref(false)
const tasks = ref<BrandTaskItem[]>([])
const hasBridge = computed(() => Boolean(getPywebviewApi()))
const hasUid = computed(() => {
const raw = typeof window === 'undefined' ? '' : window.localStorage.getItem('uid') || ''
const numeric = Number(raw.trim())
return Number.isFinite(numeric) && numeric > 0
})
const displayPaths = computed(() => selectedPaths.value.slice(0, 8))
function baseName(path: string) {
return path.split(/[\\/]/).pop() || path
}
let pollTimer: number | null = null
function hasRunning() {
return tasks.value.some((item) => isRunning(item))
}
function isRunning(item: BrandTaskItem) {
return (item.status || '').toLowerCase() === 'running'
}
function isTerminal(item: BrandTaskItem) {
const status = (item.status || '').toLowerCase()
return status === 'success' || status === 'failed' || status === 'cancelled'
}
function canDelete(item: BrandTaskItem) {
return !isRunning(item)
}
function canDownload(item: BrandTaskItem) {
const status = (item.status || '').toLowerCase()
return status === 'success' && Boolean(item.result_paths?.zip_url)
}
function taskProgress(item: BrandTaskItem) {
const total = Number(item.progress_total) || 0
const current = Number(item.progress_current) || 0
if (!total) return 0
return Math.max(0, Math.min(100, Math.round((current / total) * 100)))
}
function taskDesc(item: BrandTaskItem) {
const desc = (item.desc || '').trim()
return desc ? (desc.length > 60 ? `${desc.slice(0, 60)}` : desc) : `任务 #${item.id}`
}
function errorMessage(item: BrandTaskItem) {
return (item.error_message || '').trim()
}
function statusText(status?: BrandTaskItem['status']) {
const value = (status || '').toLowerCase()
const map: Record<string, string> = {
pending: '等待中',
running: '执行中',
success: '已完成',
failed: '失败',
cancelled: '已取消',
}
return map[value] || String(status ?? '未知')
}
function statusClass(status?: BrandTaskItem['status']) {
const value = (status || '').toLowerCase()
if (value === 'running') return 'running'
if (value === 'success') return 'success'
if (value === 'failed') return 'failed'
if (value === 'cancelled') return 'cancelled'
return 'pending'
}
function formatDateTime(value?: string) {
if (!value) return '-'
const date = new Date(value)
if (Number.isNaN(date.getTime())) return value
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
const seconds = String(date.getSeconds()).padStart(2, '0')
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
}
function normalizeSelected(paths: string[]) {
selectedPaths.value = paths
.filter((path) => /\.xlsx$/i.test(path))
.filter((path, index, array) => array.indexOf(path) === index)
}
async function selectFiles() {
const bridge = getPywebviewApi()
if (!bridge?.select_brand_xlsx_files) {
ElMessage.warning('当前环境不支持文件选择,请在本机客户端中打开')
return
}
try {
const paths = await bridge.select_brand_xlsx_files()
if (!paths?.length) return
normalizeSelected(paths)
ElMessage.success(`已选择 ${selectedPaths.value.length} 个 Excel 文件`)
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : '文件选择失败')
}
}
async function selectFolder() {
const bridge = getPywebviewApi()
if (!bridge?.select_brand_folder) {
ElMessage.warning('当前环境不支持文件夹选择,请在本机客户端中打开')
return
}
try {
const folder = await bridge.select_brand_folder()
if (!folder) return
const res = await requestPostJson<BrandExpandFolderPathsResponse>('/api/brand/expand-folder', { folder })
if (!res.success || !res.paths?.length) {
ElMessage.warning(res.error || '该文件夹下没有 xlsx 文件')
return
}
normalizeSelected(res.paths)
ElMessage.success(`已选择文件夹内 ${selectedPaths.value.length} 个 Excel 文件`)
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : '文件夹选择失败')
}
}
async function submitRun() {
if (!hasBridge.value) {
ElMessage.warning('请在桌面客户端中打开本页面执行品牌检测')
return
}
if (!hasUid.value) {
ElMessage.warning('未获取到登录用户,请先在本机客户端登录后再试')
return
}
if (!selectedPaths.value.length) {
ElMessage.warning('请先选择待检 Excel 文件')
return
}
submitting.value = true
try {
if (runMode.value === 'immediate') {
const res = await runBrandNow(selectedPaths.value, strategy.value)
if (res.success && res.task_id) {
ElMessage.success(`已创建任务 ${res.task_id},客户端开始检测`)
} else {
throw new Error((res as { error?: string }).error || '运行失败')
}
} else {
const res = await createBrandTask(selectedPaths.value, strategy.value)
if (res.success && res.task_id) {
ElMessage.success(`任务 ${res.task_id} 已添加到队列`)
} else {
throw new Error((res as { error?: string }).error || '添加失败')
}
}
selectedPaths.value = []
await loadTasks()
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : '提交失败')
} finally {
submitting.value = false
}
}
async function loadTasks() {
try {
const res = await getBrandTasks()
if (res.success && Array.isArray(res.items)) {
tasks.value = res.items
}
} catch (error) {
// 浏览器预览(无 uid / 无桌面后端)时静默,避免误报
if (hasBridge.value && hasUid.value) {
ElMessage.error(error instanceof Error ? error.message : '任务列表加载失败')
}
}
}
async function cancelTask(item: BrandTaskItem) {
try {
await ElMessageBox.confirm(`确认取消任务 ${item.id}`, '取消任务', { type: 'warning' })
} catch {
return
}
try {
const res = await cancelBrandTask(item.id)
if (!res.success) throw new Error((res as { error?: string }).error || '取消失败')
ElMessage.success('已取消')
await loadTasks()
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : '取消失败')
}
}
async function deleteTask(item: BrandTaskItem) {
try {
await ElMessageBox.confirm(`确认删除任务 ${item.id}?删除后不可恢复。`, '删除任务', { type: 'warning' })
} catch {
return
}
try {
const res = await deleteBrandTask(item.id)
if (!res.success) throw new Error((res as { error?: string }).error || '删除失败')
ElMessage.success('已删除')
await loadTasks()
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : '删除失败')
}
}
async function downloadResult(item: BrandTaskItem) {
const zipUrl = item.result_paths?.zip_url
const filename = `brand_task_${item.id}.zip`
const bridge = getPywebviewApi()
if (bridge?.save_file_from_url && zipUrl) {
try {
const res = await bridge.save_file_from_url(zipUrl, filename)
if (res.success) {
ElMessage.success(`已保存:${res.path || filename}`)
} else if (res.error && res.error !== '用户取消') {
ElMessage.error(res.error)
}
return
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : '下载失败')
return
}
}
if (zipUrl) {
window.open(zipUrl, '_blank')
return
}
window.open(getBrandTaskDownloadUrl(item.id), '_blank')
}
async function downloadTemplate(kind: 'xlsx' | 'zip') {
const bridge = getPywebviewApi()
try {
let res: { success: boolean; path?: string; error?: string } | undefined
if (kind === 'xlsx') {
res = bridge?.save_template_xlsx ? await bridge.save_template_xlsx() : undefined
} else {
res = bridge?.save_template_zip ? await bridge.save_template_zip() : undefined
}
if (res?.success) {
ElMessage.success(`模板已保存至:${res.path}`)
} else if (res?.error) {
ElMessage.warning(res.error)
} else {
ElMessage.warning('当前环境不支持模板下载,请在本机客户端中打开')
}
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : '模板下载失败')
}
}
function schedulePoll() {
if (pollTimer) {
window.clearTimeout(pollTimer)
pollTimer = null
}
pollTimer = window.setTimeout(async () => {
await loadTasks()
if (hasRunning()) {
schedulePoll()
}
}, 3000)
}
onMounted(() => {
void loadTasks()
})
onBeforeUnmount(() => {
if (pollTimer) {
window.clearTimeout(pollTimer)
pollTimer = null
}
})
</script>
<style scoped>
.module-page { min-height: 100vh; background: #151a25; }
.main-content { display: flex; height: calc(100vh - 56px); min-height: calc(100vh - 56px); }
.left-panel { width: 420px; background: #1c2333; padding: 20px; overflow-y: auto; border-right: 1px solid #2e3a52; }
.right-panel { flex: 1; min-width: 0; background: #151a25; display: flex; flex-direction: column; }
.section-title { font-size: 13px; color: #a0acbe; margin: 12px 0 10px; }
.hint, .loading-msg, .files, .muted { color: #5e6878; font-size: 12px; line-height: 1.5; }
.hint { margin-bottom: 12px; }
.upload-zone { border: 1px dashed #3e4a62; border-radius: 10px; padding: 16px; background: #2e3a52; margin-bottom: 6px; }
.btns, .run-row { display: flex; gap: 10px; flex-wrap: wrap; }
.run-row { margin-top: 14px; }
.opt-btn { padding: 8px 14px; color: #c8d2e2; background: #2e3a52; border: 1px solid #3e4a62; border-radius: 7px; cursor: pointer; font-size: 12px; }
.opt-btn:hover:not(:disabled) { color: #3498db; border-color: #3498db; }
.opt-btn:disabled { opacity: 0.5; cursor: not-allowed; }
.selected-files { margin-top: 12px; color: #a0acbe; font-size: 12px; word-break: break-all; }
.selected-files span { display: block; margin: 3px 0; }
.selected-files .more-line { color: #5e6878; }
.bridge-tip { margin-top: 10px; color: #e6a23c; font-size: 11px; }
.env-tip { margin-top: 10px; padding: 8px 10px; border-radius: 8px; background: rgba(230, 162, 60, 0.1); color: #e6a23c; font-size: 12px; line-height: 1.6; }
.radio-cards { display: flex; flex-direction: column; gap: 8px; margin-bottom: 6px; }
.radio-card { display: flex; flex-direction: column; gap: 3px; padding: 10px 12px; border: 1px solid #333; border-radius: 8px; background: #2e3a52; cursor: pointer; }
.radio-card.selected { border-color: #409eff; background: rgba(64, 158, 255, 0.1); }
.radio-card input { position: absolute; opacity: 0; pointer-events: none; }
.radio-card-name { color: #c8d2e2; font-size: 13px; }
.radio-card-desc { color: #5e6878; font-size: 11px; }
.run-mode-zone { display: flex; flex-direction: column; gap: 6px; padding: 10px 12px; border: 1px solid #333; border-radius: 8px; background: #2e3a52; }
.run-mode-row { display: inline-flex; align-items: center; gap: 8px; cursor: pointer; color: #c8d2e2; font-size: 12px; }
.run-mode-row input { width: 15px; height: 15px; accent-color: #409eff; }
.btn-run { padding: 10px 18px; color: #f5f8fc; background: #3498db; border: none; border-radius: 8px; font-weight: 600; cursor: pointer; }
.btn-run:disabled { opacity: 0.5; cursor: not-allowed; }
.loading-msg { margin-top: 10px; }
.panel-header { display: flex; justify-content: space-between; align-items: center; padding: 14px 20px; border-bottom: 1px solid #2e3a52; color: #f5f8fc; font-size: 15px; font-weight: 600; }
.btn-refresh { padding: 6px 12px; color: #c8d2e2; background: #2e3a52; border: 1px solid #3e4a62; border-radius: 6px; cursor: pointer; font-size: 12px; }
.task-list-wrap { flex: 1; padding: 16px 20px; overflow: auto; }
.empty-tasks { color: #5e6878; font-size: 13px; padding: 28px; text-align: center; }
.task-list { list-style: none; margin: 0; padding: 0; }
.task-item { display: flex; justify-content: space-between; align-items: flex-start; gap: 12px; padding: 12px 14px; margin-bottom: 8px; border: 1px solid #2e3a52; border-radius: 8px; background: #1c2333; }
.left { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 4px; }
.id { color: #f5f8fc; font-size: 13px; font-weight: 600; }
.files.error, .error { color: #ff6b6b; }
.task-right { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; justify-content: flex-end; }
.status { padding: 4px 10px; border-radius: 6px; font-size: 12px; }
.status.pending { background: rgba(149, 165, 166, 0.18); color: #a0acbe; }
.status.running { background: rgba(52, 152, 219, 0.18); color: #3498db; }
.status.success { background: rgba(46, 204, 113, 0.18); color: #2ecc71; }
.status.failed { background: rgba(231, 76, 60, 0.18); color: #ff6b6b; }
.status.cancelled { background: rgba(149, 165, 166, 0.18); color: #a0acbe; }
.act-btn { padding: 5px 10px; border-radius: 6px; font-size: 12px; background: #2e3a52; color: #c8d2e2; border: 1px solid #3e4a62; cursor: pointer; }
.act-btn.ok { background: rgba(52, 152, 219, 0.18); color: #69b6ff; border-color: transparent; }
.act-btn.danger { background: rgba(231, 76, 60, 0.15); color: #ff8f8f; border-color: transparent; }
.task-progress { margin-top: 6px; max-width: 460px; }
.task-progress-meta { display: flex; justify-content: space-between; color: #5e6878; font-size: 11px; margin-bottom: 4px; }
.task-progress-track { height: 5px; border-radius: 3px; background: #333; overflow: hidden; }
.task-progress-fill { height: 100%; background: #3498db; transition: width 0.25s ease; }
@media (max-width: 1100px) {
.main-content { flex-direction: column; height: auto; }
.left-panel { width: 100%; border-right: none; border-bottom: 1px solid #2e3a52; }
}
</style>
@@ -1,6 +1,6 @@
<template> <template>
<div class="page-shell module-page"> <div class="page-shell module-page">
<BrandTopBar active="collect-data" /> <AmazonToolPageShell tool-id="collect">
<div class="main-content"> <div class="main-content">
<aside class="left-panel left-panel--with-template"> <aside class="left-panel left-panel--with-template">
@@ -206,13 +206,14 @@
</div> </div>
</section> </section>
</div> </div>
</AmazonToolPageShell>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, reactive, ref } from 'vue' import { computed, onBeforeUnmount, onMounted, reactive, ref } from 'vue'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import BrandTopBar from '@/pages/brand/components/BrandTopBar.vue' import AmazonToolPageShell from '@/pages/amazon/components/AmazonToolPageShell.vue';
import ModuleTemplateDownload from '@/shared/components/ModuleTemplateDownload.vue' import ModuleTemplateDownload from '@/shared/components/ModuleTemplateDownload.vue'
import { expandBrandFolderRecursive, type BrandExpandFolderItem } from '@/shared/api/brand' import { expandBrandFolderRecursive, type BrandExpandFolderItem } from '@/shared/api/brand'
import { getPywebviewApi } from '@/shared/bridges/pywebview' import { getPywebviewApi } from '@/shared/bridges/pywebview'
@@ -266,9 +267,9 @@ const filters = reactive<{
}>({ }>({
minAmount: null, minAmount: null,
maxAmount: null, maxAmount: null,
rank: false, rank: true,
fba: false, fba: false,
fbm: false, fbm: true,
}) })
const selectedCountryCode = ref<string>('DE') const selectedCountryCode = ref<string>('DE')
@@ -761,28 +762,28 @@ onBeforeUnmount(() => {
</script> </script>
<style scoped> <style scoped>
.module-page { min-height: 100vh; background: #1a1a1a; } .module-page { min-height: 100vh; background: #151a25; }
.main-content { display: flex; height: calc(100vh - 56px); min-height: calc(100vh - 56px); } .main-content { display: flex; height: calc(100vh - 56px); min-height: calc(100vh - 56px); }
.left-panel { width: 400px; background: #1e1e1e; padding: 20px; overflow-y: auto; border-right: 1px solid #2a2a2a; } .left-panel { width: 400px; background: #1c2333; padding: 20px; overflow-y: auto; border-right: 1px solid #2e3a52; }
.right-panel { flex: 1; min-width: 0; background: #1a1a1a; display: flex; flex-direction: column; } .right-panel { flex: 1; min-width: 0; background: #151a25; display: flex; flex-direction: column; }
.section-title, .subsection-title { font-size: 13px; color: #bbb; margin: 12px 0 10px; } .section-title, .subsection-title { font-size: 13px; color: #a0acbe; margin: 12px 0 10px; }
.upload-zone { border: 1px dashed #3a3a3a; border-radius: 10px; padding: 18px; background: #252525; margin-bottom: 18px; } .upload-zone { border: 1px dashed #3e4a62; border-radius: 10px; padding: 18px; background: #2e3a52; margin-bottom: 18px; }
.hint, .loading-msg, .files, .muted { color: #888; font-size: 12px; line-height: 1.5; } .hint, .loading-msg, .files, .muted { color: #5e6878; font-size: 12px; line-height: 1.5; }
.btns, .run-row { display: flex; gap: 10px; flex-wrap: wrap; } .btns, .run-row { display: flex; gap: 10px; flex-wrap: wrap; }
.opt-btn, .btn-run, .btn-delete, .download { border: none; cursor: pointer; border-radius: 7px; } .opt-btn, .btn-run, .btn-delete, .download { border: none; cursor: pointer; border-radius: 7px; }
.opt-btn { padding: 8px 14px; color: #ccc; background: #2a2a2a; border: 1px solid #3a3a3a; } .opt-btn { padding: 8px 14px; color: #c8d2e2; background: #2e3a52; border: 1px solid #3e4a62; }
.btn-run { padding: 10px 18px; color: #fff; background: #3498db; font-weight: 600; } .btn-run { padding: 10px 18px; color: #f5f8fc; background: #3498db; font-weight: 600; }
.btn-run.btn-queue { background: #27ae60; } .btn-run.btn-queue { background: #27ae60; }
.btn-run:disabled { opacity: .55; cursor: not-allowed; } .btn-run:disabled { opacity: .55; cursor: not-allowed; }
.selected-files { margin-top: 14px; color: #999; font-size: 12px; word-break: break-all; } .selected-files { margin-top: 14px; color: #a0acbe; font-size: 12px; word-break: break-all; }
.selected-files span { display: block; margin: 4px 0; } .selected-files span { display: block; margin: 4px 0; }
.selected-files .more-line { color: #777; } .selected-files .more-line { color: #5e6878; }
.filter-zone { .filter-zone {
border: 1px solid #2a2a2a; border: 1px solid #2e3a52;
border-radius: 8px; border-radius: 8px;
padding: 12px; padding: 12px;
background: #252525; background: #2e3a52;
margin-bottom: 14px; margin-bottom: 14px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -795,70 +796,70 @@ onBeforeUnmount(() => {
} }
.filter-label { .filter-label {
width: 110px; width: 110px;
color: #bbb; color: #a0acbe;
font-size: 12px; font-size: 12px;
} }
.filter-input { .filter-input {
flex: 1; flex: 1;
min-width: 0; min-width: 0;
padding: 6px 10px; padding: 6px 10px;
background: #1e1e1e; background: #1c2333;
border: 1px solid #3a3a3a; border: 1px solid #3e4a62;
border-radius: 6px; border-radius: 6px;
color: #ddd; color: #c8d2e2;
font-size: 13px; font-size: 13px;
outline: none; outline: none;
} }
.filter-input:focus { border-color: #3498db; } .filter-input:focus { border-color: #3498db; }
.range-inputs { flex: 1; min-width: 0; display: flex; align-items: center; gap: 8px; } .range-inputs { flex: 1; min-width: 0; display: flex; align-items: center; gap: 8px; }
.range-separator { color: #777; font-size: 12px; white-space: nowrap; } .range-separator { color: #5e6878; font-size: 12px; white-space: nowrap; }
.filter-checks { display: flex; gap: 16px; } .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-row { display: inline-flex; align-items: center; gap: 6px; cursor: pointer; user-select: none; color: #c8d2e2; font-size: 12px; }
.filter-check-input { width: 16px; height: 16px; accent-color: #409eff; cursor: pointer; } .filter-check-input { width: 16px; height: 16px; accent-color: #409eff; cursor: pointer; }
.country-pref-hint { margin-top: -4px; } .country-pref-hint { margin-top: -4px; }
.country-section-title { display: flex; align-items: center; justify-content: space-between; gap: 12px; } .country-section-title { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.country-pref-saving { color: #3498db; font-size: 11px; } .country-pref-saving { color: #3498db; font-size: 11px; }
.country-pref-radios { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; margin-bottom: 14px; } .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 { display: inline-flex; align-items: center; gap: 8px; cursor: pointer; user-select: none; font-size: 12px; color: #c8d2e2; padding: 8px 10px; border: 1px solid #333; border-radius: 8px; background: #2e3a52; }
.country-check-row.selected { border-color: #409eff; background: rgba(64, 158, 255, .12); color: #e6f2ff; } .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-input { width: 16px; height: 16px; accent-color: #409eff; cursor: pointer; flex-shrink: 0; }
.country-check-text { line-height: 1.3; } .country-check-text { line-height: 1.3; }
.queue-debug-card { margin-top: 14px; border: 1px solid #2a2a2a; border-radius: 8px; padding: 10px 12px; background: #1f1f1f; } .queue-debug-card { margin-top: 14px; border: 1px solid #2e3a52; border-radius: 8px; padding: 10px 12px; background: #1f1f1f; }
.queue-debug-title { margin-top: 0; } .queue-debug-title { margin-top: 0; }
.queue-debug-line { color: #9ad; font-size: 12px; } .queue-debug-line { color: #9ad; font-size: 12px; }
.queue-debug-payload { margin-top: 8px; padding: 8px 10px; background: #181818; border-radius: 6px; color: #bbb; font-size: 11px; overflow: auto; max-height: 200px; } .queue-debug-payload { margin-top: 8px; padding: 8px 10px; background: #151a25; border-radius: 6px; color: #a0acbe; font-size: 11px; overflow: auto; max-height: 200px; }
.panel-header { padding: 16px 20px; border-bottom: 1px solid #2a2a2a; font-size: 15px; font-weight: 600; color: #ddd; } .panel-header { padding: 16px 20px; border-bottom: 1px solid #2e3a52; font-size: 15px; font-weight: 600; color: #c8d2e2; }
.task-list-wrap { flex: 1; padding: 16px 20px; overflow: auto; } .task-list-wrap { flex: 1; padding: 16px 20px; overflow: auto; }
.clean-result-summary { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 12px; margin-bottom: 16px; } .clean-result-summary { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 12px; margin-bottom: 16px; }
.summary-card { padding: 14px 16px; border: 1px solid #2a2a2a; border-radius: 8px; background: #1e1e1e; } .summary-card { padding: 14px 16px; border: 1px solid #2e3a52; border-radius: 8px; background: #1c2333; }
.summary-card strong { display: block; margin-top: 8px; color: #eaf4ff; font-size: 22px; } .summary-card strong { display: block; margin-top: 8px; color: #f5f8fc; font-size: 22px; }
.summary-label { color: #8d8d8d; font-size: 12px; } .summary-label { color: #5e6878; font-size: 12px; }
.result-list-wrap { border: 1px solid #2a2a2a; border-radius: 8px; background: #1e1e1e; min-height: 180px; margin: 0 0 16px; } .result-list-wrap { border: 1px solid #2e3a52; border-radius: 8px; background: #1c2333; min-height: 180px; margin: 0 0 16px; }
.result-list-header { display: flex; justify-content: space-between; padding: 12px 16px; border-bottom: 1px solid #2a2a2a; color: #ddd; font-size: 14px; } .result-list-header { display: flex; justify-content: space-between; padding: 12px 16px; border-bottom: 1px solid #2e3a52; color: #c8d2e2; font-size: 14px; }
.empty-tasks { color: #666; font-size: 13px; padding: 18px; text-align: center; } .empty-tasks { color: #5e6878; font-size: 13px; padding: 18px; text-align: center; }
.task-list { list-style: none; margin: 0; padding: 12px; } .task-list { list-style: none; margin: 0; padding: 12px; }
.task-item { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; padding: 12px 14px; border: 1px solid #2a2a2a; border-radius: 8px; margin-bottom: 8px; background: #222; } .task-item { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; padding: 12px 14px; border: 1px solid #2e3a52; border-radius: 8px; margin-bottom: 8px; background: #222; }
.left { flex: 1; min-width: 0; } .left { flex: 1; min-width: 0; }
.id { color: #e0e0e0; font-size: 13px; font-weight: 600; } .id { color: #f5f8fc; font-size: 13px; font-weight: 600; }
.task-right { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; } .task-right { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
.status { padding: 4px 10px; border-radius: 6px; font-size: 12px; } .status { padding: 4px 10px; border-radius: 6px; font-size: 12px; }
.status.success { background: rgba(46, 204, 113, .18); color: #2ecc71; } .status.success { background: rgba(46, 204, 113, .18); color: #2ecc71; }
.status.failed { background: rgba(231, 76, 60, .18); color: #ff6b6b; } .status.failed { background: rgba(231, 76, 60, .18); color: #ff6b6b; }
.status.running { background: rgba(52, 152, 219, .18); color: #3498db; } .status.running { background: rgba(52, 152, 219, .18); color: #3498db; }
.status.pending { background: rgba(149, 165, 166, .18); color: #bdc3c7; } .status.pending { background: rgba(149, 165, 166, .18); color: #a0acbe; }
.status-filter-summary { font-weight: 400; } .status-filter-summary { font-weight: 400; }
.download { padding: 6px 10px; color: #d6ecff; background: rgba(52, 152, 219, .18); } .download { padding: 6px 10px; color: #c8d2e2; background: rgba(52, 152, 219, .18); }
.btn-delete { padding: 6px 10px; color: #ff8f8f; background: rgba(231, 76, 60, .12); } .btn-delete { padding: 6px 10px; color: #ff8f8f; background: rgba(231, 76, 60, .12); }
.task-progress { margin-top: 8px; max-width: 520px; } .task-progress { margin-top: 8px; max-width: 520px; }
.task-progress-meta { display: flex; justify-content: space-between; color: #888; font-size: 11px; margin-bottom: 4px; } .task-progress-meta { display: flex; justify-content: space-between; color: #5e6878; font-size: 11px; margin-bottom: 4px; }
.task-progress-track { height: 5px; border-radius: 3px; background: #333; overflow: hidden; } .task-progress-track { height: 5px; border-radius: 3px; background: #333; overflow: hidden; }
.task-progress-fill { height: 100%; border-radius: inherit; background: #3498db; transition: width .25s ease; } .task-progress-fill { height: 100%; border-radius: inherit; background: #3498db; transition: width .25s ease; }
@media (max-width: 1100px) { @media (max-width: 1100px) {
.main-content { flex-direction: column; height: auto; } .main-content { flex-direction: column; height: auto; }
.left-panel { width: 100%; border-right: none; border-bottom: 1px solid #2a2a2a; } .left-panel { width: 100%; border-right: none; border-bottom: 1px solid #2e3a52; }
.clean-result-summary { grid-template-columns: repeat(2, minmax(0, 1fr)); } .clean-result-summary { grid-template-columns: repeat(2, minmax(0, 1fr)); }
} }
</style> </style>
@@ -1,6 +1,6 @@
<template> <template>
<div class="page-shell module-page"> <div class="page-shell module-page">
<BrandTopBar active="convert" /> <AmazonToolPageShell tool-id="convert">
<div class="main-content"> <div class="main-content">
<aside class="left-panel"> <aside class="left-panel">
@@ -166,13 +166,14 @@
</div> </div>
</section> </section>
</div> </div>
</AmazonToolPageShell>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, onMounted, ref } from 'vue' import { computed, onMounted, ref } from 'vue'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import BrandTopBar from './BrandTopBar.vue' import AmazonToolPageShell from '@/pages/amazon/components/AmazonToolPageShell.vue';
import { expandBrandFolderRecursive, getBrandTemplateXlsxUrl, getBrandTemplateZipUrl } from '@/shared/api/brand' import { expandBrandFolderRecursive, getBrandTemplateXlsxUrl, getBrandTemplateZipUrl } from '@/shared/api/brand'
import type { BrandExpandFolderItem } from '@/shared/api/brand' import type { BrandExpandFolderItem } from '@/shared/api/brand'
import { import {
@@ -530,7 +531,7 @@ onMounted(() => {
<style scoped> <style scoped>
.module-page { .module-page {
min-height: 100vh; min-height: 100vh;
background: #1a1a1a; background: #151a25;
} }
.main-content { .main-content {
@@ -541,10 +542,10 @@ onMounted(() => {
.left-panel { .left-panel {
width: 380px; width: 380px;
background: #1e1e1e; background: #1c2333;
padding: 20px; padding: 20px;
overflow-y: auto; overflow-y: auto;
border-right: 1px solid #2a2a2a; border-right: 1px solid #2e3a52;
} }
.right-panel { .right-panel {
@@ -552,32 +553,32 @@ onMounted(() => {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
min-width: 0; min-width: 0;
background: #1a1a1a; background: #151a25;
} }
.section-title { .section-title {
font-size: 13px; font-size: 13px;
color: #bbb; color: #a0acbe;
margin-bottom: 10px; margin-bottom: 10px;
} }
.upload-zone { .upload-zone {
border: 1px dashed #3a3a3a; border: 1px dashed #3e4a62;
border-radius: 10px; border-radius: 10px;
padding: 24px; padding: 24px;
text-align: center; text-align: center;
background: #252525; background: #2e3a52;
margin-bottom: 20px; margin-bottom: 20px;
transition: all 0.2s ease; transition: all 0.2s ease;
} }
.upload-zone:hover { .upload-zone:hover {
border-color: #3498db; border-color: #3498db;
background: #2a2a2a; background: #2e3a52;
} }
.hint { .hint {
color: #888; color: #5e6878;
font-size: 13px; font-size: 13px;
margin-bottom: 12px; margin-bottom: 12px;
line-height: 1.5; line-height: 1.5;
@@ -603,9 +604,9 @@ onMounted(() => {
.opt-btn { .opt-btn {
padding: 8px 16px; padding: 8px 16px;
font-size: 13px; font-size: 13px;
color: #ccc; color: #c8d2e2;
background: #2a2a2a; background: #2e3a52;
border: 1px solid #3a3a3a; border: 1px solid #3e4a62;
border-radius: 6px; border-radius: 6px;
} }
@@ -633,7 +634,7 @@ onMounted(() => {
.selected-files { .selected-files {
margin-top: 14px; margin-top: 14px;
font-size: 12px; font-size: 12px;
color: #888; color: #5e6878;
max-height: 112px; max-height: 112px;
overflow-y: auto; overflow-y: auto;
text-align: left; text-align: left;
@@ -646,7 +647,7 @@ onMounted(() => {
} }
.more-line { .more-line {
color: #b8c1cc; color: #a0acbe;
} }
.option-group { .option-group {
@@ -660,8 +661,8 @@ onMounted(() => {
cursor: pointer; cursor: pointer;
padding: 10px 12px; padding: 10px 12px;
border-radius: 8px; border-radius: 8px;
background: #252525; background: #2e3a52;
border: 1px solid #2a2a2a; border: 1px solid #2e3a52;
margin-bottom: 8px; margin-bottom: 8px;
transition: all 0.2s ease; transition: all 0.2s ease;
} }
@@ -676,8 +677,8 @@ onMounted(() => {
.radio-item:hover, .radio-item:hover,
.radio-item.active { .radio-item.active {
background: #2a2a2a; background: #2e3a52;
border-color: #3a3a3a; border-color: #3e4a62;
} }
.radio-item input { .radio-item input {
@@ -686,13 +687,13 @@ onMounted(() => {
.label { .label {
font-weight: 500; font-weight: 500;
color: #e0e0e0; color: #f5f8fc;
font-size: 13px; font-size: 13px;
} }
.desc { .desc {
font-size: 12px; font-size: 12px;
color: #888; color: #5e6878;
margin-top: 2px; margin-top: 2px;
line-height: 1.5; line-height: 1.5;
font-weight: 400; font-weight: 400;
@@ -719,9 +720,9 @@ onMounted(() => {
.empty-option { .empty-option {
padding: 12px; padding: 12px;
border: 1px dashed #3a3a3a; border: 1px dashed #3e4a62;
border-radius: 8px; border-radius: 8px;
color: #777; color: #5e6878;
font-size: 12px; font-size: 12px;
} }
@@ -735,7 +736,7 @@ onMounted(() => {
.template-actions-hint { .template-actions-hint {
grid-column: 1 / -1; grid-column: 1 / -1;
font-size: 12px; font-size: 12px;
color: #888; color: #5e6878;
line-height: 1.5; line-height: 1.5;
} }
@@ -752,7 +753,7 @@ onMounted(() => {
.template-download-row { .template-download-row {
width: 100%; width: 100%;
padding: 14px 16px; padding: 14px 16px;
background: linear-gradient(135deg, #252525 0%, #1f2b1f 100%); background: linear-gradient(135deg, #2e3a52 0%, #1f2b1f 100%);
border: 1px solid #2a3a2a; border: 1px solid #2a3a2a;
border-radius: 10px; border-radius: 10px;
display: flex; display: flex;
@@ -763,7 +764,7 @@ onMounted(() => {
.template-download-row:hover { .template-download-row:hover {
border-color: #2e7d32; border-color: #2e7d32;
background: linear-gradient(135deg, #2a2a2a 0%, #243324 100%); background: linear-gradient(135deg, #2e3a52 0%, #243324 100%);
box-shadow: 0 4px 12px rgba(46, 125, 50, 0.15); box-shadow: 0 4px 12px rgba(46, 125, 50, 0.15);
} }
@@ -776,7 +777,7 @@ onMounted(() => {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
color: #fff; color: #f5f8fc;
font-size: 11px; font-size: 11px;
font-weight: 700; font-weight: 700;
letter-spacing: 0.08em; letter-spacing: 0.08em;
@@ -790,14 +791,14 @@ onMounted(() => {
.template-download-text .title { .template-download-text .title {
font-size: 14px; font-size: 14px;
color: #e0e0e0; color: #f5f8fc;
font-weight: 600; font-weight: 600;
} }
.template-download-text .hint { .template-download-text .hint {
margin: 2px 0 0; margin: 2px 0 0;
font-size: 12px; font-size: 12px;
color: #888; color: #5e6878;
} }
.run-row { .run-row {
@@ -811,7 +812,7 @@ onMounted(() => {
.btn-run { .btn-run {
padding: 10px 22px; padding: 10px 22px;
background: #3498db; background: #3498db;
color: #fff; color: #f5f8fc;
border-radius: 8px; border-radius: 8px;
font-size: 14px; font-size: 14px;
} }
@@ -822,7 +823,7 @@ onMounted(() => {
.btn-run:disabled { .btn-run:disabled {
background: #555; background: #555;
color: #999; color: #a0acbe;
cursor: not-allowed; cursor: not-allowed;
} }
@@ -833,10 +834,10 @@ onMounted(() => {
.panel-header { .panel-header {
padding: 16px 20px; padding: 16px 20px;
border-bottom: 1px solid #2a2a2a; border-bottom: 1px solid #2e3a52;
font-size: 15px; font-size: 15px;
font-weight: 600; font-weight: 600;
color: #ddd; color: #c8d2e2;
} }
.task-list-wrap { .task-list-wrap {
@@ -857,10 +858,10 @@ onMounted(() => {
justify-content: space-between; justify-content: space-between;
gap: 16px; gap: 16px;
padding: 14px 16px; padding: 14px 16px;
border: 1px solid #2a2a2a; border: 1px solid #2e3a52;
border-radius: 8px; border-radius: 8px;
margin-bottom: 10px; margin-bottom: 10px;
background: #1e1e1e; background: #1c2333;
} }
.left { .left {
@@ -871,13 +872,13 @@ onMounted(() => {
.id { .id {
display: inline-block; display: inline-block;
font-weight: 600; font-weight: 600;
color: #e0e0e0; color: #f5f8fc;
font-size: 13px; font-size: 13px;
} }
.files { .files {
font-size: 12px; font-size: 12px;
color: #888; color: #5e6878;
margin-top: 4px; margin-top: 4px;
} }
@@ -931,7 +932,7 @@ onMounted(() => {
} }
.empty-tasks { .empty-tasks {
color: #666; color: #5e6878;
font-size: 13px; font-size: 13px;
padding: 24px 8px; padding: 24px 8px;
} }
@@ -949,27 +950,27 @@ onMounted(() => {
.summary-card { .summary-card {
padding: 16px 18px; padding: 16px 18px;
border: 1px solid #2a2a2a; border: 1px solid #2e3a52;
border-radius: 10px; border-radius: 10px;
background: #1e1e1e; background: #1c2333;
} }
.summary-card strong { .summary-card strong {
display: block; display: block;
margin-top: 8px; margin-top: 8px;
font-size: 24px; font-size: 24px;
color: #eaf4ff; color: #f5f8fc;
} }
.summary-label { .summary-label {
font-size: 12px; font-size: 12px;
color: #8d8d8d; color: #5e6878;
} }
.result-list-wrap { .result-list-wrap {
border: 1px solid #2a2a2a; border: 1px solid #2e3a52;
border-radius: 10px; border-radius: 10px;
background: #1e1e1e; background: #1c2333;
min-height: 260px; min-height: 260px;
} }
@@ -979,9 +980,9 @@ onMounted(() => {
justify-content: space-between; justify-content: space-between;
gap: 12px; gap: 12px;
padding: 14px 16px; padding: 14px 16px;
border-bottom: 1px solid #2a2a2a; border-bottom: 1px solid #2e3a52;
font-size: 14px; font-size: 14px;
color: #ddd; color: #c8d2e2;
} }
@media (max-width: 1100px) { @media (max-width: 1100px) {
@@ -993,7 +994,7 @@ onMounted(() => {
.left-panel { .left-panel {
width: 100%; width: 100%;
border-right: none; border-right: none;
border-bottom: 1px solid #2a2a2a; border-bottom: 1px solid #2e3a52;
} }
.right-panel { .right-panel {
@@ -1,6 +1,6 @@
<template> <template>
<div class="page-shell module-page"> <div class="page-shell module-page">
<BrandTopBar active="dedupe" /> <AmazonToolPageShell tool-id="dedupe">
<div class="main-content"> <div class="main-content">
<aside class="left-panel"> <aside class="left-panel">
@@ -140,13 +140,14 @@
</div> </div>
</section> </section>
</div> </div>
</AmazonToolPageShell>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, onMounted, ref } from 'vue' import { computed, onMounted, ref } from 'vue'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import BrandTopBar from './BrandTopBar.vue' import AmazonToolPageShell from '@/pages/amazon/components/AmazonToolPageShell.vue';
import { expandBrandFolderRecursive, type BrandExpandFolderItem } from '@/shared/api/brand' import { expandBrandFolderRecursive, type BrandExpandFolderItem } from '@/shared/api/brand'
import { deleteDedupeHistory, getDedupeHistory, getDedupeResultDownloadUrl, getDedupeRunProgress, getExcelInfo, runDedupe, type DedupeResultItem, type DedupeRunProgressVo, type DedupeRunVo } from '@/shared/api/java-modules' import { deleteDedupeHistory, getDedupeHistory, getDedupeResultDownloadUrl, getDedupeRunProgress, getExcelInfo, runDedupe, type DedupeResultItem, type DedupeRunProgressVo, type DedupeRunVo } from '@/shared/api/java-modules'
import { getPywebviewApi, type UploadedJavaFile } from '@/shared/bridges/pywebview' import { getPywebviewApi, type UploadedJavaFile } from '@/shared/bridges/pywebview'
@@ -414,7 +415,7 @@ onMounted(() => {
<style scoped> <style scoped>
.module-page { .module-page {
min-height: 100vh; min-height: 100vh;
background: #1a1a1a; background: #151a25;
} }
.main-content { .main-content {
@@ -425,10 +426,10 @@ onMounted(() => {
.left-panel { .left-panel {
width: 380px; width: 380px;
background: #1e1e1e; background: #1c2333;
padding: 20px; padding: 20px;
overflow-y: auto; overflow-y: auto;
border-right: 1px solid #2a2a2a; border-right: 1px solid #2e3a52;
} }
.right-panel { .right-panel {
@@ -436,32 +437,32 @@ onMounted(() => {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
min-width: 0; min-width: 0;
background: #1a1a1a; background: #151a25;
} }
.section-title { .section-title {
font-size: 13px; font-size: 13px;
color: #bbb; color: #a0acbe;
margin-bottom: 10px; margin-bottom: 10px;
} }
.upload-zone { .upload-zone {
border: 1px dashed #3a3a3a; border: 1px dashed #3e4a62;
border-radius: 10px; border-radius: 10px;
padding: 24px; padding: 24px;
text-align: center; text-align: center;
background: #252525; background: #2e3a52;
margin-bottom: 20px; margin-bottom: 20px;
transition: all 0.2s ease; transition: all 0.2s ease;
} }
.upload-zone:hover { .upload-zone:hover {
border-color: #3498db; border-color: #3498db;
background: #2a2a2a; background: #2e3a52;
} }
.hint { .hint {
color: #888; color: #5e6878;
font-size: 13px; font-size: 13px;
margin-bottom: 12px; margin-bottom: 12px;
line-height: 1.5; line-height: 1.5;
@@ -486,9 +487,9 @@ onMounted(() => {
.opt-btn { .opt-btn {
padding: 8px 16px; padding: 8px 16px;
font-size: 13px; font-size: 13px;
color: #ccc; color: #c8d2e2;
background: #2a2a2a; background: #2e3a52;
border: 1px solid #3a3a3a; border: 1px solid #3e4a62;
border-radius: 6px; border-radius: 6px;
} }
@@ -506,7 +507,7 @@ onMounted(() => {
.selected-files { .selected-files {
margin-top: 14px; margin-top: 14px;
font-size: 12px; font-size: 12px;
color: #888; color: #5e6878;
max-height: 112px; max-height: 112px;
overflow-y: auto; overflow-y: auto;
text-align: left; text-align: left;
@@ -519,7 +520,7 @@ onMounted(() => {
} }
.more-line { .more-line {
color: #b8c1cc; color: #a0acbe;
} }
.option-group { .option-group {
@@ -529,8 +530,8 @@ onMounted(() => {
.column-option-group { .column-option-group {
padding: 14px; padding: 14px;
border-radius: 10px; border-radius: 10px;
background: #252525; background: #2e3a52;
border: 1px solid #2a2a2a; border: 1px solid #2e3a52;
} }
.column-toolbar { .column-toolbar {
@@ -543,7 +544,7 @@ onMounted(() => {
.column-count { .column-count {
font-size: 12px; font-size: 12px;
color: #a9b4bf; color: #a0acbe;
} }
.column-actions { .column-actions {
@@ -562,8 +563,8 @@ onMounted(() => {
.empty-column-state { .empty-column-state {
padding: 16px 12px; padding: 16px 12px;
border-radius: 8px; border-radius: 8px;
border: 1px dashed #3a3a3a; border: 1px dashed #3e4a62;
background: #202020; background: #222b3d;
color: #7f8790; color: #7f8790;
font-size: 12px; font-size: 12px;
line-height: 1.6; line-height: 1.6;
@@ -575,8 +576,8 @@ onMounted(() => {
gap: 10px; gap: 10px;
padding: 10px 12px; padding: 10px 12px;
border-radius: 8px; border-radius: 8px;
border: 1px solid #2f2f2f; border: 1px solid #323e58;
background: #202020; background: #222b3d;
cursor: pointer; cursor: pointer;
transition: all 0.2s ease; transition: all 0.2s ease;
font-size: 13px; font-size: 13px;
@@ -604,16 +605,16 @@ onMounted(() => {
cursor: pointer; cursor: pointer;
padding: 10px 12px; padding: 10px 12px;
border-radius: 8px; border-radius: 8px;
background: #252525; background: #2e3a52;
border: 1px solid #2a2a2a; border: 1px solid #2e3a52;
margin-bottom: 8px; margin-bottom: 8px;
transition: all 0.2s ease; transition: all 0.2s ease;
} }
.radio-item:hover, .radio-item:hover,
.radio-item.active { .radio-item.active {
background: #2a2a2a; background: #2e3a52;
border-color: #3a3a3a; border-color: #3e4a62;
} }
.radio-item input { .radio-item input {
@@ -622,13 +623,13 @@ onMounted(() => {
.label { .label {
font-weight: 500; font-weight: 500;
color: #e0e0e0; color: #f5f8fc;
font-size: 13px; font-size: 13px;
} }
.desc { .desc {
font-size: 12px; font-size: 12px;
color: #888; color: #5e6878;
margin-top: 2px; margin-top: 2px;
line-height: 1.5; line-height: 1.5;
font-weight: 400; font-weight: 400;
@@ -645,7 +646,7 @@ onMounted(() => {
.btn-run { .btn-run {
padding: 10px 22px; padding: 10px 22px;
background: #3498db; background: #3498db;
color: #fff; color: #f5f8fc;
border-radius: 8px; border-radius: 8px;
font-size: 14px; font-size: 14px;
} }
@@ -656,7 +657,7 @@ onMounted(() => {
.btn-run:disabled { .btn-run:disabled {
background: #555; background: #555;
color: #999; color: #a0acbe;
cursor: not-allowed; cursor: not-allowed;
} }
@@ -667,10 +668,10 @@ onMounted(() => {
.panel-header { .panel-header {
padding: 16px 20px; padding: 16px 20px;
border-bottom: 1px solid #2a2a2a; border-bottom: 1px solid #2e3a52;
font-size: 15px; font-size: 15px;
font-weight: 600; font-weight: 600;
color: #ddd; color: #c8d2e2;
} }
.task-list-wrap { .task-list-wrap {
@@ -691,10 +692,10 @@ onMounted(() => {
justify-content: space-between; justify-content: space-between;
gap: 16px; gap: 16px;
padding: 14px 16px; padding: 14px 16px;
border: 1px solid #2a2a2a; border: 1px solid #2e3a52;
border-radius: 8px; border-radius: 8px;
margin-bottom: 10px; margin-bottom: 10px;
background: #1e1e1e; background: #1c2333;
} }
.left { .left {
@@ -705,13 +706,13 @@ onMounted(() => {
.id { .id {
display: inline-block; display: inline-block;
font-weight: 600; font-weight: 600;
color: #e0e0e0; color: #f5f8fc;
font-size: 13px; font-size: 13px;
} }
.files { .files {
font-size: 12px; font-size: 12px;
color: #888; color: #5e6878;
margin-top: 4px; margin-top: 4px;
} }
@@ -765,7 +766,7 @@ onMounted(() => {
} }
.empty-tasks { .empty-tasks {
color: #666; color: #5e6878;
font-size: 13px; font-size: 13px;
padding: 24px 8px; padding: 24px 8px;
} }
@@ -783,27 +784,27 @@ onMounted(() => {
.summary-card { .summary-card {
padding: 16px 18px; padding: 16px 18px;
border: 1px solid #2a2a2a; border: 1px solid #2e3a52;
border-radius: 10px; border-radius: 10px;
background: #1e1e1e; background: #1c2333;
} }
.summary-card strong { .summary-card strong {
display: block; display: block;
margin-top: 8px; margin-top: 8px;
font-size: 24px; font-size: 24px;
color: #eaf4ff; color: #f5f8fc;
} }
.summary-label { .summary-label {
font-size: 12px; font-size: 12px;
color: #8d8d8d; color: #5e6878;
} }
.result-list-wrap { .result-list-wrap {
border: 1px solid #2a2a2a; border: 1px solid #2e3a52;
border-radius: 10px; border-radius: 10px;
background: #1e1e1e; background: #1c2333;
min-height: 260px; min-height: 260px;
} }
@@ -813,9 +814,9 @@ onMounted(() => {
justify-content: space-between; justify-content: space-between;
gap: 12px; gap: 12px;
padding: 14px 16px; padding: 14px 16px;
border-bottom: 1px solid #2a2a2a; border-bottom: 1px solid #2e3a52;
font-size: 14px; font-size: 14px;
color: #ddd; color: #c8d2e2;
} }
@media (max-width: 1100px) { @media (max-width: 1100px) {
@@ -827,7 +828,7 @@ onMounted(() => {
.left-panel { .left-panel {
width: 100%; width: 100%;
border-right: none; border-right: none;
border-bottom: 1px solid #2a2a2a; border-bottom: 1px solid #2e3a52;
} }
.right-panel { .right-panel {
@@ -1,6 +1,6 @@
<template> <template>
<div class="page-shell module-page"> <div class="page-shell module-page">
<BrandTopBar active="delete-brand" /> <AmazonToolPageShell tool-id="delasin">
<div class="main-content"> <div class="main-content">
<aside class="left-panel left-panel--with-template"> <aside class="left-panel left-panel--with-template">
@@ -208,13 +208,14 @@
</div> </div>
</section> </section>
</div> </div>
</AmazonToolPageShell>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, onMounted, onUnmounted, ref } from 'vue' import { computed, onMounted, onUnmounted, ref } from 'vue'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import BrandTopBar from './BrandTopBar.vue' import AmazonToolPageShell from '@/pages/amazon/components/AmazonToolPageShell.vue';
import ModuleTemplateDownload from '@/shared/components/ModuleTemplateDownload.vue' import ModuleTemplateDownload from '@/shared/components/ModuleTemplateDownload.vue'
import ZiniaoVersionSetting from '@/shared/components/ZiniaoVersionSetting.vue' import ZiniaoVersionSetting from '@/shared/components/ZiniaoVersionSetting.vue'
import { expandBrandFolderRecursive, type BrandExpandFolderItem } from '@/shared/api/brand' import { expandBrandFolderRecursive, type BrandExpandFolderItem } from '@/shared/api/brand'
@@ -1540,9 +1541,9 @@ onUnmounted(() => {
.queue-debug-card { .queue-debug-card {
margin-top: 18px; margin-top: 18px;
padding: 14px; padding: 14px;
border: 1px solid #2a2a2a; border: 1px solid #2e3a52;
border-radius: 10px; border-radius: 10px;
background: #202020; background: #222b3d;
} }
.queue-debug-title { .queue-debug-title {
@@ -1550,7 +1551,7 @@ onUnmounted(() => {
} }
.queue-debug-line { .queue-debug-line {
color: #b8c1cc; color: #a0acbe;
font-size: 12px; font-size: 12px;
line-height: 1.6; line-height: 1.6;
margin-bottom: 8px; margin-bottom: 8px;
@@ -1562,7 +1563,7 @@ onUnmounted(() => {
overflow: auto; overflow: auto;
padding: 10px; padding: 10px;
border-radius: 8px; border-radius: 8px;
background: #141414; background: #10141f;
color: #8fd3ff; color: #8fd3ff;
font-size: 12px; font-size: 12px;
line-height: 1.5; line-height: 1.5;
@@ -1572,7 +1573,7 @@ onUnmounted(() => {
.module-page { .module-page {
min-height: 100vh; min-height: 100vh;
background: #1a1a1a; background: #151a25;
} }
.main-content { .main-content {
@@ -1583,10 +1584,10 @@ onUnmounted(() => {
.left-panel { .left-panel {
width: 380px; width: 380px;
background: #1e1e1e; background: #1c2333;
padding: 20px; padding: 20px;
overflow-y: auto; overflow-y: auto;
border-right: 1px solid #2a2a2a; border-right: 1px solid #2e3a52;
} }
.right-panel { .right-panel {
@@ -1594,32 +1595,32 @@ onUnmounted(() => {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
min-width: 0; min-width: 0;
background: #1a1a1a; background: #151a25;
} }
.section-title { .section-title {
font-size: 13px; font-size: 13px;
color: #bbb; color: #a0acbe;
margin-bottom: 10px; margin-bottom: 10px;
} }
.upload-zone { .upload-zone {
border: 1px dashed #3a3a3a; border: 1px dashed #3e4a62;
border-radius: 10px; border-radius: 10px;
padding: 24px; padding: 24px;
text-align: center; text-align: center;
background: #252525; background: #2e3a52;
margin-bottom: 20px; margin-bottom: 20px;
transition: all 0.2s ease; transition: all 0.2s ease;
} }
.upload-zone:hover { .upload-zone:hover {
border-color: #3498db; border-color: #3498db;
background: #2a2a2a; background: #2e3a52;
} }
.hint { .hint {
color: #888; color: #5e6878;
font-size: 13px; font-size: 13px;
margin-bottom: 12px; margin-bottom: 12px;
line-height: 1.5; line-height: 1.5;
@@ -1644,9 +1645,9 @@ onUnmounted(() => {
.opt-btn { .opt-btn {
padding: 8px 16px; padding: 8px 16px;
font-size: 13px; font-size: 13px;
color: #ccc; color: #c8d2e2;
background: #2a2a2a; background: #2e3a52;
border: 1px solid #3a3a3a; border: 1px solid #3e4a62;
border-radius: 6px; border-radius: 6px;
} }
@@ -1659,7 +1660,7 @@ onUnmounted(() => {
.selected-files { .selected-files {
margin-top: 14px; margin-top: 14px;
font-size: 12px; font-size: 12px;
color: #888; color: #5e6878;
max-height: 112px; max-height: 112px;
overflow-y: auto; overflow-y: auto;
text-align: left; text-align: left;
@@ -1672,7 +1673,7 @@ onUnmounted(() => {
} }
.more-line { .more-line {
color: #b8c1cc; color: #a0acbe;
} }
.split-selected-files { .split-selected-files {
@@ -1692,16 +1693,16 @@ onUnmounted(() => {
cursor: pointer; cursor: pointer;
padding: 10px 12px; padding: 10px 12px;
border-radius: 8px; border-radius: 8px;
background: #252525; background: #2e3a52;
border: 1px solid #2a2a2a; border: 1px solid #2e3a52;
margin-bottom: 8px; margin-bottom: 8px;
transition: all 0.2s ease; transition: all 0.2s ease;
} }
.radio-item:hover, .radio-item:hover,
.radio-item.active { .radio-item.active {
background: #2a2a2a; background: #2e3a52;
border-color: #3a3a3a; border-color: #3e4a62;
} }
.radio-item input { .radio-item input {
@@ -1710,13 +1711,13 @@ onUnmounted(() => {
.label { .label {
font-weight: 500; font-weight: 500;
color: #e0e0e0; color: #f5f8fc;
font-size: 13px; font-size: 13px;
} }
.desc { .desc {
font-size: 12px; font-size: 12px;
color: #888; color: #5e6878;
margin-top: 2px; margin-top: 2px;
line-height: 1.5; line-height: 1.5;
font-weight: 400; font-weight: 400;
@@ -1733,7 +1734,7 @@ onUnmounted(() => {
.btn-run { .btn-run {
padding: 10px 22px; padding: 10px 22px;
background: #3498db; background: #3498db;
color: #fff; color: #f5f8fc;
border-radius: 8px; border-radius: 8px;
font-size: 14px; font-size: 14px;
} }
@@ -1744,7 +1745,7 @@ onUnmounted(() => {
.btn-run:disabled { .btn-run:disabled {
background: #555; background: #555;
color: #999; color: #a0acbe;
cursor: not-allowed; cursor: not-allowed;
} }
@@ -1755,10 +1756,10 @@ onUnmounted(() => {
.panel-header { .panel-header {
padding: 16px 20px; padding: 16px 20px;
border-bottom: 1px solid #2a2a2a; border-bottom: 1px solid #2e3a52;
font-size: 15px; font-size: 15px;
font-weight: 600; font-weight: 600;
color: #ddd; color: #c8d2e2;
} }
.task-list-wrap { .task-list-wrap {
@@ -1779,10 +1780,10 @@ onUnmounted(() => {
justify-content: space-between; justify-content: space-between;
gap: 16px; gap: 16px;
padding: 14px 16px; padding: 14px 16px;
border: 1px solid #2a2a2a; border: 1px solid #2e3a52;
border-radius: 8px; border-radius: 8px;
margin-bottom: 10px; margin-bottom: 10px;
background: #1e1e1e; background: #1c2333;
} }
.split-result-item { .split-result-item {
@@ -1803,19 +1804,19 @@ onUnmounted(() => {
.id { .id {
display: inline-block; display: inline-block;
font-weight: 600; font-weight: 600;
color: #e0e0e0; color: #f5f8fc;
font-size: 13px; font-size: 13px;
} }
.files { .files {
font-size: 12px; font-size: 12px;
color: #888; color: #5e6878;
margin-top: 4px; margin-top: 4px;
} }
.time { .time {
font-size: 12px; font-size: 12px;
color: #666; color: #5e6878;
margin-top: 2px; margin-top: 2px;
} }
@@ -1926,7 +1927,7 @@ onUnmounted(() => {
} }
.empty-tasks { .empty-tasks {
color: #666; color: #5e6878;
font-size: 13px; font-size: 13px;
padding: 24px 8px; padding: 24px 8px;
} }
@@ -1944,27 +1945,27 @@ onUnmounted(() => {
.summary-card { .summary-card {
padding: 16px 18px; padding: 16px 18px;
border: 1px solid #2a2a2a; border: 1px solid #2e3a52;
border-radius: 10px; border-radius: 10px;
background: #1e1e1e; background: #1c2333;
} }
.summary-card strong { .summary-card strong {
display: block; display: block;
margin-top: 8px; margin-top: 8px;
font-size: 24px; font-size: 24px;
color: #eaf4ff; color: #f5f8fc;
} }
.summary-label { .summary-label {
font-size: 12px; font-size: 12px;
color: #8d8d8d; color: #5e6878;
} }
.result-list-wrap { .result-list-wrap {
border: 1px solid #2a2a2a; border: 1px solid #2e3a52;
border-radius: 10px; border-radius: 10px;
background: #1e1e1e; background: #1c2333;
min-height: 260px; min-height: 260px;
} }
@@ -1974,9 +1975,9 @@ onUnmounted(() => {
justify-content: space-between; justify-content: space-between;
gap: 12px; gap: 12px;
padding: 14px 16px; padding: 14px 16px;
border-bottom: 1px solid #2a2a2a; border-bottom: 1px solid #2e3a52;
font-size: 14px; font-size: 14px;
color: #ddd; color: #c8d2e2;
} }
@media (max-width: 1100px) { @media (max-width: 1100px) {
@@ -1988,7 +1989,7 @@ onUnmounted(() => {
.left-panel { .left-panel {
width: 100%; width: 100%;
border-right: none; border-right: none;
border-bottom: 1px solid #2a2a2a; border-bottom: 1px solid #2e3a52;
} }
.right-panel { .right-panel {
@@ -1,6 +1,6 @@
<template> <template>
<div class="page-shell module-page"> <div class="page-shell module-page">
<BrandTopBar active="patrol-delete" /> <AmazonToolPageShell tool-id="patrol">
<div class="main-content"> <div class="main-content">
<aside class="left-panel"> <aside class="left-panel">
@@ -365,13 +365,14 @@
</div> </div>
</section> </section>
</div> </div>
</AmazonToolPageShell>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, onMounted, onUnmounted, ref, watch } from "vue"; import { computed, onMounted, onUnmounted, ref, watch } from "vue";
import { ElMessage } from "element-plus"; import { ElMessage } from "element-plus";
import BrandTopBar from "@/pages/brand/components/BrandTopBar.vue"; import AmazonToolPageShell from '@/pages/amazon/components/AmazonToolPageShell.vue';
import { import {
addPatrolDeleteCandidate, addPatrolDeleteCandidate,
addPatrolDeleteCondition, addPatrolDeleteCondition,
@@ -1322,29 +1323,29 @@ onUnmounted(() => {
</script> </script>
<style scoped> <style scoped>
.module-page { min-height: 100vh; background: #1a1a1a; } .module-page { min-height: 100vh; background: #151a25; }
.main-content { display: flex; min-height: calc(100vh - 56px); height: calc(100vh - 56px); } .main-content { display: flex; min-height: calc(100vh - 56px); height: calc(100vh - 56px); }
.left-panel { width: 400px; background: #1e1e1e; padding: 20px; overflow-y: auto; border-right: 1px solid #2a2a2a; } .left-panel { width: 400px; background: #1c2333; padding: 20px; overflow-y: auto; border-right: 1px solid #2e3a52; }
.right-panel { flex: 1; display: flex; flex-direction: column; min-width: 0; background: #1a1a1a; } .right-panel { flex: 1; display: flex; flex-direction: column; min-width: 0; background: #151a25; }
.section-title { font-size: 13px; color: #bbb; margin-bottom: 10px; } .section-title { font-size: 13px; color: #a0acbe; margin-bottom: 10px; }
.condition-title { display: flex; align-items: center; justify-content: space-between; gap: 10px; } .condition-title { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.condition-title-hint { flex: 1; min-width: 0; color: #d6a95a; font-size: 12px; text-align: right; line-height: 1.35; } .condition-title-hint { flex: 1; min-width: 0; color: #d6a95a; font-size: 12px; text-align: right; line-height: 1.35; }
.input-zone { border: 1px dashed #3a3a3a; border-radius: 10px; padding: 16px; background: #252525; margin-bottom: 20px; } .input-zone { border: 1px dashed #3e4a62; border-radius: 10px; padding: 16px; background: #2e3a52; margin-bottom: 20px; }
.hint { color: #888; font-size: 12px; margin-bottom: 12px; line-height: 1.5; text-align: left; } .hint { color: #5e6878; font-size: 12px; margin-bottom: 12px; line-height: 1.5; text-align: left; }
.input-row { display: flex; gap: 10px; align-items: center; } .input-row { display: flex; gap: 10px; align-items: center; }
.input-row :deep(.el-input) { flex: 1; } .input-row :deep(.el-input) { flex: 1; }
.opt-btn { padding: 8px 12px; font-size: 12px; color: #ccc; background: #2a2a2a; border: 1px solid #3a3a3a; border-radius: 6px; cursor: pointer; white-space: nowrap; } .opt-btn { padding: 8px 12px; font-size: 12px; color: #c8d2e2; background: #2e3a52; border: 1px solid #3e4a62; border-radius: 6px; cursor: pointer; white-space: nowrap; }
.opt-btn:hover:not(:disabled) { color: #3498db; border-color: #3498db; } .opt-btn:hover:not(:disabled) { color: #3498db; border-color: #3498db; }
.opt-btn:disabled { opacity: 0.6; cursor: not-allowed; } .opt-btn:disabled { opacity: 0.6; cursor: not-allowed; }
.empty-candidates { color: #666; font-size: 13px; padding: 16px; border: 1px dashed #333; border-radius: 8px; margin-bottom: 16px; } .empty-candidates { color: #5e6878; font-size: 13px; padding: 16px; border: 1px dashed #333; border-radius: 8px; margin-bottom: 16px; }
.candidate-table-scroll { max-height: 320px; overflow: auto; margin-bottom: 18px; border-radius: 8px; border: 1px solid #2a2a2a; } .candidate-table-scroll { max-height: 320px; overflow: auto; margin-bottom: 18px; border-radius: 8px; border: 1px solid #2e3a52; }
.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; } .candidate-table { --el-table-bg-color: #2e3a52; --el-table-tr-bg-color: #2e3a52; --el-table-header-bg-color: #2e3a52; --el-table-text-color: #c8d2e2; --el-table-border-color: #333; }
.condition-panel { margin-bottom: 18px; border: 1px solid #2a2a2a; border-radius: 8px; background: #222; padding: 12px; } .condition-panel { margin-bottom: 18px; border: 1px solid #2e3a52; border-radius: 8px; background: #222; padding: 12px; }
.condition-input-row { display: flex; gap: 10px; align-items: center; margin-bottom: 10px; } .condition-input-row { display: flex; gap: 10px; align-items: center; margin-bottom: 10px; }
.condition-input-row :deep(.el-input) { flex: 1; } .condition-input-row :deep(.el-input) { flex: 1; }
.empty-conditions { color: #666; font-size: 12px; padding: 10px 4px; } .empty-conditions { color: #5e6878; font-size: 12px; padding: 10px 4px; }
.condition-list { list-style: none; margin: 0; padding: 0; max-height: 150px; overflow: auto; display: flex; flex-direction: column; gap: 8px; } .condition-list { list-style: none; margin: 0; padding: 0; max-height: 150px; overflow: auto; display: flex; flex-direction: column; gap: 8px; }
.condition-item { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 8px 6px; border-bottom: 1px solid #303030; } .condition-item { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 8px 6px; border-bottom: 1px solid #333f55; }
.condition-item:last-child { border-bottom: none; } .condition-item:last-child { border-bottom: none; }
.condition-check { display: flex; align-items: center; gap: 8px; min-width: 0; color: #cfd6df; font-size: 12px; cursor: pointer; } .condition-check { display: flex; align-items: center; gap: 8px; min-width: 0; color: #cfd6df; font-size: 12px; cursor: pointer; }
.condition-check input { width: 16px; height: 16px; margin: 0; flex: 0 0 auto; } .condition-check input { width: 16px; height: 16px; margin: 0; flex: 0 0 auto; }
@@ -1352,40 +1353,40 @@ onUnmounted(() => {
.link-danger { background: none; border: none; color: #e74c3c; cursor: pointer; font-size: 12px; padding: 0; } .link-danger { background: none; border: none; color: #e74c3c; cursor: pointer; font-size: 12px; padding: 0; }
.link-danger:hover { text-decoration: underline; } .link-danger:hover { text-decoration: underline; }
.run-row { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 8px; } .run-row { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 8px; }
.btn-run { padding: 10px 18px; font-size: 14px; font-weight: 600; color: #fff; background: #3498db; border: none; border-radius: 8px; cursor: pointer; } .btn-run { padding: 10px 18px; font-size: 14px; font-weight: 600; color: #f5f8fc; background: #3498db; border: none; border-radius: 8px; cursor: pointer; }
.btn-run:hover:not(:disabled) { background: #2980b9; } .btn-run:hover:not(:disabled) { background: #2980b9; }
.btn-run:disabled { opacity: 0.55; cursor: not-allowed; } .btn-run:disabled { opacity: 0.55; cursor: not-allowed; }
.btn-queue { background: #27ae60; } .btn-queue { background: #27ae60; }
.btn-queue:hover:not(:disabled) { background: #219a52; } .btn-queue:hover:not(:disabled) { background: #219a52; }
.loading-msg { margin-top: 12px; font-size: 12px; color: #777; line-height: 1.5; } .loading-msg { margin-top: 12px; font-size: 12px; color: #5e6878; line-height: 1.5; }
.queue-debug-card { margin-top: 18px; padding: 14px; border: 1px solid #2a2a2a; border-radius: 10px; background: #202020; } .queue-debug-card { margin-top: 18px; padding: 14px; border: 1px solid #2e3a52; border-radius: 10px; background: #222b3d; }
.queue-debug-title { margin-bottom: 8px; } .queue-debug-title { margin-bottom: 8px; }
.queue-debug-line { color: #b8c1cc; font-size: 12px; line-height: 1.6; margin-bottom: 8px; } .queue-debug-line { color: #a0acbe; font-size: 12px; line-height: 1.6; margin-bottom: 8px; }
.queue-debug-payload { margin: 0; max-height: 220px; overflow: auto; padding: 10px; border-radius: 8px; background: #141414; color: #8fd3ff; font-size: 11px; line-height: 1.5; white-space: pre-wrap; word-break: break-all; } .queue-debug-payload { margin: 0; max-height: 220px; overflow: auto; padding: 10px; border-radius: 8px; background: #10141f; color: #8fd3ff; font-size: 11px; line-height: 1.5; white-space: pre-wrap; word-break: break-all; }
.panel-header { padding: 16px 20px; font-size: 15px; font-weight: 600; color: #e0e0e0; border-bottom: 1px solid #2a2a2a; } .panel-header { padding: 16px 20px; font-size: 15px; font-weight: 600; color: #f5f8fc; border-bottom: 1px solid #2e3a52; }
.task-list-wrap { flex: 1; padding: 16px 20px; overflow: auto; } .task-list-wrap { flex: 1; padding: 16px 20px; overflow: auto; }
.clean-result-summary { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 12px; margin-bottom: 16px; } .clean-result-summary { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 12px; margin-bottom: 16px; }
.summary-card { padding: 14px 16px; border: 1px solid #2a2a2a; border-radius: 10px; background: #1e1e1e; } .summary-card { padding: 14px 16px; border: 1px solid #2e3a52; border-radius: 10px; background: #1c2333; }
.summary-card strong { display: block; margin-top: 8px; font-size: 22px; color: #eaf4ff; } .summary-card strong { display: block; margin-top: 8px; font-size: 22px; color: #f5f8fc; }
.summary-label { font-size: 12px; color: #8d8d8d; } .summary-label { font-size: 12px; color: #5e6878; }
.subsection-title { font-size: 13px; color: #999; margin: 8px 0 10px; } .subsection-title { font-size: 13px; color: #a0acbe; margin: 8px 0 10px; }
.empty-tasks { color: #666; font-size: 13px; padding: 16px; text-align: center; } .empty-tasks { color: #5e6878; font-size: 13px; padding: 16px; text-align: center; }
.empty-tasks.narrow { padding: 12px 8px; } .empty-tasks.narrow { padding: 12px 8px; }
.match-table { margin-bottom: 18px; } .match-table { margin-bottom: 18px; }
.match-table :deep(.el-table__body tr:hover > td.el-table__cell), .match-table :deep(.el-table__body tr.hover-row > td.el-table__cell), .match-table :deep(.el-table__body tr.current-row > td.el-table__cell) { background-color: var(--el-table-tr-bg-color, #222) !important; } .match-table :deep(.el-table__body tr:hover > td.el-table__cell), .match-table :deep(.el-table__body tr.hover-row > td.el-table__cell), .match-table :deep(.el-table__body tr.current-row > td.el-table__cell) { background-color: var(--el-table-tr-bg-color, #222) !important; }
.result-table { --el-table-bg-color: #222; --el-table-tr-bg-color: #222; --el-table-header-bg-color: #2a2a2a; --el-table-text-color: #ccc; --el-table-border-color: #333; } .result-table { --el-table-bg-color: #222; --el-table-tr-bg-color: #222; --el-table-header-bg-color: #2e3a52; --el-table-text-color: #c8d2e2; --el-table-border-color: #333; }
.ok { color: #27ae60; } .ok { color: #27ae60; }
.fail { color: #e67e22; } .fail { color: #e67e22; }
.result-list-wrap { border: 1px solid #2a2a2a; border-radius: 10px; background: #1e1e1e; min-height: 220px; margin-top: 8px; } .result-list-wrap { border: 1px solid #2e3a52; border-radius: 10px; background: #1c2333; min-height: 220px; margin-top: 8px; }
.result-list-header { padding: 12px 16px; border-bottom: 1px solid #2a2a2a; font-size: 14px; color: #ddd; } .result-list-header { padding: 12px 16px; border-bottom: 1px solid #2e3a52; font-size: 14px; color: #c8d2e2; }
.result-subsection { padding: 12px 12px 4px; } .result-subsection { padding: 12px 12px 4px; }
.result-subsection-title { font-size: 12px; color: #888; margin-bottom: 8px; } .result-subsection-title { font-size: 12px; color: #5e6878; margin-bottom: 8px; }
.task-list { list-style: none; margin: 0; padding: 0; } .task-list { list-style: none; margin: 0; padding: 0; }
.task-item { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; padding: 12px 14px; border: 1px solid #2a2a2a; border-radius: 8px; margin-bottom: 8px; background: #222; } .task-item { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; padding: 12px 14px; border: 1px solid #2e3a52; border-radius: 8px; margin-bottom: 8px; background: #222; }
.left { flex: 1; min-width: 0; } .left { flex: 1; min-width: 0; }
.split-result-main { display: flex; flex-direction: column; gap: 4px; } .split-result-main { display: flex; flex-direction: column; gap: 4px; }
.id { font-weight: 600; color: #e0e0e0; font-size: 13px; } .id { font-weight: 600; color: #f5f8fc; font-size: 13px; }
.files { font-size: 12px; color: #888; } .files { font-size: 12px; color: #5e6878; }
.task-right { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; justify-content: flex-end; } .task-right { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; justify-content: flex-end; }
.status { padding: 4px 10px; border-radius: 6px; font-size: 12px; white-space: nowrap; } .status { padding: 4px 10px; border-radius: 6px; font-size: 12px; white-space: nowrap; }
.status.success { background: rgba(46, 204, 113, 0.18); color: #2ecc71; } .status.success { background: rgba(46, 204, 113, 0.18); color: #2ecc71; }
@@ -1397,7 +1398,7 @@ onUnmounted(() => {
.btn-delete:hover { background: rgba(231, 76, 60, 0.22); } .btn-delete:hover { background: rgba(231, 76, 60, 0.22); }
@media (max-width: 1100px) { @media (max-width: 1100px) {
.main-content { flex-direction: column; height: auto; } .main-content { flex-direction: column; height: auto; }
.left-panel { width: 100%; border-right: none; border-bottom: 1px solid #2a2a2a; } .left-panel { width: 100%; border-right: none; border-bottom: 1px solid #2e3a52; }
.clean-result-summary { grid-template-columns: repeat(2, minmax(0, 1fr)); } .clean-result-summary { grid-template-columns: repeat(2, minmax(0, 1fr)); }
} }
</style> </style>
@@ -1,6 +1,6 @@
<template> <template>
<div class="page-shell module-page"> <div class="page-shell module-page">
<BrandTopBar active="pricing" /> <AmazonToolPageShell tool-id="follow">
<div class="main-content"> <div class="main-content">
<aside class="left-panel left-panel--with-template"> <aside class="left-panel left-panel--with-template">
@@ -257,13 +257,14 @@
</div> </div>
</section> </section>
</div> </div>
</AmazonToolPageShell>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, onMounted, onUnmounted, ref } from 'vue' import { computed, onMounted, onUnmounted, ref } from 'vue'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import BrandTopBar from '@/pages/brand/components/BrandTopBar.vue' import AmazonToolPageShell from '@/pages/amazon/components/AmazonToolPageShell.vue';
import ModuleTemplateDownload from '@/shared/components/ModuleTemplateDownload.vue' import ModuleTemplateDownload from '@/shared/components/ModuleTemplateDownload.vue'
import ZiniaoVersionSetting from '@/shared/components/ZiniaoVersionSetting.vue' import ZiniaoVersionSetting from '@/shared/components/ZiniaoVersionSetting.vue'
import { LISTING_FILTER_OPTIONS, type ListingFilterValue } from '@/pages/brand/components/listingFilters' import { LISTING_FILTER_OPTIONS, type ListingFilterValue } from '@/pages/brand/components/listingFilters'
@@ -1774,7 +1775,7 @@ onUnmounted(() => {
<style scoped> <style scoped>
.module-page { .module-page {
min-height: 100vh; min-height: 100vh;
background: #1a1a1a; background: #151a25;
} }
.main-content { .main-content {
@@ -1785,10 +1786,10 @@ onUnmounted(() => {
.left-panel { .left-panel {
width: 400px; width: 400px;
background: #1e1e1e; background: #1c2333;
padding: 20px; padding: 20px;
overflow-y: auto; overflow-y: auto;
border-right: 1px solid #2a2a2a; border-right: 1px solid #2e3a52;
} }
.price-track-shop-candidates { .price-track-shop-candidates {
@@ -1800,25 +1801,25 @@ onUnmounted(() => {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
min-width: 0; min-width: 0;
background: #1a1a1a; background: #151a25;
} }
.section-title { .section-title {
font-size: 13px; font-size: 13px;
color: #bbb; color: #a0acbe;
margin-bottom: 10px; margin-bottom: 10px;
} }
.upload-zone { .upload-zone {
border: 1px dashed #3a3a3a; border: 1px dashed #3e4a62;
border-radius: 10px; border-radius: 10px;
padding: 16px; padding: 16px;
background: #252525; background: #2e3a52;
margin-bottom: 20px; margin-bottom: 20px;
} }
.hint { .hint {
color: #888; color: #5e6878;
font-size: 12px; font-size: 12px;
margin-bottom: 12px; margin-bottom: 12px;
line-height: 1.5; line-height: 1.5;
@@ -1840,7 +1841,7 @@ onUnmounted(() => {
} }
.clean-placeholder { .clean-placeholder {
color: #666; color: #5e6878;
font-size: 12px; font-size: 12px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -1848,24 +1849,24 @@ onUnmounted(() => {
} }
.selected-files span:not(.more-line) { .selected-files span:not(.more-line) {
color: #aaa; color: #a0acbe;
} }
.more-line { .more-line {
color: #777; color: #5e6878;
font-style: italic; font-style: italic;
} }
.text-muted { .text-muted {
color: #777; color: #5e6878;
font-style: italic; font-style: italic;
} }
.mode-zone { .mode-zone {
border: 1px dashed #3a3a3a; border: 1px dashed #3e4a62;
border-radius: 10px; border-radius: 10px;
padding: 14px 16px; padding: 14px 16px;
background: #252525; background: #2e3a52;
margin-bottom: 20px; margin-bottom: 20px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -1891,12 +1892,12 @@ onUnmounted(() => {
} }
.mode-check-text { .mode-check-text {
color: #ddd; color: #c8d2e2;
font-weight: 600; font-weight: 600;
} }
.mode-hint-inline { .mode-hint-inline {
color: #888; color: #5e6878;
font-size: 12px; font-size: 12px;
} }
@@ -1912,8 +1913,8 @@ onUnmounted(() => {
} }
.loop-round-input :deep(.el-input__wrapper) { .loop-round-input :deep(.el-input__wrapper) {
background-color: #2a2a2a; background-color: #2e3a52;
box-shadow: 0 0 0 1px #3a3a3a inset; box-shadow: 0 0 0 1px #3e4a62 inset;
} }
.tag-badge { .tag-badge {
@@ -1953,14 +1954,14 @@ onUnmounted(() => {
.mode-status-text { .mode-status-text {
font-size: 12px; font-size: 12px;
color: #aaa; color: #a0acbe;
} }
.input-zone { .input-zone {
border: 1px dashed #3a3a3a; border: 1px dashed #3e4a62;
border-radius: 10px; border-radius: 10px;
padding: 16px; padding: 16px;
background: #252525; background: #2e3a52;
margin-bottom: 20px; margin-bottom: 20px;
} }
@@ -1977,9 +1978,9 @@ onUnmounted(() => {
.opt-btn { .opt-btn {
padding: 8px 14px; padding: 8px 14px;
font-size: 13px; font-size: 13px;
color: #ccc; color: #c8d2e2;
background: #2a2a2a; background: #2e3a52;
border: 1px solid #3a3a3a; border: 1px solid #3e4a62;
border-radius: 6px; border-radius: 6px;
cursor: pointer; cursor: pointer;
white-space: nowrap; white-space: nowrap;
@@ -2013,7 +2014,7 @@ onUnmounted(() => {
cursor: pointer; cursor: pointer;
user-select: none; user-select: none;
font-size: 12px; font-size: 12px;
color: #ccc; color: #c8d2e2;
} }
.country-check-input { .country-check-input {
@@ -2032,13 +2033,13 @@ onUnmounted(() => {
border: 1px solid #333; border: 1px solid #333;
border-radius: 8px; border-radius: 8px;
padding: 10px 12px; padding: 10px 12px;
background: #252525; background: #2e3a52;
margin-bottom: 14px; margin-bottom: 14px;
} }
.country-order-caption { .country-order-caption {
font-size: 11px; font-size: 11px;
color: #777; color: #5e6878;
margin-bottom: 8px; margin-bottom: 8px;
} }
@@ -2055,7 +2056,7 @@ onUnmounted(() => {
padding: 8px 10px; padding: 8px 10px;
background: #2c2c2c; background: #2c2c2c;
border-radius: 6px; border-radius: 6px;
border: 1px solid #3a3a3a; border: 1px solid #3e4a62;
cursor: grab; cursor: grab;
user-select: none; user-select: none;
} }
@@ -2066,19 +2067,19 @@ onUnmounted(() => {
} }
.drag-handle { .drag-handle {
color: #666; color: #5e6878;
font-size: 12px; font-size: 12px;
letter-spacing: -2px; letter-spacing: -2px;
} }
.country-drag-label { .country-drag-label {
font-size: 12px; font-size: 12px;
color: #ccc; color: #c8d2e2;
} }
.country-pref-status { .country-pref-status {
font-size: 11px; font-size: 11px;
color: #888; color: #5e6878;
margin-bottom: 10px; margin-bottom: 10px;
} }
@@ -2095,13 +2096,13 @@ onUnmounted(() => {
} }
.listing-filter-row :deep(.el-select__wrapper) { .listing-filter-row :deep(.el-select__wrapper) {
background-color: #2a2a2a; background-color: #2e3a52;
box-shadow: 0 0 0 1px #3a3a3a inset; box-shadow: 0 0 0 1px #3e4a62 inset;
} }
.listing-filter-row :deep(.el-select__placeholder), .listing-filter-row :deep(.el-select__placeholder),
.listing-filter-row :deep(.el-select__selected-item) { .listing-filter-row :deep(.el-select__selected-item) {
color: #ccc; color: #c8d2e2;
} }
@@ -2112,7 +2113,7 @@ onUnmounted(() => {
.empty-candidates { .empty-candidates {
color: #666; color: #5e6878;
font-size: 13px; font-size: 13px;
padding: 16px; padding: 16px;
border: 1px dashed #333; border: 1px dashed #333;
@@ -2125,14 +2126,14 @@ onUnmounted(() => {
overflow: auto; overflow: auto;
margin-bottom: 16px; margin-bottom: 16px;
border-radius: 8px; border-radius: 8px;
border: 1px solid #2a2a2a; border: 1px solid #2e3a52;
} }
.candidate-table { .candidate-table {
--el-table-bg-color: #252525; --el-table-bg-color: #2e3a52;
--el-table-tr-bg-color: #252525; --el-table-tr-bg-color: #2e3a52;
--el-table-header-bg-color: #2a2a2a; --el-table-header-bg-color: #2e3a52;
--el-table-text-color: #ccc; --el-table-text-color: #c8d2e2;
--el-table-border-color: #333; --el-table-border-color: #333;
} }
@@ -2160,7 +2161,7 @@ onUnmounted(() => {
padding: 10px 18px; padding: 10px 18px;
font-size: 14px; font-size: 14px;
font-weight: 600; font-weight: 600;
color: #fff; color: #f5f8fc;
background: #3498db; background: #3498db;
border: none; border: none;
border-radius: 8px; border-radius: 8px;
@@ -2187,7 +2188,7 @@ onUnmounted(() => {
.loading-msg { .loading-msg {
margin-top: 12px; margin-top: 12px;
font-size: 12px; font-size: 12px;
color: #777; color: #5e6878;
line-height: 1.5; line-height: 1.5;
} }
@@ -2221,9 +2222,9 @@ onUnmounted(() => {
.queue-debug-card { .queue-debug-card {
margin-top: 18px; margin-top: 18px;
padding: 14px; padding: 14px;
border: 1px solid #2a2a2a; border: 1px solid #2e3a52;
border-radius: 10px; border-radius: 10px;
background: #202020; background: #222b3d;
} }
.queue-debug-title { .queue-debug-title {
@@ -2231,7 +2232,7 @@ onUnmounted(() => {
} }
.queue-debug-line { .queue-debug-line {
color: #b8c1cc; color: #a0acbe;
font-size: 12px; font-size: 12px;
line-height: 1.6; line-height: 1.6;
margin-bottom: 8px; margin-bottom: 8px;
@@ -2243,7 +2244,7 @@ onUnmounted(() => {
overflow: auto; overflow: auto;
padding: 10px; padding: 10px;
border-radius: 8px; border-radius: 8px;
background: #141414; background: #10141f;
color: #8fd3ff; color: #8fd3ff;
font-size: 11px; font-size: 11px;
line-height: 1.5; line-height: 1.5;
@@ -2256,8 +2257,8 @@ onUnmounted(() => {
padding: 16px 20px; padding: 16px 20px;
font-size: 15px; font-size: 15px;
font-weight: 600; font-weight: 600;
color: #e0e0e0; color: #f5f8fc;
border-bottom: 1px solid #2a2a2a; border-bottom: 1px solid #2e3a52;
} }
.task-list-wrap { .task-list-wrap {
@@ -2275,31 +2276,31 @@ onUnmounted(() => {
.summary-card { .summary-card {
padding: 14px 16px; padding: 14px 16px;
border: 1px solid #2a2a2a; border: 1px solid #2e3a52;
border-radius: 10px; border-radius: 10px;
background: #1e1e1e; background: #1c2333;
} }
.summary-card strong { .summary-card strong {
display: block; display: block;
margin-top: 8px; margin-top: 8px;
font-size: 22px; font-size: 22px;
color: #eaf4ff; color: #f5f8fc;
} }
.summary-label { .summary-label {
font-size: 12px; font-size: 12px;
color: #8d8d8d; color: #5e6878;
} }
.subsection-title { .subsection-title {
font-size: 13px; font-size: 13px;
color: #999; color: #a0acbe;
margin: 8px 0 10px; margin: 8px 0 10px;
} }
.empty-tasks { .empty-tasks {
color: #666; color: #5e6878;
font-size: 13px; font-size: 13px;
padding: 16px; padding: 16px;
text-align: center; text-align: center;
@@ -2316,8 +2317,8 @@ onUnmounted(() => {
.result-table { .result-table {
--el-table-bg-color: #222; --el-table-bg-color: #222;
--el-table-tr-bg-color: #222; --el-table-tr-bg-color: #222;
--el-table-header-bg-color: #2a2a2a; --el-table-header-bg-color: #2e3a52;
--el-table-text-color: #ccc; --el-table-text-color: #c8d2e2;
--el-table-border-color: #333; --el-table-border-color: #333;
} }
@@ -2325,18 +2326,18 @@ onUnmounted(() => {
.fail { color: #e67e22; } .fail { color: #e67e22; }
.result-list-wrap { .result-list-wrap {
border: 1px solid #2a2a2a; border: 1px solid #2e3a52;
border-radius: 10px; border-radius: 10px;
background: #1e1e1e; background: #1c2333;
min-height: 200px; min-height: 200px;
margin-top: 8px; margin-top: 8px;
} }
.result-list-header { .result-list-header {
padding: 12px 16px; padding: 12px 16px;
border-bottom: 1px solid #2a2a2a; border-bottom: 1px solid #2e3a52;
font-size: 14px; font-size: 14px;
color: #ddd; color: #c8d2e2;
} }
.result-subsection { .result-subsection {
@@ -2345,7 +2346,7 @@ onUnmounted(() => {
.result-subsection-title { .result-subsection-title {
font-size: 12px; font-size: 12px;
color: #888; color: #5e6878;
margin-bottom: 8px; margin-bottom: 8px;
} }
@@ -2361,7 +2362,7 @@ onUnmounted(() => {
justify-content: space-between; justify-content: space-between;
gap: 12px; gap: 12px;
padding: 12px 14px; padding: 12px 14px;
border: 1px solid #2a2a2a; border: 1px solid #2e3a52;
border-radius: 8px; border-radius: 8px;
margin-bottom: 8px; margin-bottom: 8px;
background: #222; background: #222;
@@ -2375,13 +2376,13 @@ onUnmounted(() => {
.id { .id {
font-weight: 600; font-weight: 600;
color: #e0e0e0; color: #f5f8fc;
font-size: 13px; font-size: 13px;
} }
.files { .files {
font-size: 12px; font-size: 12px;
color: #888; color: #5e6878;
} }
.task-right { .task-right {
@@ -2451,7 +2452,7 @@ onUnmounted(() => {
.left-panel { .left-panel {
width: 100%; width: 100%;
border-right: none; border-right: none;
border-bottom: 1px solid #2a2a2a; border-bottom: 1px solid #2e3a52;
} }
.pr-summary { .pr-summary {
@@ -1,6 +1,6 @@
<template> <template>
<div class="page-shell module-page"> <div class="page-shell module-page">
<BrandTopBar active="product-risk" /> <AmazonToolPageShell tool-id="risk">
<div class="main-content"> <div class="main-content">
<aside class="left-panel"> <aside class="left-panel">
@@ -190,13 +190,14 @@
</div> </div>
</section> </section>
</div> </div>
</AmazonToolPageShell>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, onMounted, onUnmounted, ref } from 'vue' import { computed, onMounted, onUnmounted, ref } from 'vue'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import BrandTopBar from '@/pages/brand/components/BrandTopBar.vue' import AmazonToolPageShell from '@/pages/amazon/components/AmazonToolPageShell.vue';
import { LISTING_FILTER_OPTIONS, type ListingFilterValue } from '@/pages/brand/components/listingFilters' import { LISTING_FILTER_OPTIONS, type ListingFilterValue } from '@/pages/brand/components/listingFilters'
import { import {
addProductRiskCandidate, addProductRiskCandidate,
@@ -1271,7 +1272,7 @@ onUnmounted(() => {
<style scoped> <style scoped>
.module-page { .module-page {
min-height: 100vh; min-height: 100vh;
background: #1a1a1a; background: #151a25;
} }
.main-content { .main-content {
@@ -1282,10 +1283,10 @@ onUnmounted(() => {
.left-panel { .left-panel {
width: 400px; width: 400px;
background: #1e1e1e; background: #1c2333;
padding: 20px; padding: 20px;
overflow-y: auto; overflow-y: auto;
border-right: 1px solid #2a2a2a; border-right: 1px solid #2e3a52;
} }
.right-panel { .right-panel {
@@ -1293,25 +1294,25 @@ onUnmounted(() => {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
min-width: 0; min-width: 0;
background: #1a1a1a; background: #151a25;
} }
.section-title { .section-title {
font-size: 13px; font-size: 13px;
color: #bbb; color: #a0acbe;
margin-bottom: 10px; margin-bottom: 10px;
} }
.input-zone { .input-zone {
border: 1px dashed #3a3a3a; border: 1px dashed #3e4a62;
border-radius: 10px; border-radius: 10px;
padding: 16px; padding: 16px;
background: #252525; background: #2e3a52;
margin-bottom: 20px; margin-bottom: 20px;
} }
.hint { .hint {
color: #888; color: #5e6878;
font-size: 12px; font-size: 12px;
margin-bottom: 12px; margin-bottom: 12px;
line-height: 1.5; line-height: 1.5;
@@ -1336,7 +1337,7 @@ onUnmounted(() => {
cursor: pointer; cursor: pointer;
user-select: none; user-select: none;
font-size: 12px; font-size: 12px;
color: #ccc; color: #c8d2e2;
} }
.country-check-input { .country-check-input {
@@ -1355,13 +1356,13 @@ onUnmounted(() => {
border: 1px solid #333; border: 1px solid #333;
border-radius: 8px; border-radius: 8px;
padding: 10px 12px; padding: 10px 12px;
background: #252525; background: #2e3a52;
margin-bottom: 14px; margin-bottom: 14px;
} }
.country-order-caption { .country-order-caption {
font-size: 11px; font-size: 11px;
color: #777; color: #5e6878;
margin-bottom: 8px; margin-bottom: 8px;
} }
@@ -1378,7 +1379,7 @@ onUnmounted(() => {
padding: 8px 10px; padding: 8px 10px;
background: #2c2c2c; background: #2c2c2c;
border-radius: 6px; border-radius: 6px;
border: 1px solid #3a3a3a; border: 1px solid #3e4a62;
cursor: grab; cursor: grab;
user-select: none; user-select: none;
} }
@@ -1389,19 +1390,19 @@ onUnmounted(() => {
} }
.drag-handle { .drag-handle {
color: #666; color: #5e6878;
font-size: 12px; font-size: 12px;
letter-spacing: -2px; letter-spacing: -2px;
} }
.country-drag-label { .country-drag-label {
font-size: 12px; font-size: 12px;
color: #ccc; color: #c8d2e2;
} }
.country-pref-status { .country-pref-status {
font-size: 11px; font-size: 11px;
color: #888; color: #5e6878;
margin-bottom: 10px; margin-bottom: 10px;
} }
@@ -1418,13 +1419,13 @@ onUnmounted(() => {
} }
.listing-filter-row :deep(.el-select__wrapper) { .listing-filter-row :deep(.el-select__wrapper) {
background-color: #2a2a2a; background-color: #2e3a52;
box-shadow: 0 0 0 1px #3a3a3a inset; box-shadow: 0 0 0 1px #3e4a62 inset;
} }
.listing-filter-row :deep(.el-select__placeholder), .listing-filter-row :deep(.el-select__placeholder),
.listing-filter-row :deep(.el-select__selected-item) { .listing-filter-row :deep(.el-select__selected-item) {
color: #ccc; color: #c8d2e2;
} }
.input-row { .input-row {
@@ -1440,9 +1441,9 @@ onUnmounted(() => {
.opt-btn { .opt-btn {
padding: 8px 14px; padding: 8px 14px;
font-size: 13px; font-size: 13px;
color: #ccc; color: #c8d2e2;
background: #2a2a2a; background: #2e3a52;
border: 1px solid #3a3a3a; border: 1px solid #3e4a62;
border-radius: 6px; border-radius: 6px;
cursor: pointer; cursor: pointer;
white-space: nowrap; white-space: nowrap;
@@ -1459,7 +1460,7 @@ onUnmounted(() => {
} }
.empty-candidates { .empty-candidates {
color: #666; color: #5e6878;
font-size: 13px; font-size: 13px;
padding: 16px; padding: 16px;
border: 1px dashed #333; border: 1px dashed #333;
@@ -1472,14 +1473,14 @@ onUnmounted(() => {
overflow: auto; overflow: auto;
margin-bottom: 16px; margin-bottom: 16px;
border-radius: 8px; border-radius: 8px;
border: 1px solid #2a2a2a; border: 1px solid #2e3a52;
} }
.candidate-table { .candidate-table {
--el-table-bg-color: #252525; --el-table-bg-color: #2e3a52;
--el-table-tr-bg-color: #252525; --el-table-tr-bg-color: #2e3a52;
--el-table-header-bg-color: #2a2a2a; --el-table-header-bg-color: #2e3a52;
--el-table-text-color: #ccc; --el-table-text-color: #c8d2e2;
--el-table-border-color: #333; --el-table-border-color: #333;
} }
@@ -1507,7 +1508,7 @@ onUnmounted(() => {
padding: 10px 18px; padding: 10px 18px;
font-size: 14px; font-size: 14px;
font-weight: 600; font-weight: 600;
color: #fff; color: #f5f8fc;
background: #3498db; background: #3498db;
border: none; border: none;
border-radius: 8px; border-radius: 8px;
@@ -1534,7 +1535,7 @@ onUnmounted(() => {
.loading-msg { .loading-msg {
margin-top: 12px; margin-top: 12px;
font-size: 12px; font-size: 12px;
color: #777; color: #5e6878;
line-height: 1.5; line-height: 1.5;
} }
@@ -1546,9 +1547,9 @@ onUnmounted(() => {
.queue-debug-card { .queue-debug-card {
margin-top: 18px; margin-top: 18px;
padding: 14px; padding: 14px;
border: 1px solid #2a2a2a; border: 1px solid #2e3a52;
border-radius: 10px; border-radius: 10px;
background: #202020; background: #222b3d;
} }
.queue-debug-title { .queue-debug-title {
@@ -1556,7 +1557,7 @@ onUnmounted(() => {
} }
.queue-debug-line { .queue-debug-line {
color: #b8c1cc; color: #a0acbe;
font-size: 12px; font-size: 12px;
line-height: 1.6; line-height: 1.6;
margin-bottom: 8px; margin-bottom: 8px;
@@ -1568,7 +1569,7 @@ onUnmounted(() => {
overflow: auto; overflow: auto;
padding: 10px; padding: 10px;
border-radius: 8px; border-radius: 8px;
background: #141414; background: #10141f;
color: #8fd3ff; color: #8fd3ff;
font-size: 11px; font-size: 11px;
line-height: 1.5; line-height: 1.5;
@@ -1580,8 +1581,8 @@ onUnmounted(() => {
padding: 16px 20px; padding: 16px 20px;
font-size: 15px; font-size: 15px;
font-weight: 600; font-weight: 600;
color: #e0e0e0; color: #f5f8fc;
border-bottom: 1px solid #2a2a2a; border-bottom: 1px solid #2e3a52;
} }
.task-list-wrap { .task-list-wrap {
@@ -1599,31 +1600,31 @@ onUnmounted(() => {
.summary-card { .summary-card {
padding: 14px 16px; padding: 14px 16px;
border: 1px solid #2a2a2a; border: 1px solid #2e3a52;
border-radius: 10px; border-radius: 10px;
background: #1e1e1e; background: #1c2333;
} }
.summary-card strong { .summary-card strong {
display: block; display: block;
margin-top: 8px; margin-top: 8px;
font-size: 22px; font-size: 22px;
color: #eaf4ff; color: #f5f8fc;
} }
.summary-label { .summary-label {
font-size: 12px; font-size: 12px;
color: #8d8d8d; color: #5e6878;
} }
.subsection-title { .subsection-title {
font-size: 13px; font-size: 13px;
color: #999; color: #a0acbe;
margin: 8px 0 10px; margin: 8px 0 10px;
} }
.empty-tasks { .empty-tasks {
color: #666; color: #5e6878;
font-size: 13px; font-size: 13px;
padding: 16px; padding: 16px;
text-align: center; text-align: center;
@@ -1647,8 +1648,8 @@ onUnmounted(() => {
.result-table { .result-table {
--el-table-bg-color: #222; --el-table-bg-color: #222;
--el-table-tr-bg-color: #222; --el-table-tr-bg-color: #222;
--el-table-header-bg-color: #2a2a2a; --el-table-header-bg-color: #2e3a52;
--el-table-text-color: #ccc; --el-table-text-color: #c8d2e2;
--el-table-border-color: #333; --el-table-border-color: #333;
} }
@@ -1661,18 +1662,18 @@ onUnmounted(() => {
} }
.result-list-wrap { .result-list-wrap {
border: 1px solid #2a2a2a; border: 1px solid #2e3a52;
border-radius: 10px; border-radius: 10px;
background: #1e1e1e; background: #1c2333;
min-height: 200px; min-height: 200px;
margin-top: 8px; margin-top: 8px;
} }
.result-list-header { .result-list-header {
padding: 12px 16px; padding: 12px 16px;
border-bottom: 1px solid #2a2a2a; border-bottom: 1px solid #2e3a52;
font-size: 14px; font-size: 14px;
color: #ddd; color: #c8d2e2;
} }
.result-subsection { .result-subsection {
@@ -1681,7 +1682,7 @@ onUnmounted(() => {
.result-subsection-title { .result-subsection-title {
font-size: 12px; font-size: 12px;
color: #888; color: #5e6878;
margin-bottom: 8px; margin-bottom: 8px;
} }
@@ -1697,7 +1698,7 @@ onUnmounted(() => {
justify-content: space-between; justify-content: space-between;
gap: 12px; gap: 12px;
padding: 12px 14px; padding: 12px 14px;
border: 1px solid #2a2a2a; border: 1px solid #2e3a52;
border-radius: 8px; border-radius: 8px;
margin-bottom: 8px; margin-bottom: 8px;
background: #222; background: #222;
@@ -1716,13 +1717,13 @@ onUnmounted(() => {
.id { .id {
font-weight: 600; font-weight: 600;
color: #e0e0e0; color: #f5f8fc;
font-size: 13px; font-size: 13px;
} }
.files { .files {
font-size: 12px; font-size: 12px;
color: #888; color: #5e6878;
} }
.task-right { .task-right {
@@ -1792,7 +1793,7 @@ onUnmounted(() => {
.left-panel { .left-panel {
width: 100%; width: 100%;
border-right: none; border-right: none;
border-bottom: 1px solid #2a2a2a; border-bottom: 1px solid #2e3a52;
} }
.clean-result-summary { .clean-result-summary {
@@ -1,6 +1,6 @@
<template> <template>
<div class="page-shell module-page"> <div class="page-shell module-page">
<BrandTopBar active="publish" /> <AmazonToolPageShell tool-id="list">
<div class="main-content"> <div class="main-content">
<aside class="left-panel left-panel--with-template"> <aside class="left-panel left-panel--with-template">
@@ -197,6 +197,7 @@
</div> </div>
</section> </section>
</div> </div>
</AmazonToolPageShell>
</div> </div>
</template> </template>
@@ -204,7 +205,7 @@
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue' import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import BrandTopBar from './BrandTopBar.vue' import AmazonToolPageShell from '@/pages/amazon/components/AmazonToolPageShell.vue';
import ModuleTemplateDownload from '@/shared/components/ModuleTemplateDownload.vue' import ModuleTemplateDownload from '@/shared/components/ModuleTemplateDownload.vue'
import ZiniaoVersionSetting from '@/shared/components/ZiniaoVersionSetting.vue' import ZiniaoVersionSetting from '@/shared/components/ZiniaoVersionSetting.vue'
import { expandBrandFolderRecursive, type BrandExpandFolderItem } from '@/shared/api/brand' import { expandBrandFolderRecursive, type BrandExpandFolderItem } from '@/shared/api/brand'
@@ -1160,47 +1161,47 @@ onBeforeUnmount(() => {
</script> </script>
<style scoped> <style scoped>
.module-page { min-height: 100vh; background: #1a1a1a; } .module-page { min-height: 100vh; background: #151a25; }
.main-content { display: flex; min-height: calc(100vh - 88px); height: calc(100vh - 88px); } .main-content { display: flex; min-height: calc(100vh - 88px); height: calc(100vh - 88px); }
.left-panel { width: 400px; background: #1e1e1e; padding: 20px; overflow-y: auto; border-right: 1px solid #2a2a2a; } .left-panel { width: 400px; background: #1c2333; padding: 20px; overflow-y: auto; border-right: 1px solid #2e3a52; }
.right-panel { flex: 1; display: flex; flex-direction: column; min-width: 0; background: #1a1a1a; } .right-panel { flex: 1; display: flex; flex-direction: column; min-width: 0; background: #151a25; }
.section-title { margin-bottom: 10px; color: #bbb; font-size: 13px; } .section-title { margin-bottom: 10px; color: #a0acbe; font-size: 13px; }
.upload-zone { margin-bottom: 20px; padding: 20px; border: 1px dashed #3a3a3a; border-radius: 8px; background: #252525; text-align: center; } .upload-zone { margin-bottom: 20px; padding: 20px; border: 1px dashed #3e4a62; border-radius: 8px; background: #2e3a52; text-align: center; }
.hint, .loading-msg, .task-meta, .file-info, .history-count { color: #888; font-size: 12px; line-height: 1.5; } .hint, .loading-msg, .task-meta, .file-info, .history-count { color: #5e6878; font-size: 12px; line-height: 1.5; }
.btns, .run-row, .task-actions { display: flex; align-items: center; flex-wrap: wrap; gap: 10px; } .btns, .run-row, .task-actions { display: flex; align-items: center; flex-wrap: wrap; gap: 10px; }
.btns { justify-content: center; } .btns { justify-content: center; }
.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, .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 { padding: 8px 16px; border: 1px solid #3e4a62; background: #2e3a52; color: #c8d2e2; font-size: 13px; }
.opt-btn:hover:not(:disabled) { border-color: #3498db; color: #67b7ef; } .opt-btn:hover:not(:disabled) { border-color: #3498db; color: #67b7ef; }
.opt-btn:disabled, .btn-run:disabled { opacity: .55; cursor: not-allowed; } .opt-btn:disabled, .btn-run:disabled { opacity: .55; cursor: not-allowed; }
.selected-files { max-height: 120px; min-height: 72px; margin-top: 14px; overflow-y: auto; color: #888; font-size: 12px; text-align: left; } .selected-files { max-height: 120px; min-height: 72px; margin-top: 14px; overflow-y: auto; color: #5e6878; font-size: 12px; text-align: left; }
.selected-files span { display: block; margin: 4px 0; word-break: break-all; } .selected-files span { display: block; margin: 4px 0; word-break: break-all; }
.more-line { color: #b8c1cc; } .more-line { color: #a0acbe; }
.country-group { margin-bottom: 12px; padding: 14px; border: 1px solid #2f2f2f; border-radius: 8px; background: #252525; } .country-group { margin-bottom: 12px; padding: 14px; border: 1px solid #323e58; border-radius: 8px; background: #2e3a52; }
.country-group-title { margin-bottom: 10px; color: #a9b4bf; font-size: 12px; } .country-group-title { margin-bottom: 10px; color: #a0acbe; font-size: 12px; }
.country-checks { display: flex; flex-wrap: wrap; gap: 10px 16px; } .country-checks { display: flex; flex-wrap: wrap; gap: 10px 16px; }
.country-check-row { display: inline-flex; align-items: center; gap: 8px; color: #ccc; cursor: pointer; font-size: 12px; user-select: none; } .country-check-row { display: inline-flex; align-items: center; gap: 8px; color: #c8d2e2; cursor: pointer; font-size: 12px; user-select: none; }
.country-check-row.disabled { opacity: .45; cursor: not-allowed; } .country-check-row.disabled { opacity: .45; cursor: not-allowed; }
.country-check-input { width: 16px; height: 16px; flex-shrink: 0; accent-color: #409eff; cursor: pointer; } .country-check-input { width: 16px; height: 16px; flex-shrink: 0; accent-color: #409eff; cursor: pointer; }
.country-check-input:disabled { cursor: not-allowed; } .country-check-input:disabled { cursor: not-allowed; }
.country-hint { margin: 10px 0 0; color: #777; font-size: 11px; } .country-hint { margin: 10px 0 0; color: #5e6878; font-size: 11px; }
.run-row { margin-top: 16px; } .run-row { margin-top: 16px; }
.btn-run { min-height: 38px; padding: 9px 20px; background: #3498db; color: #fff; font-size: 14px; } .btn-run { min-height: 38px; padding: 9px 20px; background: #3498db; color: #f5f8fc; font-size: 14px; }
.btn-run:hover:not(:disabled) { background: #2980b9; } .btn-run:hover:not(:disabled) { background: #2980b9; }
.queue-status { margin-top: 16px; padding: 12px; border-left: 3px solid #409eff; background: #252525; color: #b8d8ef; font-size: 12px; line-height: 1.6; } .queue-status { margin-top: 16px; padding: 12px; border-left: 3px solid #409eff; background: #2e3a52; color: #b8d8ef; font-size: 12px; line-height: 1.6; }
.queue-status-title { margin-bottom: 4px; } .queue-status-title { margin-bottom: 4px; }
.panel-header { padding: 16px 20px; border-bottom: 1px solid #2a2a2a; color: #ddd; font-size: 15px; font-weight: 600; } .panel-header { padding: 16px 20px; border-bottom: 1px solid #2e3a52; color: #c8d2e2; font-size: 15px; font-weight: 600; }
.task-list-wrap { flex: 1; padding: 16px; overflow-y: auto; } .task-list-wrap { flex: 1; padding: 16px; overflow-y: auto; }
.clean-result-summary { display: grid; grid-template-columns: repeat(4, minmax(120px, 1fr)); gap: 12px; margin-bottom: 18px; } .clean-result-summary { display: grid; grid-template-columns: repeat(4, minmax(120px, 1fr)); gap: 12px; margin-bottom: 18px; }
.summary-card { padding: 14px 16px; border: 1px solid #2a2a2a; border-radius: 8px; background: #1e1e1e; } .summary-card { padding: 14px 16px; border: 1px solid #2e3a52; border-radius: 8px; background: #1c2333; }
.summary-card strong { display: block; margin-top: 8px; color: #eaf4ff; font-size: 22px; } .summary-card strong { display: block; margin-top: 8px; color: #f5f8fc; font-size: 22px; }
.summary-label { color: #8d8d8d; font-size: 12px; } .summary-label { color: #5e6878; font-size: 12px; }
.result-list-wrap { min-height: 260px; border: 1px solid #2a2a2a; border-radius: 8px; background: #1e1e1e; } .result-list-wrap { min-height: 260px; border: 1px solid #2e3a52; border-radius: 8px; background: #1c2333; }
.result-list-header { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 14px 16px; border-bottom: 1px solid #2a2a2a; color: #ddd; font-size: 14px; } .result-list-header { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 14px 16px; border-bottom: 1px solid #2e3a52; color: #c8d2e2; font-size: 14px; }
.empty-tasks { padding: 24px 16px; color: #666; font-size: 13px; } .empty-tasks { padding: 24px 16px; color: #5e6878; font-size: 13px; }
.task-section + .task-section { border-top: 1px solid #303030; } .task-section + .task-section { border-top: 1px solid #333f55; }
.task-section-header { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; padding: 14px 16px; background: #222; } .task-section-header { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; padding: 14px 16px; background: #222; }
.task-title-wrap { display: flex; min-width: 0; flex-direction: column; gap: 4px; color: #ddd; } .task-title-wrap { display: flex; min-width: 0; flex-direction: column; gap: 4px; color: #c8d2e2; }
.task-error, .file-error { color: #ef8d8d; font-size: 12px; } .task-error, .file-error { color: #ef8d8d; font-size: 12px; }
.task-error { padding: 10px 16px 0; } .task-error { padding: 10px 16px 0; }
.download { padding: 7px 12px; background: #2d6b46; color: #dff7e8; font-size: 12px; white-space: nowrap; } .download { padding: 7px 12px; background: #2d6b46; color: #dff7e8; font-size: 12px; white-space: nowrap; }
@@ -1219,7 +1220,7 @@ onBeforeUnmount(() => {
.file-title { overflow: hidden; color: #e1e1e1; font-size: 13px; font-weight: 600; text-overflow: ellipsis; white-space: nowrap; } .file-title { overflow: hidden; color: #e1e1e1; font-size: 13px; font-weight: 600; text-overflow: ellipsis; white-space: nowrap; }
.file-info { display: flex; flex-wrap: wrap; gap: 4px 16px; margin-top: 6px; } .file-info { display: flex; flex-wrap: wrap; gap: 4px 16px; margin-top: 6px; }
.file-progress-header { display: flex; justify-content: space-between; gap: 12px; margin-top: 10px; color: #969696; font-size: 11px; } .file-progress-header { display: flex; justify-content: space-between; gap: 12px; margin-top: 10px; color: #969696; font-size: 11px; }
.file-progress-track { height: 6px; margin-top: 5px; overflow: hidden; border-radius: 3px; background: #303030; } .file-progress-track { height: 6px; margin-top: 5px; overflow: hidden; border-radius: 3px; background: #333f55; }
.file-progress-fill { height: 100%; border-radius: inherit; background: #8d7b3f; transition: width .25s ease; } .file-progress-fill { height: 100%; border-radius: inherit; background: #8d7b3f; transition: width .25s ease; }
.file-progress-fill.running { background: #409eff; } .file-progress-fill.running { background: #409eff; }
.file-progress-fill.success { background: #42b36b; } .file-progress-fill.success { background: #42b36b; }
@@ -1229,7 +1230,7 @@ onBeforeUnmount(() => {
@media (max-width: 1100px) { @media (max-width: 1100px) {
.main-content { height: auto; flex-direction: column; } .main-content { height: auto; flex-direction: column; }
.left-panel { width: 100%; border-right: 0; border-bottom: 1px solid #2a2a2a; } .left-panel { width: 100%; border-right: 0; border-bottom: 1px solid #2e3a52; }
.right-panel { min-height: 420px; } .right-panel { min-height: 420px; }
.clean-result-summary { grid-template-columns: repeat(2, minmax(0, 1fr)); } .clean-result-summary { grid-template-columns: repeat(2, minmax(0, 1fr)); }
} }
@@ -1,6 +1,6 @@
<template> <template>
<div class="page-shell module-page"> <div class="page-shell module-page">
<BrandTopBar active="query-asin" /> <AmazonToolPageShell tool-id="qasin">
<div class="main-content"> <div class="main-content">
<aside class="left-panel"> <aside class="left-panel">
@@ -312,13 +312,14 @@
</div> </div>
</section> </section>
</div> </div>
</AmazonToolPageShell>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, onMounted, onUnmounted, ref } from "vue"; import { computed, onMounted, onUnmounted, ref } from "vue";
import { ElMessage } from "element-plus"; import { ElMessage } from "element-plus";
import BrandTopBar from "@/pages/brand/components/BrandTopBar.vue"; import AmazonToolPageShell from '@/pages/amazon/components/AmazonToolPageShell.vue';
import { import {
addQueryAsinCandidate, addQueryAsinCandidate,
createQueryAsinTask, createQueryAsinTask,
@@ -1208,58 +1209,58 @@ onUnmounted(() => {
</script> </script>
<style scoped> <style scoped>
.module-page { min-height: 100vh; background: #1a1a1a; } .module-page { min-height: 100vh; background: #151a25; }
.main-content { display: flex; min-height: calc(100vh - 56px); height: calc(100vh - 56px); } .main-content { display: flex; min-height: calc(100vh - 56px); height: calc(100vh - 56px); }
.left-panel { width: 400px; background: #1e1e1e; padding: 20px; overflow-y: auto; border-right: 1px solid #2a2a2a; } .left-panel { width: 400px; background: #1c2333; padding: 20px; overflow-y: auto; border-right: 1px solid #2e3a52; }
.right-panel { flex: 1; display: flex; flex-direction: column; min-width: 0; background: #1a1a1a; } .right-panel { flex: 1; display: flex; flex-direction: column; min-width: 0; background: #151a25; }
.section-title { font-size: 13px; color: #bbb; margin-bottom: 10px; } .section-title { font-size: 13px; color: #a0acbe; margin-bottom: 10px; }
.input-zone { border: 1px dashed #3a3a3a; border-radius: 10px; padding: 16px; background: #252525; margin-bottom: 20px; } .input-zone { border: 1px dashed #3e4a62; border-radius: 10px; padding: 16px; background: #2e3a52; margin-bottom: 20px; }
.hint { color: #888; font-size: 12px; margin-bottom: 12px; line-height: 1.5; text-align: left; } .hint { color: #5e6878; font-size: 12px; margin-bottom: 12px; line-height: 1.5; text-align: left; }
.input-row { display: flex; gap: 10px; align-items: center; } .input-row { display: flex; gap: 10px; align-items: center; }
.input-row :deep(.el-input) { flex: 1; } .input-row :deep(.el-input) { flex: 1; }
.opt-btn { padding: 8px 12px; font-size: 12px; color: #ccc; background: #2a2a2a; border: 1px solid #3a3a3a; border-radius: 6px; cursor: pointer; white-space: nowrap; } .opt-btn { padding: 8px 12px; font-size: 12px; color: #c8d2e2; background: #2e3a52; border: 1px solid #3e4a62; border-radius: 6px; cursor: pointer; white-space: nowrap; }
.opt-btn:hover:not(:disabled) { color: #3498db; border-color: #3498db; } .opt-btn:hover:not(:disabled) { color: #3498db; border-color: #3498db; }
.opt-btn:disabled { opacity: 0.6; cursor: not-allowed; } .opt-btn:disabled { opacity: 0.6; cursor: not-allowed; }
.empty-candidates { color: #666; font-size: 13px; padding: 16px; border: 1px dashed #333; border-radius: 8px; margin-bottom: 16px; } .empty-candidates { color: #5e6878; font-size: 13px; padding: 16px; border: 1px dashed #333; border-radius: 8px; margin-bottom: 16px; }
.candidate-table-scroll { max-height: 320px; overflow: auto; margin-bottom: 18px; border-radius: 8px; border: 1px solid #2a2a2a; } .candidate-table-scroll { max-height: 320px; overflow: auto; margin-bottom: 18px; border-radius: 8px; border: 1px solid #2e3a52; }
.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; } .candidate-table { --el-table-bg-color: #2e3a52; --el-table-tr-bg-color: #2e3a52; --el-table-header-bg-color: #2e3a52; --el-table-text-color: #c8d2e2; --el-table-border-color: #333; }
.link-danger { background: none; border: none; color: #e74c3c; cursor: pointer; font-size: 12px; padding: 0; } .link-danger { background: none; border: none; color: #e74c3c; cursor: pointer; font-size: 12px; padding: 0; }
.link-danger:hover { text-decoration: underline; } .link-danger:hover { text-decoration: underline; }
.run-row { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 8px; } .run-row { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 8px; }
.btn-run { padding: 10px 18px; font-size: 14px; font-weight: 600; color: #fff; background: #3498db; border: none; border-radius: 8px; cursor: pointer; } .btn-run { padding: 10px 18px; font-size: 14px; font-weight: 600; color: #f5f8fc; background: #3498db; border: none; border-radius: 8px; cursor: pointer; }
.btn-run:hover:not(:disabled) { background: #2980b9; } .btn-run:hover:not(:disabled) { background: #2980b9; }
.btn-run:disabled { opacity: 0.55; cursor: not-allowed; } .btn-run:disabled { opacity: 0.55; cursor: not-allowed; }
.btn-queue { background: #27ae60; } .btn-queue { background: #27ae60; }
.btn-queue:hover:not(:disabled) { background: #219a52; } .btn-queue:hover:not(:disabled) { background: #219a52; }
.loading-msg { margin-top: 12px; font-size: 12px; color: #777; line-height: 1.5; } .loading-msg { margin-top: 12px; font-size: 12px; color: #5e6878; line-height: 1.5; }
.queue-debug-card { margin-top: 18px; padding: 14px; border: 1px solid #2a2a2a; border-radius: 10px; background: #202020; } .queue-debug-card { margin-top: 18px; padding: 14px; border: 1px solid #2e3a52; border-radius: 10px; background: #222b3d; }
.queue-debug-title { margin-bottom: 8px; } .queue-debug-title { margin-bottom: 8px; }
.queue-debug-line { color: #b8c1cc; font-size: 12px; line-height: 1.6; margin-bottom: 8px; } .queue-debug-line { color: #a0acbe; font-size: 12px; line-height: 1.6; margin-bottom: 8px; }
.queue-debug-payload { margin: 0; max-height: 220px; overflow: auto; padding: 10px; border-radius: 8px; background: #141414; color: #8fd3ff; font-size: 11px; line-height: 1.5; white-space: pre-wrap; word-break: break-all; } .queue-debug-payload { margin: 0; max-height: 220px; overflow: auto; padding: 10px; border-radius: 8px; background: #10141f; color: #8fd3ff; font-size: 11px; line-height: 1.5; white-space: pre-wrap; word-break: break-all; }
.panel-header { padding: 16px 20px; font-size: 15px; font-weight: 600; color: #e0e0e0; border-bottom: 1px solid #2a2a2a; } .panel-header { padding: 16px 20px; font-size: 15px; font-weight: 600; color: #f5f8fc; border-bottom: 1px solid #2e3a52; }
.task-list-wrap { flex: 1; padding: 16px 20px; overflow: auto; } .task-list-wrap { flex: 1; padding: 16px 20px; overflow: auto; }
.clean-result-summary { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 12px; margin-bottom: 16px; } .clean-result-summary { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 12px; margin-bottom: 16px; }
.summary-card { padding: 14px 16px; border: 1px solid #2a2a2a; border-radius: 10px; background: #1e1e1e; } .summary-card { padding: 14px 16px; border: 1px solid #2e3a52; border-radius: 10px; background: #1c2333; }
.summary-card strong { display: block; margin-top: 8px; font-size: 22px; color: #eaf4ff; } .summary-card strong { display: block; margin-top: 8px; font-size: 22px; color: #f5f8fc; }
.summary-label { font-size: 12px; color: #8d8d8d; } .summary-label { font-size: 12px; color: #5e6878; }
.subsection-title { font-size: 13px; color: #999; margin: 8px 0 10px; } .subsection-title { font-size: 13px; color: #a0acbe; margin: 8px 0 10px; }
.empty-tasks { color: #666; font-size: 13px; padding: 16px; text-align: center; } .empty-tasks { color: #5e6878; font-size: 13px; padding: 16px; text-align: center; }
.empty-tasks.narrow { padding: 12px 8px; } .empty-tasks.narrow { padding: 12px 8px; }
.match-table { margin-bottom: 18px; } .match-table { margin-bottom: 18px; }
.match-table :deep(.el-table__body tr:hover > td.el-table__cell), .match-table :deep(.el-table__body tr.hover-row > td.el-table__cell), .match-table :deep(.el-table__body tr.current-row > td.el-table__cell) { background-color: var(--el-table-tr-bg-color, #222) !important; } .match-table :deep(.el-table__body tr:hover > td.el-table__cell), .match-table :deep(.el-table__body tr.hover-row > td.el-table__cell), .match-table :deep(.el-table__body tr.current-row > td.el-table__cell) { background-color: var(--el-table-tr-bg-color, #222) !important; }
.result-table { --el-table-bg-color: #222; --el-table-tr-bg-color: #222; --el-table-header-bg-color: #2a2a2a; --el-table-text-color: #ccc; --el-table-border-color: #333; } .result-table { --el-table-bg-color: #222; --el-table-tr-bg-color: #222; --el-table-header-bg-color: #2e3a52; --el-table-text-color: #c8d2e2; --el-table-border-color: #333; }
.ok { color: #27ae60; } .ok { color: #27ae60; }
.fail { color: #e67e22; } .fail { color: #e67e22; }
.result-list-wrap { border: 1px solid #2a2a2a; border-radius: 10px; background: #1e1e1e; min-height: 220px; margin-top: 8px; } .result-list-wrap { border: 1px solid #2e3a52; border-radius: 10px; background: #1c2333; min-height: 220px; margin-top: 8px; }
.result-list-header { padding: 12px 16px; border-bottom: 1px solid #2a2a2a; font-size: 14px; color: #ddd; } .result-list-header { padding: 12px 16px; border-bottom: 1px solid #2e3a52; font-size: 14px; color: #c8d2e2; }
.result-subsection { padding: 12px 12px 4px; } .result-subsection { padding: 12px 12px 4px; }
.result-subsection-title { font-size: 12px; color: #888; margin-bottom: 8px; } .result-subsection-title { font-size: 12px; color: #5e6878; margin-bottom: 8px; }
.task-list { list-style: none; margin: 0; padding: 0; } .task-list { list-style: none; margin: 0; padding: 0; }
.task-item { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; padding: 12px 14px; border: 1px solid #2a2a2a; border-radius: 8px; margin-bottom: 8px; background: #222; } .task-item { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; padding: 12px 14px; border: 1px solid #2e3a52; border-radius: 8px; margin-bottom: 8px; background: #222; }
.left { flex: 1; min-width: 0; } .left { flex: 1; min-width: 0; }
.split-result-main { display: flex; flex-direction: column; gap: 4px; } .split-result-main { display: flex; flex-direction: column; gap: 4px; }
.id { font-weight: 600; color: #e0e0e0; font-size: 13px; } .id { font-weight: 600; color: #f5f8fc; font-size: 13px; }
.files { font-size: 12px; color: #888; } .files { font-size: 12px; color: #5e6878; }
.task-right { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; justify-content: flex-end; } .task-right { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; justify-content: flex-end; }
.status { padding: 4px 10px; border-radius: 6px; font-size: 12px; white-space: nowrap; } .status { padding: 4px 10px; border-radius: 6px; font-size: 12px; white-space: nowrap; }
.status.success { background: rgba(46, 204, 113, 0.18); color: #2ecc71; } .status.success { background: rgba(46, 204, 113, 0.18); color: #2ecc71; }
@@ -1271,7 +1272,7 @@ onUnmounted(() => {
.btn-delete:hover { background: rgba(231, 76, 60, 0.22); } .btn-delete:hover { background: rgba(231, 76, 60, 0.22); }
@media (max-width: 1100px) { @media (max-width: 1100px) {
.main-content { flex-direction: column; height: auto; } .main-content { flex-direction: column; height: auto; }
.left-panel { width: 100%; border-right: none; border-bottom: 1px solid #2a2a2a; } .left-panel { width: 100%; border-right: none; border-bottom: 1px solid #2e3a52; }
.clean-result-summary { grid-template-columns: repeat(2, minmax(0, 1fr)); } .clean-result-summary { grid-template-columns: repeat(2, minmax(0, 1fr)); }
} }
</style> </style>
@@ -1,6 +1,6 @@
<template> <template>
<div class="page-shell module-page"> <div class="page-shell module-page">
<BrandTopBar active="shop-data-crawl" /> <AmazonToolPageShell tool-id="scrape">
<div class="main-content"> <div class="main-content">
<aside class="left-panel"> <aside class="left-panel">
@@ -114,13 +114,14 @@
</div> </div>
</section> </section>
</div> </div>
</AmazonToolPageShell>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, defineComponent, h, onMounted, onUnmounted, ref } from 'vue' import { computed, defineComponent, h, onMounted, onUnmounted, ref } from 'vue'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import BrandTopBar from '@/pages/brand/components/BrandTopBar.vue' import AmazonToolPageShell from '@/pages/amazon/components/AmazonToolPageShell.vue';
import ZiniaoVersionSetting from '@/shared/components/ZiniaoVersionSetting.vue' import ZiniaoVersionSetting from '@/shared/components/ZiniaoVersionSetting.vue'
import { useZiniaoVersion } from '@/shared/utils/ziniao-version' import { useZiniaoVersion } from '@/shared/utils/ziniao-version'
import { getPywebviewApi } from '@/shared/bridges/pywebview' import { getPywebviewApi } from '@/shared/bridges/pywebview'
@@ -568,51 +569,51 @@ onUnmounted(() => {
</script> </script>
<style scoped> <style scoped>
.module-page { min-height: 100vh; background: #1a1a1a; } .module-page { min-height: 100vh; background: #151a25; }
.main-content { display: flex; height: calc(100vh - 56px); min-height: calc(100vh - 56px); } .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; } .left-panel { width: 400px; padding: 20px; overflow-y: auto; border-right: 1px solid #2e3a52; background: #1c2333; }
.right-panel { flex: 1; min-width: 0; background: #1a1a1a; } .right-panel { flex: 1; min-width: 0; background: #151a25; }
.section-title { margin-bottom: 10px; color: #bbb; font-size: 13px; } .section-title { margin-bottom: 10px; color: #a0acbe; font-size: 13px; }
.input-zone { margin-bottom: 18px; padding: 14px; border: 1px dashed #3a3a3a; border-radius: 8px; background: #252525; } .input-zone { margin-bottom: 18px; padding: 14px; border: 1px dashed #3e4a62; border-radius: 8px; background: #2e3a52; }
.input-row { display: flex; gap: 10px; } .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, .btn-run { min-height: 36px; padding: 0 16px; border: 0; border-radius: 5px; background: #409eff; color: #f5f8fc; white-space: nowrap; cursor: pointer; }
.opt-btn { flex: 0 0 auto; } .opt-btn { flex: 0 0 auto; }
.opt-btn:disabled, .btn-run:disabled { opacity: .5; cursor: not-allowed; } .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; } .empty-candidates, .empty-tasks { padding: 16px; border: 1px dashed #333; border-radius: 6px; color: #5e6878; font-size: 13px; }
.candidate-table-scroll { margin-bottom: 18px; border: 1px solid #2a2a2a; border-radius: 6px; overflow: hidden; } .candidate-table-scroll { margin-bottom: 18px; border: 1px solid #2e3a52; 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; } .candidate-table { --el-table-bg-color: #2e3a52; --el-table-tr-bg-color: #2e3a52; --el-table-header-bg-color: #2e3a52; --el-table-text-color: #c8d2e2; --el-table-border-color: #333; }
.link-danger { border: 0; background: transparent; color: #f56c6c; cursor: pointer; } .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-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-row { display: flex; align-items: center; gap: 7px; min-height: 32px; color: #c8d2e2; font-size: 13px; }
.country-check-input { width: 15px; height: 15px; } .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-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-caption { margin-bottom: 8px; color: #5e6878; font-size: 12px; }
.country-order-list { display: flex; flex-direction: column; gap: 6px; } .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 { display: flex; align-items: center; gap: 8px; min-height: 30px; padding: 0 9px; border: 1px solid #383838; border-radius: 4px; color: #c8d2e2; font-size: 13px; cursor: grab; }
.country-drag-row.dragging { opacity: .5; } .country-drag-row.dragging { opacity: .5; }
.drag-handle { color: #777; } .drag-handle { color: #5e6878; }
.country-pref-status, .queue-status { color: #8dc4ff; font-size: 12px; line-height: 1.5; } .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; } .run-row { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-top: 16px; }
.btn-queue { background: #67c23a; } .btn-queue { background: #67c23a; }
.panel-header { height: 52px; padding: 16px 22px; border-bottom: 1px solid #2a2a2a; color: #eee; font-size: 15px; font-weight: 700; } .panel-header { height: 52px; padding: 16px 22px; border-bottom: 1px solid #2e3a52; color: #f5f8fc; font-size: 15px; font-weight: 700; }
.task-list-wrap { height: calc(100% - 52px); padding: 18px 22px 28px; overflow-y: auto; } .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; } .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-card { display: flex; min-height: 70px; flex-direction: column; justify-content: center; padding: 12px 16px; border: 1px solid #333f55; border-radius: 6px; background: #222; }
.summary-label { margin-bottom: 4px; color: #888; font-size: 12px; } .summary-label { margin-bottom: 4px; color: #5e6878; font-size: 12px; }
.summary-card strong { color: #fff; font-size: 22px; } .summary-card strong { color: #f5f8fc; font-size: 22px; }
.subsection-title, .result-subsection-title { margin: 18px 0 10px; color: #bbb; font-size: 13px; font-weight: 700; } .subsection-title, .result-subsection-title { margin: 18px 0 10px; color: #a0acbe; 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; } .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: #c8d2e2; --el-table-border-color: #333; }
.ok { color: #67c23a; }.fail, .error-text { color: #f56c6c; } .ok { color: #67c23a; }.fail, .error-text { color: #f56c6c; }
.result-list-header { padding: 13px 0; border-bottom: 1px solid #303030; color: #eee; font-weight: 700; } .result-list-header { padding: 13px 0; border-bottom: 1px solid #333f55; color: #f5f8fc; font-weight: 700; }
.task-list { margin: 0; padding: 0; list-style: none; } .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(.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(.split-result-main) { min-width: 0; }
:deep(.id) { display: block; margin-bottom: 5px; color: #eee; font-weight: 700; } :deep(.id) { display: block; margin-bottom: 5px; color: #f5f8fc; font-weight: 700; }
:deep(.files) { margin-top: 3px; color: #888; font-size: 12px; } :deep(.files) { margin-top: 3px; color: #5e6878; font-size: 12px; }
:deep(.task-right) { display: flex; align-items: center; gap: 10px; } :deep(.task-right) { display: flex; align-items: center; gap: 10px; }
:deep(.status) { min-width: 88px; font-size: 12px; text-align: center; white-space: nowrap; } :deep(.status) { min-width: 88px; font-size: 12px; text-align: center; white-space: nowrap; }
:deep(.status.success) { color: #67c23a; }:deep(.status.failed) { color: #f56c6c; }:deep(.status.running) { color: #e6a23c; } :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), :deep(.btn-delete) { padding: 5px 10px; border: 1px solid #444; border-radius: 4px; background: transparent; color: #c8d2e2; cursor: pointer; }
:deep(.download) { border-color: #409eff; color: #8dc4ff; }:deep(.btn-delete) { color: #f56c6c; } :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); } } @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> </style>
@@ -1,6 +1,6 @@
<template> <template>
<div class="page-shell module-page"> <div class="page-shell module-page">
<BrandTopBar active="shop-match" /> <AmazonToolPageShell tool-id="time">
<div class="main-content"> <div class="main-content">
<aside class="left-panel"> <aside class="left-panel">
<div class="section-title">店铺候选</div> <div class="section-title">店铺候选</div>
@@ -102,13 +102,14 @@
</div> </div>
</section> </section>
</div> </div>
</AmazonToolPageShell>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, onMounted, onUnmounted, ref } from 'vue' import { computed, onMounted, onUnmounted, ref } from 'vue'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import BrandTopBar from '@/pages/brand/components/BrandTopBar.vue' import AmazonToolPageShell from '@/pages/amazon/components/AmazonToolPageShell.vue';
import ZiniaoVersionSetting from '@/shared/components/ZiniaoVersionSetting.vue' import ZiniaoVersionSetting from '@/shared/components/ZiniaoVersionSetting.vue'
import { LISTING_FILTER_OPTIONS, type ListingFilterValue } from '@/pages/brand/components/listingFilters' import { LISTING_FILTER_OPTIONS, type ListingFilterValue } from '@/pages/brand/components/listingFilters'
import { activateShopMatchTask, addShopMatchCandidate, createShopMatchTask, deleteShopMatchCandidate, deleteShopMatchHistory, deleteShopMatchTask, getShopMatchCountryPreference, getShopMatchDashboard, getShopMatchHistory, getShopMatchResultDownloadUrl, getShopMatchTaskProgressBatch, getShopMatchTasksBatch, listShopMatchCandidates, matchShopMatchShops, putShopMatchCountryPreference, type ShopMatchCandidateVo, type ShopMatchCreateTaskItem, type ShopMatchDashboardVo, type ShopMatchHistoryItem, type ShopMatchShopQueueItem, type ShopMatchTaskDetailVo } from '@/shared/api/java-modules' import { activateShopMatchTask, addShopMatchCandidate, createShopMatchTask, deleteShopMatchCandidate, deleteShopMatchHistory, deleteShopMatchTask, getShopMatchCountryPreference, getShopMatchDashboard, getShopMatchHistory, getShopMatchResultDownloadUrl, getShopMatchTaskProgressBatch, getShopMatchTasksBatch, listShopMatchCandidates, matchShopMatchShops, putShopMatchCountryPreference, type ShopMatchCandidateVo, type ShopMatchCreateTaskItem, type ShopMatchDashboardVo, type ShopMatchHistoryItem, type ShopMatchShopQueueItem, type ShopMatchTaskDetailVo } from '@/shared/api/java-modules'
@@ -509,80 +510,80 @@ onUnmounted(() => { disposed = true; stopPolling(); stopScheduleHeartbeat(); cle
</script> </script>
<style scoped> <style scoped>
.module-page { min-height: 100vh; background: #1a1a1a; } .module-page { min-height: 100vh; background: #151a25; }
.main-content { display: flex; min-height: calc(100vh - 56px); height: calc(100vh - 56px); } .main-content { display: flex; min-height: calc(100vh - 56px); height: calc(100vh - 56px); }
.left-panel { width: 400px; background: #1e1e1e; padding: 20px; overflow-y: auto; border-right: 1px solid #2a2a2a; } .left-panel { width: 400px; background: #1c2333; padding: 20px; overflow-y: auto; border-right: 1px solid #2e3a52; }
.right-panel { flex: 1; display: flex; flex-direction: column; min-width: 0; background: #1a1a1a; } .right-panel { flex: 1; display: flex; flex-direction: column; min-width: 0; background: #151a25; }
.section-title { font-size: 13px; color: #bbb; margin-bottom: 10px; } .section-title { font-size: 13px; color: #a0acbe; margin-bottom: 10px; }
.input-zone { border: 1px dashed #3a3a3a; border-radius: 10px; padding: 16px; background: #252525; margin-bottom: 20px; } .input-zone { border: 1px dashed #3e4a62; border-radius: 10px; padding: 16px; background: #2e3a52; margin-bottom: 20px; }
.hint { color: #888; font-size: 12px; margin-bottom: 12px; line-height: 1.5; text-align: left; } .hint { color: #5e6878; font-size: 12px; margin-bottom: 12px; line-height: 1.5; text-align: left; }
.country-pref-hint { margin-top: -4px; } .country-pref-hint { margin-top: -4px; }
.country-pref-checks { display: flex; flex-wrap: wrap; gap: 10px 16px; margin-bottom: 12px; } .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-check-row { display: inline-flex; align-items: center; gap: 8px; cursor: pointer; user-select: none; font-size: 12px; color: #c8d2e2; }
.country-check-input { width: 16px; height: 16px; accent-color: #409eff; cursor: pointer; flex-shrink: 0; } .country-check-input { width: 16px; height: 16px; accent-color: #409eff; cursor: pointer; flex-shrink: 0; }
.country-check-text { line-height: 1.3; } .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-panel { border: 1px solid #333; border-radius: 8px; padding: 10px 12px; background: #2e3a52; margin-bottom: 14px; }
.country-order-caption { font-size: 11px; color: #777; margin-bottom: 8px; } .country-order-caption { font-size: 11px; color: #5e6878; margin-bottom: 8px; }
.country-order-list { display: flex; flex-direction: column; gap: 6px; } .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 { display: flex; align-items: center; gap: 10px; padding: 8px 10px; background: #2c2c2c; border-radius: 6px; border: 1px solid #3e4a62; cursor: grab; user-select: none; }
.country-drag-row.dragging { opacity: 0.55; border-color: #3498db; } .country-drag-row.dragging { opacity: 0.55; border-color: #3498db; }
.drag-handle { color: #666; font-size: 12px; letter-spacing: -2px; } .drag-handle { color: #5e6878; font-size: 12px; letter-spacing: -2px; }
.country-drag-label { font-size: 12px; color: #ccc; } .country-drag-label { font-size: 12px; color: #c8d2e2; }
.country-pref-status { font-size: 11px; color: #888; margin-bottom: 10px; } .country-pref-status { font-size: 11px; color: #5e6878; margin-bottom: 10px; }
.input-row { display: flex; gap: 10px; align-items: center; } .input-row { display: flex; gap: 10px; align-items: center; }
.input-row :deep(.el-input) { flex: 1; } .input-row :deep(.el-input) { flex: 1; }
.opt-btn,.schedule-remove,.schedule-add { padding: 8px 12px; font-size: 12px; color: #ccc; background: #2a2a2a; border: 1px solid #3a3a3a; border-radius: 6px; cursor: pointer; white-space: nowrap; } .opt-btn,.schedule-remove,.schedule-add { padding: 8px 12px; font-size: 12px; color: #c8d2e2; background: #2e3a52; border: 1px solid #3e4a62; border-radius: 6px; cursor: pointer; white-space: nowrap; }
.opt-btn:hover:not(:disabled),.schedule-remove:hover:not(:disabled),.schedule-add:hover:not(:disabled) { color: #3498db; border-color: #3498db; } .opt-btn:hover:not(:disabled),.schedule-remove:hover:not(:disabled),.schedule-add:hover:not(:disabled) { color: #3498db; border-color: #3498db; }
.opt-btn:disabled,.schedule-remove:disabled,.schedule-add:disabled { opacity: 0.6; cursor: not-allowed; } .opt-btn:disabled,.schedule-remove:disabled,.schedule-add:disabled { opacity: 0.6; cursor: not-allowed; }
.empty-candidates { color: #666; font-size: 13px; padding: 16px; border: 1px dashed #333; border-radius: 8px; margin-bottom: 16px; } .empty-candidates { color: #5e6878; font-size: 13px; padding: 16px; border: 1px dashed #333; border-radius: 8px; margin-bottom: 16px; }
.candidate-table-scroll { max-height: 280px; overflow: auto; margin-bottom: 16px; border-radius: 8px; border: 1px solid #2a2a2a; } .candidate-table-scroll { max-height: 280px; overflow: auto; margin-bottom: 16px; border-radius: 8px; border: 1px solid #2e3a52; }
.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; } .candidate-table { --el-table-bg-color: #2e3a52; --el-table-tr-bg-color: #2e3a52; --el-table-header-bg-color: #2e3a52; --el-table-text-color: #c8d2e2; --el-table-border-color: #333; }
.schedule-switch { display: flex; align-items: center; gap: 8px; color: #ccc; font-size: 12px; margin-bottom: 12px; } .schedule-switch { display: flex; align-items: center; gap: 8px; color: #c8d2e2; font-size: 12px; margin-bottom: 12px; }
.schedule-config { border: 1px solid #333; border-radius: 8px; padding: 12px; background: #252525; margin-bottom: 16px; } .schedule-config { border: 1px solid #333; border-radius: 8px; padding: 12px; background: #2e3a52; margin-bottom: 16px; }
.schedule-row { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; } .schedule-row { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; }
.schedule-picker { flex: 1; } .schedule-picker { flex: 1; }
.schedule-picker :deep(.el-input__wrapper) { background-color: #2a2a2a; box-shadow: 0 0 0 1px #3a3a3a inset; } .schedule-picker :deep(.el-input__wrapper) { background-color: #2e3a52; box-shadow: 0 0 0 1px #3e4a62 inset; }
.schedule-picker :deep(.el-input__inner) { color: #ccc; } .schedule-picker :deep(.el-input__inner) { color: #c8d2e2; }
.schedule-add { margin-top: 2px; } .schedule-add { margin-top: 2px; }
.schedule-hint { margin-top: 10px; margin-bottom: 0; } .schedule-hint { margin-top: 10px; margin-bottom: 0; }
.link-danger { background: none; border: none; color: #e74c3c; cursor: pointer; font-size: 12px; padding: 0; } .link-danger { background: none; border: none; color: #e74c3c; cursor: pointer; font-size: 12px; padding: 0; }
.link-danger:hover { text-decoration: underline; } .link-danger:hover { text-decoration: underline; }
.run-row { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 8px; } .run-row { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 8px; }
.btn-run { padding: 10px 18px; font-size: 14px; font-weight: 600; color: #fff; background: #3498db; border: none; border-radius: 8px; cursor: pointer; } .btn-run { padding: 10px 18px; font-size: 14px; font-weight: 600; color: #f5f8fc; background: #3498db; border: none; border-radius: 8px; cursor: pointer; }
.btn-run:hover:not(:disabled) { background: #2980b9; } .btn-run:hover:not(:disabled) { background: #2980b9; }
.btn-run:disabled { opacity: 0.55; cursor: not-allowed; } .btn-run:disabled { opacity: 0.55; cursor: not-allowed; }
.btn-queue { background: #27ae60; } .btn-queue { background: #27ae60; }
.btn-queue:hover:not(:disabled) { background: #219a52; } .btn-queue:hover:not(:disabled) { background: #219a52; }
.loading-msg { margin-top: 12px; font-size: 12px; color: #777; line-height: 1.5; } .loading-msg { margin-top: 12px; font-size: 12px; color: #5e6878; line-height: 1.5; }
.loading-msg code { font-size: 11px; color: #8fd3ff; } .loading-msg code { font-size: 11px; color: #8fd3ff; }
.queue-debug-card { margin-top: 18px; padding: 14px; border: 1px solid #2a2a2a; border-radius: 10px; background: #202020; } .queue-debug-card { margin-top: 18px; padding: 14px; border: 1px solid #2e3a52; border-radius: 10px; background: #222b3d; }
.queue-debug-title { margin-bottom: 8px; } .queue-debug-title { margin-bottom: 8px; }
.queue-debug-line { color: #b8c1cc; font-size: 12px; line-height: 1.6; margin-bottom: 8px; } .queue-debug-line { color: #a0acbe; font-size: 12px; line-height: 1.6; margin-bottom: 8px; }
.queue-debug-payload { margin: 0; max-height: 220px; overflow: auto; padding: 10px; border-radius: 8px; background: #141414; color: #8fd3ff; font-size: 11px; line-height: 1.5; white-space: pre-wrap; word-break: break-all; } .queue-debug-payload { margin: 0; max-height: 220px; overflow: auto; padding: 10px; border-radius: 8px; background: #10141f; color: #8fd3ff; font-size: 11px; line-height: 1.5; white-space: pre-wrap; word-break: break-all; }
.panel-header { padding: 16px 20px; font-size: 15px; font-weight: 600; color: #e0e0e0; border-bottom: 1px solid #2a2a2a; } .panel-header { padding: 16px 20px; font-size: 15px; font-weight: 600; color: #f5f8fc; border-bottom: 1px solid #2e3a52; }
.task-list-wrap { flex: 1; padding: 16px 20px; overflow: auto; } .task-list-wrap { flex: 1; padding: 16px 20px; overflow: auto; }
.clean-result-summary { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 12px; margin-bottom: 16px; } .clean-result-summary { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 12px; margin-bottom: 16px; }
.summary-card { padding: 14px 16px; border: 1px solid #2a2a2a; border-radius: 10px; background: #1e1e1e; } .summary-card { padding: 14px 16px; border: 1px solid #2e3a52; border-radius: 10px; background: #1c2333; }
.summary-card strong { display: block; margin-top: 8px; font-size: 22px; color: #eaf4ff; } .summary-card strong { display: block; margin-top: 8px; font-size: 22px; color: #f5f8fc; }
.summary-label { font-size: 12px; color: #8d8d8d; } .summary-label { font-size: 12px; color: #5e6878; }
.subsection-title { font-size: 13px; color: #999; margin: 8px 0 10px; } .subsection-title { font-size: 13px; color: #a0acbe; margin: 8px 0 10px; }
.empty-tasks { color: #666; font-size: 13px; padding: 16px; text-align: center; } .empty-tasks { color: #5e6878; font-size: 13px; padding: 16px; text-align: center; }
.empty-tasks.narrow { padding: 12px 8px; } .empty-tasks.narrow { padding: 12px 8px; }
.match-table { margin-bottom: 18px; } .match-table { margin-bottom: 18px; }
.match-table :deep(.el-table__body tr:hover > td.el-table__cell),.match-table :deep(.el-table__body tr.hover-row > td.el-table__cell),.match-table :deep(.el-table__body tr.current-row > td.el-table__cell) { background-color: var(--el-table-tr-bg-color, #222) !important; } .match-table :deep(.el-table__body tr:hover > td.el-table__cell),.match-table :deep(.el-table__body tr.hover-row > td.el-table__cell),.match-table :deep(.el-table__body tr.current-row > td.el-table__cell) { background-color: var(--el-table-tr-bg-color, #222) !important; }
.result-table { --el-table-bg-color: #222; --el-table-tr-bg-color: #222; --el-table-header-bg-color: #2a2a2a; --el-table-text-color: #ccc; --el-table-border-color: #333; } .result-table { --el-table-bg-color: #222; --el-table-tr-bg-color: #222; --el-table-header-bg-color: #2e3a52; --el-table-text-color: #c8d2e2; --el-table-border-color: #333; }
.ok { color: #27ae60; } .ok { color: #27ae60; }
.fail { color: #e67e22; } .fail { color: #e67e22; }
.result-list-wrap { border: 1px solid #2a2a2a; border-radius: 10px; background: #1e1e1e; min-height: 200px; margin-top: 8px; } .result-list-wrap { border: 1px solid #2e3a52; border-radius: 10px; background: #1c2333; min-height: 200px; margin-top: 8px; }
.result-list-header { padding: 12px 16px; border-bottom: 1px solid #2a2a2a; font-size: 14px; color: #ddd; } .result-list-header { padding: 12px 16px; border-bottom: 1px solid #2e3a52; font-size: 14px; color: #c8d2e2; }
.result-subsection { padding: 12px 12px 4px; } .result-subsection { padding: 12px 12px 4px; }
.result-subsection-title { font-size: 12px; color: #888; margin-bottom: 8px; } .result-subsection-title { font-size: 12px; color: #5e6878; margin-bottom: 8px; }
.task-list { list-style: none; margin: 0; padding: 0; } .task-list { list-style: none; margin: 0; padding: 0; }
.task-item { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; padding: 12px 14px; border: 1px solid #2a2a2a; border-radius: 8px; margin-bottom: 8px; background: #222; } .task-item { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; padding: 12px 14px; border: 1px solid #2e3a52; border-radius: 8px; margin-bottom: 8px; background: #222; }
.left { flex: 1; min-width: 0; } .left { flex: 1; min-width: 0; }
.split-result-main { display: flex; flex-direction: column; gap: 4px; } .split-result-main { display: flex; flex-direction: column; gap: 4px; }
.id { font-weight: 600; color: #e0e0e0; font-size: 13px; } .id { font-weight: 600; color: #f5f8fc; font-size: 13px; }
.files { font-size: 12px; color: #888; } .files { font-size: 12px; color: #5e6878; }
.task-right { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; justify-content: flex-end; } .task-right { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; justify-content: flex-end; }
.status { padding: 4px 10px; border-radius: 6px; font-size: 12px; white-space: nowrap; } .status { padding: 4px 10px; border-radius: 6px; font-size: 12px; white-space: nowrap; }
.status.success { background: rgba(46, 204, 113, 0.18); color: #2ecc71; } .status.success { background: rgba(46, 204, 113, 0.18); color: #2ecc71; }
@@ -592,6 +593,6 @@ onUnmounted(() => { disposed = true; stopPolling(); stopScheduleHeartbeat(); cle
.download:hover { background: rgba(52, 152, 219, 0.28); } .download:hover { background: rgba(52, 152, 219, 0.28); }
.btn-delete { padding: 6px 10px; border-radius: 6px; font-size: 12px; background: rgba(231, 76, 60, 0.12); color: #ff8f8f; border: none; cursor: pointer; } .btn-delete { padding: 6px 10px; border-radius: 6px; font-size: 12px; background: rgba(231, 76, 60, 0.12); color: #ff8f8f; border: none; cursor: pointer; }
.btn-delete:hover { background: rgba(231, 76, 60, 0.22); } .btn-delete:hover { background: rgba(231, 76, 60, 0.22); }
@media (max-width: 1100px) { .main-content { flex-direction: column; height: auto; } .left-panel { width: 100%; border-right: none; border-bottom: 1px solid #2a2a2a; } .clean-result-summary { grid-template-columns: repeat(2, minmax(0, 1fr)); } .schedule-row { flex-direction: column; align-items: stretch; } } @media (max-width: 1100px) { .main-content { flex-direction: column; height: auto; } .left-panel { width: 100%; border-right: none; border-bottom: 1px solid #2e3a52; } .clean-result-summary { grid-template-columns: repeat(2, minmax(0, 1fr)); } .schedule-row { flex-direction: column; align-items: stretch; } }
</style> </style>
@@ -1,6 +1,6 @@
<template> <template>
<div class="page-shell module-page"> <div class="page-shell module-page">
<BrandTopBar active="similar-asin" /> <AmazonToolPageShell tool-id="source">
<div class="main-content"> <div class="main-content">
<aside class="left-panel left-panel--with-template"> <aside class="left-panel left-panel--with-template">
@@ -181,13 +181,14 @@
</section> </section>
</div> </div>
</AmazonToolPageShell>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, onMounted, onUnmounted, ref } from 'vue' import { computed, onMounted, onUnmounted, ref } from 'vue'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import BrandTopBar from '@/pages/brand/components/BrandTopBar.vue' import AmazonToolPageShell from '@/pages/amazon/components/AmazonToolPageShell.vue';
import ModuleTemplateDownload from '@/shared/components/ModuleTemplateDownload.vue' import ModuleTemplateDownload from '@/shared/components/ModuleTemplateDownload.vue'
import { import {
activateSimilarAsinTask, activateSimilarAsinTask,
@@ -1126,7 +1127,7 @@ onUnmounted(() => {
<style scoped> <style scoped>
.module-page { .module-page {
min-height: 100vh; min-height: 100vh;
background: #1a1a1a; background: #151a25;
} }
.main-content { .main-content {
@@ -1137,16 +1138,16 @@ onUnmounted(() => {
.left-panel { .left-panel {
width: 400px; width: 400px;
background: #1e1e1e; background: #1c2333;
padding: 20px; padding: 20px;
overflow-y: auto; overflow-y: auto;
border-right: 1px solid #2a2a2a; border-right: 1px solid #2e3a52;
} }
.right-panel { .right-panel {
flex: 1; flex: 1;
min-width: 0; min-width: 0;
background: #1a1a1a; background: #151a25;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
@@ -1154,15 +1155,15 @@ onUnmounted(() => {
.section-title, .section-title,
.subsection-title { .subsection-title {
font-size: 13px; font-size: 13px;
color: #bbb; color: #a0acbe;
margin-bottom: 10px; margin-bottom: 10px;
} }
.upload-zone { .upload-zone {
border: 1px dashed #3a3a3a; border: 1px dashed #3e4a62;
border-radius: 10px; border-radius: 10px;
padding: 18px; padding: 18px;
background: #252525; background: #2e3a52;
margin-bottom: 18px; margin-bottom: 18px;
} }
@@ -1171,7 +1172,7 @@ onUnmounted(() => {
padding: 14px 16px; padding: 14px 16px;
border: 1px solid #2f3a34; border: 1px solid #2f3a34;
border-radius: 10px; border-radius: 10px;
background: linear-gradient(135deg, #222a25, #202020); background: linear-gradient(135deg, #222a25, #222b3d);
} }
.aliprice-card { .aliprice-card {
@@ -1187,7 +1188,7 @@ onUnmounted(() => {
grid-template-columns: 44px minmax(0, 1fr); grid-template-columns: 44px minmax(0, 1fr);
align-items: center; align-items: center;
gap: 10px; gap: 10px;
color: #bbb; color: #a0acbe;
font-size: 13px; font-size: 13px;
} }
@@ -1204,7 +1205,7 @@ onUnmounted(() => {
border-radius: 6px; border-radius: 6px;
outline: none; outline: none;
background: #1b1b1b; background: #1b1b1b;
color: #ddd; color: #c8d2e2;
font: inherit; font: inherit;
} }
@@ -1253,7 +1254,7 @@ onUnmounted(() => {
height: 24px; height: 24px;
flex: 0 0 auto; flex: 0 0 auto;
border-radius: 999px; border-radius: 999px;
background: #3a3a3a; background: #3e4a62;
box-shadow: inset 0 0 0 1px #4b4b4b; box-shadow: inset 0 0 0 1px #4b4b4b;
transition: background .2s ease, box-shadow .2s ease; transition: background .2s ease, box-shadow .2s ease;
} }
@@ -1284,7 +1285,7 @@ onUnmounted(() => {
.loading-msg, .loading-msg,
.files, .files,
.muted { .muted {
color: #888; color: #5e6878;
font-size: 12px; font-size: 12px;
line-height: 1.5; line-height: 1.5;
} }
@@ -1316,14 +1317,14 @@ onUnmounted(() => {
.opt-btn { .opt-btn {
padding: 8px 14px; padding: 8px 14px;
color: #ccc; color: #c8d2e2;
background: #2a2a2a; background: #2e3a52;
border: 1px solid #3a3a3a; border: 1px solid #3e4a62;
} }
.btn-run { .btn-run {
padding: 10px 18px; padding: 10px 18px;
color: #fff; color: #f5f8fc;
background: #3498db; background: #3498db;
font-weight: 600; font-weight: 600;
} }
@@ -1339,7 +1340,7 @@ onUnmounted(() => {
.selected-files { .selected-files {
margin-top: 14px; margin-top: 14px;
color: #999; color: #a0acbe;
font-size: 12px; font-size: 12px;
word-break: break-all; word-break: break-all;
} }
@@ -1353,10 +1354,10 @@ onUnmounted(() => {
.queue-payload { .queue-payload {
margin-top: 14px; margin-top: 14px;
padding: 12px; padding: 12px;
border: 1px solid #2a2a2a; border: 1px solid #2e3a52;
border-radius: 8px; border-radius: 8px;
background: #202020; background: #222b3d;
color: #b8c1cc; color: #a0acbe;
font-size: 12px; font-size: 12px;
} }
@@ -1369,10 +1370,10 @@ onUnmounted(() => {
.panel-header { .panel-header {
padding: 16px 20px; padding: 16px 20px;
border-bottom: 1px solid #2a2a2a; border-bottom: 1px solid #2e3a52;
font-size: 15px; font-size: 15px;
font-weight: 600; font-weight: 600;
color: #ddd; color: #c8d2e2;
} }
.task-list-wrap { .task-list-wrap {
@@ -1390,27 +1391,27 @@ onUnmounted(() => {
.summary-card { .summary-card {
padding: 14px 16px; padding: 14px 16px;
border: 1px solid #2a2a2a; border: 1px solid #2e3a52;
border-radius: 8px; border-radius: 8px;
background: #1e1e1e; background: #1c2333;
} }
.summary-card strong { .summary-card strong {
display: block; display: block;
margin-top: 8px; margin-top: 8px;
color: #eaf4ff; color: #f5f8fc;
font-size: 22px; font-size: 22px;
} }
.summary-label { .summary-label {
color: #8d8d8d; color: #5e6878;
font-size: 12px; font-size: 12px;
} }
.result-list-wrap { .result-list-wrap {
border: 1px solid #2a2a2a; border: 1px solid #2e3a52;
border-radius: 8px; border-radius: 8px;
background: #1e1e1e; background: #1c2333;
min-height: 180px; min-height: 180px;
margin: 0 0 16px; margin: 0 0 16px;
} }
@@ -1419,13 +1420,13 @@ onUnmounted(() => {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
padding: 12px 16px; padding: 12px 16px;
border-bottom: 1px solid #2a2a2a; border-bottom: 1px solid #2e3a52;
color: #ddd; color: #c8d2e2;
font-size: 14px; font-size: 14px;
} }
.empty-tasks { .empty-tasks {
color: #666; color: #5e6878;
font-size: 13px; font-size: 13px;
padding: 18px; padding: 18px;
text-align: center; text-align: center;
@@ -1443,7 +1444,7 @@ onUnmounted(() => {
justify-content: space-between; justify-content: space-between;
gap: 12px; gap: 12px;
padding: 12px 14px; padding: 12px 14px;
border: 1px solid #2a2a2a; border: 1px solid #2e3a52;
border-radius: 8px; border-radius: 8px;
margin-bottom: 8px; margin-bottom: 8px;
background: #222; background: #222;
@@ -1455,7 +1456,7 @@ onUnmounted(() => {
} }
.id { .id {
color: #e0e0e0; color: #f5f8fc;
font-size: 13px; font-size: 13px;
font-weight: 600; font-weight: 600;
} }
@@ -1490,7 +1491,7 @@ onUnmounted(() => {
.status.pending { .status.pending {
background: rgba(149, 165, 166, .18); background: rgba(149, 165, 166, .18);
color: #bdc3c7; color: #a0acbe;
} }
.result-hint { .result-hint {
@@ -1516,7 +1517,7 @@ onUnmounted(() => {
height: 8px; height: 8px;
border-radius: 999px; border-radius: 999px;
overflow: hidden; overflow: hidden;
background: #303030; background: #333f55;
border: 1px solid #3b3b3b; border: 1px solid #3b3b3b;
} }
@@ -1535,7 +1536,7 @@ onUnmounted(() => {
.download { .download {
padding: 6px 10px; padding: 6px 10px;
color: #d6ecff; color: #c8d2e2;
background: rgba(52, 152, 219, .18); background: rgba(52, 152, 219, .18);
} }
@@ -1554,7 +1555,7 @@ onUnmounted(() => {
.left-panel { .left-panel {
width: 100%; width: 100%;
border-right: none; border-right: none;
border-bottom: 1px solid #2a2a2a; border-bottom: 1px solid #2e3a52;
} }
.clean-result-summary { .clean-result-summary {
@@ -1,6 +1,6 @@
<template> <template>
<div class="page-shell module-page"> <div class="page-shell module-page">
<BrandTopBar active="split" /> <AmazonToolPageShell tool-id="split">
<div class="main-content"> <div class="main-content">
<aside class="left-panel"> <aside class="left-panel">
@@ -159,13 +159,14 @@
</div> </div>
</section> </section>
</div> </div>
</AmazonToolPageShell>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, onMounted, ref } from 'vue' import { computed, onMounted, ref } from 'vue'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import BrandTopBar from './BrandTopBar.vue' import AmazonToolPageShell from '@/pages/amazon/components/AmazonToolPageShell.vue';
import { expandBrandFolderRecursive, type BrandExpandFolderItem } from '@/shared/api/brand' import { expandBrandFolderRecursive, type BrandExpandFolderItem } from '@/shared/api/brand'
import { deleteSplitHistory, getExcelInfo, getSplitHistory, getSplitResultDownloadUrl, runSplit, type SplitResultItem, type SplitRunVo } from '@/shared/api/java-modules' import { deleteSplitHistory, getExcelInfo, getSplitHistory, getSplitResultDownloadUrl, runSplit, type SplitResultItem, type SplitRunVo } from '@/shared/api/java-modules'
import { getPywebviewApi, type UploadedJavaFile } from '@/shared/bridges/pywebview' import { getPywebviewApi, type UploadedJavaFile } from '@/shared/bridges/pywebview'
@@ -391,7 +392,7 @@ onMounted(() => {
<style scoped> <style scoped>
.module-page { .module-page {
min-height: 100vh; min-height: 100vh;
background: #1a1a1a; background: #151a25;
} }
.main-content { .main-content {
@@ -402,10 +403,10 @@ onMounted(() => {
.left-panel { .left-panel {
width: 380px; width: 380px;
background: #1e1e1e; background: #1c2333;
padding: 20px; padding: 20px;
overflow-y: auto; overflow-y: auto;
border-right: 1px solid #2a2a2a; border-right: 1px solid #2e3a52;
} }
.right-panel { .right-panel {
@@ -413,32 +414,32 @@ onMounted(() => {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
min-width: 0; min-width: 0;
background: #1a1a1a; background: #151a25;
} }
.section-title { .section-title {
font-size: 13px; font-size: 13px;
color: #bbb; color: #a0acbe;
margin-bottom: 10px; margin-bottom: 10px;
} }
.upload-zone { .upload-zone {
border: 1px dashed #3a3a3a; border: 1px dashed #3e4a62;
border-radius: 10px; border-radius: 10px;
padding: 24px; padding: 24px;
text-align: center; text-align: center;
background: #252525; background: #2e3a52;
margin-bottom: 20px; margin-bottom: 20px;
transition: all 0.2s ease; transition: all 0.2s ease;
} }
.upload-zone:hover { .upload-zone:hover {
border-color: #3498db; border-color: #3498db;
background: #2a2a2a; background: #2e3a52;
} }
.hint { .hint {
color: #888; color: #5e6878;
font-size: 13px; font-size: 13px;
margin-bottom: 12px; margin-bottom: 12px;
line-height: 1.5; line-height: 1.5;
@@ -463,9 +464,9 @@ onMounted(() => {
.opt-btn { .opt-btn {
padding: 8px 16px; padding: 8px 16px;
font-size: 13px; font-size: 13px;
color: #ccc; color: #c8d2e2;
background: #2a2a2a; background: #2e3a52;
border: 1px solid #3a3a3a; border: 1px solid #3e4a62;
border-radius: 6px; border-radius: 6px;
} }
@@ -483,7 +484,7 @@ onMounted(() => {
.selected-files { .selected-files {
margin-top: 14px; margin-top: 14px;
font-size: 12px; font-size: 12px;
color: #888; color: #5e6878;
max-height: 112px; max-height: 112px;
overflow-y: auto; overflow-y: auto;
text-align: left; text-align: left;
@@ -496,7 +497,7 @@ onMounted(() => {
} }
.more-line { .more-line {
color: #b8c1cc; color: #a0acbe;
} }
.split-selected-files { .split-selected-files {
@@ -512,8 +513,8 @@ onMounted(() => {
.column-option-group { .column-option-group {
padding: 14px; padding: 14px;
border-radius: 10px; border-radius: 10px;
background: #252525; background: #2e3a52;
border: 1px solid #2a2a2a; border: 1px solid #2e3a52;
} }
.column-toolbar { .column-toolbar {
@@ -526,7 +527,7 @@ onMounted(() => {
.column-count { .column-count {
font-size: 12px; font-size: 12px;
color: #a9b4bf; color: #a0acbe;
} }
.column-actions { .column-actions {
@@ -545,8 +546,8 @@ onMounted(() => {
.empty-column-state { .empty-column-state {
padding: 16px 12px; padding: 16px 12px;
border-radius: 8px; border-radius: 8px;
border: 1px dashed #3a3a3a; border: 1px dashed #3e4a62;
background: #202020; background: #222b3d;
color: #7f8790; color: #7f8790;
font-size: 12px; font-size: 12px;
line-height: 1.6; line-height: 1.6;
@@ -558,8 +559,8 @@ onMounted(() => {
gap: 10px; gap: 10px;
padding: 10px 12px; padding: 10px 12px;
border-radius: 8px; border-radius: 8px;
border: 1px solid #2f2f2f; border: 1px solid #323e58;
background: #202020; background: #222b3d;
cursor: pointer; cursor: pointer;
transition: all 0.2s ease; transition: all 0.2s ease;
font-size: 13px; font-size: 13px;
@@ -587,16 +588,16 @@ onMounted(() => {
cursor: pointer; cursor: pointer;
padding: 10px 12px; padding: 10px 12px;
border-radius: 8px; border-radius: 8px;
background: #252525; background: #2e3a52;
border: 1px solid #2a2a2a; border: 1px solid #2e3a52;
margin-bottom: 8px; margin-bottom: 8px;
transition: all 0.2s ease; transition: all 0.2s ease;
} }
.radio-item:hover, .radio-item:hover,
.radio-item.active { .radio-item.active {
background: #2a2a2a; background: #2e3a52;
border-color: #3a3a3a; border-color: #3e4a62;
} }
.radio-item input { .radio-item input {
@@ -605,13 +606,13 @@ onMounted(() => {
.label { .label {
font-weight: 500; font-weight: 500;
color: #e0e0e0; color: #f5f8fc;
font-size: 13px; font-size: 13px;
} }
.desc { .desc {
font-size: 12px; font-size: 12px;
color: #888; color: #5e6878;
margin-top: 2px; margin-top: 2px;
line-height: 1.5; line-height: 1.5;
font-weight: 400; font-weight: 400;
@@ -623,17 +624,17 @@ onMounted(() => {
gap: 10px; gap: 10px;
padding: 14px 16px; padding: 14px 16px;
border-radius: 10px; border-radius: 10px;
border: 1px solid #2f2f2f; border: 1px solid #323e58;
background: #252525; background: #2e3a52;
} }
.split-number-input { .split-number-input {
width: 100%; width: 100%;
padding: 12px 14px; padding: 12px 14px;
border-radius: 8px; border-radius: 8px;
border: 1px solid #3a3a3a; border: 1px solid #3e4a62;
background: #1b1b1b; background: #1b1b1b;
color: #eaf4ff; color: #f5f8fc;
font-size: 22px; font-size: 22px;
font-weight: 700; font-weight: 700;
line-height: 1.2; line-height: 1.2;
@@ -664,7 +665,7 @@ onMounted(() => {
.btn-run { .btn-run {
padding: 10px 22px; padding: 10px 22px;
background: #3498db; background: #3498db;
color: #fff; color: #f5f8fc;
border-radius: 8px; border-radius: 8px;
font-size: 14px; font-size: 14px;
} }
@@ -675,7 +676,7 @@ onMounted(() => {
.btn-run:disabled { .btn-run:disabled {
background: #555; background: #555;
color: #999; color: #a0acbe;
cursor: not-allowed; cursor: not-allowed;
} }
@@ -686,10 +687,10 @@ onMounted(() => {
.panel-header { .panel-header {
padding: 16px 20px; padding: 16px 20px;
border-bottom: 1px solid #2a2a2a; border-bottom: 1px solid #2e3a52;
font-size: 15px; font-size: 15px;
font-weight: 600; font-weight: 600;
color: #ddd; color: #c8d2e2;
} }
.task-list-wrap { .task-list-wrap {
@@ -710,10 +711,10 @@ onMounted(() => {
justify-content: space-between; justify-content: space-between;
gap: 16px; gap: 16px;
padding: 14px 16px; padding: 14px 16px;
border: 1px solid #2a2a2a; border: 1px solid #2e3a52;
border-radius: 8px; border-radius: 8px;
margin-bottom: 10px; margin-bottom: 10px;
background: #1e1e1e; background: #1c2333;
} }
.split-result-item { .split-result-item {
@@ -734,19 +735,19 @@ onMounted(() => {
.id { .id {
display: inline-block; display: inline-block;
font-weight: 600; font-weight: 600;
color: #e0e0e0; color: #f5f8fc;
font-size: 13px; font-size: 13px;
} }
.files { .files {
font-size: 12px; font-size: 12px;
color: #888; color: #5e6878;
margin-top: 4px; margin-top: 4px;
} }
.time { .time {
font-size: 12px; font-size: 12px;
color: #666; color: #5e6878;
margin-top: 2px; margin-top: 2px;
} }
@@ -813,7 +814,7 @@ onMounted(() => {
} }
.empty-tasks { .empty-tasks {
color: #666; color: #5e6878;
font-size: 13px; font-size: 13px;
padding: 24px 8px; padding: 24px 8px;
} }
@@ -831,27 +832,27 @@ onMounted(() => {
.summary-card { .summary-card {
padding: 16px 18px; padding: 16px 18px;
border: 1px solid #2a2a2a; border: 1px solid #2e3a52;
border-radius: 10px; border-radius: 10px;
background: #1e1e1e; background: #1c2333;
} }
.summary-card strong { .summary-card strong {
display: block; display: block;
margin-top: 8px; margin-top: 8px;
font-size: 24px; font-size: 24px;
color: #eaf4ff; color: #f5f8fc;
} }
.summary-label { .summary-label {
font-size: 12px; font-size: 12px;
color: #8d8d8d; color: #5e6878;
} }
.result-list-wrap { .result-list-wrap {
border: 1px solid #2a2a2a; border: 1px solid #2e3a52;
border-radius: 10px; border-radius: 10px;
background: #1e1e1e; background: #1c2333;
min-height: 260px; min-height: 260px;
} }
@@ -861,9 +862,9 @@ onMounted(() => {
justify-content: space-between; justify-content: space-between;
gap: 12px; gap: 12px;
padding: 14px 16px; padding: 14px 16px;
border-bottom: 1px solid #2a2a2a; border-bottom: 1px solid #2e3a52;
font-size: 14px; font-size: 14px;
color: #ddd; color: #c8d2e2;
} }
@media (max-width: 1100px) { @media (max-width: 1100px) {
@@ -875,7 +876,7 @@ onMounted(() => {
.left-panel { .left-panel {
width: 100%; width: 100%;
border-right: none; border-right: none;
border-bottom: 1px solid #2a2a2a; border-bottom: 1px solid #2e3a52;
} }
.right-panel { .right-panel {
@@ -56,6 +56,7 @@ type ActiveNavKey =
| 'withdraw' | 'withdraw'
| 'collect-data' | 'collect-data'
| 'image-video' | 'image-video'
| 'amazon-console'
type NavItem = { type NavItem = {
key: string key: string
@@ -94,9 +95,9 @@ const navGroups: ReadonlyArray<NavGroup> = [
label: '前端工具', label: '前端工具',
items: [ items: [
{ key: 'collect-data', label: '采集数据', href: '/new_web_source/collect-data.html' }, { key: 'collect-data', label: '采集数据', href: '/new_web_source/collect-data.html' },
{ key: 'variant-collection', label: 'ASIN变体采集', href: '/web_source/variant-collection.html' }, { key: 'variant-collection', label: 'ASIN变体采集', href: '/new_web_source/variant-collection.html' },
// { key: 'image-video', label: '', href: '/new_web_source/image-video.html' }, // { key: 'image-video', label: '', href: '/new_web_source/image-video.html' },
{ key: 'brand', label: '品牌检测', href: '/brand' }, { key: 'brand', label: '品牌检测', href: '/new_web_source/brand.html' },
{ key: 'appearance-patent', label: '外观专利检测', href: '/new_web_source/appearance-patent.html' }, { key: 'appearance-patent', label: '外观专利检测', href: '/new_web_source/appearance-patent.html' },
{ key: 'similar-asin', label: '货源查询', href: '/new_web_source/similar-asin.html' }, { key: 'similar-asin', label: '货源查询', href: '/new_web_source/similar-asin.html' },
{ key: 'dedupe', label: '数据去重', href: '/new_web_source/dedupe.html' }, { key: 'dedupe', label: '数据去重', href: '/new_web_source/dedupe.html' },
@@ -0,0 +1,688 @@
<template>
<div class="page-shell module-page">
<AmazonToolPageShell tool-id="variant">
<div class="main-content">
<aside class="left-panel">
<div class="section-title">输入方式</div>
<div class="input-tabs">
<button
type="button"
class="input-tab"
:class="{ active: inputMode === 'asin' }"
@click="inputMode = 'asin'"
>粘贴 ASIN</button>
<button
type="button"
class="input-tab"
:class="{ active: inputMode === 'file' }"
@click="inputMode = 'file'"
>Excel 文件</button>
</div>
<div v-if="inputMode === 'asin'" class="input-panel">
<div class="hint">每行一个 ASIN支持批量粘贴</div>
<textarea
v-model="asinText"
class="asin-textarea"
rows="9"
placeholder="每行一个 ASIN,例如:&#10;B0xxxxxxxx"
></textarea>
<div class="asin-count">已输入 <b>{{ asinLines.length }}</b> ASIN</div>
</div>
<div v-else class="input-panel">
<div class="hint">选择含 ASIN Excel需含表头行或多个文件</div>
<div class="btns">
<button type="button" class="opt-btn" :disabled="!hasBridge" @click="selectFiles">
选择 Excel
</button>
</div>
<div class="selected-files">
<template v-if="selectedFiles.length">
<span v-for="file in selectedFiles" :key="file" :title="file">{{ baseName(file) }}</span>
</template>
<span v-else>暂未选择文件</span>
</div>
</div>
<div class="section-title">采集国家可多选</div>
<div class="country-group">
<label
v-for="country in COUNTRY_OPTIONS"
:key="country.code"
class="country-chip"
:class="{ selected: selectedCountries.includes(country.code) }"
>
<input
type="checkbox"
:value="country.code"
v-model="selectedCountries"
/>
<span>{{ country.label }}{{ country.code }}</span>
</label>
</div>
<p class="country-hint" :class="{ error: selectedCountries.length === 0 }">
{{ selectedCountries.length === 0 ? '至少选择一个国家' : `已选 ${selectedCountries.length} 个国家` }}
</p>
<div v-if="!hasBridge" class="bridge-tip">
变体采集在本机客户端执行当前浏览器环境无桌面桥接仅可预览页面
</div>
<div class="run-row">
<button
type="button"
class="btn-run"
:disabled="!hasBridge || submitting || !canSubmit"
@click="submitTask"
>
{{ submitting ? '提交中...' : '开始采集(添加到任务队列)' }}
</button>
</div>
<p class="loading-msg">
任务在远程批处理队列执行提交后到右侧查看进度并下载结果文件
</p>
</aside>
<section class="right-panel">
<div class="panel-header">
<span>变体采集任务</span>
<div class="panel-actions">
<button type="button" class="btn-refresh" @click="refreshList">刷新任务状态</button>
</div>
</div>
<div class="task-list-wrap">
<div v-if="!currentTasks.length" class="empty-tasks">暂无变体采集任务</div>
<ul v-else class="task-list">
<li v-for="item in currentTasks" :key="`vc-${item.task_id}`" class="task-item">
<div class="left">
<span class="id">任务 {{ item.task_id }}</span>
<div v-if="taskMeta(item).source" class="files">来源{{ taskMeta(item).source }}</div>
<div class="files">采集国家{{ countryText(item) || '-' }}</div>
<div class="files">更新时间{{ item.update_time || formatDateTime(item.updated_at) }}</div>
<div v-if="latestMessage(item)" class="files">{{ latestMessage(item) }}</div>
</div>
<div class="task-right">
<span class="status" :class="statusClass(item.status)">{{ statusText(item.status) }}</span>
<button type="button" class="act-btn" @click="refreshTask(item)">更新</button>
<button type="button" class="act-btn" @click="showDetail(item)">详情</button>
<button
v-if="hasResultFiles(item)"
type="button"
class="act-btn ok"
@click="downloadResult(item)"
>下载结果</button>
<button
v-if="hasSourceFile(item)"
type="button"
class="act-btn"
@click="downloadSource(item)"
>下载源文件</button>
<button type="button" class="act-btn warn" @click="reexportTask(item)">重新导出</button>
</div>
</li>
</ul>
<div v-if="currentTasks.length" class="pagination">
<span class="page-info"> {{ totalCount }} · {{ currentPage }} / {{ totalPages }} </span>
<div class="page-btns">
<button type="button" class="page-btn" :disabled="currentPage <= 1" @click="goPage(currentPage - 1)">上一页</button>
<button type="button" class="page-btn" :disabled="currentPage >= totalPages" @click="goPage(currentPage + 1)">下一页</button>
</div>
</div>
</div>
</section>
</div>
</AmazonToolPageShell>
<el-dialog v-model="detailVisible" title="任务详情" width="620px">
<div v-if="detailItem" class="detail-body">
<div class="kv-row"><span class="kv-key">任务 ID</span><span class="kv-val">{{ detailItem.task_id }}</span></div>
<div class="kv-row"><span class="kv-key">状态</span><span class="kv-val">{{ statusText(detailItem.status) }}</span></div>
<div class="kv-row"><span class="kv-key">采集国家</span><span class="kv-val">{{ countryText(detailItem) || '-' }}</span></div>
<div class="kv-row"><span class="kv-key">更新时间</span><span class="kv-val">{{ detailItem.update_time || formatDateTime(detailItem.updated_at) }}</span></div>
<div class="kv-row"><span class="kv-key">进度信息</span><span class="kv-val">{{ latestMessage(detailItem) || '-' }}</span></div>
<template v-if="resultFiles(detailItem).length">
<div class="kv-section">结果文件</div>
<div v-for="(url, index) in resultFiles(detailItem)" :key="index" class="kv-row">
<span class="kv-key">文件 {{ index + 1 }}</span>
<a class="kv-val link" :href="url" target="_blank" rel="noopener">{{ url }}</a>
</div>
</template>
<template v-if="detailItem.task_data?.file_url">
<div class="kv-section">源文件</div>
<div class="kv-row">
<span class="kv-key">源文件</span>
<a class="kv-val link" :href="detailItem.task_data.file_url" target="_blank" rel="noopener">{{ detailItem.task_data.file_url }}</a>
</div>
</template>
</div>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, reactive, ref } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import AmazonToolPageShell from '@/pages/amazon/components/AmazonToolPageShell.vue'
import {
getPywebviewApi,
type VariantTaskListItem,
} from '@/shared/bridges/pywebview'
import { saveUrlWithProgress } from '@/shared/utils/download-progress'
/** 本地任务记录:桥接返回的列表项 + 前端维护字段 */
type VariantEntry = VariantTaskListItem & {
updated_at?: number
message?: string
meta?: { source?: string; execute_country?: string[] }
}
const COUNTRY_OPTIONS = [
{ code: 'DE', label: '德国' },
{ code: 'UK', label: '英国' },
{ code: 'FR', label: '法国' },
{ code: 'IT', label: '意大利' },
{ code: 'ES', label: '西班牙' },
] as const
const PAGE_SIZE = 8
const inputMode = ref<'asin' | 'file'>('asin')
const asinText = ref('')
const selectedFiles = ref<string[]>([])
const selectedCountries = ref<string[]>([])
const submitting = ref(false)
const tasks = reactive<Record<string, VariantEntry>>({})
const currentTaskIds = ref<string[]>([])
const currentPage = ref(1)
const totalPages = ref(1)
const totalCount = ref(0)
const isLoading = ref(false)
const detailVisible = ref(false)
const detailItem = ref<VariantEntry | null>(null)
const hasBridge = computed(() => Boolean(getPywebviewApi()?.vc_start_collect))
const asinLines = computed(() => asinText.value.split(/\r?\n/).map((s) => s.trim()).filter(Boolean))
const currentTasks = computed(() =>
currentTaskIds.value.map((id) => tasks[id]).filter((item): item is VariantEntry => Boolean(item)),
)
const canSubmit = computed(() =>
inputMode.value === 'asin' ? asinLines.value.length > 0 : selectedFiles.value.length > 0,
)
let autoTimer: number | null = null
let refreshTimer: number | null = null
function uid() {
const raw = typeof window === 'undefined' ? '' : window.localStorage.getItem('uid') || ''
return raw.trim()
}
function baseName(path: string) {
return path.split(/[\\/]/).pop() || path
}
function statusText(status?: number | string) {
const numeric = Number(status)
if (numeric === 0 || String(status).toLowerCase() === 'pending' || String(status).toLowerCase() === 'queued') return '排队中'
if (numeric === 1 || String(status).toLowerCase() === 'running' || String(status).toLowerCase() === 'processing') return '执行中'
if (numeric === 2 || ['success', 'completed', 'done'].includes(String(status).toLowerCase())) return '已完成'
if (numeric === 3 || ['failed', 'error'].includes(String(status).toLowerCase())) return '失败'
if (['cancelled', 'stopped'].includes(String(status).toLowerCase())) return '已取消'
if (status === 0 || status === 1 || status === 2 || status === 3) return statusText(String(status))
return String(status ?? '未知')
}
function statusClass(status?: number | string) {
const numeric = Number(status)
if (numeric === 1 || String(status).toLowerCase() === 'running') return 'running'
if (numeric === 2 || ['success', 'completed', 'done'].includes(String(status).toLowerCase())) return 'success'
if (numeric === 3 || ['failed', 'error'].includes(String(status).toLowerCase())) return 'failed'
if (['cancelled', 'stopped'].includes(String(status).toLowerCase())) return 'cancelled'
return 'pending'
}
function isBusy(status?: number | string) {
const numeric = Number(status)
return numeric === 0 || numeric === 1 || String(status).toLowerCase() === 'pending' || String(status).toLowerCase() === 'running'
}
function toArray(value?: string[] | string) {
if (!value) return []
return Array.isArray(value) ? value.filter(Boolean) : [value]
}
function countriesOf(item?: VariantEntry) {
return item?.meta?.execute_country || item?.task_data?.execute_country || []
}
function countryText(item?: VariantEntry) {
return countriesOf(item).join('、') || '-'
}
function latestMessage(item: VariantEntry) {
const raw = item.res_mes || item.message
return (raw || '').trim() || '等待后台处理…'
}
function taskMeta(item: VariantEntry) {
return { source: item.meta?.source || '' }
}
function resultFiles(item?: VariantEntry) {
return toArray(item?.file_url)
}
function hasResultFiles(item: VariantTaskListItem) {
const resType = Number(item.res_type)
return (resType === 1 || String(item.res_type) === '1') && resultFiles(item).length > 0
}
function hasSourceFile(item: VariantTaskListItem) {
return Boolean(item.task_data?.file_url)
}
function formatDateTime(value?: number | string) {
if (!value) return '-'
const ms = typeof value === 'number' ? value : Number(new Date(String(value)))
if (!Number.isFinite(ms)) return String(value)
const date = new Date(ms)
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
const seconds = String(date.getSeconds()).padStart(2, '0')
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
}
async function selectFiles() {
const bridge = getPywebviewApi()
if (!bridge?.vc_select_input_files) {
ElMessage.warning('当前环境不支持文件选择,请在本机客户端中打开')
return
}
try {
const files = await bridge.vc_select_input_files()
selectedFiles.value = files || []
if (selectedFiles.value.length) {
ElMessage.success(`已选择 ${selectedFiles.value.length} 个文件`)
}
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : '选择文件失败')
}
}
function registerTask(taskId: string, meta?: { source?: string; execute_country?: string[] }) {
if (!taskId) return
if (!tasks[taskId]) {
tasks[taskId] = { task_id: taskId, status: 0, updated_at: Date.now(), meta: meta || {} }
} else if (meta) {
tasks[taskId].meta = { ...tasks[taskId].meta, ...meta }
}
}
async function submitTask() {
if (!hasBridge.value) {
ElMessage.warning('请在桌面客户端中打开本页面执行变体采集')
return
}
if (!selectedCountries.value.length) {
ElMessage.warning('请至少选择一个采集国家')
return
}
if (!uid()) {
ElMessage.warning('未获取到用户信息,请重新登录后再试')
return
}
const bridge = getPywebviewApi()!
const params: {
mode: 'asin' | 'file'
countries: string[]
uid: string
asins?: string[]
files?: string[]
} = {
mode: inputMode.value,
countries: selectedCountries.value,
uid: uid(),
}
if (inputMode.value === 'asin') {
params.asins = asinLines.value
} else {
params.files = selectedFiles.value
}
submitting.value = true
try {
const res = await bridge.vc_start_collect!(params)
if (res?.success) {
const added = res.tasks || []
ElMessage.success(`已添加 ${added.length} 个任务`)
added.forEach((task) => {
registerTask(task.task_id, { source: task.source, execute_country: task.execute_country })
})
if (inputMode.value === 'asin') {
asinText.value = ''
} else {
selectedFiles.value = []
}
await loadTasks(1)
} else {
throw new Error(res?.error || '添加任务失败')
}
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : '添加任务失败')
} finally {
submitting.value = false
}
}
async function refreshTask(item: VariantTaskListItem) {
const bridge = getPywebviewApi()
if (!bridge?.vc_query_task) return
try {
const res = await bridge.vc_query_task(item.task_id)
if (!res?.success) {
ElMessage.error(res?.error || '查询失败')
return
}
const entry = tasks[item.task_id] || { task_id: item.task_id, meta: {} }
if (res.status !== undefined && res.status !== null) {
entry.status = res.status
}
const data = res.data
if (data && typeof data === 'object') {
entry.message = (data as { message?: string }).message || (data as { msg?: string }).msg || ''
if (data.res_mes) {
entry.res_mes = data.res_mes
entry.message = data.res_mes
}
if (data.res_type !== undefined) entry.res_type = data.res_type
if (data.file_url) entry.file_url = data.file_url
if (data.task_data) entry.task_data = data.task_data
} else if (typeof data === 'string') {
entry.message = data
}
entry.updated_at = Date.now()
tasks[item.task_id] = entry
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : '查询失败')
}
}
async function loadTasks(page?: number) {
const bridge = getPywebviewApi()
if (!bridge?.vc_list_tasks) return
if (isLoading.value) return
if (!uid()) return
isLoading.value = true
try {
const targetPage = page || currentPage.value || 1
const res = await bridge.vc_list_tasks({ uid: uid(), page: targetPage, page_size: PAGE_SIZE })
if (!res?.success) return
currentPage.value = res.page || targetPage || 1
totalPages.value = Math.max(1, res.total_page || 1)
totalCount.value = res.total || (res.tasks || []).length
const ids: string[] = []
;(res.tasks || []).forEach((item) => {
if (!item.task_id) return
registerTask(item.task_id, {})
const entry = tasks[item.task_id]
entry.status = item.status
if (item.update_time) entry.update_time = item.update_time
entry.res_type = item.res_type
entry.res_mes = item.res_mes
entry.file_url = item.file_url
entry.task_data = item.task_data
if (item.res_mes) entry.message = item.res_mes
entry.updated_at = Date.now()
ids.push(item.task_id)
})
currentTaskIds.value = ids
} catch {
/* 静默:列表加载失败时保留现状 */
} finally {
isLoading.value = false
}
}
function goPage(page: number) {
if (page < 1 || page > totalPages.value) return
void loadTasks(page)
}
function refreshList() {
ElMessage.info('正在刷新任务状态…')
void loadTasks(currentPage.value || 1)
}
async function downloadResult(item: VariantTaskListItem) {
const bridge = getPywebviewApi()
const urls = resultFiles(item)
if (!bridge?.vc_download_files_as_zip) {
ElMessage.warning('当前环境不支持下载,请在桌面客户端中打开')
return
}
if (!urls.length) {
ElMessage.warning('没有可下载的结果文件')
return
}
try {
const downloadId = `res_${item.task_id}_${Date.now()}`
const res = await bridge.vc_download_files_as_zip({
urls,
zip_name: `variant_collection_${item.task_id}.zip`,
download_id: downloadId,
})
if (res?.success) {
ElMessage.success('结果已打包保存')
} else {
throw new Error(res?.error || '下载失败')
}
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : '下载失败')
}
}
async function downloadSource(item: VariantTaskListItem) {
const srcUrl = item.task_data?.file_url
if (!srcUrl) {
ElMessage.warning('没有可下载的源文件')
return
}
const name = srcUrl.split(/[\\/?]/).filter(Boolean).pop() || 'source.xlsx'
const result = await saveUrlWithProgress(srcUrl, name, `src_${item.task_id}_${Date.now()}`)
if (result.success) {
ElMessage.success(`源文件已保存:${result.path || name}`)
} else if (result.error && result.error !== '用户取消') {
ElMessage.error(result.error)
}
}
async function reexportTask(item: VariantTaskListItem) {
const bridge = getPywebviewApi()
if (!bridge?.vc_export_task) {
ElMessage.warning('当前环境不支持导出,请在桌面客户端中打开')
return
}
const numeric = Number(item.status)
if (numeric === 1) {
try {
await ElMessageBox.confirm('任务正在执行中,导出会终止任务,是否继续导出?', '重新导出', { type: 'warning' })
} catch {
return
}
}
try {
const startRes = await bridge.vc_export_task(item.task_id)
if (!startRes?.success || !startRes.file_id) {
throw new Error(startRes?.error || '发起导出失败')
}
ElMessage.info('已发起导出,正在轮询结果…')
const result = await pollExport(bridge, startRes.file_id)
if (result && Number(result.status) === 2 && result.file_url) {
const name = result.file_name || result.file_url.split(/[\\/?]/).filter(Boolean).pop() || 'export.xlsx'
const dlRes = await saveUrlWithProgress(result.file_url, name, `exp_${startRes.file_id}`)
if (dlRes.success) {
ElMessage.success(`导出文件已保存:${dlRes.path || name}`)
} else if (dlRes.error && dlRes.error !== '用户取消') {
ElMessage.error(dlRes.error)
}
void loadTasks(currentPage.value || 1)
} else if (result && Number(result.status) === 3) {
ElMessage.error(result.error || '后台导出失败')
} else {
ElMessage.warning('导出超时或状态未知,请稍后重试')
}
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : '导出失败')
}
}
function pollExport(bridge: NonNullable<ReturnType<typeof getPywebviewApi>>, fileId: string) {
return new Promise<{ status?: number | string; file_url?: string; file_name?: string; error?: string } | null>((resolve) => {
let count = 0
const maxAttempts = 60
const tick = async () => {
count += 1
try {
const res = await bridge.vc_query_export!(fileId)
if (res?.success) {
const st = Number(res.status)
if (st === 2 || st === 3) {
resolve(res)
return
}
}
} catch {
/* 网络异常继续轮询 */
}
if (count >= maxAttempts) {
resolve(null)
return
}
autoTimer = window.setTimeout(tick, 2000)
}
void tick()
})
}
function showDetail(item: VariantTaskListItem) {
detailItem.value = { ...item, task_data: item.task_data ? { ...item.task_data } : undefined }
detailVisible.value = true
}
function scheduleAutoRefresh() {
if (autoTimer) {
window.clearTimeout(autoTimer)
autoTimer = null
}
autoTimer = window.setTimeout(async () => {
await loadTasks(currentPage.value || 1)
if (currentTasks.value.some((item) => isBusy(item.status))) {
scheduleAutoRefresh()
}
}, 3000)
}
function startPolling() {
void loadTasks(1)
refreshTimer = window.setInterval(() => {
void loadTasks(currentPage.value || 1)
}, 60 * 1000)
}
function cleanup() {
if (autoTimer) {
window.clearTimeout(autoTimer)
autoTimer = null
}
if (refreshTimer) {
window.clearInterval(refreshTimer)
refreshTimer = null
}
}
onMounted(() => {
startPolling()
scheduleAutoRefresh()
})
onBeforeUnmount(() => {
cleanup()
})
</script>
<style scoped>
.module-page { min-height: 100vh; background: #151a25; }
.main-content { display: flex; height: calc(100vh - 56px); min-height: calc(100vh - 56px); }
.left-panel { width: 420px; background: #1c2333; padding: 20px; overflow-y: auto; border-right: 1px solid #2e3a52; }
.right-panel { flex: 1; min-width: 0; background: #151a25; display: flex; flex-direction: column; }
.section-title { font-size: 13px; color: #a0acbe; margin: 14px 0 10px; }
.hint, .loading-msg, .files, .country-hint { color: #5e6878; font-size: 12px; line-height: 1.5; }
.hint { margin-bottom: 8px; }
.input-tabs { display: flex; gap: 6px; margin-bottom: 12px; }
.input-tab { flex: 1; padding: 9px 0; border: 1px solid #3e4a62; border-radius: 8px; background: #2e3a52; color: #a0acbe; font-size: 13px; cursor: pointer; }
.input-tab.active { background: rgba(64, 158, 255, 0.15); color: #409eff; border-color: #409eff; }
.input-panel { margin-bottom: 4px; }
.asin-textarea { width: 100%; box-sizing: border-box; min-height: 150px; padding: 10px; background: #151a25; color: #c8d2e2; border: 1px solid #3e4a62; border-radius: 8px; font-size: 13px; line-height: 1.6; resize: vertical; outline: none; }
.asin-textarea:focus { border-color: #409eff; }
.asin-count { margin-top: 8px; color: #5e6878; font-size: 12px; }
.asin-count b { color: #c8d2e2; }
.btns, .run-row { display: flex; gap: 10px; flex-wrap: wrap; }
.opt-btn { padding: 8px 14px; color: #c8d2e2; background: #2e3a52; border: 1px solid #3e4a62; border-radius: 7px; cursor: pointer; font-size: 12px; }
.opt-btn:hover:not(:disabled) { color: #3498db; border-color: #3498db; }
.opt-btn:disabled { opacity: 0.5; cursor: not-allowed; }
.selected-files { margin-top: 10px; color: #a0acbe; font-size: 12px; word-break: break-all; }
.selected-files span { display: block; margin: 3px 0; }
.country-group { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; }
.country-chip { display: inline-flex; align-items: center; gap: 7px; padding: 9px 11px; border: 1px solid #333; border-radius: 8px; background: #2e3a52; color: #c8d2e2; font-size: 12px; cursor: pointer; user-select: none; }
.country-chip.selected { border-color: #409eff; background: rgba(64, 158, 255, 0.12); color: #e6f2ff; }
.country-chip input { width: 15px; height: 15px; accent-color: #409eff; cursor: pointer; }
.country-hint { margin: 10px 0 4px; }
.country-hint.error { color: #e6a23c; }
.country-hint b { color: #c8d2e2; }
.bridge-tip { margin: 12px 0; padding: 9px 12px; border-radius: 8px; background: rgba(230, 162, 60, 0.1); color: #e6a23c; font-size: 11px; line-height: 1.6; }
.btn-run { padding: 10px 18px; color: #f5f8fc; background: #27ae60; border: none; border-radius: 8px; font-weight: 600; cursor: pointer; }
.btn-run:disabled { opacity: 0.5; cursor: not-allowed; }
.loading-msg { margin-top: 10px; }
.panel-header { display: flex; justify-content: space-between; align-items: center; padding: 14px 20px; border-bottom: 1px solid #2e3a52; color: #f5f8fc; font-size: 15px; font-weight: 600; }
.panel-actions { display: flex; gap: 8px; }
.btn-refresh { padding: 6px 12px; color: #c8d2e2; background: #2e3a52; border: 1px solid #3e4a62; border-radius: 6px; cursor: pointer; font-size: 12px; }
.task-list-wrap { flex: 1; padding: 16px 20px; overflow: auto; }
.empty-tasks { color: #5e6878; font-size: 13px; padding: 28px; text-align: center; }
.task-list { list-style: none; margin: 0; padding: 0; }
.task-item { display: flex; justify-content: space-between; align-items: flex-start; gap: 12px; padding: 12px 14px; margin-bottom: 8px; border: 1px solid #2e3a52; border-radius: 8px; background: #1c2333; }
.left { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 3px; }
.id { color: #f5f8fc; font-size: 13px; font-weight: 600; }
.task-right { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; justify-content: flex-end; }
.status { padding: 4px 10px; border-radius: 6px; font-size: 12px; white-space: nowrap; }
.status.pending { background: rgba(149, 165, 166, 0.18); color: #a0acbe; }
.status.running { background: rgba(52, 152, 219, 0.18); color: #3498db; }
.status.success { background: rgba(46, 204, 113, 0.18); color: #2ecc71; }
.status.failed { background: rgba(231, 76, 60, 0.18); color: #ff6b6b; }
.status.cancelled { background: rgba(149, 165, 166, 0.18); color: #a0acbe; }
.act-btn { padding: 4px 9px; border-radius: 6px; font-size: 12px; background: #2e3a52; color: #c8d2e2; border: 1px solid #3e4a62; cursor: pointer; }
.act-btn.ok { background: rgba(52, 152, 219, 0.18); color: #69b6ff; border-color: transparent; }
.act-btn.warn { background: rgba(230, 162, 60, 0.14); color: #e6a23c; border-color: transparent; }
.pagination { display: flex; justify-content: space-between; align-items: center; padding: 10px 0 4px; }
.page-info { color: #5e6878; font-size: 12px; }
.page-btns { display: flex; gap: 8px; }
.page-btn { padding: 5px 12px; border-radius: 6px; background: #2e3a52; color: #c8d2e2; border: 1px solid #3e4a62; cursor: pointer; font-size: 12px; }
.page-btn:disabled { opacity: 0.4; cursor: not-allowed; }
.detail-body { max-height: 60vh; overflow: auto; }
.kv-row { display: flex; gap: 12px; padding: 6px 0; font-size: 13px; }
.kv-key { flex-shrink: 0; width: 110px; color: #5e6878; }
.kv-val { color: #c8d2e2; word-break: break-all; }
.kv-val.link { color: #69b6ff; }
.kv-section { margin: 10px 0 4px; font-size: 12px; color: #a0acbe; border-bottom: 1px solid #333; padding-bottom: 4px; }
@media (max-width: 1100px) {
.main-content { flex-direction: column; height: auto; }
.left-panel { width: 100%; border-right: none; border-bottom: 1px solid #2e3a52; }
}
</style>
@@ -1,6 +1,6 @@
<template> <template>
<div class="page-shell module-page"> <div class="page-shell module-page">
<BrandTopBar active="withdraw" /> <AmazonToolPageShell tool-id="cash">
<div class="main-content"> <div class="main-content">
<aside class="left-panel"> <aside class="left-panel">
@@ -325,13 +325,14 @@
</div> </div>
</section> </section>
</div> </div>
</AmazonToolPageShell>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, onMounted, onUnmounted, ref } from "vue"; import { computed, onMounted, onUnmounted, ref } from "vue";
import { ElMessage } from "element-plus"; import { ElMessage } from "element-plus";
import BrandTopBar from "@/pages/brand/components/BrandTopBar.vue"; import AmazonToolPageShell from '@/pages/amazon/components/AmazonToolPageShell.vue';
import { import {
addWithdrawCandidate, addWithdrawCandidate,
createWithdrawTask, createWithdrawTask,
@@ -1315,61 +1316,61 @@ onUnmounted(() => {
</script> </script>
<style scoped> <style scoped>
.module-page { min-height: 100vh; background: #1a1a1a; } .module-page { min-height: 100vh; background: #151a25; }
.main-content { display: flex; min-height: calc(100vh - 56px); height: calc(100vh - 56px); } .main-content { display: flex; min-height: calc(100vh - 56px); height: calc(100vh - 56px); }
.left-panel { width: 400px; background: #1e1e1e; padding: 20px; overflow-y: auto; border-right: 1px solid #2a2a2a; } .left-panel { width: 400px; background: #1c2333; padding: 20px; overflow-y: auto; border-right: 1px solid #2e3a52; }
.right-panel { flex: 1; display: flex; flex-direction: column; min-width: 0; background: #1a1a1a; } .right-panel { flex: 1; display: flex; flex-direction: column; min-width: 0; background: #151a25; }
.section-title { font-size: 13px; color: #bbb; margin-bottom: 10px; } .section-title { font-size: 13px; color: #a0acbe; margin-bottom: 10px; }
.input-zone { border: 1px dashed #3a3a3a; border-radius: 10px; padding: 16px; background: #252525; margin-bottom: 20px; } .input-zone { border: 1px dashed #3e4a62; border-radius: 10px; padding: 16px; background: #2e3a52; margin-bottom: 20px; }
.hint { color: #888; font-size: 12px; margin-bottom: 12px; line-height: 1.5; text-align: left; } .hint { color: #5e6878; font-size: 12px; margin-bottom: 12px; line-height: 1.5; text-align: left; }
.input-row { display: flex; gap: 10px; align-items: center; } .input-row { display: flex; gap: 10px; align-items: center; }
.input-row :deep(.el-input) { flex: 1; } .input-row :deep(.el-input) { flex: 1; }
.filter-row { display: flex; align-items: center; gap: 10px; margin-top: 12px; } .filter-row { display: flex; align-items: center; gap: 10px; margin-top: 12px; }
.filter-label { color: #aaa; font-size: 12px; white-space: nowrap; } .filter-label { color: #a0acbe; font-size: 12px; white-space: nowrap; }
.reserved-input { width: 180px; } .reserved-input { width: 180px; }
.opt-btn { padding: 8px 12px; font-size: 12px; color: #ccc; background: #2a2a2a; border: 1px solid #3a3a3a; border-radius: 6px; cursor: pointer; white-space: nowrap; } .opt-btn { padding: 8px 12px; font-size: 12px; color: #c8d2e2; background: #2e3a52; border: 1px solid #3e4a62; border-radius: 6px; cursor: pointer; white-space: nowrap; }
.opt-btn:hover:not(:disabled) { color: #3498db; border-color: #3498db; } .opt-btn:hover:not(:disabled) { color: #3498db; border-color: #3498db; }
.opt-btn:disabled { opacity: 0.6; cursor: not-allowed; } .opt-btn:disabled { opacity: 0.6; cursor: not-allowed; }
.empty-candidates { color: #666; font-size: 13px; padding: 16px; border: 1px dashed #333; border-radius: 8px; margin-bottom: 16px; } .empty-candidates { color: #5e6878; font-size: 13px; padding: 16px; border: 1px dashed #333; border-radius: 8px; margin-bottom: 16px; }
.candidate-table-scroll { max-height: 320px; overflow: auto; margin-bottom: 18px; border-radius: 8px; border: 1px solid #2a2a2a; } .candidate-table-scroll { max-height: 320px; overflow: auto; margin-bottom: 18px; border-radius: 8px; border: 1px solid #2e3a52; }
.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; } .candidate-table { --el-table-bg-color: #2e3a52; --el-table-tr-bg-color: #2e3a52; --el-table-header-bg-color: #2e3a52; --el-table-text-color: #c8d2e2; --el-table-border-color: #333; }
.link-danger { background: none; border: none; color: #e74c3c; cursor: pointer; font-size: 12px; padding: 0; } .link-danger { background: none; border: none; color: #e74c3c; cursor: pointer; font-size: 12px; padding: 0; }
.link-danger:hover { text-decoration: underline; } .link-danger:hover { text-decoration: underline; }
.run-row { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 8px; } .run-row { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 8px; }
.btn-run { padding: 10px 18px; font-size: 14px; font-weight: 600; color: #fff; background: #3498db; border: none; border-radius: 8px; cursor: pointer; } .btn-run { padding: 10px 18px; font-size: 14px; font-weight: 600; color: #f5f8fc; background: #3498db; border: none; border-radius: 8px; cursor: pointer; }
.btn-run:hover:not(:disabled) { background: #2980b9; } .btn-run:hover:not(:disabled) { background: #2980b9; }
.btn-run:disabled { opacity: 0.55; cursor: not-allowed; } .btn-run:disabled { opacity: 0.55; cursor: not-allowed; }
.btn-queue { background: #27ae60; } .btn-queue { background: #27ae60; }
.btn-queue:hover:not(:disabled) { background: #219a52; } .btn-queue:hover:not(:disabled) { background: #219a52; }
.loading-msg { margin-top: 12px; font-size: 12px; color: #777; line-height: 1.5; } .loading-msg { margin-top: 12px; font-size: 12px; color: #5e6878; line-height: 1.5; }
.queue-debug-card { margin-top: 18px; padding: 14px; border: 1px solid #2a2a2a; border-radius: 10px; background: #202020; } .queue-debug-card { margin-top: 18px; padding: 14px; border: 1px solid #2e3a52; border-radius: 10px; background: #222b3d; }
.queue-debug-title { margin-bottom: 8px; } .queue-debug-title { margin-bottom: 8px; }
.queue-debug-line { color: #b8c1cc; font-size: 12px; line-height: 1.6; margin-bottom: 8px; } .queue-debug-line { color: #a0acbe; font-size: 12px; line-height: 1.6; margin-bottom: 8px; }
.queue-debug-payload { margin: 0; max-height: 220px; overflow: auto; padding: 10px; border-radius: 8px; background: #141414; color: #8fd3ff; font-size: 11px; line-height: 1.5; white-space: pre-wrap; word-break: break-all; } .queue-debug-payload { margin: 0; max-height: 220px; overflow: auto; padding: 10px; border-radius: 8px; background: #10141f; color: #8fd3ff; font-size: 11px; line-height: 1.5; white-space: pre-wrap; word-break: break-all; }
.panel-header { padding: 16px 20px; font-size: 15px; font-weight: 600; color: #e0e0e0; border-bottom: 1px solid #2a2a2a; } .panel-header { padding: 16px 20px; font-size: 15px; font-weight: 600; color: #f5f8fc; border-bottom: 1px solid #2e3a52; }
.task-list-wrap { flex: 1; padding: 16px 20px; overflow: auto; } .task-list-wrap { flex: 1; padding: 16px 20px; overflow: auto; }
.clean-result-summary { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 12px; margin-bottom: 16px; } .clean-result-summary { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 12px; margin-bottom: 16px; }
.summary-card { padding: 14px 16px; border: 1px solid #2a2a2a; border-radius: 10px; background: #1e1e1e; } .summary-card { padding: 14px 16px; border: 1px solid #2e3a52; border-radius: 10px; background: #1c2333; }
.summary-card strong { display: block; margin-top: 8px; font-size: 22px; color: #eaf4ff; } .summary-card strong { display: block; margin-top: 8px; font-size: 22px; color: #f5f8fc; }
.summary-label { font-size: 12px; color: #8d8d8d; } .summary-label { font-size: 12px; color: #5e6878; }
.subsection-title { font-size: 13px; color: #999; margin: 8px 0 10px; } .subsection-title { font-size: 13px; color: #a0acbe; margin: 8px 0 10px; }
.empty-tasks { color: #666; font-size: 13px; padding: 16px; text-align: center; } .empty-tasks { color: #5e6878; font-size: 13px; padding: 16px; text-align: center; }
.empty-tasks.narrow { padding: 12px 8px; } .empty-tasks.narrow { padding: 12px 8px; }
.match-table { margin-bottom: 18px; } .match-table { margin-bottom: 18px; }
.match-table :deep(.el-table__body tr:hover > td.el-table__cell), .match-table :deep(.el-table__body tr.hover-row > td.el-table__cell), .match-table :deep(.el-table__body tr.current-row > td.el-table__cell) { background-color: var(--el-table-tr-bg-color, #222) !important; } .match-table :deep(.el-table__body tr:hover > td.el-table__cell), .match-table :deep(.el-table__body tr.hover-row > td.el-table__cell), .match-table :deep(.el-table__body tr.current-row > td.el-table__cell) { background-color: var(--el-table-tr-bg-color, #222) !important; }
.result-table { --el-table-bg-color: #222; --el-table-tr-bg-color: #222; --el-table-header-bg-color: #2a2a2a; --el-table-text-color: #ccc; --el-table-border-color: #333; } .result-table { --el-table-bg-color: #222; --el-table-tr-bg-color: #222; --el-table-header-bg-color: #2e3a52; --el-table-text-color: #c8d2e2; --el-table-border-color: #333; }
.ok { color: #27ae60; } .ok { color: #27ae60; }
.fail { color: #e67e22; } .fail { color: #e67e22; }
.result-list-wrap { border: 1px solid #2a2a2a; border-radius: 10px; background: #1e1e1e; min-height: 220px; margin-top: 8px; } .result-list-wrap { border: 1px solid #2e3a52; border-radius: 10px; background: #1c2333; min-height: 220px; margin-top: 8px; }
.result-list-header { padding: 12px 16px; border-bottom: 1px solid #2a2a2a; font-size: 14px; color: #ddd; } .result-list-header { padding: 12px 16px; border-bottom: 1px solid #2e3a52; font-size: 14px; color: #c8d2e2; }
.result-subsection { padding: 12px 12px 4px; } .result-subsection { padding: 12px 12px 4px; }
.result-subsection-title { font-size: 12px; color: #888; margin-bottom: 8px; } .result-subsection-title { font-size: 12px; color: #5e6878; margin-bottom: 8px; }
.task-list { list-style: none; margin: 0; padding: 0; } .task-list { list-style: none; margin: 0; padding: 0; }
.task-item { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; padding: 12px 14px; border: 1px solid #2a2a2a; border-radius: 8px; margin-bottom: 8px; background: #222; } .task-item { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; padding: 12px 14px; border: 1px solid #2e3a52; border-radius: 8px; margin-bottom: 8px; background: #222; }
.left { flex: 1; min-width: 0; } .left { flex: 1; min-width: 0; }
.split-result-main { display: flex; flex-direction: column; gap: 4px; } .split-result-main { display: flex; flex-direction: column; gap: 4px; }
.id { font-weight: 600; color: #e0e0e0; font-size: 13px; } .id { font-weight: 600; color: #f5f8fc; font-size: 13px; }
.files { font-size: 12px; color: #888; } .files { font-size: 12px; color: #5e6878; }
.task-right { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; justify-content: flex-end; } .task-right { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; justify-content: flex-end; }
.status { padding: 4px 10px; border-radius: 6px; font-size: 12px; white-space: nowrap; } .status { padding: 4px 10px; border-radius: 6px; font-size: 12px; white-space: nowrap; }
.status.success { background: rgba(46, 204, 113, 0.18); color: #2ecc71; } .status.success { background: rgba(46, 204, 113, 0.18); color: #2ecc71; }
@@ -1381,7 +1382,7 @@ onUnmounted(() => {
.btn-delete:hover { background: rgba(231, 76, 60, 0.22); } .btn-delete:hover { background: rgba(231, 76, 60, 0.22); }
@media (max-width: 1100px) { @media (max-width: 1100px) {
.main-content { flex-direction: column; height: auto; } .main-content { flex-direction: column; height: auto; }
.left-panel { width: 100%; border-right: none; border-bottom: 1px solid #2a2a2a; } .left-panel { width: 100%; border-right: none; border-bottom: 1px solid #2e3a52; }
.clean-result-summary { grid-template-columns: repeat(2, minmax(0, 1fr)); } .clean-result-summary { grid-template-columns: repeat(2, minmax(0, 1fr)); }
} }
</style> </style>
@@ -0,0 +1,471 @@
<template>
<div class="home-root" :class="{ 'nav-permissions-loading': !permissionReady }">
<header class="header">
<span class="header-title">数富AI</span>
<div class="header-right">
<button
type="button"
class="header-update-btn"
title="检测更新"
@click="toggleUpdatePanel"
></button>
<div class="header-update-panel" v-show="updatePanel">
<div class="header-update-title"><span></span> 软件更新</div>
<div class="update-block">
<span class="update-version-text">当前版本: {{ versionText }}</span>
<button type="button" class="btn-check-update" @click="checkUpdate">
<span></span> 检测
</button>
</div>
<div class="update-hint">{{ updateHint }}</div>
<div style="margin-top: 4px;">
<button type="button" class="btn-download-update" v-if="updateReady" @click="doUpdate">
立即更新
</button>
</div>
</div>
<span class="username-text">{{ username || '未登录' }}</span>
<a href="/logout" class="logout-link">退出</a>
</div>
</header>
<div class="entrances">
<a
v-if="canShow.amazon"
:href="amazonHref || undefined"
class="entrance-btn"
:class="{ disabled: !amazonHref }"
data-column-key="brand"
>亚马逊</a>
<a
v-else-if="showAmazonDisabled"
class="entrance-btn disabled"
data-column-key="brand"
>亚马逊</a>
<a
v-if="canShow.video"
:href="videoHref || undefined"
class="entrance-btn wb"
:class="{ disabled: !videoHref }"
data-column-key="wb"
>视频</a>
<a
v-else-if="showVideoDisabled"
class="entrance-btn wb disabled"
data-column-key="wb"
>视频</a>
<a
v-if="canShow.image"
:href="imageHref || undefined"
class="entrance-btn image"
:class="{ disabled: !imageHref }"
data-column-key="image"
>图片</a>
<a
v-else-if="showImageDisabled"
class="entrance-btn image disabled"
data-column-key="image"
>图片</a>
</div>
<div class="toast" :class="{ show: Boolean(toastText) }">{{ toastText }}</div>
</div>
</template>
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue'
import { requestGetJson } from '@/shared/api/http'
import { buildJavaUrl } from '@/shared/api/url'
import { resolvePageHref } from '@/shared/page-prefix'
const username = ref('')
const versionText = ref('—')
const updatePanel = ref(false)
const updateHint = ref('')
const updateReady = ref(false)
const updateFileUrl = ref('')
const toastText = ref('')
type PermissionState = { amazon: boolean; video: boolean; image: boolean }
const permissionState = ref<PermissionState>({ amazon: false, video: false, image: false })
const permissionUnavailable = ref(false)
const permissionReady = ref(false)
const amazonHref = computed(() => resolvePageHref('/new_web_source/amazon-console.html'))
const videoHref = computed(() => resolvePageHref('/new_web_source/image-video.html'))
const imageHref = computed(() => resolvePageHref('/image'))
const canShow = computed(() =>
permissionUnavailable.value ? { amazon: true, video: true, image: true } : permissionState.value,
)
// dev /image
const showAmazonDisabled = computed(() => !permissionUnavailable.value && permissionState.value.amazon && !amazonHref.value)
const showVideoDisabled = computed(() => !permissionUnavailable.value && permissionState.value.video && !videoHref.value)
const showImageDisabled = computed(() => !permissionUnavailable.value && permissionState.value.image && !imageHref.value)
function getUid() {
const raw = typeof window === 'undefined' ? '' : window.localStorage.getItem('uid') || ''
const numeric = Number(raw.trim())
return Number.isFinite(numeric) && numeric > 0 ? String(numeric) : ''
}
function computePermissionState(items: Array<Record<string, unknown>>): PermissionState {
const allowedKeys: string[] = []
const allowedColumnKeys: string[] = []
const allowedRootKeys: string[] = []
let hasRootMetadata = false
const push = (list: string[], value: unknown) => {
const key = String(value || '').trim().toLowerCase()
if (key && list.indexOf(key) < 0) list.push(key)
}
;(items || []).forEach((item) => {
push(allowedColumnKeys, item.columnKey || item.column_key)
const root = item.rootColumnKey || item.root_column_key
if (root) {
hasRootMetadata = true
push(allowedRootKeys, root)
}
;[item.columnKey, item.column_key, item.routePath, item.route_path].forEach((v) => push(allowedKeys, v))
})
const videoVisible = allowedKeys.indexOf('wb') >= 0
const imageVisible = allowedKeys.indexOf('image') >= 0
let amazonVisible =
allowedRootKeys.indexOf('brand_front_tools') >= 0 ||
allowedRootKeys.indexOf('brand_operation_tools') >= 0 ||
allowedRootKeys.indexOf('brand_logistics_tools') >= 0
if (!hasRootMetadata) {
amazonVisible = allowedColumnKeys.some((key) => key !== 'wb' && key !== 'image')
}
return { amazon: amazonVisible, video: videoVisible, image: imageVisible }
}
async function loadPermissions() {
const currentUid = getUid()
if (!currentUid) {
permissionUnavailable.value = true
permissionReady.value = true
return
}
try {
const resp = await requestGetJson<{ success: boolean; data?: Array<Record<string, unknown>> }>(
buildJavaUrl(`/api/admin/permission-users/${currentUid}/column-permissions?menuType=app`),
)
if (resp?.success && Array.isArray(resp.data)) {
permissionState.value = computePermissionState(resp.data)
try {
window.localStorage.setItem(`app_column_permissions:${currentUid}`, JSON.stringify(resp.data))
} catch {
/* 忽略 */
}
} else {
permissionUnavailable.value = true
}
} catch {
permissionUnavailable.value = true
}
permissionReady.value = true
}
async function loadCurrentUser() {
try {
const localName = typeof window === 'undefined' ? '' : window.localStorage.getItem('username') || ''
if (localName) username.value = localName
const resp = await window.fetch('/newApi/check_login', { credentials: 'include' })
const body = await resp.json().catch(() => ({}))
if (body?.success && body.data) {
if (body.data.username) username.value = body.data.username
if (body.data.userId) {
try {
window.localStorage.setItem('uid', String(body.data.userId))
} catch {
/* 忽略 */
}
}
}
} catch {
/* 保留本地用户名 */
}
}
function toggleUpdatePanel() {
updatePanel.value = !updatePanel.value
}
function showTip(message: string) {
toastText.value = message
window.setTimeout(() => {
if (toastText.value === message) toastText.value = ''
}, 2600)
}
async function fetchVersion() {
try {
const resp = await window.fetch('/api/version', { credentials: 'same-origin' })
const data = await resp.json().catch(() => ({}))
const current = String((data && data.version) || '').replace(/^v/i, '')
if (current) versionText.value = current || '—'
return data || {}
} catch {
return {}
}
}
async function checkUpdate() {
updateHint.value = '正在检测更新...'
updateReady.value = false
try {
const data = await fetchVersion()
if (data.has_update && data.latest_version) {
updateHint.value =
`发现新版本 v${String(data.latest_version).replace(/^v/i, '')}` +
(data.desc ? `${data.desc}` : '')
if (data.file_url) {
updateReady.value = true
updateFileUrl.value = data.file_url
} else {
updateHint.value = '暂无下载地址,请关注官方渠道。'
}
} else {
updateHint.value = '已是最新版本'
}
} catch {
updateHint.value = '检测更新失败'
}
}
async function doUpdate() {
if (!updateFileUrl.value) return
if (!window.confirm('有更新,是否现在更新?\n更新将下载安装包并重启程序。')) return
updateHint.value = '正在下载并准备更新,程序将自动退出...'
updateReady.value = false
try {
const resp = await window.fetch('/api/update/do', {
method: 'POST',
credentials: 'same-origin',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ file_url: updateFileUrl.value }),
})
const result = await resp.json().catch(() => ({}))
updateHint.value = result.success ? '更新已启动,程序即将退出...' : (result.error || '更新启动失败')
} catch {
updateHint.value = '请求更新失败,请重试'
}
}
onMounted(() => {
void loadCurrentUser()
void loadPermissions()
void fetchVersion().catch(() => undefined)
})
</script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: "Microsoft YaHei", "PingFang SC", "Hiragino Sans GB", sans-serif;
background: linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.5)),
url("/static/bg.jpg") center/cover no-repeat;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px;
}
.header {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 56px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 24px;
background: rgba(255, 255, 255, 0.5);
}
.header-title {
font-size: 18px;
font-weight: 600;
color: #333;
}
.header-right {
display: flex;
align-items: center;
gap: 16px;
position: relative;
}
.header-right a,
.header-right span {
font-size: 14px;
color: #555;
text-decoration: none;
}
.header-right a:hover {
color: #667eea;
}
.logout-link {
cursor: pointer;
}
.header-update-btn {
width: 28px;
height: 28px;
border-radius: 50%;
border: none;
background: rgba(102, 126, 234, 0.12);
color: #4c5bd4;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 15px;
padding: 0;
}
.header-update-btn:hover {
background: rgba(102, 126, 234, 0.2);
}
.header-update-panel {
position: absolute;
top: 44px;
right: 0;
width: 280px;
padding: 14px 16px;
background: rgba(255, 255, 255, 0.98);
border-radius: 10px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
border: 1px solid rgba(0, 0, 0, 0.04);
z-index: 10;
}
.header-update-title {
font-size: 13px;
color: #333;
margin-bottom: 8px;
display: flex;
align-items: center;
gap: 6px;
}
.update-block {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
}
.update-version-text {
font-size: 13px;
color: #555;
}
.btn-check-update {
padding: 6px 12px;
font-size: 13px;
background: #667eea;
color: #fff;
border: none;
border-radius: 8px;
cursor: pointer;
}
.btn-check-update:hover {
background: #5a6fd6;
}
.btn-download-update {
margin-top: 8px;
padding: 6px 12px;
font-size: 13px;
background: #28a745;
color: #fff;
border: none;
border-radius: 8px;
cursor: pointer;
}
.btn-download-update:hover {
background: #218838;
}
.update-hint {
font-size: 12px;
color: #666;
margin-top: 8px;
min-height: 18px;
}
.home-root.nav-permissions-loading .entrance-btn {
visibility: hidden;
}
.entrances {
display: flex;
gap: 32px;
flex-wrap: wrap;
justify-content: center;
}
.entrance-btn {
width: 160px;
height: 100px;
background: #c5c1c1;
border: 4px solid #b8d4e3;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
font-weight: 600;
color: #333;
cursor: pointer;
transition: all 0.2s;
text-decoration: none;
}
.entrance-btn:hover {
background: #f8fbfd;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
}
.entrance-btn.disabled {
cursor: not-allowed;
opacity: 0.9;
color: #888;
}
.toast {
position: fixed;
bottom: 40px;
left: 50%;
transform: translateX(-50%);
background: rgba(0, 0, 0, 0.75);
color: #fff;
padding: 12px 24px;
border-radius: 8px;
font-size: 14px;
opacity: 0;
transition: opacity 0.3s;
pointer-events: none;
z-index: 20;
}
.toast.show {
opacity: 1;
}
</style>
@@ -0,0 +1,366 @@
<template>
<div class="legacy-login-root">
<header class="header">
<span class="header-title">数富AI</span>
</header>
<div class="login-box">
<h1 class="login-title">登录</h1>
<p v-if="errorMessage" class="error-msg">{{ errorMessage }}</p>
<form id="loginForm" @submit.prevent="submitLogin">
<div class="form-group">
<label for="username">用户名</label>
<input id="username" v-model="username" type="text" placeholder="请输入用户名" required autofocus />
</div>
<div class="form-group">
<label for="password">密码</label>
<div class="password-field">
<input
id="password"
v-model="password"
:type="passwordVisible ? 'text' : 'password'"
placeholder="请输入密码"
required
/>
<button
type="button"
class="password-toggle"
:class="{ 'is-visible': passwordVisible }"
:aria-pressed="passwordVisible"
:aria-label="passwordVisible ? '隐藏密码' : '显示密码'"
@click="togglePassword"
>
<svg class="icon-eye" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0"></path>
<circle cx="12" cy="12" r="3"></circle>
</svg>
<svg class="icon-eye-off" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="m2 2 20 20"></path>
<path d="M10.733 5.076a10.744 10.744 0 0 1 9.205 6.272 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-1.651 2.59"></path>
<path d="M14.084 14.158a3 3 0 0 1-4.242-4.242"></path>
<path d="M17.479 17.499a10.75 10.75 0 0 1-5.479 1.501c-4.478 0-8.268-2.943-9.876-6.652a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 2.646-3.393"></path>
</svg>
</button>
</div>
</div>
<button type="submit" class="btn-login" id="btnLogin" :disabled="loggingIn">
{{ loggingIn ? '登录中...' : '登录' }}
</button>
</form>
</div>
</div>
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue'
const AUTH_TOKEN_KEY = 'aiimage_auth_token'
const DEVICE_ID_KEY = 'aiimage_device_id'
const username = ref('')
const password = ref('')
const passwordVisible = ref(false)
const loggingIn = ref(false)
const errorMessage = ref('')
function togglePassword() {
passwordVisible.value = !passwordVisible.value
}
function isDesktopClient() {
return Boolean(window.pywebview?.api) || window.location.pathname === '/login'
}
function clearAppPermissionCaches() {
try {
const keys: string[] = []
for (let i = 0; i < window.localStorage.length; i += 1) {
const key = window.localStorage.key(i)
if (key && key.indexOf('app_column_permissions:') === 0) keys.push(key)
}
keys.forEach((key) => window.localStorage.removeItem(key))
} catch {
/* 忽略 localStorage 异常 */
}
}
function makeBrowserDeviceId() {
let deviceId = ''
try {
deviceId = window.localStorage.getItem(DEVICE_ID_KEY) || ''
} catch {
deviceId = ''
}
if (!deviceId) {
deviceId = `web-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 12)}`
try {
window.localStorage.setItem(DEVICE_ID_KEY, deviceId)
} catch {
/* 忽略 */
}
}
return deviceId
}
async function fetchDeviceId(): Promise<string> {
if (window.pywebview?.api?.get_device_id) {
try {
const res = await window.pywebview.api.get_device_id()
if (res?.success && res.device_id) {
try {
window.localStorage.setItem(DEVICE_ID_KEY, res.device_id)
} catch {
/* 忽略 */
}
return res.device_id
}
} catch {
/* 回退浏览器生成 */
}
}
return makeBrowserDeviceId()
}
async function submitLogin() {
if (loggingIn.value) return
const account = username.value.trim()
if (!account || !password.value) {
errorMessage.value = '请输入账号和密码'
return
}
loggingIn.value = true
errorMessage.value = ''
try {
const deviceId = await fetchDeviceId()
if (!deviceId) {
errorMessage.value = '未获取到设备ID,请在桌面端打开'
return
}
const loginResp = await window.fetch('/newApi/login', {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
'X-Device-Id': deviceId,
'X-Requested-With': 'XMLHttpRequest',
},
body: JSON.stringify({ username: account, password: password.value, deviceId }),
})
const loginBody = await loginResp.json().catch(() => ({}))
if (!loginBody || loginBody.success !== true || !loginBody.data) {
errorMessage.value = (loginBody && (loginBody.message || loginBody.msg)) || '登录失败'
return
}
const data = loginBody.data || {}
if (data.token) {
try {
window.localStorage.setItem(AUTH_TOKEN_KEY, data.token)
} catch {
/* 忽略 */
}
}
if (data.userId) {
try {
window.localStorage.setItem('uid', String(data.userId))
} catch {
/* 忽略 */
}
}
if (data.username) {
try {
window.localStorage.setItem('username', String(data.username))
} catch {
/* 忽略 */
}
}
// Java JWT Python cookie
try {
await window.fetch('/api/auth/sync', {
method: 'POST',
credentials: 'same-origin',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token: data.token }),
})
} catch {
/* cookie 同步失败不阻塞跳转(页面仍可读 localStorage */
}
clearAppPermissionCaches()
window.location.href = isDesktopClient() ? '/home' : '/home.html'
} catch {
errorMessage.value = '网络异常,请稍后重试'
} finally {
loggingIn.value = false
}
}
onMounted(() => {
const params = new URLSearchParams(window.location.search || '')
if (params.get('logout') === '1' || params.get('switch') === '1') {
try {
window.localStorage.removeItem(AUTH_TOKEN_KEY)
} catch {
/* 忽略 */
}
}
})
</script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: "Microsoft YaHei", "PingFang SC", "Hiragino Sans GB", sans-serif;
background: #d8e4ec;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px;
}
.header {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 56px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 24px;
background: rgba(255, 255, 255, 0.5);
}
.header-title {
font-size: 18px;
font-weight: 600;
color: #333;
}
.login-box {
background: #fff;
border: 1px solid #b8d4e3;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.06);
padding: 40px;
width: 100%;
max-width: 380px;
}
.login-title {
font-size: 24px;
font-weight: 600;
color: #333;
text-align: center;
margin-bottom: 30px;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
font-size: 14px;
color: #555;
margin-bottom: 8px;
}
.form-group input {
width: 100%;
padding: 12px 14px;
font-size: 14px;
border: 1px solid #b8d4e3;
border-radius: 8px;
outline: none;
transition: border-color 0.2s;
background: #fff;
}
.form-group input:focus {
border-color: #3498db;
}
.password-field {
position: relative;
}
.password-field input {
padding-right: 48px;
}
.password-toggle {
position: absolute;
top: 50%;
right: 10px;
display: inline-flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
padding: 0;
color: #71808d;
background: transparent;
border: 0;
border-radius: 6px;
cursor: pointer;
transform: translateY(-50%);
}
.password-toggle:hover {
color: #3498db;
background: #f1f7fb;
}
.password-toggle svg {
width: 18px;
height: 18px;
pointer-events: none;
}
.password-toggle .icon-eye-off {
display: none;
}
.password-toggle.is-visible .icon-eye {
display: none;
}
.password-toggle.is-visible .icon-eye-off {
display: block;
}
.error-msg {
color: #e74c3c;
font-size: 13px;
margin-bottom: 12px;
text-align: center;
}
.btn-login {
width: 100%;
padding: 14px;
font-size: 16px;
font-weight: 500;
color: #fff;
background: linear-gradient(135deg, #3498db 0%, #2980b9 100%);
border: none;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
}
.btn-login:hover:not(:disabled) {
opacity: 0.9;
}
.btn-login:disabled {
opacity: 0.6;
cursor: not-allowed;
}
</style>
@@ -32,6 +32,7 @@ export interface BrandTaskItem {
created_at?: string created_at?: string
progress_total?: number progress_total?: number
progress_current?: number progress_current?: number
error_message?: string
result_paths?: BrandTaskResultPaths result_paths?: BrandTaskResultPaths
} }
@@ -8,6 +8,35 @@ export interface UploadedJavaFile {
export type ProxyMode = 1 | 2; export type ProxyMode = 1 | 2;
/** ASIN 变体采集任务项(/batch/list 返回元素,后端数值状态 0排队/1执行/2成功/3失败) */
export interface VariantTaskListItem {
task_id: string;
status?: number | string;
task_type?: string;
update_time?: string;
res_type?: number | string;
res_mes?: string;
file_url?: string[] | string;
task_data?: {
file_url?: string;
execute_country?: string[];
proxy_url?: string;
proxy_mode?: number | string;
[key: string]: unknown;
};
}
/** ASIN 变体采集单任务查询 data 段 */
export interface VariantTaskQueryData {
message?: string;
msg?: string;
res_mes?: string;
res_type?: number | string;
file_url?: string[] | string;
task_data?: VariantTaskListItem['task_data'];
[key: string]: unknown;
}
export interface DesktopConfig { export interface DesktopConfig {
proxy_url?: string; proxy_url?: string;
proxy_mode?: number | string; proxy_mode?: number | string;
@@ -58,6 +87,10 @@ export interface PywebviewApi {
data?: UploadedJavaFile; data?: UploadedJavaFile;
error?: string; error?: string;
}>; }>;
save_file_from_url?: (
url: string,
filename: string,
) => Promise<{ success: boolean; path?: string; error?: string }>;
save_file_from_url_new?: ( save_file_from_url_new?: (
url: string, url: string,
filename: string, filename: string,
@@ -86,8 +119,59 @@ export interface PywebviewApi {
enqueue_json?: ( enqueue_json?: (
data: unknown, data: unknown,
) => Promise<{ success: boolean; queue_size?: number; error?: string }>; ) => Promise<{ success: boolean; queue_size?: number; error?: string }>;
// —— ASIN 变体采集(VariantCollectionAPIvc_* 前缀)——
vc_select_input_files?: () => Promise<string[]>;
vc_start_collect?: (params: {
mode: 'asin' | 'file';
files?: string[];
asins?: string[];
countries: string[];
uid: string;
}) => Promise<{
success: boolean;
tasks?: Array<{ task_id: string; source?: string; execute_country?: string[] }>;
error?: string;
}>;
vc_query_task?: (
taskId: string,
) => Promise<{
success: boolean;
status?: number | string;
data?: VariantTaskQueryData;
error?: string;
}>;
vc_list_tasks?: (query: {
uid: string;
page: number;
page_size: number;
}) => Promise<{
success: boolean;
page?: number;
total_page?: number;
total?: number;
tasks?: VariantTaskListItem[];
error?: string;
}>;
vc_export_task?: (
taskId: string,
) => Promise<{ success: boolean; file_id?: string; error?: string }>;
vc_query_export?: (
fileId: string,
) => Promise<{
success: boolean;
status?: number | string;
file_url?: string;
file_name?: string;
error?: string;
}>;
vc_download_files_as_zip?: (params: {
urls: string[];
zip_name: string;
download_id: string;
}) => Promise<{ success: boolean; path?: string; error?: string }>;
read_config?: () => Promise<DesktopConfig>; read_config?: () => Promise<DesktopConfig>;
save_config?: (data: DesktopConfigUpdate) => Promise<DesktopConfig>; save_config?: (data: DesktopConfigUpdate) => Promise<DesktopConfig>;
get_device_id?: () => Promise<{ success: boolean; device_id?: string; error?: string }>;
} }
declare global { declare global {
+20
View File
@@ -0,0 +1,20 @@
/**
*
*
* Flask /new_web_source/
* vite dev /brand/web_source/*
*/
/** 当前是否运行在桌面客户端生产环境(页面路径以 /new_web_source/ 开头) */
export function isDesktopClientPage(): boolean {
return typeof window !== 'undefined' && window.location.pathname.startsWith('/new_web_source')
}
/** 链接本地化:dev 预览去掉 /new_web_source 前缀;/brand、/web_source/* 等旧静态页仅桌面端可用 */
export function resolvePageHref(rawHref?: string): string {
if (!rawHref) return ''
if (rawHref.startsWith('/new_web_source/')) {
return `${isDesktopClientPage() ? '/new_web_source' : ''}${rawHref.slice('/new_web_source'.length)}`
}
return isDesktopClientPage() ? rawHref : ''
}
+7 -1
View File
@@ -2,7 +2,7 @@
@import './reset.css'; @import './reset.css';
body { body {
background: #1a1a1a; background: #151a25;
font-family: var(--font-family-base); font-family: var(--font-family-base);
} }
@@ -97,6 +97,12 @@ html {
overscroll-behavior: contain; overscroll-behavior: contain;
scrollbar-gutter: stable; scrollbar-gutter: stable;
} }
/* 亚马逊工具台外壳:旧功能页中间区(.main-content)由外壳撑满剩余高度 */
.amazon-tool-shell > .tool-shell-body > .main-content {
height: 100%;
min-height: 0;
}
} }
.theme-light { .theme-light {
@@ -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 BrandVariantTab from '@/pages/brand/components/BrandVariantTab.vue'
dayjs.locale('zh-cn')
createApp(BrandVariantTab).use(ElementPlus, { locale: zhCn }).mount('#app')
@@ -0,0 +1,90 @@
import assert from 'node:assert/strict'
import test from 'node:test'
import {
filterGroupsByPermission,
filterWorkflowByPermission,
TOOL_DETAILS,
TOOL_GROUPS,
TOOL_ICON_GRADIENTS,
TOOL_ICON_SHAPES,
TOOL_MAP,
} from '../src/pages/amazon/tool-catalog.ts'
test('工具目录:20 个工具 id 唯一且每个都有图标与渐变配色', () => {
const ids = TOOL_GROUPS.flatMap((g) => g.tools.map((t) => t.id))
assert.equal(ids.length, 20, '工具总数应为 20(前端8 + 运营10 + 后勤2')
assert.equal(new Set(ids).size, ids.length, '工具 id 不应重复')
for (const id of ids) {
assert.ok(TOOL_ICON_SHAPES[id], `图标形状缺失: ${id}`)
assert.ok(TOOL_ICON_GRADIENTS[id], `图标渐变缺失: ${id}`)
assert.equal(TOOL_MAP.get(id)?.id, id, `TOOL_MAP 应覆盖: ${id}`)
}
})
test('工具目录:无页面的工具应在 catalog 里标记为未开通(无 href', () => {
const pending = TOOL_GROUPS.flatMap((g) => g.tools).filter((t) => !t.href)
const pendingIds = pending.map((t) => t.id).sort()
assert.deepEqual(pendingIds, ['erp', 'purchase', 'shop'], '只有 店铺状态查询/采购/ERP 未开通')
})
test('权限过滤:组键命中整组保留,未命中按工具权限过滤并隐藏空组', () => {
const allowedAll = ['brand_front_tools', 'brand_operation_tools', 'brand_logistics_tools']
const all = filterGroupsByPermission(TOOL_GROUPS, allowedAll)
assert.equal(all.length, 3)
assert.ok(all[0].tools.every((t) => t.permissionKeys.length > 0))
// 只给前端组键:仅前端工具整组可见
const frontOnly = filterGroupsByPermission(TOOL_GROUPS, ['brand_front_tools'])
assert.equal(frontOnly.length, 1)
assert.equal(frontOnly[0].key, 'front')
assert.equal(frontOnly[0].tools.length, 8)
// 无组键时按单个工具权限过滤
const partial = filterGroupsByPermission(TOOL_GROUPS, ['collect-data', 'publish'])
const groups = new Map(partial.map((g) => [g.key, g]))
assert.deepEqual(groups.get('front')?.tools.map((t) => t.id), ['collect'])
assert.deepEqual(groups.get('ops')?.tools.map((t) => t.id), ['list'])
assert.equal(groups.has('logi'), false, '后勤无可见工具应整体隐藏')
// 无任何权限:全部隐藏
assert.equal(filterGroupsByPermission(TOOL_GROUPS, []).length, 0)
})
test('权限过滤:工作流时间线按工具权限截断', () => {
const front = TOOL_GROUPS.find((g) => g.key === 'front')
assert.ok(front?.workflow?.length === 8)
// 只允许 采集数据,时间线只剩它一步
const steps = filterWorkflowByPermission(front?.workflow ?? [], ['collect-data'])
assert.equal(steps.length, 1)
assert.equal(steps[0].name, '采集数据')
// 无权限 → 空
assert.equal(filterWorkflowByPermission(front?.workflow ?? [], []).length, 0)
})
test('工具目录:页面链接与现有 BrandTopBar 导航映射一致', () => {
const expectHref = new Map<string, string>([
['collect', '/new_web_source/collect-data.html'],
['dedupe', '/new_web_source/dedupe.html'],
['source', '/new_web_source/similar-asin.html'],
['brand', '/brand'],
['list', '/new_web_source/publish.html'],
['follow', '/new_web_source/price-track.html'],
['scrape', '/new_web_source/shop-data-crawl.html'],
['cash', '/new_web_source/withdraw.html'],
])
for (const [id, href] of expectHref) {
assert.equal(TOOL_MAP.get(id)?.href, href, `工具 ${id} 应指向 ${href}`)
}
})
test('工具目录:页头长描述(detail)只覆盖已知工具,且不为空', () => {
for (const [id, detail] of Object.entries(TOOL_DETAILS)) {
assert.ok(TOOL_MAP.has(id), `TOOL_DETAILS 引用了未知工具: ${id}`)
assert.ok(detail.trim().length > 0, `工具 ${id} 的 detail 不应为空`)
const toolsWithHref = TOOL_MAP.get(id)
assert.ok(toolsWithHref?.href, `detail 只应用于有页面的工具: ${id}`)
}
})
+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>ASIN变体采集 - 数富AI</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/variant-collection-main.ts"></script>
</body>
</html>
+16
View File
@@ -34,6 +34,17 @@ export default defineConfig({
changeOrigin: true, changeOrigin: true,
rewrite: (path) => path.replace(/^\/newApi/, ''), rewrite: (path) => path.replace(/^\/newApi/, ''),
}, },
// /login、/check_login、/logout 等无 /api 前缀的 Java 接口(登录/用户信息)
'/newApi/login': {
target: process.env.VITE_API_TARGET || 'http://127.0.0.1:18080/',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/newApi/, ''),
},
'/newApi/check_login': {
target: process.env.VITE_API_TARGET || 'http://127.0.0.1:18080/',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/newApi/, ''),
},
}, },
}, },
build: { build: {
@@ -57,7 +68,12 @@ export default defineConfig({
'shop-data-crawl': resolve(__dirname, 'shop-data-crawl.html'), 'shop-data-crawl': resolve(__dirname, 'shop-data-crawl.html'),
withdraw: resolve(__dirname, 'withdraw.html'), withdraw: resolve(__dirname, 'withdraw.html'),
'collect-data': resolve(__dirname, 'collect-data.html'), 'collect-data': resolve(__dirname, 'collect-data.html'),
'variant-collection': resolve(__dirname, 'variant-collection.html'),
brand: resolve(__dirname, 'brand.html'),
'image-video': resolve(__dirname, 'image-video.html'), 'image-video': resolve(__dirname, 'image-video.html'),
'amazon-console': resolve(__dirname, 'amazon-console.html'),
home: resolve(__dirname, 'home.html'),
login: resolve(__dirname, 'login.html'),
}, },
output: { output: {
entryFileNames: (chunkInfo) => entryFileNames: (chunkInfo) =>