《Vue3+TS》开发一个自己的起始页

Posted Oliver尹

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了《Vue3+TS》开发一个自己的起始页相关的知识,希望对你有一定的参考价值。

前言

之前习惯使用的是百度的作为浏览器的首页,百度登录账号后可以在首页上收藏很多书签,但是近期因为使用不同电脑协同办公,导致一台电脑登录百度账号后,其它的电脑的百度会被提下线,这就很烦,还不如自己做个起始页;

效果图

项目下载

项目代码已上传,可以前往下载相关代,下载地址如下:下载地址,如果不能下载且有需要的小伙伴及时联系博主或评论留下邮箱,我看到后会第一时间发送代码;

技术栈

本项目前台的主要技术栈使用的是:Vue3+VueX+VueRouter+Axios+TypeScript,后台主要是用的Node+TypeScript,当然本文主要讲述的是前台相关,后台部分代码有兴趣的可以留言;

分析

在开始码代码前简单的分析一下,理一下逻辑,同时看看有可能会涉及到哪些知识点,这样在准备阶段大致心里就有了一个底,对于重复度高的部分可以提前预计到并封装成组件,公共样式也尽量提前抽离;

思维导图

功能点概述

首页

主要用途是搜索,有搜索框组件和时钟组件组成,并且搜索框组件聚焦时可以看到历史记录以及热门搜索,同时聚焦时背景图上有一个放大以及毛玻璃的效果;

快捷页

快捷入口的集合,打开时背景图同样有一个放大以及毛玻璃的效果;

另外,在整个项目里,使用到了lowdb来对数据进行缓存和读取(其实就是localstage),使用到了loadsh作为函数库

初始化项目

既然是基于Vue,那么开始必须是基于Vue3的脚手架搭建项目,具体命令如下:

vue create start

注意的是,在配置项目的时候需要选择安装TypeScript,以及使用到了CSS的预处理器:less,实际上我觉得Sass更符合我个人的使用习惯,但是公司的项目都是用的less,那么这里也就继续使用less作为预处理器,两者其实差不多,没什么根本性的不同;

功能点

感觉没有必要将每一行代码都放在博客里,那样的话感觉整篇博客都是长篇代码而且效果也不好,博客里又不能调试,要看整体代码直接下载项目就是了,还能一边看一边调试,本文主要记录一下涉及到的功能点的代码,方便理解Vue3+Typescript(由于代码写的比较早,现在回过头来看还是又一些可以优化的地方的,写的不好的地方多多见谅);

布局组件

这个组件的主要目的是用来做布局,因为新的界面交付到我们开发手里时,不能直接开始写代码,肯定要写一些布局组件,用来对页面进行合理的布局,布局组件写完后,只要在对应的位置填入对应的组件就行了,这样就不用在每一个页面都写一遍布局,这样不科学也太累,一旦需求发生变化,如果每个页面都是进行布局那工作了就成倍的增加了;
布局组件部分代码如下,在本项目中这个布局组件除了用来布局,还用来做各个子组件的数据的一个交互的链接点,因为不管是快捷入口还是搜索栏组件都会对背景组件的状态产生修改,一旦状态发生变化,那么这个组件就可以用来临时做数据的中转:

<template>
    <base-layout :shortcuts="shortcuts">
        <template #backgourndImage>
            <base-background
                :glass="glass"
                @changeGlass="changeGlass"
                :shortcuts="shortcuts"
                @changeShortcuts="changeShortcuts"
            >
                <template #clock>
                    <clock-component
                        :glass="glass"
                        :search="searchState"
                        v-show="!shortcuts"
                    />
                </template>
            </base-background>
        </template>
        <template #search>
            <search-component
                @changeFocus="changeFocus"
                :glass="glass"
                v-show="!shortcuts"
            />
        </template>
        <template #copyright>
            <copyright-component />
        </template>
        <template #shortcuts>
            <shortcutsComponent v-show="shortcuts" />
        </template>
    </base-layout>
</template>
// 组件、库、接口等引入

<script lang="ts">
	 // ...组件注册
	 setup() 
        const store: Store<any> = useStore();
        const glass: Ref<boolean> = ref(false);
        const searchState: Ref<boolean> = ref(false);
        const shortcuts: Ref<boolean> = ref(false);

        function changeGlass(value: boolean) 
            changeComponentState(value);
        
        function changeFocus(value: boolean) 
            glass.value = value;
            searchState.value = value;
        
        function changeShortcuts(value: boolean) 
            changeComponentState(value);
        

        function changeComponentState(value: boolean) 
            glass.value = value;
            shortcuts.value = value;
        
        /**
         * 基本信息
         * @returns Object 基本信息
         */
        async function getBasicInfo(): Promise<any> 
            const basicInfo: any = await GetBasicInfo();
            return new Promise((resolve) => 
                resolve(basicInfo);
            );
        

        /**
         * 初始化基本信息
         */
        async function initBasic() 
            // 初始化本地信息
            const info = await store.dispatch("basic/basic/load");

            // 本地无数据,启用默认参数
            if (loadsh.isEmpty(info)) 
                const data: dataApiInterface = await getBasicInfo();
                const saveInfo = await store.dispatch(
                    "basic/basic/saveBasicInfo",
                    data
                );

                if (loadsh.isEmpty(saveInfo))
                    console.error("初始化异常,请联系管理员");
            
        
        /**
         * 初始化
         */
        async function init(): Promise<void> 
            // 初始化基本信息
            await initBasic();
        

        init();

        return 
            glass,
            searchState,
            shortcuts,
            changeGlass,
            changeFocus,
            changeShortcuts,
        ;
    ,
