前端登录页面模板(有趣味的登录页它踏着七彩祥云来了)

前端登录页面模板(有趣味的登录页它踏着七彩祥云来了)
有趣味的登录页它踏着七彩祥云来了

最近,有一个比较火的很有趣且灵动的登录页火了。

  • 角色视觉跟随鼠标
  • 输入框打字时扯脖子瞅
  • 显示密码明文时避开视线

已经有大神(katavii)复刻了动画效果,并在github上开源了:https://github.com/katavii/animated-login ,基于React实现。

如果你的项目是用Vue开发的,可以考虑用AI将此项目转换成了Vue3的语法写法。

前端登录页面模板(有趣味的登录页它踏着七彩祥云来了)

最简单的方式,直接用Claude Code一句话就能完成,根据模型能力,你可能需要多次调试。

claude帮我把这个项目转成vue3 + ant-design-vue的前端项目

以下是我的转换代码,如果你的AI代码没有调试成功,可以参考下。

创建项目

现在开发前端项目,肯定首选Vite。

pnpm create vite# 选择Vue模板、TypeScript语法

封装组件

在src/components/创建animated-characters文件夹

EyeBall

创建 src/components/animated-characters/EyeBall.vue,制作动画的大眼睛

[xss_clean]interface Props {  size?: string  pupilSize?: string  maxDistance?: number  eyeColor?: string  pupilColor?: string}const {  size,  pupilSize,  maxDistance,  eyeColor,  pupilColor} = withDefaults(defineProps(), {  size: '48px',  pupilSize: '16px',  maxDistance: 10,  eyeColor: 'white',  pupilColor: 'black'})const eyeballStyle = {  width: size,  height: size,  borderRadius: '50%',  backgroundColor: eyeColor,  display: 'flex',  alignItems: 'center',  justifyContent: 'center',  overflow: 'hidden',  willChange: 'height'}const pupilStyle = {  width: pupilSize,  height: pupilSize,  borderRadius: '50%',  backgroundColor: pupilColor,  willChange: 'transform'}[xss_clean]

Pupil

创建 src/components/animated-characters/Pupil.vue,制作动画的小眼睛

[xss_clean]interface Props {  size?: string  maxDistance?: number  pupilColor?: string}const {  size,  maxDistance,  pupilColor} = withDefaults(defineProps(), {  size: '12px',  maxDistance: 5,  pupilColor: 'black'})const pupilStyle = {  width: size,  height: size,  borderRadius: '50%',  backgroundColor: pupilColor,  willChange: 'transform'}[xss_clean]

角色

安装依赖

pnpm install gsap --save

创建 src/components/animated-characters/Index.vue,制作动画的角色

