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)既有未提交改动一并提交
This commit is contained in:
2026-09-08 10:02:55 +08:00
parent abcfa5bef7
commit e248b6e43a
125 changed files with 7482 additions and 4304 deletions
@@ -1,100 +1,107 @@
<template>
<div class="page-shell module-page amazon-console">
<AmazonTopBar />
<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 宣传区 -->
<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>
<!-- 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 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>
<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>
<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>
<!-- 三组工具标题 + 标准工作流 + 卡片墙 -->
<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">
<!-- 当前分类标准工作流 + 卡片墙分类切换式 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">{{ group.workflowTitle }}</span>
<span class="workflow-badge" :style="{ color: group.color }">{{ group.workflowBadge }}</span>
<span class="workflow-hint">{{ group.workflowHint }}</span>
<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">
<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>
<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 group.tools" :key="tool.id">
<a v-if="resolveHref(tool.href)" :href="resolveHref(tool.href)!" class="tool-card">
<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="{ color: group.color }">{{ tool.tag }}</span>
<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>
</a>
</router-link>
<button v-else type="button" class="tool-card tool-card--disabled" @click="onPendingClick(tool)">
<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="{ color: group.color }">{{ tool.tag }}</span>
<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>
@@ -112,25 +119,33 @@
</template>
<script setup lang="ts">
import { computed, nextTick, onMounted, ref } from 'vue'
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 { ToolInfo, VisibleGroup, WorkflowStep } 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
/** 链接本地化:dev 预览去掉 /new_web_source 前缀;/brand、/web_source/* 等旧静态页仅桌面端存在 */
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)
}
@@ -145,6 +160,20 @@ function stepHref(toolId?: string) {
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
@@ -152,7 +181,7 @@ function onWorkflowStepClick(step: WorkflowStep) {
const target = TOOL_MAP.get(step.toolId)
const href = resolveHref(target?.href ?? '')
if (href) {
window.location.href = href
router.push(href)
return
}
showSoon(target?.href ? `${step.name}仅桌面客户端可用` : `${step.name}暂未开通,敬请期待`)
@@ -167,31 +196,46 @@ function onPendingClick(tool: ToolInfo) {
}
function showSoon(name: string) {
statusText.value = `${name}暂未开通,敬请期待`
statusType.value = 'normal'
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 = ''
}, 2200)
}, 2500)
}
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' })
})
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 () => {
@@ -204,7 +248,7 @@ onMounted(async () => {
visibleGroups.value = permissionUnavailable.value
? TOOL_GROUPS.map((group) => ({ ...group, tools: [...group.tools] }))
: filterGroupsByPermission(TOOL_GROUPS, allowedKeys.value)
scrollToHashGroup()
parseHashGroup()
})
</script>
@@ -214,7 +258,11 @@ onMounted(async () => {
flex-direction: column;
min-height: 0;
overflow: hidden;
background: #151a25;
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 {
@@ -222,23 +270,46 @@ onMounted(async () => {
min-height: 0;
overflow-y: auto;
overscroll-behavior: contain;
padding: 22px 34px 44px;
background: #151a25;
padding: 14px 28px 28px;
background: transparent;
/* 滚动条滑块随分类色(保持各分类有独立主色) */
scrollbar-color: var(--scroll-color, #2dd4bf) #1a1f2e;
}
/* ===== Hero ===== */
.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: 1180px;
margin: 0 auto 26px;
border: 1px solid #2e3a52;
border-radius: 16px;
background: linear-gradient(135deg, #1b2233 0%, #151a24 100%);
}
.hero-inner {
max-width: 1440px;
margin: 0 auto 20px;
display: flex;
justify-content: space-between;
align-items: center;
gap: 24px;
padding: 26px 28px;
padding: 26px 30px;
border: 1px solid #3e4a62;
border-radius: 12px;
background: #232a3b;
}
.hero-left {
@@ -248,75 +319,78 @@ onMounted(async () => {
.hero-pill {
display: inline-block;
padding: 5px 12px;
border-radius: 999px;
background: rgba(45, 212, 191, 0.14);
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;
letter-spacing: 0.04em;
}
.hero-title {
margin: 14px 0 0;
margin: 0 0 8px;
color: #f5f8fc;
font-size: 28px;
font-weight: 800;
line-height: 1.3;
font-size: 26px;
font-weight: 700;
}
.hero-sub {
max-width: 620px;
margin: 10px 0 0;
color: #a0acbe;
max-width: 650px;
margin: 0 0 20px;
color: #c8d2e2;
font-size: 12px;
line-height: 1.7;
line-height: 1.6;
}
.hero-stats {
display: flex;
gap: 44px;
margin-top: 22px;
gap: 40px;
}
.stat strong {
display: block;
font-size: 22px;
font-weight: 800;
line-height: 1.2;
font-weight: 700;
line-height: 1.1;
}
.stat span {
display: block;
margin-top: 3px;
color: #5e6878;
font-size: 10px;
margin-top: 4px;
color: #a0acbe;
font-size: 11px;
}
/* Hero 右侧工作台卡 */
/* Hero 右侧工作台卡350 宽 / 10px 圆角,对齐 gemini-code hero-right-card */
.hero-card {
width: 300px;
width: 350px;
flex-shrink: 0;
padding: 18px 20px;
border: 1px solid #2e3a52;
border-radius: 12px;
background: #202a3e;
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: 12px;
gap: 8px;
margin-bottom: 14px;
}
.hero-card-dots {
display: flex;
align-items: center;
gap: 5px;
}
.dot {
width: 10px;
height: 10px;
width: 8px;
height: 8px;
border-radius: 50%;
}
@@ -326,70 +400,189 @@ onMounted(async () => {
.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;
color: #a0acbe;
font-size: 11px;
}
.hero-card-title {
margin: 16px 0 0;
margin: 0 0 12px;
color: #f5f8fc;
font-size: 17px;
font-weight: 800;
line-height: 1.6;
font-weight: 700;
line-height: 1.4;
}
.hero-card-divider {
height: 1px;
margin: 14px 0;
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;
line-height: 1.7;
white-space: nowrap;
}
.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;
.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: background 0.18s ease;
transition: transform 0.15s;
}
.hero-card-btn:hover {
background: #f7b13c;
.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: 1180px;
margin: 0 auto 34px;
max-width: 1440px;
margin: 0 auto;
}
.group-head {
@@ -407,177 +600,101 @@ onMounted(async () => {
.group-name {
color: #f5f8fc;
font-size: 17px;
font-weight: 800;
font-size: 16px;
font-weight: 700;
}
.group-desc {
color: #5e6878;
color: #a0acbe;
font-size: 11px;
}
.group-count {
margin-left: auto;
color: #5e6878;
color: #a0acbe;
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 底 + 2px #2A3344 边框 + 10px 圆角 min-height 165,对齐 gemini-code tool-card */
.card-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 12px;
gap: 14px;
margin-bottom: 28px;
}
.tool-card {
display: flex;
flex-direction: column;
justify-content: space-between;
min-width: 0;
padding: 16px 18px 14px;
border: 2px solid #2e3a52;
border-radius: 12px;
background: #1c2333;
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: border-color 0.18s ease, background 0.18s ease;
transition: all 0.2s;
}
/* 悬浮:边框高亮为分类色 + 背景变亮 + 上浮阴影(对齐 gemini-code hover 并按要求加边框高亮) */
.tool-card:hover {
border-color: #2dd4bf;
background: #262f42;
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: flex-start;
justify-content: space-between;
gap: 8px;
align-items: center;
gap: 12px;
margin-bottom: 12px;
}
.tool-tag {
padding: 3px 9px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.06);
font-size: 9px;
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-top: 12px;
margin-bottom: 6px;
color: #f5f8fc;
font-size: 15px;
font-weight: 700;
}
.tool-card-desc {
margin-top: 6px;
color: #5e6878;
flex: 1;
color: #a0acbe;
font-size: 11px;
line-height: 1.6;
line-height: 1.4;
}
.tool-card-go {
margin-top: 14px;
color: #5e6878;
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 12px;
color: #a0acbe;
font-size: 11px;
transition: color 0.18s ease;
transition: color 0.2s;
}
.tool-card:hover .tool-card-go {
color: #2dd4bf;
color: var(--gc, #2dd4bf);
font-weight: 700;
}
@@ -587,31 +704,33 @@ onMounted(async () => {
}
.tool-card--disabled:hover {
border-color: #2e3a52;
background: #1c2333;
border-color: #2a3344;
background: #2b3447;
transform: none;
box-shadow: none;
}
.tool-card--disabled:hover .tool-card-go {
color: #5e6878;
color: #a0acbe;
font-weight: 400;
}
/* ===== toast ===== */
/* ===== toast(右下角 + 品牌色左边条,对齐 gemini-code toast ===== */
.toast {
position: fixed;
bottom: 40px;
left: 50%;
bottom: 60px;
right: 30px;
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;
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;
transform: translateX(-50%);
transition: opacity 0.3s;
}
@@ -620,6 +739,7 @@ onMounted(async () => {
}
.toast.error {
border-left-color: #f87171;
background: rgba(176, 42, 55, 0.92);
}
@@ -628,8 +748,9 @@ onMounted(async () => {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.hero-inner {
.hero {
flex-direction: column;
align-items: stretch;
}
.hero-card {
@@ -637,7 +758,7 @@ onMounted(async () => {
}
.console-body {
padding: 18px 20px 36px;
padding: 14px 16px 24px;
}
}
</style>