</script>

我们写了一个base-layout的组件用来做布局,并且在组件中预留了4个插槽,分别用来放置背景组件,主界面,快捷入口,版权信息,这样整体的结构不会凌乱,而base-layout部分代码如下:

<template>
    <div class="threeS-home-container">
        <div class="threeS-home-bg">
            <slot name="backgourndImage"></slot>
        </div>
        <div class="threeS-home-search">
            <slot name="search"></slot>
        </div>
        <div class="threeS-home-copyright">
            <slot name="copyright"></slot>
        </div>
        <div class="threeS-home-shortcuts" v-if="shortcuts">
            <slot name="shortcuts"></slot>
        </div>
    </div>
</template>

<script lang="ts">
import  defineComponent  from "vue";

export default defineComponent(
    props: 
        shortcuts: 
            type: Boolean,
            default: false,
        ,
    ,
    setup() ,
);
</script>

<style scoped lang="less">
// ... 具体的样式
</style>

就是做了一个布局,对每一块内容都进行圈定范围,并且预留了插槽用来内容填充;

背景组件

这个组件主要的功能就是背景图的放大以及毛玻璃,这两块效果其实都是CSS实现的,JS实现的主要是在什么时机触发这个效果,在本项目中,触发的时机一共有两个:

  1. 处于首页的时候,输入框聚焦,此时的背景组件会触发放大和毛玻璃效果;
  2. 右击首页,打开快捷入口的时候,也会触发放大和毛玻璃效果;

部分代码如下:

<script lang="ts">
import  defineComponent, computed, ref, nextTick, watch  from "vue";

import  useStore, Store  from "vuex";

export default defineComponent(
    props: 
        glass: 
            type: Boolean,
            default: false,
        ,
        shortcuts: 
            type: Boolean,
            default: false,
        ,
    ,
    setup(props, context) 
        // 表单DOM
        const threeSBgRef: any = ref(null);
        // 获取vuex信息
        const store: Store<any> = useStore();

        // 背景图路径
        const imgSrcPath = ref("");
        // 背景图状态,false-隐藏,true-显示
        const showImg = ref(false);
        // 背景图信息
        const bagImg = computed(() => store.getters["basic/basic/getBg"]);

        /**
         * 背景图DOM
         * @returns htmlElement 背景图DOM
         */
        async function getImgDom(): Promise<any> 
            return new Promise((resolve) => 
                nextTick(() => 
                    resolve(threeSBgRef.value);
                );
            );
        

        watch(
            () => bagImg.value,
            async (value) => 
                if (Array.isArray(value)) 
                    return false;
                

                const img: HTMLElement = await getImgDom();
                img.onload = () => 
                    showImg.value = true;
                ;

                imgSrcPath.value = value.path;
            
        );

        /**
         * 右击
         */
        function handleRightClick(): void | boolean 
            if (imgSrcPath.value === "") return false;
            context.emit("changeShortcuts", !props.shortcuts);
        

        /**
         * 左击
         */
        function handleLeftClick(): void | boolean 
            if (!props.glass) return false;
            context.emit("changeGlass", false);
        
        return 
            imgSrc: computed(() =>
                imgSrcPath.value === ""
                    ? null
                    : require(`../../../../../src/assets/bg/$imgSrcPath.value`)
            ),
            showImg,
            threeSBgRef,
            handleRightClick,
            handleLeftClick,
        ;
    ,
);
</script>

另外,这里还有一个骨架屏的效果,就是当背景图还没有被加载的时候,整个背景图处于灰色,并且有一个从左往右的动效,代表正在加载中,骨架屏效果代码如下:

.threeS-loading 
    background-color: #f2f2f2;

    background: linear-gradient(
            100deg,
            rgba(255, 255, 255, 0) 40%,
            rgba(255, 255, 255, 0.5) 50%,
            rgba(255, 255, 255, 0) 60%
        )
        #f6f6f6;
    background-size: 200% 100%;
    background-position-x: 120%;

    animation: 1s loading ease-in-out infinite;


