App.vue 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521
  1. <template>
  2. <div id="app" class="bg-white text-slate-800 font-sans antialiased">
  3. <!-- 背景装饰 -->
  4. <div class="fixed top-0 left-0 w-full h-full overflow-hidden -z-10 pointer-events-none">
  5. <div class="absolute top-[-20%] left-[-20%] w-[60%] h-[60%] bg-blue-100/30 rounded-full filter blur-3xl animate-pulse"></div>
  6. <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>
  7. </div>
  8. <ElConfigProvider :locale="langStore.elLocale">
  9. <div class="min-h-screen flex flex-col">
  10. <!-- Header -->
  11. <header
  12. ref="navRef"
  13. class="sticky top-0 z-50 w-full transition-all duration-300"
  14. :class="scrolled
  15. ? 'bg-white/95 backdrop-blur-xl shadow-lg shadow-slate-200/50 border-b border-slate-200/40'
  16. : 'bg-white/80 backdrop-blur-lg border-b border-slate-200/60'"
  17. >
  18. <!-- 顶部阅读进度条 -->
  19. <div class="relative h-0.5 w-full bg-slate-100/60 overflow-hidden">
  20. <div
  21. 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"
  22. :style="{ width: readProgress + '%' }"
  23. ></div>
  24. </div>
  25. <div class="max-w-screen-2xl mx-auto px-4 lg:px-8 w-full">
  26. <div class="flex items-center h-16 gap-4">
  27. <!-- Logo(左侧,flex-1让它占据左边空间)-->
  28. <div class="flex-1 min-w-0">
  29. <a href="/" @click.prevent="navigate('/')" class="flex items-center gap-2 group">
  30. <!-- Logo 图标 -->
  31. <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">
  32. <svg class="w-4 h-4 text-white" fill="currentColor" viewBox="0 0 24 24">
  33. <path d="M13 10V3L4 14h7v7l9-11h-7z"/>
  34. </svg>
  35. </div>
  36. <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">
  37. {{ $t('common.title') }}
  38. </span>
  39. </a>
  40. </div>
  41. <!-- Navigation(中间,flex-none不压缩)-->
  42. <nav class="hidden sm:hidden md:flex items-center gap-0.5 whitespace-nowrap flex-none">
  43. <template v-for="item in navigation" :key="item.name">
  44. <!-- 有子菜单的导航项 -->
  45. <div v-if="item.children" class="relative group">
  46. <button
  47. class="flex items-center gap-1 px-2.5 py-2 rounded-lg text-sm font-medium transition-all duration-200 whitespace-nowrap"
  48. :class="isActive(item.path)
  49. ? 'text-blue-600 bg-blue-50'
  50. : 'text-slate-600 hover:text-blue-500 hover:bg-slate-50'"
  51. @click="navigate(item.path)"
  52. >
  53. {{ $t(item.name) }}
  54. <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">
  55. <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
  56. </svg>
  57. <!-- 活跃下划线 -->
  58. <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>
  59. </button>
  60. <!-- 下拉子菜单 -->
  61. <div class="absolute top-full left-1/2 -translate-x-1/2 opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-200 translate-y-1 group-hover:translate-y-0">
  62. <div class="bg-white rounded-2xl shadow-xl shadow-slate-200/60 border border-slate-100 p-2 min-w-[200px]">
  63. <!-- 小三角 -->
  64. <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>
  65. <a
  66. v-for="child in item.children"
  67. :key="child.name"
  68. href="#"
  69. @click.prevent="navigate(child.path)"
  70. 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"
  71. >
  72. <span class="text-lg">{{ child.icon }}</span>
  73. <div>
  74. <div class="font-medium">{{ child.label }}</div>
  75. <div class="text-xs text-slate-400">{{ child.desc }}</div>
  76. </div>
  77. </a>
  78. </div>
  79. </div>
  80. </div>
  81. <!-- 普通导航项 -->
  82. <a
  83. v-else
  84. :href="item.href"
  85. class="relative px-2.5 py-2 rounded-lg text-sm font-medium transition-all duration-200 whitespace-nowrap"
  86. :class="isActive(item.path)
  87. ? 'text-blue-600 bg-blue-50'
  88. : 'text-slate-600 hover:text-blue-500 hover:bg-slate-50'"
  89. @click.prevent="navigate(item.path)"
  90. >
  91. {{ $t(item.name) }}
  92. <!-- 活跃下划线动画 -->
  93. <span
  94. class="absolute bottom-0 left-3 right-3 h-0.5 rounded-full transition-all duration-300"
  95. :class="isActive(item.path) ? 'bg-gradient-to-r from-blue-500 to-purple-500 opacity-100' : 'opacity-0'"
  96. ></span>
  97. </a>
  98. </template>
  99. </nav>
  100. <!-- Mobile Menu Button -->
  101. <button class="md:hidden sm:flex items-center justify-center w-10 h-10 rounded-lg hover:bg-slate-100 transition-colors ml-auto"
  102. aria-label="菜单"
  103. @click="mobileMenuOpen = !mobileMenuOpen">
  104. <svg class="w-5 h-5 text-slate-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
  105. <path v-if="!mobileMenuOpen" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"/>
  106. <path v-else stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
  107. </svg>
  108. </button>
  109. <!-- User Menu(右侧,flex-1 justify-end)-->
  110. <div class="hidden md:flex items-center gap-2 flex-1 justify-end">
  111. <!-- 搜索图标按钮 -->
  112. <!-- <button
  113. @click="searchOpen = !searchOpen"
  114. 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"
  115. :aria-label="$t('common.search') || '搜索'"
  116. >
  117. <svg class="w-4.5 h-4.5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
  118. <circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/>
  119. </svg>
  120. </button> -->
  121. <!-- 通知图标 -->
  122. <!-- <button
  123. 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"
  124. aria-label="通知"
  125. >
  126. <svg class="w-4.5 h-4.5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
  127. <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"/>
  128. </svg>
  129. <span class="absolute top-1.5 right-1.5 w-2 h-2 bg-red-500 rounded-full ring-2 ring-white"></span>
  130. </button> -->
  131. <!-- 分割线 -->
  132. <!-- <div class="w-px h-5 bg-slate-200 mx-1"></div> -->
  133. <template v-if="appStore.token">
  134. <el-dropdown>
  135. <span class="flex items-center gap-2 cursor-pointer px-2 py-1.5 rounded-xl hover:bg-slate-50 transition-colors">
  136. <el-avatar :size="28" :src="appStore.userInfo?.userAvatar || appStore.avatarDefault" />
  137. <span class="text-sm font-medium text-slate-700">{{ appStore.userInfo?.nickName || '用户' }}</span>
  138. <el-icon class="el-icon--right text-slate-400"><arrow-down /></el-icon>
  139. </span>
  140. <template #dropdown>
  141. <el-dropdown-menu>
  142. <el-dropdown-item @click="toPersonal">{{ $t('personalCenter.personalCenter') }}</el-dropdown-item>
  143. <el-dropdown-item @click="handleLogout">{{ $t('common.logout') }}</el-dropdown-item>
  144. </el-dropdown-menu>
  145. </template>
  146. </el-dropdown>
  147. </template>
  148. <template v-else>
  149. <button
  150. @click="openLoginDialog"
  151. 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"
  152. >
  153. {{ $t('common.login') }}
  154. </button>
  155. <button
  156. @click="openLoginDialog"
  157. 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"
  158. >
  159. <!-- 渐变背景 -->
  160. <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>
  161. <!-- 光晕动效 -->
  162. <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>
  163. <span class="relative">注册</span>
  164. </button>
  165. </template>
  166. <LangSwitch />
  167. </div>
  168. </div>
  169. </div>
  170. <!-- 搜索框展开区域 -->
  171. <Transition name="search-bar">
  172. <div v-if="searchOpen" class="border-t border-slate-100 bg-white/98 backdrop-blur-xl">
  173. <div class="max-w-screen-2xl mx-auto px-4 lg:px-8 py-3">
  174. <div class="relative max-w-2xl mx-auto">
  175. <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">
  176. <circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/>
  177. </svg>
  178. <input
  179. ref="searchInputRef"
  180. v-model="searchQuery"
  181. type="text"
  182. :placeholder="$t('common.search_placeholder')"
  183. 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"
  184. @keyup.enter="doSearch"
  185. @keyup.escape="searchOpen = false"
  186. />
  187. <button
  188. @click="searchOpen = false"
  189. 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"
  190. >
  191. <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
  192. <path d="M18 6 6 18M6 6l12 12"/>
  193. </svg>
  194. </button>
  195. </div>
  196. </div>
  197. </div>
  198. </Transition>
  199. <!-- Mobile Menu -->
  200. <Transition name="mobile-menu">
  201. <div v-if="mobileMenuOpen" class="md:hidden border-t border-slate-200/60 bg-white/95 backdrop-blur-lg">
  202. <div class="max-w-screen-2xl mx-auto px-4 lg:px-8 py-4 space-y-2">
  203. <a v-for="item in navigation" :key="item.name" :href="item.href"
  204. 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"
  205. :class="{ 'text-blue-600 bg-blue-50 font-semibold': isActive(item.path) }"
  206. @click.prevent="navigate(item.path); mobileMenuOpen = false">
  207. {{ $t(item.name) }}
  208. </a>
  209. <div class="pt-2 border-t border-slate-100 flex items-center gap-3">
  210. <template v-if="appStore.token">
  211. <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">
  212. {{ $t('personalCenter.personalCenter') }}
  213. </button>
  214. <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">
  215. {{ $t('common.logout') }}
  216. </button>
  217. </template>
  218. <template v-else>
  219. <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">
  220. {{ $t('common.login') }}
  221. </button>
  222. <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">
  223. 注册
  224. </button>
  225. </template>
  226. <LangSwitch />
  227. </div>
  228. </div>
  229. </div>
  230. </Transition>
  231. </header>
  232. <!-- Main Content -->
  233. <main class="flex-grow">
  234. <router-view />
  235. </main>
  236. <!-- Footer -->
  237. <footer class="bg-white border-t border-slate-100">
  238. <div class="max-w-screen-2xl mx-auto px-6 py-8">
  239. <div class="text-center text-sm text-slate-500">
  240. <div class="flex justify-center items-center space-x-4 mb-4">
  241. <a href="#" @click.prevent="router.push({name:'Agreement',query:{type:'service_agreement'}})" class="hover:text-blue-500 transition-colors">{{ $t('agreement.service_agreement') }}</a>
  242. <span class="text-slate-300">|</span>
  243. <a href="#" @click.prevent="router.push({name:'Agreement',query:{type:'privacy_policy'}})" class="hover:text-blue-500 transition-colors">{{ $t('agreement.privacy_policy') }}</a>
  244. </div>
  245. <div class="space-x-4">
  246. <a href="https://beian.miit.gov.cn/" target="_blank" rel="noopener noreferrer" class="hover:text-blue-500 transition-colors">粤ICP备2025364959号-1</a>
  247. <span class="text-slate-300">|</span>
  248. <span>广州暴米智能科技有限公司</span>
  249. </div>
  250. </div>
  251. </div>
  252. </footer>
  253. </div>
  254. <LoginDialog ref="loginDialogRef" @login-success="handleLoginSuccess" />
  255. </ElConfigProvider>
  256. </div>
  257. </template>
  258. <script setup>
  259. import { logout } from '@/api/auth.js'
  260. import LoginDialog from './components/LoginDialog.vue'
  261. import { computed, ref, onMounted, onUnmounted, provide, watch, nextTick } from 'vue'
  262. import LangSwitch from './components/LangSwitch.vue'
  263. import { ElConfigProvider, ElMessage } from 'element-plus'
  264. import { useRoute, useRouter } from 'vue-router'
  265. import { isLogin } from '@/utils/util.js'
  266. import { useLangStore } from '@/pinia/langStore'
  267. import { useAppStore } from '@/pinia/appStore'
  268. import { useI18n } from 'vue-i18n'
  269. const langStore = useLangStore()
  270. const appStore = useAppStore()
  271. const route = useRoute()
  272. const router = useRouter()
  273. const { t } = useI18n()
  274. // 移动端菜单状态
  275. const mobileMenuOpen = ref(false)
  276. // 滚动状态
  277. const scrolled = ref(false)
  278. // 阅读进度条
  279. const readProgress = ref(0)
  280. // 导航栏 ref,用于动态设置 --nav-height CSS 变量
  281. const navRef = ref(null)
  282. let resizeObserver = null
  283. let navResizeObserver = null
  284. const updateNavHeight = () => {
  285. if (navRef.value) {
  286. const h = navRef.value.offsetHeight
  287. document.documentElement.style.setProperty('--nav-height', h + 'px')
  288. }
  289. }
  290. const calcProgress = () => {
  291. const scrollTop = window.scrollY || document.documentElement.scrollTop
  292. // 可滚动总高度 = 文档总高度 - 视口高度
  293. const docHeight = Math.max(
  294. document.body.scrollHeight,
  295. document.documentElement.scrollHeight,
  296. document.body.offsetHeight,
  297. document.documentElement.offsetHeight
  298. )
  299. const winHeight = window.innerHeight
  300. const scrollable = docHeight - winHeight
  301. if (scrollable <= 0) {
  302. readProgress.value = 100
  303. return
  304. }
  305. readProgress.value = Math.min(100, Math.round((scrollTop / scrollable) * 1000) / 10)
  306. }
  307. const handleScroll = () => {
  308. scrolled.value = window.scrollY > 20
  309. calcProgress()
  310. }
  311. onMounted(() => {
  312. window.addEventListener('scroll', handleScroll, { passive: true })
  313. // 监听页面高度变化(懒加载、动态内容等)
  314. resizeObserver = new ResizeObserver(() => {
  315. calcProgress()
  316. })
  317. resizeObserver.observe(document.body)
  318. calcProgress()
  319. // 动态设置导航栏高度 CSS 变量
  320. updateNavHeight()
  321. navResizeObserver = new ResizeObserver(updateNavHeight)
  322. if (navRef.value) navResizeObserver.observe(navRef.value)
  323. })
  324. onUnmounted(() => {
  325. window.removeEventListener('scroll', handleScroll)
  326. resizeObserver?.disconnect()
  327. navResizeObserver?.disconnect()
  328. })
  329. // 路由切换时重置进度条
  330. watch(() => route.path, () => {
  331. readProgress.value = 0
  332. // 等待新页面渲染完成后重新计算
  333. nextTick(() => calcProgress())
  334. })
  335. // 搜索状态
  336. const searchOpen = ref(false)
  337. const searchQuery = ref('')
  338. const searchInputRef = ref(null)
  339. watch(searchOpen, async (val) => {
  340. if (val) {
  341. await nextTick()
  342. searchInputRef.value?.focus()
  343. }
  344. })
  345. const doSearch = () => {
  346. if (searchQuery.value.trim()) {
  347. router.push({ path: '/', query: { q: searchQuery.value.trim() } })
  348. searchOpen.value = false
  349. searchQuery.value = ''
  350. }
  351. }
  352. // Update i18n locale and dynamic title
  353. watch(() => langStore.currentLang, () => {
  354. langStore.updateDynamicTitle()
  355. })
  356. const navigation = ref([
  357. {
  358. name: 'common.gongzuoliu',
  359. href: '#',
  360. path: '/',
  361. // children: [
  362. // { path: '/', icon: '🔍', label: t('common.gongzuoliu_search'), desc: t('common.gongzuoliu_search_desc') },
  363. // { path: '/workflow-trade', icon: '💼', label: t('common.gongzuoliu_trade'), desc: t('common.gongzuoliu_trade_desc') },
  364. // ]
  365. },
  366. { name: 'common.gongzuoliu_trade', href: '#', path: '/workflow-trade' },
  367. {
  368. name: 'route.learning_system',
  369. href: '#',
  370. path: '/learning-system',
  371. // children: [
  372. // { path: '/learning-system', icon: '🎓', label: t('common.learning_course'), desc: t('common.learning_course_desc') },
  373. // { path: '/learn-note', icon: '📝', label: t('common.learning_note'), desc: t('common.learning_note_desc') },
  374. // ]
  375. },
  376. { name: 'common.xuxibiji', href: '#', path: '/learn-note' },
  377. { name: 'route.mibiShop', href: '#', path: '/mibi-shop' },
  378. ])
  379. // navigation 配置与参考图一致:工作流(下拉) > 工作流交易 > 学习教程(下拉) > 学习笔记 > 米币商城
  380. const isActive = (path) => {
  381. if (path === '/') return route.path === '/' || route.path === '/index'
  382. return route.path.startsWith(path)
  383. }
  384. const navigate = (path) => {
  385. if (['/learn-note'].includes(path) && !isLogin({ callback: openLoginDialog, t })) {
  386. return
  387. }
  388. router.push(path)
  389. mobileMenuOpen.value = false
  390. }
  391. const loginDialogRef = ref(null)
  392. watch(() => appStore.showLoginDialog, (newVal) => {
  393. if (newVal) openLoginDialog()
  394. })
  395. onMounted(() => {
  396. appStore.USERINFO()
  397. })
  398. const openLoginDialog = () => {
  399. loginDialogRef.value?.open()
  400. }
  401. const handleLoginSuccess = () => {
  402. }
  403. const toPersonal = () => {
  404. router.push('/personal-center/wallet')
  405. }
  406. const handleLogout = () => {
  407. logout().then(() => {
  408. appStore.LOGOUT()
  409. ElMessage.success(t('login.logoutSuccess'))
  410. router.push('/')
  411. })
  412. }
  413. provide('openLoginDialog', openLoginDialog)
  414. const routerChildRef = ref(null)
  415. </script>
  416. <style>
  417. /* 页面切换过渡动画 */
  418. .page-enter-active,
  419. .page-leave-active {
  420. transition: opacity 0.2s ease, transform 0.2s ease;
  421. }
  422. .page-enter-from {
  423. opacity: 0;
  424. transform: translateY(8px);
  425. }
  426. .page-leave-to {
  427. opacity: 0;
  428. transform: translateY(-8px);
  429. }
  430. /* 移动端菜单过渡动画 */
  431. .mobile-menu-enter-active,
  432. .mobile-menu-leave-active {
  433. transition: opacity 0.2s ease, max-height 0.3s ease;
  434. max-height: 400px;
  435. overflow: hidden;
  436. }
  437. .mobile-menu-enter-from,
  438. .mobile-menu-leave-to {
  439. opacity: 0;
  440. max-height: 0;
  441. }
  442. /* 搜索栏过渡动画 */
  443. .search-bar-enter-active,
  444. .search-bar-leave-active {
  445. transition: opacity 0.2s ease, max-height 0.25s ease;
  446. max-height: 80px;
  447. overflow: hidden;
  448. }
  449. .search-bar-enter-from,
  450. .search-bar-leave-to {
  451. opacity: 0;
  452. max-height: 0;
  453. }
  454. /* 背景动画延迟 */
  455. .animation-delay-4000 {
  456. animation-delay: 4s;
  457. }
  458. /* 注册按钮光晕动效 */
  459. .register-btn-shine {
  460. animation: shine 3s infinite;
  461. }
  462. @keyframes shine {
  463. 0% { transform: translateX(-100%) skewX(-12deg); }
  464. 20% { transform: translateX(200%) skewX(-12deg); }
  465. 100% { transform: translateX(200%) skewX(-12deg); }
  466. }
  467. </style>