props属性- is-typing         是否正在输入- show-password     显示密码明文- password-length   密码输入框是否有值
[xss_clean]import { ref, reactive, onMounted, onBeforeUnmount, watch, toRef } from 'vue'import gsap from 'gsap'import Pupil from './Pupil.vue'import EyeBall from './EyeBall.vue'interface Props {  isTyping?: boolean  showPassword?: boolean  passwordLength?: number}const props = withDefaults(defineProps(), {  isTyping: false,  showPassword: false,  passwordLength: 0})const containerRef = ref(null)const mouseRef = reactive({ x: 0, y: 0 })const rafIdRef = ref(0)const purpleRef = ref(null)const blackRef = ref(null)const yellowRef = ref(null)const orangeRef = ref(null)const purpleFaceRef = ref(null)const blackFaceRef = ref(null)const yellowFaceRef = ref(null)const orangeFaceRef = ref(null)const yellowMouthRef = ref(null)const purpleBlinkTimerRef = ref>()const blackBlinkTimerRef = ref>()const purplePeekTimerRef = ref>()const isHidingPassword = toRef(() => props.passwordLength > 0 && !props.showPassword)const isShowingPassword = toRef(() => props.passwordLength > 0 && props.showPassword)const isLookingRef = ref(false)const lookingTimerRef = ref>()const stateRef = reactive({  isTyping: false,  isHidingPassword: false,  isShowingPassword: false,  isLooking: false})watch(  () => [props.isTyping, isHidingPassword.value, isShowingPassword.value, isLookingRef.value] as const,  ([isTyping, isHiding, isShowing, isLooking]) => {    stateRef.isTyping = isTyping    stateRef.isHidingPassword = isHiding    stateRef.isShowingPassword = isShowing    stateRef.isLooking = isLooking  })// GSAP quickTo instancesconst quickToRef = ref | null>(null)const containerStyle = {  position: 'relative' as const,  width: '550px',  height: '400px'}const purpleBodyStyle = ref({  position: 'absolute',  bottom: 0,  left: '70px',  width: '180px',  height: '400px',  backgroundColor: '#6C3FF5',  borderRadius: '10px 10px 0 0',  zIndex: 1,  transformOrigin: 'bottom center',  willChange: 'transform'})const blackBodyStyle = ref({  position: 'absolute',  bottom: 0,  left: '240px',  width: '120px',  height: '310px',  backgroundColor: '#2D2D2D',  borderRadius: '8px 8px 0 0',  zIndex: 2,  transformOrigin: 'bottom center',  willChange: 'transform'})const orangeBodyStyle = ref({  position: 'absolute',  bottom: 0,  left: 0,  width: '240px',  height: '200px',  backgroundColor: '#FF9B6B',  borderRadius: '120px 120px 0 0',  zIndex: 3,  transformOrigin: 'bottom center',  willChange: 'transform'})const yellowBodyStyle = ref({  position: 'absolute',  bottom: 0,  left: '310px',  width: '140px',  height: '230px',  backgroundColor: '#E8D754',  borderRadius: '70px 70px 0 0',  zIndex: 4,  transformOrigin: 'bottom center',  willChange: 'transform'})const purpleFaceStyle = ref({  position: 'absolute',  display: 'flex',  gap: '32px',  left: '45px',  top: '40px'})const blackFaceStyle = ref({  position: 'absolute',  display: 'flex',  gap: '24px',  left: '26px',  top: '32px'})const orangeFaceStyle = ref({  position: 'absolute',  display: 'flex',  gap: '32px',  left: '82px',  top: '90px'})const yellowFaceStyle = ref({  position: 'absolute',  display: 'flex',  gap: '24px',  left: '52px',  top: '40px'})const yellowMouthStyle = ref({  position: 'absolute',  width: '80px',  height: '4px',  backgroundColor: '#2D2D2D',  borderRadius: '9999px',  left: '40px',  top: '88px'})// Initialize GSAPonMounted(() => {  gsap.set('.pupil', { x: 0, y: 0 })  gsap.set('.eyeball-pupil', { x: 0, y: 0 })})onMounted(() => {  if (    !purpleRef.value ||    !blackRef.value ||    !orangeRef.value ||    !yellowRef.value ||    !purpleFaceRef.value ||    !blackFaceRef.value ||    !orangeFaceRef.value ||    !yellowFaceRef.value ||    !yellowMouthRef.value  )    return  const qt = {    purpleSkew: gsap.quickTo(purpleRef.value, 'skewX', { duration: 0.3, ease: 'power2.out' }),    blackSkew: gsap.quickTo(blackRef.value, 'skewX', { duration: 0.3, ease: 'power2.out' }),    orangeSkew: gsap.quickTo(orangeRef.value, 'skewX', { duration: 0.3, ease: 'power2.out' }),    yellowSkew: gsap.quickTo(yellowRef.value, 'skewX', { duration: 0.3, ease: 'power2.out' }),    purpleX: gsap.quickTo(purpleRef.value, 'x', { duration: 0.3, ease: 'power2.out' }),    blackX: gsap.quickTo(blackRef.value, 'x', { duration: 0.3, ease: 'power2.out' }),    purpleHeight: gsap.quickTo(purpleRef.value, 'height', { duration: 0.3, ease: 'power2.out' }),    purpleFaceLeft: gsap.quickTo(purpleFaceRef.value, 'left', { duration: 0.3, ease: 'power2.out' }),    purpleFaceTop: gsap.quickTo(purpleFaceRef.value, 'top', { duration: 0.3, ease: 'power2.out' }),    blackFaceLeft: gsap.quickTo(blackFaceRef.value, 'left', { duration: 0.3, ease: 'power2.out' }),    blackFaceTop: gsap.quickTo(blackFaceRef.value, 'top', { duration: 0.3, ease: 'power2.out' }),    orangeFaceX: gsap.quickTo(orangeFaceRef.value, 'x', { duration: 0.2, ease: 'power2.out' }),    orangeFaceY: gsap.quickTo(orangeFaceRef.value, 'y', { duration: 0.2, ease: 'power2.out' }),    yellowFaceX: gsap.quickTo(yellowFaceRef.value, 'x', { duration: 0.2, ease: 'power2.out' }),    yellowFaceY: gsap.quickTo(yellowFaceRef.value, 'y', { duration: 0.2, ease: 'power2.out' }),    mouthX: gsap.quickTo(yellowMouthRef.value, 'x', { duration: 0.2, ease: 'power2.out' }),    mouthY: gsap.quickTo(yellowMouthRef.value, 'y', { duration: 0.2, ease: 'power2.out' })  }  quickToRef.value = qt  const calcPos = (el: HTMLElement) => {    const rect = el.getBoundingClientRect()    const cx = rect.left + rect.width / 2    const cy = rect.top + rect.height / 3    const dx = mouseRef.x - cx    const dy = mouseRef.y - cy    return {      faceX: Math.max(-15, Math.min(15, dx / 20)),      faceY: Math.max(-10, Math.min(10, dy / 30)),      bodySkew: Math.max(-6, Math.min(6, -dx / 120))    }  }  const calcEyePos = (el: HTMLElement, maxDist: number) => {    const r = el.getBoundingClientRect()    const cx = r.left + r.width / 2    const cy = r.top + r.height / 2    const dx = mouseRef.x - cx    const dy = mouseRef.y - cy    const dist = Math.min(Math.sqrt(dx ** 2 + dy ** 2), maxDist)    const angle = Math.atan2(dy, dx)    return { x: Math.cos(angle) * dist, y: Math.sin(angle) * dist }  }  const tick = () => {    const container = containerRef.value    if (!container) return    const { isTyping: typing, isHidingPassword: hiding, isShowingPassword: showing, isLooking: looking } = stateRef    if (purpleRef.value && !showing) {      const pp = calcPos(purpleRef.value)      if (typing || hiding) {        qt.purpleSkew(pp.bodySkew - 12)        qt.purpleX(40)        qt.purpleHeight(440)      } else {        qt.purpleSkew(pp.bodySkew)        qt.purpleX(0)        qt.purpleHeight(400)      }    }    if (blackRef.value && !showing) {      const bp = calcPos(blackRef.value)      if (looking) {        qt.blackSkew(bp.bodySkew * 1.5 + 10)        qt.blackX(20)      } else if (typing || hiding) {        qt.blackSkew(bp.bodySkew * 1.5)        qt.blackX(0)      } else {        qt.blackSkew(bp.bodySkew)        qt.blackX(0)      }    }    if (orangeRef.value && !showing) {      const op = calcPos(orangeRef.value)      qt.orangeSkew(op.bodySkew)    }    if (yellowRef.value && !showing) {      const yp = calcPos(yellowRef.value)      qt.yellowSkew(yp.bodySkew)    }    if (purpleRef.value && !showing && !looking) {      const pp = calcPos(purpleRef.value)      const purpleFaceX = pp.faceX >= 0 ? Math.min(25, pp.faceX * 1.5) : pp.faceX      qt.purpleFaceLeft(45 + purpleFaceX)      qt.purpleFaceTop(40 + pp.faceY)    }    if (blackRef.value && !showing && !looking) {      const bp = calcPos(blackRef.value)      qt.blackFaceLeft(26 + bp.faceX)      qt.blackFaceTop(32 + bp.faceY)    }    if (orangeRef.value && !showing) {      const op = calcPos(orangeRef.value)      qt.orangeFaceX(op.faceX)      qt.orangeFaceY(op.faceY)    }    if (yellowRef.value && !showing) {      const yp = calcPos(yellowRef.value)      qt.yellowFaceX(yp.faceX)      qt.yellowFaceY(yp.faceY)      qt.mouthX(yp.faceX)      qt.mouthY(yp.faceY)    }    if (!showing) {      const allPupils = container.querySelectorAll('.pupil')      allPupils.forEach((p) => {        const el = p as HTMLElement        const maxDist = Number(el.dataset.maxDistance) || 5        const ePos = calcEyePos(el, maxDist)        gsap.set(el, { x: ePos.x, y: ePos.y })      })      if (!looking) {        const allEyeballs = container.querySelectorAll('.eyeball')        allEyeballs.forEach((eb) => {          const el = eb as HTMLElement          const maxDist = Number(el.dataset.maxDistance) || 10          const pupil = el.querySelector('.eyeball-pupil') as HTMLElement          if (!pupil) return          const ePos = calcEyePos(el, maxDist)          gsap.set(pupil, { x: ePos.x, y: ePos.y })        })      }    }    rafIdRef.value = requestAnimationFrame(tick)  }  const onMove = (e: MouseEvent) => {    mouseRef.x = e.clientX    mouseRef.y = e.clientY  }  window.addEventListener('mousemove', onMove, { passive: true })  rafIdRef.value = requestAnimationFrame(tick)  onBeforeUnmount(() => {    window.removeEventListener('mousemove', onMove)    cancelAnimationFrame(rafIdRef.value)  })})// Purple character blinkonMounted(() => {  const purpleEyeballs = purpleRef.value?.querySelectorAll('.eyeball')  if (!purpleEyeballs?.length) return  const scheduleBlink = () => {    purpleBlinkTimerRef.value = setTimeout(() => {      purpleEyeballs.forEach((el) => {        gsap.to(el, { height: 2, duration: 0.08, ease: 'power2.in' })      })      setTimeout(() => {        purpleEyeballs.forEach((el) => {          const size = Number((el as HTMLElement).style.width.replace('px', '')) || 18          gsap.to(el, { height: size, duration: 0.08, ease: 'power2.out' })        })        scheduleBlink()      }, 150)    }, Math.random() * 4000 + 3000)  }  scheduleBlink()  onBeforeUnmount(() => clearTimeout(purpleBlinkTimerRef.value))})// Black character blinkonMounted(() => {  const blackEyeballs = blackRef.value?.querySelectorAll('.eyeball')  if (!blackEyeballs?.length) return  const scheduleBlink = () => {    blackBlinkTimerRef.value = setTimeout(() => {      blackEyeballs.forEach((el) => {        gsap.to(el, { height: 2, duration: 0.08, ease: 'power2.in' })      })      setTimeout(() => {        blackEyeballs.forEach((el) => {          const size = Number((el as HTMLElement).style.width.replace('px', '')) || 16          gsap.to(el, { height: size, duration: 0.08, ease: 'power2.out' })        })        scheduleBlink()      }, 150)    }, Math.random() * 4000 + 3000)  }  scheduleBlink()  onBeforeUnmount(() => clearTimeout(blackBlinkTimerRef.value))})const applyLookAtEachOther = () => {  const qt = quickToRef.value  if (qt) {    qt.purpleFaceLeft(55)    qt.purpleFaceTop(65)    qt.blackFaceLeft(32)    qt.blackFaceTop(12)  }  purpleRef.value?.querySelectorAll('.eyeball-pupil').forEach((p) => {    gsap.to(p, { x: 3, y: 4, duration: 0.3, ease: 'power2.out', overwrite: 'auto' })  })  blackRef.value?.querySelectorAll('.eyeball-pupil').forEach((p) => {    gsap.to(p, { x: 0, y: -4, duration: 0.3, ease: 'power2.out', overwrite: 'auto' })  })}const applyHidingPassword = () => {  const qt = quickToRef.value  if (qt) {    qt.purpleFaceLeft(55)    qt.purpleFaceTop(65)  }}const applyShowPassword = () => {  const qt = quickToRef.value  if (qt) {    qt.purpleSkew(0)    qt.blackSkew(0)    qt.orangeSkew(0)    qt.yellowSkew(0)    qt.purpleX(0)    qt.blackX(0)    qt.purpleHeight(400)    qt.purpleFaceLeft(20)    qt.purpleFaceTop(35)    qt.blackFaceLeft(10)    qt.blackFaceTop(28)    qt.orangeFaceX(50 - 82)    qt.orangeFaceY(85 - 90)    qt.yellowFaceX(20 - 52)    qt.yellowFaceY(35 - 40)    qt.mouthX(10 - 40)    qt.mouthY(0)  }  purpleRef.value?.querySelectorAll('.eyeball-pupil').forEach((p) => {    gsap.to(p, { x: -4, y: -4, duration: 0.3, ease: 'power2.out', overwrite: 'auto' })  })  blackRef.value?.querySelectorAll('.eyeball-pupil').forEach((p) => {    gsap.to(p, { x: -4, y: -4, duration: 0.3, ease: 'power2.out', overwrite: 'auto' })  })  orangeRef.value?.querySelectorAll('.pupil').forEach((p) => {    gsap.to(p, { x: -5, y: -4, duration: 0.3, ease: 'power2.out', overwrite: 'auto' })  })  yellowRef.value?.querySelectorAll('.pupil').forEach((p) => {    gsap.to(p, { x: -5, y: -4, duration: 0.3, ease: 'power2.out', overwrite: 'auto' })  })}// Password peek effectwatch(  () => [isShowingPassword.value, props.passwordLength],  ([showing, len]) => {    if (!showing || (len as number) <= 0) {      clearTimeout(purplePeekTimerRef.value)      return    }    const purpleEyePupils = purpleRef.value?.querySelectorAll('.eyeball-pupil')    if (!purpleEyePupils?.length) return    const schedulePeek = () => {      purplePeekTimerRef.value = setTimeout(() => {        purpleEyePupils.forEach((p) => {          gsap.to(p, {            x: 4,            y: 5,            duration: 0.3,            ease: 'power2.out',            overwrite: 'auto'          })        })        const qt = quickToRef.value        if (qt) {          qt.purpleFaceLeft(20)          qt.purpleFaceTop(35)        }        setTimeout(() => {          purpleEyePupils.forEach((p) => {            gsap.to(p, {              x: -4,              y: -4,              duration: 0.3,              ease: 'power2.out',              overwrite: 'auto'            })          })          schedulePeek()        }, 800)      }, Math.random() * 3000 + 2000)    }    schedulePeek()    onBeforeUnmount(() => clearTimeout(purplePeekTimerRef.value))  })// Look at each other when typingwatch(  () => [props.isTyping, isShowingPassword.value],  ([typing, showing]) => {    if (typing && !showing) {      isLookingRef.value = true      stateRef.isLooking = true      applyLookAtEachOther()      clearTimeout(lookingTimerRef.value)      lookingTimerRef.value = setTimeout(() => {        isLookingRef.value = false        stateRef.isLooking = false        purpleRef.value?.querySelectorAll('.eyeball-pupil').forEach((p) => {          gsap.killTweensOf(p)        })      }, 800)    } else {      clearTimeout(lookingTimerRef.value)      isLookingRef.value = false      stateRef.isLooking = false    }  })// Password state effectswatch(  () => [isShowingPassword.value, isHidingPassword.value],  ([showing, hiding]) => {    if (showing) {      applyShowPassword()    } else if (hiding) {      applyHidingPassword()    }  })[xss_clean]

