Files
crawler-plugin/frontend-vue/src/pages/amazon/AmazonConsolePage.vue
T
huangzd1997 e248b6e43a feat(web): 前端 SPA 化 + 工具页任务面板统一与历史批量删除
- SPA 化:22 个 MPA html 入口与 *-main.ts 合并为 index.html + vue-router(URL 无 .html 后缀),
  页面跳转全部 router-link,/new_web_source/xxx.html 旧路径归一为 /xxx
- 任务面板统一:共享 TaskCenterPanel/TaskItemCard/TaskStatCards/HistoryTaskLayer,
  16 个工具页右侧统一为统计卡 + 当前任务 + 历史任务弹层(任务ID/开始/结束/状态必展示)
- 历史记录支持单条删除 + 批量勾选删除(确认框/全选/失败提示)
- Java 7 模块(dedupe/convert/split/productrisk/shopmatch/pricetrack/deletebrand)
  history 接口补齐任务时间字段(VO+Service,复用 biz_file_task 列)
- 图片工作台/API 层(brand/permission/user)既有未提交改动一并提交
2026-09-08 10:02:55 +08:00

765 lines
19 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div
class="page-shell module-page amazon-console"
:style="{ '--scroll-color': currentGroup?.color ?? '#2dd4bf' }"
>
<AmazonTopBar :active-cat="activeCat" @change="onCatChange" />
<main class="console-body">
<!-- Hero仅前端工具分类显示对齐 gemini-code 主页 -->
<section v-if="currentGroup && currentGroup.key === 'front'" class="hero">
<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>
<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>
</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>
</div>
<div class="hero-card-btn-row">
<span class="hero-card-sub">每一步流程清晰可查</span>
<button class="hero-dl" type="button" @click="onDownloadClick">
<span>📥</span>
<span>立即下载教程</span>
</button>
</div>
</div>
</section>
<!-- 当前分类标准工作流 + 卡片墙分类切换式 gemini-code -->
<section v-if="currentGroup" class="tool-group" :id="`group-${currentGroup.key}`">
<section v-if="visibleWorkflow(currentGroup)?.length" class="workflow-panel">
<div class="workflow-head">
<span class="workflow-title">{{ currentGroup.workflowTitle }}</span>
<span class="workflow-badge">{{ currentGroup.workflowBadge }}</span>
<span class="workflow-hint">{{ currentGroup.workflowHint }}</span>
</div>
<ol class="workflow-steps">
<template v-for="(step, index) in visibleWorkflow(currentGroup)" :key="`${currentGroup.key}-${index}`">
<li
class="workflow-step"
:class="{ 'workflow-step--disabled': !step.toolId || !stepHref(step.toolId) }"
:style="{ '--gc': currentGroup.color }"
@click="onWorkflowStepClick(step)"
>
<span class="step-circle" :style="circleStyle(currentGroup)">{{ index + 1 }}</span>
<span class="step-name">{{ step.name }}</span>
<span class="step-desc">{{ step.desc }}</span>
</li>
<span v-if="index < visibleWorkflow(currentGroup).length - 1" class="step-line" :style="lineStyle()"></span>
</template>
</ol>
</section>
<header class="group-head">
<i class="group-dot" :style="{ background: currentGroup.color }"></i>
<span class="group-name">{{ currentGroup.name }}</span>
<span class="group-desc">{{ currentGroup.desc }}</span>
<span class="group-count">{{ currentGroup.tools.length }} 个工具</span>
</header>
<div class="card-grid">
<template v-for="tool in currentGroup.tools" :key="tool.id">
<router-link v-if="resolveHref(tool.href)" :to="resolveHref(tool.href)!" class="tool-card" :style="cardStyle(currentGroup)">
<div class="tool-card-top">
<ToolIcon :name="tool.id" />
<span v-if="tool.tag" class="tool-tag" :style="tagStyle(currentGroup)">{{ 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>
</router-link>
<button v-else type="button" class="tool-card tool-card--disabled" :style="cardStyle(currentGroup)" @click="onPendingClick(tool)">
<div class="tool-card-top">
<ToolIcon :name="tool.id" />
<span v-if="tool.tag" class="tool-tag" :style="tagStyle(currentGroup)">{{ 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, onMounted, ref } from 'vue'
import { useRouter } from 'vue-router'
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 { TUTORIAL_DOWNLOAD_URL } from '@/pages/amazon/download-btn'
import { filterGroupsByPermission, filterWorkflowByPermission, TOOL_GROUPS, TOOL_MAP } from '@/pages/amazon/tool-catalog'
import type { ToolGroup, ToolInfo, VisibleGroup, WorkflowStep } from '@/pages/amazon/tool-catalog'
import { resolvePageHref } from '@/shared/page-prefix'
import { getPywebviewApi } from '@/shared/bridges/pywebview'
import { getCurrentUserAppColumnKeys } from '@/shared/api/permission'
const router = useRouter()
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')
const activeCat = ref<'front' | 'ops' | 'logi'>('front')
let statusTimer: number | undefined
const currentGroup = computed(() => visibleGroups.value.find((g) => g.key === activeCat.value) ?? visibleGroups.value[0])
/** 链接归一:/new_web_source/xxx.html → /xxxSPA 路径,见 shared/page-prefix */
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 ?? '')
}
/* 卡片/tag:weak 弱底 + 分类色文字(同 gemini-code,无核心特判) */
function tagStyle(group: ToolGroup) {
return { background: group.weak, color: group.color }
}
function circleStyle(group: ToolGroup) {
return { background: group.weak, color: group.color }
}
function lineStyle() {
return { background: '#4C5A75' }
}
function cardStyle(group: ToolGroup) {
return { '--gc': group.color } as Record<string, string>
}
function onWorkflowStepClick(step: WorkflowStep) {
if (!step.toolId) {
return
}
const target = TOOL_MAP.get(step.toolId)
const href = resolveHref(target?.href ?? '')
if (href) {
router.push(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) {
toast(`${name}暂未开通,敬请期待`, 'normal')
}
function toast(msg: string, type: 'normal' | 'error' = 'normal') {
statusText.value = msg
statusType.value = type
if (statusTimer) {
window.clearTimeout(statusTimer)
}
statusTimer = window.setTimeout(() => {
statusText.value = ''
}, 2500)
}
async function onDownloadClick() {
// 教程客户端压缩包取自 MinIO 公开直链(client/tutorial/,见 download-btn.ts
const api = getPywebviewApi()
if (api?.open_external_url) {
// 桌面端:交系统默认浏览器下载,可看到下载进度
try {
const res = await api.open_external_url(TUTORIAL_DOWNLOAD_URL)
if (!res?.success) toast(`下载失败:${res?.error ?? '请重试'}`, 'error')
} catch (_error) {
toast('下载失败,请重试', 'error')
}
return
}
// 网页端:新窗口打开直链下载
window.open(TUTORIAL_DOWNLOAD_URL, '_blank', 'noopener')
}
function onCatChange(key: 'front' | 'ops' | 'logi') {
activeCat.value = key
history.replaceState(null, '', `#group-${key}`)
}
function parseHashGroup() {
const hash = window.location.hash.replace('#', '')
const m = /^group-(front|ops|logi)$/.exec(hash)
if (m) activeCat.value = m[1] as 'front' | 'ops' | 'logi'
}
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)
parseHashGroup()
})
</script>
<style scoped>
.amazon-console {
display: flex;
flex-direction: column;
min-height: 0;
overflow: hidden;
background-color: #1a1f2e;
/* 深色底 + 青色网点纹理(对齐 gemini-code body */
background-image: radial-gradient(rgba(45, 212, 191, 0.08) 1px, transparent 1px);
background-size: 48px 48px;
font-family: "PingFang SC", "Microsoft YaHei UI", "Microsoft YaHei", "微软雅黑", sans-serif;
}
.console-body {
flex: 1 1 auto;
min-height: 0;
overflow-y: auto;
overscroll-behavior: contain;
padding: 14px 28px 28px;
background: transparent;
/* 滚动条滑块随分类色(保持各分类有独立主色) */
scrollbar-color: var(--scroll-color, #2dd4bf) #1a1f2e;
}
.console-body::-webkit-scrollbar {
width: 10px;
}
.console-body::-webkit-scrollbar-track {
background: #1a1f2e;
}
.console-body::-webkit-scrollbar-thumb {
min-height: 40px;
background: var(--scroll-color, #2dd4bf);
border-radius: 5px;
}
.console-body::-webkit-scrollbar-thumb:hover {
filter: brightness(1.2);
}
.console-body::-webkit-scrollbar-button {
display: none;
}
/* ===== Hero(仅 front 显示;PANEL 底 + LINE 边框 + 12px 圆角,对齐 gemini-code ===== */
.hero {
max-width: 1440px;
margin: 0 auto 20px;
display: flex;
justify-content: space-between;
align-items: center;
gap: 24px;
padding: 26px 30px;
border: 1px solid #3e4a62;
border-radius: 12px;
background: #232a3b;
}
.hero-left {
flex: 1 1 auto;
min-width: 0;
}
.hero-pill {
display: inline-block;
margin-bottom: 12px;
padding: 4px 12px;
border: 1px solid rgba(45, 212, 191, 0.2);
border-radius: 20px;
background: #0f2e28;
color: #2dd4bf;
font-size: 11px;
font-weight: 700;
}
.hero-title {
margin: 0 0 8px;
color: #f5f8fc;
font-size: 26px;
font-weight: 700;
}
.hero-sub {
max-width: 650px;
margin: 0 0 20px;
color: #c8d2e2;
font-size: 12px;
line-height: 1.6;
}
.hero-stats {
display: flex;
gap: 40px;
}
.stat strong {
display: block;
font-size: 22px;
font-weight: 700;
line-height: 1.1;
}
.stat span {
display: block;
margin-top: 4px;
color: #a0acbe;
font-size: 11px;
}
/* Hero 右侧工作台卡(350 宽 / 10px 圆角,对齐 gemini-code hero-right-card */
.hero-card {
width: 350px;
flex-shrink: 0;
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 20px;
border: 1px solid #3e4a62;
border-radius: 10px;
background: #2b3447;
}
.hero-card-head {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 14px;
}
.hero-card-dots {
display: flex;
gap: 5px;
}
.dot {
width: 8px;
height: 8px;
border-radius: 50%;
}
.dot--teal {
background: #2dd4bf;
}
.dot--blue {
background: #60a5fa;
}
.dot--brand {
background: #f59e0b;
}
.hero-card-caption {
color: #a0acbe;
font-size: 11px;
}
.hero-card-title {
margin: 0 0 12px;
color: #f5f8fc;
font-size: 17px;
font-weight: 700;
line-height: 1.4;
}
.hero-card-divider {
height: 1px;
margin-bottom: 12px;
background: #3e4a62;
}
.hero-card-sub {
margin: 0;
color: #c8d2e2;
font-size: 11px;
line-height: 1.5;
}
/* 下载按钮(渐变文字按钮,对齐 gemini-code download-btn;下载教学客户端 zip 直链) */
.hero-card-btn-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.hero-card-btn-row .hero-card-sub {
margin: 0;
}
.hero-dl {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 8px 18px;
border: none;
border-radius: 6px;
background: linear-gradient(135deg, #f59e0b, #d97706);
color: #ffffff;
font-size: 12px;
font-weight: 700;
font-family: inherit;
cursor: pointer;
transition: all 0.2s;
box-shadow: 0 2px 10px rgba(245, 158, 11, 0.4);
}
.hero-dl:hover {
filter: brightness(1.15);
transform: translateY(-1px);
}
/* ===== 标准工作流时间线(PANEL 底 + LINE 边框 + 圆形编号,对齐 gemini-code ===== */
.workflow-panel {
max-width: 1440px;
margin: 0 auto 22px;
padding: 18px 24px;
border: 1px solid #3e4a62;
border-radius: 10px;
background: #232a3b;
}
.workflow-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-bottom: 18px;
}
.workflow-title {
color: #f5f8fc;
font-size: 14px;
font-weight: 700;
}
.workflow-badge {
padding: 2px 8px;
border-radius: 4px;
background: #3a2a10;
color: #f59e0b;
font-size: 10px;
font-weight: 700;
white-space: nowrap;
}
.workflow-hint {
margin-left: auto;
color: #a0acbe;
font-size: 11px;
white-space: nowrap;
}
.workflow-steps {
display: flex;
align-items: center;
justify-content: space-between;
margin: 0;
padding: 0;
list-style: none;
}
.workflow-step {
display: flex;
flex-direction: column;
align-items: center;
min-width: 0;
cursor: pointer;
transition: transform 0.15s;
}
.workflow-step:not(.workflow-step--disabled):hover {
transform: translateY(-2px);
}
.workflow-step--disabled {
cursor: default;
opacity: 0.7;
}
/* 悬停:编号圆填充分类色白字、名称变亮(已确认保留的高亮反馈) */
.workflow-step:not(.workflow-step--disabled):hover .step-circle {
background: var(--gc, #2dd4bf) !important;
color: #ffffff !important;
}
.workflow-step:not(.workflow-step--disabled):hover .step-name {
color: #f5f8fc;
}
.step-circle {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border-radius: 50%;
margin-bottom: 6px;
font-size: 12px;
font-weight: 700;
}
.step-name {
margin-bottom: 2px;
color: #c8d2e2;
font-size: 11px;
font-weight: 700;
text-align: center;
}
.step-desc {
color: #a0acbe;
font-size: 9px;
text-align: center;
}
/* 步骤连接线:圆编号列之间贯穿的横线(垂直居中于编号行,对齐 gemini-code timeline-line */
.step-line {
flex: 1 1 auto;
min-width: 14px;
height: 2px;
margin: 0 4px;
transform: translateY(-16px);
}
/* ===== 分类标题 + 卡片墙 ===== */
.tool-group {
max-width: 1440px;
margin: 0 auto;
}
.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: 16px;
font-weight: 700;
}
.group-desc {
color: #a0acbe;
font-size: 11px;
}
.group-count {
margin-left: auto;
color: #a0acbe;
font-size: 11px;
}
/* 卡片墙(CARD 底 + 2px #2A3344 边框 + 10px 圆角 min-height 165,对齐 gemini-code tool-card */
.card-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 14px;
margin-bottom: 28px;
}
.tool-card {
display: flex;
flex-direction: column;
justify-content: space-between;
min-width: 0;
min-height: 165px;
padding: 16px;
border: 2px solid #2a3344;
border-radius: 10px;
background: #2b3447;
text-align: left;
text-decoration: none;
font: inherit;
cursor: pointer;
transition: all 0.2s;
}
/* 悬浮:边框高亮为分类色 + 背景变亮 + 上浮阴影(对齐 gemini-code hover 并按要求加边框高亮) */
.tool-card:hover {
background: #323d52;
border-color: var(--gc, #2dd4bf);
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.3);
}
.tool-card-top {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 12px;
}
.tool-tag {
padding: 2px 8px;
border-radius: 4px;
font-size: 10px;
font-weight: 700;
white-space: nowrap;
}
/* 悬浮:角标变分类色实底白字(保留的亮反馈) */
.tool-card:hover .tool-tag {
background: var(--gc, #2dd4bf) !important;
color: #ffffff !important;
}
.tool-card-name {
margin-bottom: 6px;
color: #f5f8fc;
font-size: 15px;
font-weight: 700;
}
.tool-card-desc {
flex: 1;
color: #a0acbe;
font-size: 11px;
line-height: 1.4;
}
.tool-card-go {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 12px;
color: #a0acbe;
font-size: 11px;
transition: color 0.2s;
}
.tool-card:hover .tool-card-go {
color: var(--gc, #2dd4bf);
font-weight: 700;
}
.tool-card--disabled {
cursor: default;
opacity: 0.55;
}
.tool-card--disabled:hover {
border-color: #2a3344;
background: #2b3447;
transform: none;
box-shadow: none;
}
.tool-card--disabled:hover .tool-card-go {
color: #a0acbe;
font-weight: 400;
}
/* ===== toast(右下角 + 品牌色左边条,对齐 gemini-code toast ===== */
.toast {
position: fixed;
bottom: 60px;
right: 30px;
z-index: 1000;
max-width: min(420px, calc(100vw - 60px));
padding: 12px 20px;
border-radius: 6px;
border-left: 4px solid #f59e0b;
background: #3d4a63;
color: #f5f8fc;
font-size: 13px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
opacity: 0;
pointer-events: none;
transition: opacity 0.3s;
}
.toast.show {
opacity: 1;
}
.toast.error {
border-left-color: #f87171;
background: rgba(176, 42, 55, 0.92);
}
@media (max-width: 1100px) {
.card-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.hero {
flex-direction: column;
align-items: stretch;
}
.hero-card {
width: 100%;
}
.console-body {
padding: 14px 16px 24px;
}
}
</style>