Kaynağa Gözat

feat: 登录页

qinlaiqiao 3 yıl önce
ebeveyn
işleme
0402d2c592

BIN
src/assets/login-aperture.png


BIN
src/assets/login-bg-poster.jpg


BIN
src/assets/login-bg-video.mp4


BIN
src/assets/login-bg-video2.mp4


BIN
src/assets/login-btn.png


BIN
src/assets/login-header-bg1.png


BIN
src/assets/login-header-bg2.png


BIN
src/assets/login-header-bg3.png


Dosya farkı çok büyük olduğundan ihmal edildi
+ 1 - 0
src/assets/login-input-password-icon.svg


Dosya farkı çok büyük olduğundan ihmal edildi
+ 1 - 0
src/assets/login-input-user-icon.svg


BIN
src/assets/login-module-item-hover.png


BIN
src/assets/login-module-item-normal.png


BIN
src/assets/login-panel.png


BIN
src/assets/login-rotate-blue.png


BIN
src/assets/login-rotate-yellow.png


BIN
src/assets/logo.png


+ 21 - 0
src/router/index.ts

@@ -0,0 +1,21 @@
+import {createRouter, createWebHistory, RouteRecordRaw} from 'vue-router';
+
+const routes: Array<RouteRecordRaw> = [
+    {
+        path: '/',
+        name: 'Home',
+        component: () => import(/* webpackChunkName: "home" */ '@/views/home/Home.vue'),
+    },
+    {
+        path: '/login',
+        name: 'Login',
+        component: () => import(/* webpackChunkName: "login" */ '@/views/login/Login.vue'),
+    },
+];
+
+const router = createRouter({
+    history: createWebHistory(import.meta.env.BASE_URL),
+    routes,
+});
+
+export default router;

+ 19 - 0
src/utils/frontend/loadImage.ts

@@ -0,0 +1,19 @@
+const load = async (imgSrc: string) => {
+    return new Promise(resolve => {
+        let image = new Image();
+        image.onload = () => resolve(image);
+        image.onerror = () => resolve(null);
+        image.src = imgSrc;
+    });
+};
+
+// 预加载图片
+export async function loadImage(imgSrcs: string | string[]) {
+    if (typeof imgSrcs === "string") {
+        await load(imgSrcs);
+    } else if (Array.isArray(imgSrcs)) {
+        await Promise.all(imgSrcs.map(async src => {
+            await load(src);
+        }))
+    }
+}

+ 19 - 0
src/views/home/Home.vue

@@ -0,0 +1,19 @@
+<script setup lang="ts">
+import {ref} from 'vue'
+
+</script>
+
+<template>
+  <article class="home-page">
+    <h1 class="title">主页-HOME</h1>
+    <router-link to="/login">
+      <el-button size="large" type="primary">登录</el-button>
+    </router-link>
+  </article>
+</template>
+
+<style scoped>
+.home-page {
+  @apply flex flex-col items-center text-8xl mt-80;
+}
+</style>

+ 317 - 0
src/views/login/Login.vue