登录页

安装依赖

pnpm install --save ant-design-vue @ant-design/icons-vue

在src/main.js添加以下内容

import Antd from 'ant-design-vue'import 'ant-design-vue/dist/reset.css'app.use(Antd)

创建 src/pages/login/Index.vue,登录页

[xss_clean]import { ref } from 'vue'import { message } from 'ant-design-vue'import {  UserOutlined,  LockOutlined,  EyeOutlined,  EyeInvisibleOutlined,} from '@ant-design/icons-vue'import AnimatedCharacters from '../../components/animated-characters/Index.vue'import styles from './index.module.css'/** 模拟登录 API(仅前端逻辑,无真实请求) */async function mockLogin(_values: { username: string; password: string }) {  await new Promise((resolve) => setTimeout(resolve, 800))  return { data: { access_token: 'mock_token_' + Date.now() } }}const loading = ref(false)const showPassword = ref(false)const isTyping = ref(false)const passwordValue = ref('')const error = ref('')const handleLogin = async (values: { username: string; password: string }) => {  loading.value = true  error.value = ''  try {    const { data } = await mockLogin(values)    localStorage.setItem('access_token', data.access_token)    message.success('登录成功')    setTimeout(() => {      _window.location.href = '/'    }, 500)  } catch {    error.value = '账号或密码有误,请重新输入'  } finally {    loading.value = false  }}[xss_clean]