@keyframes loading 
    to 
        background-position-x: -20%;
    

时钟组件

这个组件的难点在于实现电子数字的格式,这个是最复杂的,而时间的获取就是基于浏览器Date对象,这个很简单,获取时间的方法部分如下:

/**
 * 获得时间
 * @returns String 当前时间
 */
function getTime(): string 
    const date = new Date();

    const hour = date.getHours();
    const minute = date.getMinutes();
    return (
        (hour >= 10 ? hour : "0" + hour) +
        ":" +
        (minute >= 10 ? minute : "0" + minute)
    );

当获取到时间以后,需要将转成电子格式的,这部分的原理就是先拼出一个电子格式的8,然后,不同的数字只是去掉8中的某一个笔划,这样就达到了0-9的数字,这里我挑一部分CSS代码

.digits div span 
    opacity: 0;
    position: absolute;
    -webkit-transition: 0.25s;
    -moz-transition: 0.25s;
    transition: 0.25s;

.digits div span:before,
.digits div span:after 
    content: "";
    position: absolute;
    width: 0;
    height: 0;
    border: @fontSize solid transparent;
 

.digits .d1 
    height: @fontSize;
    width: 16px;
    top: 0;
    left: 6px;

.digits .d1:before 
    border-width: 0 @fontSize @fontSize 0;
    border-right-color: inherit;
    left: -@fontSize;

.digits .d1:after 
    border-width: 0 0 @fontSize @fontSize;
    border-left-color: inherit;
    right: -@fontSize;


.digits .d2 
    height: @fontSize;
    width: 16px;
    top: 24px;
    left: 6px;

.digits .d2:before 
    border-width: 3px 4px 2px;
    border-right-color: inherit;
    left: -8px;

.digits .d2:after 
    border-width: 3px 4px 2px;
    border-left-color: inherit;
    right: -8px;

这里是1和2的组成,通过类似的方法将0-9的CSS都写完,之后获取到时间,在填入的时候判断需要填入的是哪个数字就行:

 /**
 * 填入时间
 * @param String time - 时间
 * @param HTMLElement timerClock - 填入时间的DOM
 * @returns 无
 */
function clocknum(time: string, timerClock: HTMLElement): void 
    timerClock.innerHTML = "";

    var html = "";
    var strarr: any[] = time.toString().split("");
    var digit_to_name = "zero one two three four five six seven eight nine".split(
        " "
    );
    for (var i = 0; i < strarr.length; i++) 
        if (strarr[i] == ":") 
            html += '<div class="dot"></div>';
         else 
            var clasname = digit_to_name[strarr[i]];
            html +=
                '<div class="' +
                clasname +
                '">' +
                '<span class="d1"></span>' +
                '<span class="d2"></span>' +
                '<span class="d3"></span>' +
                '<span class="d4"></span>' +
                '<span class="d5"></span>' +
                '<span class="d6"></span>' +
                '<span class="d7"></span>' +
                "</div>";
        
    

    timerClock.innerHTML = html;

这样时间组件就算基本完成了,使用的时候只需要引入主页,放在对应的位置就可以了;

搜索组件

搜索组件一共分为两部分,需要单独实现,分别为:搜索框,搜索历史(热门搜索);

搜索框

这个其实简单,其实就是一个输入框,并且在其上附加了一个聚焦事件,用于和背景组件的联动,确认搜索的时候根据搜索类型打开对应的页签就可以了,并且确认搜索的时候,将该条搜索条件作为记录存储到本地缓存中,所谓历史记录的一部分,部分实现代码如下:

<!-- 输入框 -->
<div
    :class="[
        'threeS-search-container',
         'threeS-search-hover': mouseEnter ,
         'threeS-search-active': glass ,
    ]"
    @mouseenter="handleMouseEvent"
    @mouseleave="handleMouseEvent"
>
    <div class="threeS-search-body">
        <input
            class="threeS-search-input"
            type="text"
            :placeholder="glass ? '' : '搜索内容'"
            @focus="handleFocus"
            v-model="searchValue"
            @keyup.enter="handleSearch"
        />
        <button
            v-if="glass"
            class="icon iconfont icon-magnifier search-icon"
            @click="handleSearch"
        ></button>
    </div>
</div>
<!-- 搜索类型 -->
<div
    :class="[
        'threeS-search-type-container',
         'threeS-type-focus': glass ,
    ]"
>
    <div
        class="threeS-search-type"
        以上是关于《Vue3+TS》开发一个自己的起始页的主要内容,如果未能解决你的问题,请参考以下文章

VSCode插件开发全攻略代码片段设置自定义欢迎页

vue3+vite+ts 搭建项目

vue3.0+ts+element-plus多页签应用模板:头部工具栏(下)

前端实战:electron+vue3+ts开发桌面端便签应用

博客主题 Lite

vue3+ts之忽略ts类型警告