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
@@ -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>