创建 src/pages/login/index.module.css,登录页样式

.container {    min-height: 100vh;    display: grid;    grid-template-columns: 1fr 1fr;}@media (max-width: 1024px) {    .container {        grid-template-columns: 1fr;    }}/* ─── 左侧面板 ───────────────────────────────────────────────────────────────── */.leftPanel {    position: relative;    display: flex;    flex-direction: column;    justify-content: space-between;    padding: 48px;    background: linear-gradient(145deg, #0f172a 0%, #1e3a8a 50%, #1e40af 100%);    overflow: hidden;}@media (max-width: 1024px) {    .leftPanel {        display: none;    }}.leftTop {    position: relative;    z-index: 20;    display: flex;    align-items: center;    gap: 10px;    font-size: 20px;    font-weight: 700;    color: #ffffff;    letter-spacing: 0.5px;}.brandMark {    width: 40px;    height: 40px;    border-radius: 10px;    background: rgba(255, 255, 255, 0.12);    border: 1px solid rgba(255, 255, 255, 0.2);    display: flex;    align-items: center;    justify-content: center;    flex-shrink: 0;    backdrop-filter: blur(8px);}.brandName {    color: #ffffff;    font-size: 20px;    font-weight: 700;    letter-spacing: 1px;}.charactersArea {    position: relative;    z-index: 20;    display: flex;    align-items: flex-end;    justify-content: center;    height: 500px;}.leftFooter {    position: relative;    z-index: 20;    display: flex;    align-items: center;    gap: 24px;}.leftFooter a {    font-size: 13px;    color: rgba(255, 255, 255, 0.45);    text-decoration: none;    transition: color 0.2s;    cursor: pointer;}.leftFooter a:hover {    color: rgba(255, 255, 255, 0.85);}.decorBlur1 {    position: absolute;    top: 15%;    right: 10%;    width: 300px;    height: 300px;    background: rgba(59, 130, 246, 0.25);    border-radius: 50%;    filter: blur(80px);    pointer-events: none;    z-index: 0;}.decorBlur2 {    position: absolute;    bottom: 10%;    left: 5%;    width: 400px;    height: 400px;    background: rgba(30, 64, 175, 0.3);    border-radius: 50%;    filter: blur(100px);    pointer-events: none;    z-index: 0;}.decorGrid {    position: absolute;    inset: 0;    background-image:            linear-gradient(rgba(255, 255, 255, 0.03) 1px, transparent 1px),            linear-gradient(90deg, rgba(255, 255, 255, 0.03) 1px, transparent 1px);    background-size: 40px 40px;    pointer-events: none;    z-index: 1;}/* ─── 右侧面板 ───────────────────────────────────────────────────────────────── */.rightPanel {    display: flex;    align-items: center;    justify-content: center;    padding: 32px;    background: #ffffff;}.formWrapper {    width: 100%;    max-width: 400px;}.mobileLogo {    display: none;    align-items: center;    justify-content: center;    gap: 8px;    font-size: 18px;    font-weight: 700;    color: #0f172a;    margin-bottom: 48px;}@media (max-width: 1024px) {    .mobileLogo {        display: flex;    }}.mobileLogoIcon {    width: 32px;    height: 32px;    border-radius: 8px;    background: #eff6ff;    display: flex;    align-items: center;    justify-content: center;}.formHeader {    text-align: center;    margin-bottom: 40px;}.formTitle {    font-size: 26px;    font-weight: 700;    letter-spacing: -0.02em;    color: #0f172a;    margin: 0 0 10px 0;    line-height: 1.3;}.formSubtitle {    font-size: 14px;    color: #6b7280;    margin: 0;    line-height: 1.6;}.form :global(.ant-form-item) {    margin-bottom: 20px;}.form :global(.ant-input-affix-wrapper) {    height: 48px !important;    background: #fafafa !important;    border: 1px solid #e5e7eb !important;    border-radius: 10px !important;    transition: border-color 0.2s, box-shadow 0.2s !important;}.form :global(.ant-input-affix-wrapper:hover) {    border-color: #3b82f6 !important;}.form :global(.ant-input-affix-wrapper:focus),.form :global(.ant-input-affix-wrapper-focused) {    border-color: #1e40af !important;    box-shadow: 0 0 0 3px rgba(30, 64, 175, 0.08) !important;    background: #ffffff !important;}.form :global(.ant-input-affix-wrapper .ant-input) {    background: transparent !important;    font-size: 14px !important;    color: #111827 !important;}.form :global(.ant-input-affix-wrapper .ant-input::placeholder) {    color: #c0c4cc !important;}.form :global(.ant-form-item-explain-error) {    font-size: 13px !important;    margin-top: 4px !important;}.fieldLabel {    font-size: 13px;    font-weight: 500;    color: #374151;    margin-bottom: 6px;    letter-spacing: 0.2px;}.prefixIcon {    color: #b0b7c3;    font-size: 15px;}.eyeToggle {    color: #6b7280;    cursor: pointer;    font-size: 16px;    display: flex;    align-items: center;    transition: color 0.2s;}.eyeToggle:hover {    color: #374151;}.errorBox {    padding: 10px 14px;    font-size: 13px;    color: #dc2626;    background: #fef2f2;    border: 1px solid #fecaca;    border-radius: 8px;    margin-bottom: 16px;}.submitBtn {    height: 48px !important;    font-size: 15px !important;    font-weight: 600 !important;    border-radius: 10px !important;    background: #1e40af !important;    border-color: #1e40af !important;    letter-spacing: 1px;    transition: background 0.2s, opacity 0.2s !important;    cursor: pointer;}.submitBtn:hover {    background: #1d4ed8 !important;    border-color: #1d4ed8 !important;    opacity: 1 !important;}.submitBtn:active {    opacity: 0.85 !important;}.divider {    display: flex;    align-items: center;    gap: 12px;    margin: 20px 0 0;    color: #d1d5db;    font-size: 13px;}.divider::before,.divider::after {    content: '';    flex: 1;    height: 1px;    background: #e5e7eb;}.divider span {    color: #9ca3af;    white-space: nowrap;}.googleBtn {    height: 48px !important;    font-size: 14px !important;    border-radius: 10px !important;    margin-top: 12px !important;    background: #ffffff !important;    border: 1px solid #e5e7eb !important;    color: #374151 !important;    transition: background 0.2s, border-color 0.2s !important;    cursor: pointer;}.googleBtn:hover {    background: #eff6ff !important;    border-color: rgba(30, 64, 175, 0.25) !important;    color: #1e40af !important;}.signupRow {    text-align: center;    font-size: 13px;    color: #6b7280;    margin-top: 28px;}.signupLink {    color: #1e40af;    font-weight: 500;    text-decoration: none;    cursor: pointer;}.signupLink:hover {    text-decoration: underline;    color: #1d4ed8;}

源代码

  • GitHub:https://github.com/BugShare404/animated-login
  • Gitee:https://gitee.com/bugshare/animated-login

vue2分支是Vue2 + Element-ui实现。

文章版权声明:除非注明,否则均为边学边练网络文章,版权归原作者所有