|
|
@@ -1,84 +1,273 @@
|
|
|
+
|
|
|
<template>
|
|
|
- <div id="app"
|
|
|
- :class="{
|
|
|
- 'isHomePage': $route.path === '/' || $route.path === '/index',
|
|
|
- vipHomePage: $route.path === '/member',
|
|
|
- isScroll: appStore.isScroll,
|
|
|
- }"
|
|
|
- v-infinite-scroll="scrollLoad"
|
|
|
- :infinite-scroll-disabled="infiniteDisabled"
|
|
|
- infinite-scroll-distance="0"
|
|
|
- :infinite-scroll-immediate="false"
|
|
|
- >
|
|
|
+ <div id="app" class="bg-white text-slate-800 font-sans antialiased">
|
|
|
+ <!-- 背景装饰 -->
|
|
|
+ <div class="fixed top-0 left-0 w-full h-full overflow-hidden -z-10 pointer-events-none">
|
|
|
+ <div class="absolute top-[-20%] left-[-20%] w-[60%] h-[60%] bg-blue-100/30 rounded-full filter blur-3xl animate-pulse"></div>
|
|
|
+ <div class="absolute bottom-[-20%] right-[-20%] w-[60%] h-[60%] bg-pink-100/30 rounded-full filter blur-3xl animate-pulse animation-delay-4000"></div>
|
|
|
+ </div>
|
|
|
+
|
|
|
<ElConfigProvider :locale="langStore.elLocale">
|
|
|
- <el-container style="min-height: 100vh;">
|
|
|
- <el-header class="box_shadow_card">
|
|
|
- <div class="header-content">
|
|
|
- <div class="logo" @click="$router.push('/')">{{ $t('common.title') }}</div>
|
|
|
- <el-menu :default-active="activeIndex" ref="el_menu"
|
|
|
- mode="horizontal" :ellipsis="false" class="meauList">
|
|
|
- <el-menu-item index="1" @click="$router.push('/')">AI {{ $t('common.gongzuoliu') }}</el-menu-item>
|
|
|
- <!-- <el-menu-item index="2" @click="$router.push('/my-learning')">工作流交易</el-menu-item> -->
|
|
|
- <el-menu-item index="2" @click="goMyLearning">{{ $t('common.gongzuoliu_trade') }}</el-menu-item>
|
|
|
- <el-menu-item index="3" @click="$router.push('/learning-system')">{{ $t('route.learning_system') }}</el-menu-item>
|
|
|
- <el-menu-item index="4" @click=" goLearnNote">{{ $t('common.xuxibiji') }}</el-menu-item>
|
|
|
- <el-menu-item index="5" @click="$router.push('/mibi-shop')">{{ $t('route.mibiShop') }}</el-menu-item>
|
|
|
- <!-- <el-menu-item index="5" @click="$router.push('/my-learning')">米币商城</el-menu-item> -->
|
|
|
- </el-menu>
|
|
|
- <div class="header-right">
|
|
|
- <el-avatar :size="32" :src="appStore.userInfo?.userAvatar || appStore.avatarDefault" />
|
|
|
- <el-dropdown v-if="appStore.token">
|
|
|
- <span class="el-dropdown-link">
|
|
|
- {{ appStore.userInfo?.nickName || '用户' }}
|
|
|
- <el-icon class="el-icon--right">
|
|
|
- <arrow-down />
|
|
|
- </el-icon>
|
|
|
- </span>
|
|
|
- <template #dropdown>
|
|
|
- <el-dropdown-menu>
|
|
|
- <el-dropdown-item @click="handleLogout">{{ $t('common.logout') }}</el-dropdown-item>
|
|
|
- <el-dropdown-item @click="toPersonal">{{ $t('personalCenter.personalCenter') }}</el-dropdown-item>
|
|
|
- </el-dropdown-menu>
|
|
|
- </template>
|
|
|
- </el-dropdown>
|
|
|
- <!-- 打开登录弹框 -->
|
|
|
- <el-button type="text" @click="openLoginDialog" v-if="!appStore.token">{{ $t('common.login') }}</el-button>
|
|
|
- <LangSwitch />
|
|
|
+ <div class="min-h-screen flex flex-col">
|
|
|
+ <!-- Header -->
|
|
|
+ <header
|
|
|
+ ref="navRef"
|
|
|
+ class="sticky top-0 z-50 w-full transition-all duration-300"
|
|
|
+ :class="scrolled
|
|
|
+ ? 'bg-white/95 backdrop-blur-xl shadow-lg shadow-slate-200/50 border-b border-slate-200/40'
|
|
|
+ : 'bg-white/80 backdrop-blur-lg border-b border-slate-200/60'"
|
|
|
+ >
|
|
|
+ <!-- 顶部阅读进度条 -->
|
|
|
+ <div class="relative h-0.5 w-full bg-slate-100/60 overflow-hidden">
|
|
|
+ <div
|
|
|
+ class="absolute top-0 left-0 h-full bg-gradient-to-r from-blue-500 via-purple-500 via-pink-500 to-orange-400 transition-[width] duration-100 ease-out"
|
|
|
+ :style="{ width: readProgress + '%' }"
|
|
|
+ ></div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="max-w-screen-2xl mx-auto px-4 lg:px-8 w-full">
|
|
|
+ <div class="flex items-center h-16 gap-4">
|
|
|
+
|
|
|
+ <!-- Logo(左侧,flex-1让它占据左边空间)-->
|
|
|
+ <div class="flex-1 min-w-0">
|
|
|
+ <a href="/" @click.prevent="navigate('/')" class="flex items-center gap-2 group">
|
|
|
+ <!-- Logo 图标 -->
|
|
|
+ <div class="w-8 h-8 rounded-lg bg-gradient-to-br from-blue-500 to-purple-600 flex items-center justify-center shadow-md group-hover:shadow-blue-300/50 transition-shadow duration-300">
|
|
|
+ <svg class="w-4 h-4 text-white" fill="currentColor" viewBox="0 0 24 24">
|
|
|
+ <path d="M13 10V3L4 14h7v7l9-11h-7z"/>
|
|
|
+ </svg>
|
|
|
+ </div>
|
|
|
+ <span class="text-xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-blue-500 to-pink-500 group-hover:from-blue-600 group-hover:to-purple-600 transition-all duration-300">
|
|
|
+ {{ $t('common.title') }}
|
|
|
+ </span>
|
|
|
+ </a>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- Navigation(中间,flex-none不压缩)-->
|
|
|
+ <nav class="hidden md:flex items-center gap-0.5 whitespace-nowrap flex-none">
|
|
|
+ <template v-for="item in navigation" :key="item.name">
|
|
|
+ <!-- 有子菜单的导航项 -->
|
|
|
+ <div v-if="item.children" class="relative group">
|
|
|
+ <button
|
|
|
+ class="flex items-center gap-1 px-2.5 py-2 rounded-lg text-sm font-medium transition-all duration-200 whitespace-nowrap"
|
|
|
+ :class="isActive(item.path)
|
|
|
+ ? 'text-blue-600 bg-blue-50'
|
|
|
+ : 'text-slate-600 hover:text-blue-500 hover:bg-slate-50'"
|
|
|
+ @click="navigate(item.path)"
|
|
|
+ >
|
|
|
+ {{ $t(item.name) }}
|
|
|
+ <svg class="w-3.5 h-3.5 transition-transform duration-200 group-hover:rotate-180" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
|
|
|
+ </svg>
|
|
|
+ <!-- 活跃下划线 -->
|
|
|
+ <span v-if="isActive(item.path)" class="absolute bottom-0 left-3 right-3 h-0.5 bg-gradient-to-r from-blue-500 to-purple-500 rounded-full"></span>
|
|
|
+ </button>
|
|
|
+ <!-- 下拉子菜单 -->
|
|
|
+ <div class="absolute top-full left-1/2 -translate-x-1/2 pt-2 opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-200 translate-y-1 group-hover:translate-y-0">
|
|
|
+ <div class="bg-white rounded-2xl shadow-xl shadow-slate-200/60 border border-slate-100 p-2 min-w-[200px]">
|
|
|
+ <!-- 小三角 -->
|
|
|
+ <div class="absolute -top-1.5 left-1/2 -translate-x-1/2 w-3 h-3 bg-white border-l border-t border-slate-100 rotate-45"></div>
|
|
|
+ <a
|
|
|
+ v-for="child in item.children"
|
|
|
+ :key="child.name"
|
|
|
+ href="#"
|
|
|
+ @click.prevent="navigate(child.path)"
|
|
|
+ class="flex items-center gap-3 px-3 py-2.5 rounded-xl text-sm text-slate-600 hover:text-blue-600 hover:bg-blue-50 transition-all duration-150 group/item"
|
|
|
+ >
|
|
|
+ <span class="text-lg">{{ child.icon }}</span>
|
|
|
+ <div>
|
|
|
+ <div class="font-medium">{{ child.label }}</div>
|
|
|
+ <div class="text-xs text-slate-400">{{ child.desc }}</div>
|
|
|
+ </div>
|
|
|
+ </a>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 普通导航项 -->
|
|
|
+ <a
|
|
|
+ v-else
|
|
|
+ :href="item.href"
|
|
|
+ class="relative px-2.5 py-2 rounded-lg text-sm font-medium transition-all duration-200 whitespace-nowrap"
|
|
|
+ :class="isActive(item.path)
|
|
|
+ ? 'text-blue-600 bg-blue-50'
|
|
|
+ : 'text-slate-600 hover:text-blue-500 hover:bg-slate-50'"
|
|
|
+ @click.prevent="navigate(item.path)"
|
|
|
+ >
|
|
|
+ {{ $t(item.name) }}
|
|
|
+ <!-- 活跃下划线动画 -->
|
|
|
+ <span
|
|
|
+ class="absolute bottom-0 left-3 right-3 h-0.5 rounded-full transition-all duration-300"
|
|
|
+ :class="isActive(item.path) ? 'bg-gradient-to-r from-blue-500 to-purple-500 opacity-100' : 'opacity-0'"
|
|
|
+ ></span>
|
|
|
+ </a>
|
|
|
+ </template>
|
|
|
+ </nav>
|
|
|
+
|
|
|
+ <!-- Mobile Menu Button -->
|
|
|
+ <button class="md:hidden flex items-center justify-center w-10 h-10 rounded-lg hover:bg-slate-100 transition-colors ml-auto"
|
|
|
+ aria-label="菜单"
|
|
|
+ @click="mobileMenuOpen = !mobileMenuOpen">
|
|
|
+ <svg class="w-5 h-5 text-slate-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
+ <path v-if="!mobileMenuOpen" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"/>
|
|
|
+ <path v-else stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
|
|
+ </svg>
|
|
|
+ </button>
|
|
|
+
|
|
|
+ <!-- User Menu(右侧,flex-1 justify-end)-->
|
|
|
+ <div class="hidden md:flex items-center gap-2 flex-1 justify-end">
|
|
|
+ <!-- 搜索图标按钮 -->
|
|
|
+ <button
|
|
|
+ @click="searchOpen = !searchOpen"
|
|
|
+ class="w-9 h-9 flex items-center justify-center rounded-lg text-slate-500 hover:text-blue-500 hover:bg-slate-100 transition-all duration-200"
|
|
|
+ :aria-label="$t('common.search') || '搜索'"
|
|
|
+ >
|
|
|
+ <svg class="w-4.5 h-4.5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
|
|
|
+ <circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/>
|
|
|
+ </svg>
|
|
|
+ </button>
|
|
|
+
|
|
|
+ <!-- 通知图标 -->
|
|
|
+ <button
|
|
|
+ class="relative w-9 h-9 flex items-center justify-center rounded-lg text-slate-500 hover:text-blue-500 hover:bg-slate-100 transition-all duration-200"
|
|
|
+ aria-label="通知"
|
|
|
+ >
|
|
|
+ <svg class="w-4.5 h-4.5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
|
|
|
+ <path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 0 1-3.46 0"/>
|
|
|
+ </svg>
|
|
|
+ <!-- 红点 -->
|
|
|
+ <span class="absolute top-1.5 right-1.5 w-2 h-2 bg-red-500 rounded-full ring-2 ring-white"></span>
|
|
|
+ </button>
|
|
|
+
|
|
|
+ <!-- 分割线 -->
|
|
|
+ <div class="w-px h-5 bg-slate-200 mx-1"></div>
|
|
|
+
|
|
|
+ <template v-if="appStore.token">
|
|
|
+ <el-dropdown>
|
|
|
+ <span class="flex items-center gap-2 cursor-pointer px-2 py-1.5 rounded-xl hover:bg-slate-50 transition-colors">
|
|
|
+ <el-avatar :size="28" :src="appStore.userInfo?.userAvatar || appStore.avatarDefault" />
|
|
|
+ <span class="text-sm font-medium text-slate-700">{{ appStore.userInfo?.nickName || '用户' }}</span>
|
|
|
+ <el-icon class="el-icon--right text-slate-400"><arrow-down /></el-icon>
|
|
|
+ </span>
|
|
|
+ <template #dropdown>
|
|
|
+ <el-dropdown-menu>
|
|
|
+ <el-dropdown-item @click="toPersonal">{{ $t('personalCenter.personalCenter') }}</el-dropdown-item>
|
|
|
+ <el-dropdown-item @click="handleLogout">{{ $t('common.logout') }}</el-dropdown-item>
|
|
|
+ </el-dropdown-menu>
|
|
|
+ </template>
|
|
|
+ </el-dropdown>
|
|
|
+ </template>
|
|
|
+ <template v-else>
|
|
|
+ <button
|
|
|
+ @click="openLoginDialog"
|
|
|
+ class="px-3 py-1.5 text-sm font-medium text-slate-600 hover:text-blue-500 hover:bg-slate-50 rounded-lg transition-all duration-200"
|
|
|
+ >
|
|
|
+ {{ $t('common.login') }}
|
|
|
+ </button>
|
|
|
+ <button
|
|
|
+ @click="openLoginDialog"
|
|
|
+ class="relative px-4 py-1.5 text-sm font-semibold text-white rounded-full overflow-hidden group transition-all duration-300 hover:shadow-lg hover:shadow-blue-300/40 hover:-translate-y-0.5"
|
|
|
+ >
|
|
|
+ <!-- 渐变背景 -->
|
|
|
+ <span class="absolute inset-0 bg-gradient-to-r from-blue-500 to-purple-600 transition-all duration-300 group-hover:from-blue-600 group-hover:to-purple-700"></span>
|
|
|
+ <!-- 光晕动效 -->
|
|
|
+ <span class="absolute inset-0 opacity-0 group-hover:opacity-100 transition-opacity duration-300 bg-gradient-to-r from-transparent via-white/20 to-transparent -skew-x-12 translate-x-[-100%] group-hover:translate-x-[200%] transition-transform duration-700"></span>
|
|
|
+ <span class="relative">注册</span>
|
|
|
+ </button>
|
|
|
+ </template>
|
|
|
+ <LangSwitch />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 搜索框展开区域 -->
|
|
|
+ <Transition name="search-bar">
|
|
|
+ <div v-if="searchOpen" class="border-t border-slate-100 bg-white/98 backdrop-blur-xl">
|
|
|
+ <div class="max-w-screen-2xl mx-auto px-4 lg:px-8 py-3">
|
|
|
+ <div class="relative max-w-2xl mx-auto">
|
|
|
+ <svg class="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
|
|
|
+ <circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/>
|
|
|
+ </svg>
|
|
|
+ <input
|
|
|
+ ref="searchInputRef"
|
|
|
+ v-model="searchQuery"
|
|
|
+ type="text"
|
|
|
+ placeholder="搜索工作流、教程、功能..."
|
|
|
+ class="w-full pl-11 pr-12 py-2.5 text-sm bg-slate-50 border border-slate-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500/30 focus:border-blue-400 transition-all"
|
|
|
+ @keyup.enter="doSearch"
|
|
|
+ @keyup.escape="searchOpen = false"
|
|
|
+ />
|
|
|
+ <button
|
|
|
+ @click="searchOpen = false"
|
|
|
+ class="absolute right-3 top-1/2 -translate-y-1/2 w-6 h-6 flex items-center justify-center rounded-md text-slate-400 hover:text-slate-600 hover:bg-slate-200 transition-colors"
|
|
|
+ >
|
|
|
+ <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
|
|
|
+ <path d="M18 6 6 18M6 6l12 12"/>
|
|
|
+ </svg>
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </Transition>
|
|
|
+
|
|
|
+ <!-- Mobile Menu -->
|
|
|
+ <Transition name="mobile-menu">
|
|
|
+ <div v-if="mobileMenuOpen" class="md:hidden border-t border-slate-200/60 bg-white/95 backdrop-blur-lg">
|
|
|
+ <div class="max-w-screen-2xl mx-auto px-4 lg:px-8 py-4 space-y-2">
|
|
|
+ <a v-for="item in navigation" :key="item.name" :href="item.href"
|
|
|
+ class="block px-4 py-2.5 rounded-xl font-medium text-slate-600 hover:text-blue-500 hover:bg-blue-50 transition-all duration-200"
|
|
|
+ :class="{ 'text-blue-600 bg-blue-50 font-semibold': isActive(item.path) }"
|
|
|
+ @click.prevent="navigate(item.path); mobileMenuOpen = false">
|
|
|
+ {{ $t(item.name) }}
|
|
|
+ </a>
|
|
|
+ <div class="pt-2 border-t border-slate-100 flex items-center gap-3">
|
|
|
+ <template v-if="appStore.token">
|
|
|
+ <button @click="toPersonal" class="flex-1 px-4 py-2 text-sm font-medium text-slate-700 bg-slate-100 rounded-xl hover:bg-slate-200 transition-colors">
|
|
|
+ {{ $t('personalCenter.personalCenter') }}
|
|
|
+ </button>
|
|
|
+ <button @click="handleLogout" class="flex-1 px-4 py-2 text-sm font-medium text-red-600 bg-red-50 rounded-xl hover:bg-red-100 transition-colors">
|
|
|
+ {{ $t('common.logout') }}
|
|
|
+ </button>
|
|
|
+ </template>
|
|
|
+ <template v-else>
|
|
|
+ <button @click="openLoginDialog; mobileMenuOpen = false" class="flex-1 px-4 py-2 text-sm font-medium text-slate-600 bg-slate-100 rounded-xl hover:bg-slate-200 transition-colors">
|
|
|
+ {{ $t('common.login') }}
|
|
|
+ </button>
|
|
|
+ <button @click="openLoginDialog; mobileMenuOpen = false" class="flex-1 px-4 py-2 text-sm font-semibold text-white bg-gradient-to-r from-blue-500 to-purple-600 rounded-xl hover:from-blue-600 hover:to-purple-700 transition-all duration-200">
|
|
|
+ 注册
|
|
|
+ </button>
|
|
|
+ </template>
|
|
|
+ <LangSwitch />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </Transition>
|
|
|
+ </header>
|
|
|
+
|
|
|
+ <!-- Main Content -->
|
|
|
+ <main class="flex-grow">
|
|
|
+ <router-view />
|
|
|
+ </main>
|
|
|
+
|
|
|
+ <!-- Footer -->
|
|
|
+ <footer class="bg-white border-t border-slate-100">
|
|
|
+ <div class="max-w-screen-2xl mx-auto px-6 py-8">
|
|
|
+ <div class="text-center text-sm text-slate-500">
|
|
|
+ <div class="flex justify-center items-center space-x-4 mb-4">
|
|
|
+ <a href="#" @click.prevent="router.push({name:'Agreement',query:{type:'service_agreement'}})" class="hover:text-blue-500 transition-colors">{{ $t('agreement.service_agreement') }}</a>
|
|
|
+ <span class="text-slate-300">|</span>
|
|
|
+ <a href="#" @click.prevent="router.push({name:'Agreement',query:{type:'privacy_policy'}})" class="hover:text-blue-500 transition-colors">{{ $t('agreement.privacy_policy') }}</a>
|
|
|
+ </div>
|
|
|
+ <div class="space-x-4">
|
|
|
+ <a href="https://beian.miit.gov.cn/" target="_blank" rel="noopener noreferrer" class="hover:text-blue-500 transition-colors">粤ICP备2025364959号-1</a>
|
|
|
+ <span class="text-slate-300">|</span>
|
|
|
+ <span>广州暴米智能科技有限公司</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
- </div>
|
|
|
- </el-header>
|
|
|
- <!-- 固定头部占位 -->
|
|
|
- <div style="height: 60px;"></div>
|
|
|
- <div class="header-bj" v-if="$route.path === '/member'"></div>
|
|
|
- <el-main class="container">
|
|
|
- <router-view v-slot="{ Component }">
|
|
|
- <component
|
|
|
- :is="Component"
|
|
|
- ref="routerChildRef"
|
|
|
- />
|
|
|
- </router-view>
|
|
|
- </el-main>
|
|
|
-
|
|
|
- <el-footer v-if="!['/index'].includes($route.path)">
|
|
|
- <div class="footer-content" >
|
|
|
- <p>
|
|
|
- <span class="cursor-pointer" @click="router.push({name:'Agreement',query:{type:'service_agreement'}})">
|
|
|
- 《{{ $t('agreement.service_agreement') }}》
|
|
|
- </span>
|
|
|
- <span class="gray999"> | </span>
|
|
|
- <span class="cursor-pointer" @click="router.push({name:'Agreement',query:{type:'privacy_policy'}})">
|
|
|
- 《{{ $t('agreement.privacy_policy') }}》
|
|
|
- </span>
|
|
|
- <span class="gray999"> | </span>
|
|
|
- <span @click="openNewTab('https://beian.miit.gov.cn/')" class="cursor-pointer">粤ICP备2025364959号-1</span>
|
|
|
- <span class="gray999"> | </span>
|
|
|
- <span>广州暴米智能科技有限公司</span>
|
|
|
- </p>
|
|
|
- </div>
|
|
|
- </el-footer>
|
|
|
- </el-container>
|
|
|
- <!-- 登录弹框组件 -->
|
|
|
- <LoginDialog ref="loginDialogRef" @login-success="handleLoginSuccess" />
|
|
|
+ </footer>
|
|
|
+ </div>
|
|
|
+ <LoginDialog ref="loginDialogRef" @login-success="handleLoginSuccess" />
|
|
|
</ElConfigProvider>
|
|
|
</div>
|
|
|
</template>
|
|
|
@@ -86,255 +275,247 @@
|
|
|
<script setup>
|
|
|
import { logout } from '@/api/auth.js'
|
|
|
import LoginDialog from './components/LoginDialog.vue'
|
|
|
-import { computed,ref,onMounted, provide, watch, nextTick,onUnmounted } from 'vue'
|
|
|
+import { computed, ref, onMounted, onUnmounted, provide, watch, nextTick } from 'vue'
|
|
|
import LangSwitch from './components/LangSwitch.vue'
|
|
|
import { ElConfigProvider, ElMessage } from 'element-plus'
|
|
|
import { useRoute, useRouter } from 'vue-router'
|
|
|
-import { openNewTab, isLogin } from '@/utils/util.js'
|
|
|
-import { debounce } from 'lodash';
|
|
|
-// 在Pinia安装后再设置初始语言
|
|
|
+import { isLogin } from '@/utils/util.js'
|
|
|
import { useLangStore } from '@/pinia/langStore'
|
|
|
import { useAppStore } from '@/pinia/appStore'
|
|
|
-const langStore = useLangStore();
|
|
|
-const appStore = useAppStore();
|
|
|
-$i18n.global.locale.value = langStore.currentLang
|
|
|
-import { useI18n } from 'vue-i18n'
|
|
|
-const { t } = useI18n()
|
|
|
-// 动态更新页面标题
|
|
|
-langStore.updateDynamicTitle()
|
|
|
-
|
|
|
-
|
|
|
+import { useI18n } from 'vue-i18n'
|
|
|
|
|
|
+const langStore = useLangStore()
|
|
|
+const appStore = useAppStore()
|
|
|
const route = useRoute()
|
|
|
const router = useRouter()
|
|
|
-const el_menu = ref(null);
|
|
|
+const { t } = useI18n()
|
|
|
|
|
|
-// 登录弹框引用
|
|
|
-const loginDialogRef = ref(null)
|
|
|
+// 移动端菜单状态
|
|
|
+const mobileMenuOpen = ref(false)
|
|
|
+
|
|
|
+// 滚动状态
|
|
|
+const scrolled = ref(false)
|
|
|
+
|
|
|
+// 阅读进度条
|
|
|
+const readProgress = ref(0)
|
|
|
+
|
|
|
+// 导航栏 ref,用于动态设置 --nav-height CSS 变量
|
|
|
+const navRef = ref(null)
|
|
|
+
|
|
|
+let resizeObserver = null
|
|
|
+let navResizeObserver = null
|
|
|
+
|
|
|
+const updateNavHeight = () => {
|
|
|
+ if (navRef.value) {
|
|
|
+ const h = navRef.value.offsetHeight
|
|
|
+ document.documentElement.style.setProperty('--nav-height', h + 'px')
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const calcProgress = () => {
|
|
|
+ const scrollTop = window.scrollY || document.documentElement.scrollTop
|
|
|
+ // 可滚动总高度 = 文档总高度 - 视口高度
|
|
|
+ const docHeight = Math.max(
|
|
|
+ document.body.scrollHeight,
|
|
|
+ document.documentElement.scrollHeight,
|
|
|
+ document.body.offsetHeight,
|
|
|
+ document.documentElement.offsetHeight
|
|
|
+ )
|
|
|
+ const winHeight = window.innerHeight
|
|
|
+ const scrollable = docHeight - winHeight
|
|
|
+ if (scrollable <= 0) {
|
|
|
+ readProgress.value = 100
|
|
|
+ return
|
|
|
+ }
|
|
|
+ readProgress.value = Math.min(100, Math.round((scrollTop / scrollable) * 1000) / 10)
|
|
|
+}
|
|
|
+
|
|
|
+const handleScroll = () => {
|
|
|
+ scrolled.value = window.scrollY > 20
|
|
|
+ calcProgress()
|
|
|
+}
|
|
|
+
|
|
|
+onMounted(() => {
|
|
|
+ window.addEventListener('scroll', handleScroll, { passive: true })
|
|
|
+ // 监听页面高度变化(懒加载、动态内容等)
|
|
|
+ resizeObserver = new ResizeObserver(() => {
|
|
|
+ calcProgress()
|
|
|
+ })
|
|
|
+ resizeObserver.observe(document.body)
|
|
|
+ calcProgress()
|
|
|
+ // 动态设置导航栏高度 CSS 变量
|
|
|
+ updateNavHeight()
|
|
|
+ navResizeObserver = new ResizeObserver(updateNavHeight)
|
|
|
+ if (navRef.value) navResizeObserver.observe(navRef.value)
|
|
|
+})
|
|
|
+onUnmounted(() => {
|
|
|
+ window.removeEventListener('scroll', handleScroll)
|
|
|
+ resizeObserver?.disconnect()
|
|
|
+ navResizeObserver?.disconnect()
|
|
|
+})
|
|
|
+
|
|
|
+// 路由切换时重置进度条
|
|
|
+watch(() => route.path, () => {
|
|
|
+ readProgress.value = 0
|
|
|
+ // 等待新页面渲染完成后重新计算
|
|
|
+ nextTick(() => calcProgress())
|
|
|
+})
|
|
|
+
|
|
|
+// 搜索状态
|
|
|
+const searchOpen = ref(false)
|
|
|
+const searchQuery = ref('')
|
|
|
+const searchInputRef = ref(null)
|
|
|
+
|
|
|
+watch(searchOpen, async (val) => {
|
|
|
+ if (val) {
|
|
|
+ await nextTick()
|
|
|
+ searchInputRef.value?.focus()
|
|
|
+ }
|
|
|
+})
|
|
|
|
|
|
-//监听showLoginDialog变化
|
|
|
-watch(() => appStore.showLoginDialog, (newVal, oldVal) => {
|
|
|
- if(newVal){
|
|
|
- openLoginDialog();
|
|
|
+const doSearch = () => {
|
|
|
+ if (searchQuery.value.trim()) {
|
|
|
+ router.push({ path: '/', query: { q: searchQuery.value.trim() } })
|
|
|
+ searchOpen.value = false
|
|
|
+ searchQuery.value = ''
|
|
|
}
|
|
|
+}
|
|
|
+
|
|
|
+// Update i18n locale and dynamic title
|
|
|
+watch(() => langStore.currentLang, () => {
|
|
|
+ langStore.updateDynamicTitle()
|
|
|
+})
|
|
|
+
|
|
|
+const navigation = ref([
|
|
|
+ {
|
|
|
+ name: 'common.gongzuoliu',
|
|
|
+ href: '#',
|
|
|
+ path: '/',
|
|
|
+ children: [
|
|
|
+ { path: '/', icon: '🔍', label: '工作流搜索', desc: '搜索 10,000+ 工作流模板' },
|
|
|
+ { path: '/workflow-trade', icon: '💼', label: '工作流交易', desc: '发布或承接定制需求' },
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ { name: 'common.gongzuoliu_trade', href: '#', path: '/workflow-trade' },
|
|
|
+ {
|
|
|
+ name: 'route.learning_system',
|
|
|
+ href: '#',
|
|
|
+ path: '/learning-system',
|
|
|
+ children: [
|
|
|
+ { path: '/learning-system', icon: '🎓', label: '学习教程', desc: '系统化 AI 工作流课程' },
|
|
|
+ { path: '/learn-note', icon: '📝', label: '学习笔记', desc: '记录你的学习心得' },
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ { name: 'common.xuxibiji', href: '#', path: '/learn-note' },
|
|
|
+ { name: 'route.mibiShop', href: '#', path: '/mibi-shop' },
|
|
|
+])
|
|
|
+// navigation 配置与参考图一致:工作流(下拉) > 工作流交易 > 学习教程(下拉) > 学习笔记 > 米币商城
|
|
|
+
|
|
|
+const isActive = (path) => {
|
|
|
+ if (path === '/') return route.path === '/' || route.path === '/index'
|
|
|
+ return route.path.startsWith(path)
|
|
|
+}
|
|
|
+
|
|
|
+const navigate = (path) => {
|
|
|
+ if (['/learn-note'].includes(path) && !isLogin({ callback: openLoginDialog, t })) {
|
|
|
+ return
|
|
|
+ }
|
|
|
+ router.push(path)
|
|
|
+ mobileMenuOpen.value = false
|
|
|
+}
|
|
|
+
|
|
|
+const loginDialogRef = ref(null)
|
|
|
+
|
|
|
+watch(() => appStore.showLoginDialog, (newVal) => {
|
|
|
+ if (newVal) openLoginDialog()
|
|
|
})
|
|
|
|
|
|
onMounted(() => {
|
|
|
- appStore.USERINFO();
|
|
|
+ appStore.USERINFO()
|
|
|
})
|
|
|
|
|
|
-// 打开登录弹框
|
|
|
const openLoginDialog = () => {
|
|
|
- loginDialogRef.value?.open();
|
|
|
+ loginDialogRef.value?.open()
|
|
|
}
|
|
|
-// 处理登录成功
|
|
|
+
|
|
|
const handleLoginSuccess = () => {
|
|
|
- console.log('登录成功')
|
|
|
}
|
|
|
-//
|
|
|
-function goMyLearning() {
|
|
|
- router.push('/workflow-trade')
|
|
|
-};
|
|
|
-const goLearnNote = () => {
|
|
|
- if (!isLogin({ callback: openLoginDialog, t })) {
|
|
|
- el_menu.value.updateActiveIndex (activeIndex.value);
|
|
|
- return; // 如果未登录则不执行跳转,也不激活菜单
|
|
|
- }
|
|
|
- router.push('/learn-note');
|
|
|
-}
|
|
|
-// 将 activeIndex 改为响应式,并根据当前路由动态计算
|
|
|
-const activeIndex = computed(() => {
|
|
|
- console.log('route.path',route.path)
|
|
|
- if (route.path === '/index') return '1'
|
|
|
- if (route.path.startsWith('/workflow-trade')) {
|
|
|
- return '2'
|
|
|
- }
|
|
|
- if (route.path.startsWith('/learning-system')) {
|
|
|
- return '3'
|
|
|
- }
|
|
|
- if (route.path.startsWith('/learn-note')) {
|
|
|
- // 如果用户未登录,不应该显示此菜单为激活状态
|
|
|
- if (!appStore.token) return null; // 或者返回上一个有效菜单
|
|
|
- return '4';
|
|
|
- }
|
|
|
- if (route.path.startsWith('/mibi-shop')) {
|
|
|
- return '5'
|
|
|
- }
|
|
|
- if (route.path.startsWith('/personal-center')) {
|
|
|
- return null
|
|
|
- }
|
|
|
- return null // 默认返回首页
|
|
|
-});
|
|
|
-// 去个人中心
|
|
|
+
|
|
|
const toPersonal = () => {
|
|
|
router.push('/personal-center/wallet')
|
|
|
-};
|
|
|
+}
|
|
|
|
|
|
-// 处理注销
|
|
|
const handleLogout = () => {
|
|
|
logout().then(() => {
|
|
|
appStore.LOGOUT()
|
|
|
ElMessage.success(t('login.logoutSuccess'))
|
|
|
router.push('/')
|
|
|
})
|
|
|
-};
|
|
|
-
|
|
|
-//监听页面是否滚动
|
|
|
-// 使用防抖优化滚动监听
|
|
|
-const handleScroll = debounce(() => {
|
|
|
- appStore.isScroll = window.scrollY > 0;
|
|
|
-}, 10);
|
|
|
+}
|
|
|
|
|
|
-// 监听滚动事件
|
|
|
-window.addEventListener('scroll', handleScroll);
|
|
|
+provide('openLoginDialog', openLoginDialog)
|
|
|
|
|
|
-// 组件卸载时移除事件监听
|
|
|
-onUnmounted(() => {
|
|
|
- window.removeEventListener('scroll', handleScroll);
|
|
|
-});
|
|
|
-
|
|
|
-
|
|
|
-provide('openLoginDialog', openLoginDialog);
|
|
|
-
|
|
|
-// 无限滚动加载
|
|
|
-const routerChildRef = ref(null);
|
|
|
-// 是否在加载中,防止多次触发 loadMore
|
|
|
-const isLoadingMore = ref(false);
|
|
|
-// 当前路由是否需要开启无限滚动(比如只有列表页开启)
|
|
|
-const isInfiniteScrollPage = computed(() => {
|
|
|
- // 按需添加需要无限滚动的路由前缀
|
|
|
- if (route.path.startsWith('/index')) return true;
|
|
|
- if (route.path.startsWith('/learning-system')) return true;
|
|
|
- if (route.path.startsWith('/learn-note')) return true;
|
|
|
- if (route.path.startsWith('/workflow-trade')) return true;
|
|
|
- return false;
|
|
|
-});
|
|
|
-// 控制 v-infinite-scroll 的禁用状态
|
|
|
-const infiniteDisabled = computed(() => {
|
|
|
- return !isInfiniteScrollPage.value || isLoadingMore.value;
|
|
|
-});
|
|
|
-
|
|
|
-const scrollLoad = () => {
|
|
|
- if (infiniteDisabled.value) return;
|
|
|
- const routerChild = routerChildRef.value;
|
|
|
- if (!routerChild || typeof routerChild.loadMore !== 'function') return;
|
|
|
-
|
|
|
- isLoadingMore.value = true;
|
|
|
- // 兼容 loadMore 返回 Promise 或同步函数两种情况
|
|
|
- const res = routerChild.loadMore();
|
|
|
- if (res && typeof res.then === 'function') {
|
|
|
- res.finally(() => {
|
|
|
- isLoadingMore.value = false;
|
|
|
- });
|
|
|
- } else {
|
|
|
- isLoadingMore.value = false;
|
|
|
- }
|
|
|
-};
|
|
|
+const routerChildRef = ref(null)
|
|
|
</script>
|
|
|
|
|
|
-<style lang="scss">
|
|
|
-#app {
|
|
|
- font-family: Avenir, Helvetica, Arial, sans-serif;
|
|
|
- -webkit-font-smoothing: antialiased;
|
|
|
- -moz-osx-font-smoothing: grayscale;
|
|
|
- color: #2c3e50;
|
|
|
- &.isHomePage {
|
|
|
- background: url('@/assets/imgs/bg.png') no-repeat center center fixed;
|
|
|
- background-size: 100% 100%;
|
|
|
- }
|
|
|
- &.vipHomePage {
|
|
|
- background: #FBF4F0;
|
|
|
- }
|
|
|
+<style>
|
|
|
+/* 页面切换过渡动画 */
|
|
|
+.page-enter-active,
|
|
|
+.page-leave-active {
|
|
|
+ transition: opacity 0.2s ease, transform 0.2s ease;
|
|
|
}
|
|
|
-.isHomePage{
|
|
|
- .el-header {
|
|
|
- background: url('@/assets/imgs/bg-header.png') no-repeat;
|
|
|
- background-size: 100% 100%;
|
|
|
- }
|
|
|
+
|
|
|
+.page-enter-from {
|
|
|
+ opacity: 0;
|
|
|
+ transform: translateY(8px);
|
|
|
}
|
|
|
-.vipHomePage {
|
|
|
- .el-header {
|
|
|
- background: rgba(255,255,255,0.5);
|
|
|
- }
|
|
|
- &.isScroll {
|
|
|
- .el-header {
|
|
|
- // background: url('@/assets/imgs/bg-header_2.png') no-repeat;
|
|
|
- background: #FCEDDA;
|
|
|
- }
|
|
|
- }
|
|
|
+
|
|
|
+.page-leave-to {
|
|
|
+ opacity: 0;
|
|
|
+ transform: translateY(-8px);
|
|
|
}
|
|
|
|
|
|
-.el-header {
|
|
|
- background: url('@/assets/imgs/bg-header_2.png') no-repeat;
|
|
|
- background-size: 100% 100%;
|
|
|
-
|
|
|
- color: #333;
|
|
|
- line-height: 60px;
|
|
|
- // border-bottom: 1px solid #eee;
|
|
|
- position: fixed;
|
|
|
- top: 0;
|
|
|
- left: 0;
|
|
|
- right: 0;
|
|
|
- z-index: 1000;
|
|
|
- padding: 0;
|
|
|
-
|
|
|
- .header-content {
|
|
|
- max-width: 1430px;
|
|
|
- width: 80%;
|
|
|
- min-width: 1430px;
|
|
|
- margin: 0 auto;
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- justify-content: space-between;
|
|
|
-
|
|
|
- .logo {
|
|
|
- min-width: 220px;
|
|
|
- font-size: 24px;
|
|
|
- font-weight: bold;
|
|
|
- cursor: pointer;
|
|
|
- }
|
|
|
- .meauList{
|
|
|
- &.el-menu{
|
|
|
- background-color: transparent;
|
|
|
- }
|
|
|
- .el-menu--horizontal.el-menu{
|
|
|
- border-bottom: none;
|
|
|
- }
|
|
|
- .el-menu-item{
|
|
|
- font-size: 16px;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- .header-right {
|
|
|
- // margin-right: 30px;
|
|
|
- display: flex;
|
|
|
- align-items: center; /* 垂直居中 */
|
|
|
- gap: 10px;
|
|
|
- }
|
|
|
- }
|
|
|
+/* 移动端菜单过渡动画 */
|
|
|
+.mobile-menu-enter-active,
|
|
|
+.mobile-menu-leave-active {
|
|
|
+ transition: opacity 0.2s ease, max-height 0.3s ease;
|
|
|
+ max-height: 400px;
|
|
|
+ overflow: hidden;
|
|
|
}
|
|
|
-.header-bj {
|
|
|
- top: 0;
|
|
|
- width: 100%;
|
|
|
- height: 463px;
|
|
|
- background: url('/src/assets/imgs/header-bj@2x.png') no-repeat;
|
|
|
- background-size: cover;
|
|
|
- position: fixed;
|
|
|
+
|
|
|
+.mobile-menu-enter-from,
|
|
|
+.mobile-menu-leave-to {
|
|
|
+ opacity: 0;
|
|
|
+ max-height: 0;
|
|
|
}
|
|
|
|
|
|
-.el-footer {
|
|
|
- color: #666;
|
|
|
- text-align: center;
|
|
|
- padding: 20px 0;
|
|
|
-
|
|
|
- .footer-content {
|
|
|
- font-size: 14px;
|
|
|
- max-width: 1200px;
|
|
|
- margin: 0 auto;
|
|
|
- }
|
|
|
+/* 搜索栏过渡动画 */
|
|
|
+.search-bar-enter-active,
|
|
|
+.search-bar-leave-active {
|
|
|
+ transition: opacity 0.2s ease, max-height 0.25s ease;
|
|
|
+ max-height: 80px;
|
|
|
+ overflow: hidden;
|
|
|
+}
|
|
|
+
|
|
|
+.search-bar-enter-from,
|
|
|
+.search-bar-leave-to {
|
|
|
+ opacity: 0;
|
|
|
+ max-height: 0;
|
|
|
+}
|
|
|
+
|
|
|
+/* 背景动画延迟 */
|
|
|
+.animation-delay-4000 {
|
|
|
+ animation-delay: 4s;
|
|
|
+}
|
|
|
+
|
|
|
+/* 注册按钮光晕动效 */
|
|
|
+.register-btn-shine {
|
|
|
+ animation: shine 3s infinite;
|
|
|
}
|
|
|
|
|
|
-.el-main {
|
|
|
- padding: 0;
|
|
|
+@keyframes shine {
|
|
|
+ 0% { transform: translateX(-100%) skewX(-12deg); }
|
|
|
+ 20% { transform: translateX(200%) skewX(-12deg); }
|
|
|
+ 100% { transform: translateX(200%) skewX(-12deg); }
|
|
|
}
|
|
|
-</style>
|
|
|
+</style>
|