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:
@@ -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>
|
||||
Reference in New Issue
Block a user