@@ -0,0 +1,317 @@
+<script setup lang="ts">
+import {onMounted, ref} from 'vue'
+import useRotateCanvas from "./scripts/rotateCanvas";
+import usePreloadImg from './scripts/preloadImg'
+import useModuleItem from './scripts/moduleItem'
+
+// video 元素的模板引用
+const loginVideoRef = ref<HTMLVideoElement>()
+// 设置视频播放速度
+const setVideoRate = (rate = 0.75) => {
+  if (loginVideoRef.value)
+    loginVideoRef.value.playbackRate = rate
+}
+
+onMounted(() => {
+  // 设置视频播放速度
+  setVideoRate()
+  // 预加载 模块item hover图片
+  usePreloadImg()
+})
+
+// 底部旋转 canvas
+const {canvasRef} = useRotateCanvas()
+const {
+  moduleRef,
+} = useModuleItem()
+</script>
+
+<template>
+  <article class="login-page">
+    <!-- 视频背景 -->
+    <video ref="loginVideoRef" class="video-bg" poster="@/assets/login-bg-poster.jpg" autoplay loop muted>
+      <source src="@/assets/login-bg-video.mp4" type="video/mp4"/>
+    </video>
+
+    <!-- 标题 -->
+    <header class="header animate__animated animate__fadeInDownBig">
+      <span class="left span"></span>
+      <span class="center span">
+        <i class="text">珠海市政府投资项目经济技术指标库</i>
+      </span>
+      <span class="right span"></span>
+    </header>
+
+    <main class="main">
+      <!-- 登陆面板 -->
+      <div class="panel">
+        <!-- 用户名 -->
+        <div class="input-wrap username">
+          <input class="input" type="text" placeholder="用户名">
+          <i class="line"/>
+        </div>
+        <!-- 密码 -->
+        <div class="input-wrap password">
+          <input class="input" type="password" placeholder="密码">
+          <i class="line"/>
+        </div>
+        <!-- 登录按钮 -->
+        <div class="login-btn">
+          <button class="button">登录</button>
+          <div class="flash-wrap">
+            <div class="flash"></div>
+          </div>
+        </div>
+      </div>
+      <!-- 旋转光圈 -->
+      <div class="aperture"></div>
+      <!-- 旋转底座 -->
+      <div class="rotating-base">
+        <canvas ref="canvasRef" width="1800" height="600"></canvas>
+      </div>
+      <!-- 功能模块 -->
+      <ul class="module" ref="moduleRef">
+        <li class="item budget animate__animated animate__bounceIn" ref="budgetRef">预算审核模块</li>
+        <li class="item estimate animate__animated animate__bounceIn" ref="estimateRef">估/概算模块</li>
+        <li class="item util animate__animated animate__bounceIn" ref="utilRef">基础工具模块</li>
+        <li class="item settlement animate__animated animate__bounceIn" ref="settlementRef">结算审核模块</li>
+        <li class="item final animate__animated animate__bounceIn" ref="finalRef">决算审核模块</li>
+        <li class="item check animate__animated animate__bounceIn" ref="checkRef">检测功能模块</li>
+      </ul>
+    </main>
+
+  </article>
+</template>
+
+<style lang="scss" scoped>
+@keyframes aperture-rotate {
+  to {
+    transform: rotate(1turn);
+  }
+}
+
+@keyframes flash-change {
+  from {
+    left: -100%;
+  }
+
+  to {
+    left: 150%;
+  }
+}
+
+.login-page {
+  @apply w-full h-full overflow-hidden;
+  //background-color: #011531;
+
+  .video-bg {
+    @apply fixed -z-10 right-0 bottom-0 min-w-full min-h-full h-auto w-auto object-fill;
+    filter: brightness(0.45);
+  }
+
+  .header {
+    @apply flex justify-center w-full bg-contain bg-no-repeat bg-center text-center text-4xl select-none;
+    height: 68px;
+
+    .span {
+      background-size: 100% 100%;
+
+      &.left {
+        flex: 100 1 auto;
+        background-image: url("@/assets/login-header-bg1.png");
+      }
+
+      &.center {
+        flex: 1 0 590px;
+        background-image: url("@/assets/login-header-bg2.png");
+
+        .text {
+          @apply text-center font-bold bg-clip-text;
+          font-size: 36px;
+          line-height: 64px;
+          background-image: linear-gradient(0deg, #3084e8, #adf0f6 80%);
+          -webkit-text-fill-color: transparent;
+        }
+      }
+
+      &.right {
+        flex: 100 1 auto;
+        background-image: url("@/assets/login-header-bg3.png");
+      }
+    }
+  }
+
+  .main {
+    @apply relative flex justify-center items-center;
+    height: calc(100% - 68px);
+
+    .panel {
+      @apply relative z-40 flex items-center justify-center flex-col;
+      width: 487px;
+      height: 487px;
+      margin-top: -80px;
+      background: url("@/assets/login-panel.png") no-repeat;
+
+      .input-wrap {
+        @apply flex justify-center relative;
+        width: 250px;
+        height: 40px;
+        margin-top: 40px;
+        border-bottom: 2px solid #145da5;
+        background: no-repeat 0 center;
+        background-size: 31px 31px;
+
+        &.username {
+          background-image: url("@/assets/login-input-user-icon.svg");
+        }
+
+        &.password {
+          margin-top: 36px;
+          background-image: url("@/assets/login-input-password-icon.svg");
+        }
+
+        .input {
+          @apply w-full h-full outline-none bg-transparent;
+          font-size: 21px;
+          color: #8ae2ff;
+          padding-left: 50px;
+
+          &::-ms-input-placeholder {
+            color: #145da5;
+          }
+
+          &::-moz-placeholder {
+            color: #145da5;
+          }
+
+          &::-webkit-input-placeholder {
+            color: #145da5;
+          }
+
+          &:focus {
+            + .line {
+              @apply w-full;
+            }
+          }
+        }
+
+        .line {
+          @apply absolute w-0;
+          height: 2px;
+          content: ' ';
+          bottom: -2px;
+          background-color: #459ef7;
+          transition: width 0.2s;
+        }
+      }
+
+      .login-btn {
+        @apply relative;
+        margin-top: 40px;
+
+        .button {
+          width: 120px;
+          height: 40px;
+          line-height: 40px;
+          background: url('@/assets/login-btn.png') no-repeat;
+          background-size: 100%;
+          color: #9ae1f9;
+          font-size: 18px;
+        }
+
+        .flash-wrap {
+          @apply absolute top-0 w-full h-full cursor-pointer;
+          clip-path: inset(0 0 round 10px);
+          transition: all 0.2s;
+
+          &:hover {
+            .flash {
+              animation: flash-change 1s ease 0s;
+            }
+          }
+
+          .flash {
+            @apply absolute top-0 w-1/3 h-full;
+            left: -100%;
+            background: linear-gradient(to right, rgba(255, 255, 255, 0) 0, rgba(255, 255, 255, 0.4) 50%, rgba(255, 255, 255, 0) 100%);
+            transform: skewX(-45deg);
+            animation-iteration-count: infinite;
+          }
+        }
+      }
+    }
+
+    .aperture {
+      @apply absolute z-20;
+      width: 617px;
+      height: 617px;
+      margin-top: -80px;
+      background: url("@/assets/login-aperture.png") no-repeat center center;
+      background-size: contain;
+      animation: aperture-rotate 9s linear infinite;
+    }
+
+    .rotating-base {
+      @apply absolute z-10;
+      transform: scale(0.6);
+      margin-top: 450px;
+      filter: brightness(2);
+    }
+
+    .module {
+      @apply absolute z-30 flex justify-center items-center;
+      width: 1100px;
+      height: 500px;
+      margin-top: -60px;
+
+      .item {
+        @apply absolute flex justify-center cursor-pointer transform-gpu select-none opacity-95;
+        width: 180px;
+        height: 156px;
+        color: #44C4EF;
+        font-size: 22px;
+        padding-top: 40px;
+        background: url("@/assets/login-module-item-normal.png") no-repeat center;
+        background-size: cover;
+        transition: all 0.26s;
+
+        &.can-hover:hover {
+          @apply opacity-100;
+          background-image: url("@/assets/login-module-item-hover.png");
+          transform: scale(1.06);
+        }
+
+        &.budget {
+          left: 40px;
+          top: 0;
+        }
+
+        &.estimate {
+          left: 0;
+          top: 170px;
+        }
+
+        &.util {
+          left: 80px;
+          top: 340px;
+        }
+
+        &.settlement {
+          right: 40px;
+          top: 0;
+        }
+
+        &.final {
+          right: 0;
+          top: 170px;
+        }
+
+        &.check {
+          right: 80px;
+          top: 340px;
+        }
+      }
+    }
+  }
+}
+</style>

+ 33 - 0
src/views/login/scripts/moduleItem.ts

@@ -0,0 +1,33 @@
+import {onMounted, ref} from 'vue'
+
+export default function useModuleItem() {
+    const budgetRef = ref<HTMLLIElement>()
+    const estimateRef = ref<HTMLLIElement>()
+    const utilRef = ref<HTMLLIElement>()
+    const settlementRef = ref<HTMLLIElement>()
+    const finalRef = ref<HTMLLIElement>()
+    const checkRef = ref<HTMLLIElement>()
+
+    const moduleRef = ref<HTMLUListElement>()
+
+    onMounted(() => {
+        if (moduleRef.value) {
+            const ul = moduleRef.value
+            const list = ul.querySelectorAll('.item')
+            list.forEach(item => {
+                item.classList.remove('animate__animated')
+                item.classList.add('can-hover')
+            })
+        }
+    })
+
+    return {
+        budgetRef,
+        estimateRef,
+        utilRef,
+        settlementRef,
+        finalRef,
+        checkRef,
+        moduleRef
+    }
+}

+ 6 - 0
src/views/login/scripts/preloadImg.ts

@@ -0,0 +1,6 @@
+import hoverImgUrl from '@/assets/login-module-item-hover.png'
+import {loadImage} from "../../../utils/frontend/loadImage";
+
+export default function usePreloadImg() {
+    loadImage(hoverImgUrl)
+}

+ 117 - 0
src/views/login/scripts/rotateCanvas.ts

@@ -0,0 +1,117 @@
+import {onMounted, ref} from "vue";
+import blueImageUrl from '@/assets/login-rotate-blue.png'
+import yellowImageUrl from '@/assets/login-rotate-yellow.png'
+
+
+interface IPoint {
+    x: number;
+    y: number;
+}
+
+class CircleItem {
+    ctx: CanvasRenderingContext2D
+    img: HTMLImageElement
+    r: number
+    w: number
+    h: number
+    x: number
+    y: number
+    speed: number
+    rectCenterPoint: IPoint
+    cacheCanvas: HTMLCanvasElement
+    cacheCtx: CanvasRenderingContext2D
+
+    constructor(ctx: CanvasRenderingContext2D, img: HTMLImageElement, r: number, x: number, y: number, speed: number) {
+        this.ctx = ctx
+        this.img = img;
+        this.r = r;
+        this.w = r * 2;
+        this.h = r * 2;
+        this.x = x;
+        this.y = y;
+        this.speed = speed;
+        this.rectCenterPoint = {
+            x: r,
+            y: r,
+        };
+
+        this.cacheCanvas = document.createElement("canvas")
+        this.cacheCtx = this.cacheCanvas.getContext("2d") as CanvasRenderingContext2D
+        this.cache();
+    }
+
+    draw() {
+        this.rotate();
+        this.ctx.scale(1, 0.3);
+        this.ctx.drawImage(
+            this.cacheCanvas,
+            0,
+            0,
+            this.w,
+            this.h,
+            this.x - this.r,
+            this.y - this.r,
+            this.r * 2,
+            this.r * 2
+        );
+        this.ctx.scale(1, 1 / 0.3);
+    }
+
+    cache() {
+        this.cacheCanvas.width = this.w;
+        this.cacheCanvas.height = this.h;
+        this.cacheCtx.save();
+        this.cacheCtx.beginPath();
+        this.cacheCtx.drawImage(this.img, 0, 0, this.r * 2, this.r * 2);
+        this.cacheCtx.closePath();
+        this.cacheCtx.restore();
+    }
+
+    rotate() {
+        this.cacheCtx.clearRect(0, 0, this.w, this.h);
+        this.cacheCtx.translate(this.rectCenterPoint.x, this.rectCenterPoint.y);
+        this.cacheCtx.rotate((this.speed * Math.PI) / 180);
+        this.cacheCtx.translate(-this.rectCenterPoint.x, -this.rectCenterPoint.y);
+        this.cacheCtx.drawImage(this.img, 0, 0, this.r * 2, this.r * 2);
+    }
+}
+
+// 旋转底座
+export default function useRotateCanvas() {
+    const CANVAS_W = 1800
+    const CANVAS_H = 600
+    const canvasRef = ref<HTMLCanvasElement>();
+    onMounted(() => {
+        if (canvasRef.value) {
+            const canvas = canvasRef.value
+            const ctx = canvas.getContext("2d") as CanvasRenderingContext2D;
+
+            const blueImg = new Image();
+            blueImg.src = blueImageUrl;
+            let rotateBlue: CircleItem
+            blueImg.onload = function () {
+                rotateBlue = new CircleItem(ctx, blueImg, 750, CANVAS_W / 2, 800, 1);
+            };
+
+            const yellowImg = new Image();
+            yellowImg.src = yellowImageUrl;
+            let rotateYellow: CircleItem
+            yellowImg.onload = function () {
+                rotateYellow = new CircleItem(ctx, yellowImg, 400, CANVAS_W / 2, 800, 0.6);
+            };
+
+            function animate() {
+                ctx.clearRect(0, 0, CANVAS_W, CANVAS_H);
+                rotateBlue && rotateBlue.draw();
+                rotateYellow && rotateYellow.draw();
+                requestAnimationFrame(animate);
+            }
+
+            animate();
+        }
+    })
+
+    return {
+        canvasRef
+    }
+}