App.vue 22 KB

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