用vue實現一個簡單的時間屏幕


前言

去年用了一個小的 app,叫做 一個木函,本來想着用來做動畫優化就刪掉了的,不過看到他有個時間屏幕的小工具,就點進去看了下,覺得挺好玩的,就想着能不能自己實現一下。

ps: 閑話不多說,先上例子點我查看,覺得沒啥意思的就不需要再往下看了

簡單的搭建一下

初始化一個 vue 項目

vue create vue-time

然后無腦下一步就好了(回車),這里我打算用 scss 方便我們書寫樣式 ,所以創建完成后,我們在安裝下 scss

cd vue-time
npm i sass-loader node-sass -D

ps: 如果網絡不好,就換下源或者用 cnpm

新建時間屏幕組件

components 目錄下新建 TimeScreen.vue 文件,然后通過 vbase 指令生成生成一個最基礎的 vue 代碼片段

ps: vbasevscode 中vue代碼片段的一個插件

<template>
    <div>

    </div>
</template>

<script>
    export default {
        name: "TimeScreen"
    }
</script>

<style lang="scss" scoped>

</style>

思考一下,如何做時間切換的動畫

emmm... 不知是否有看過我之前的一篇文章用jq實現的單個span為單個的數字動畫,沒錯,其實我們實現的思路,和這里基本一樣,所以接下來我們就分析分析我們該怎么來實現了吧

首先,我們要明確一下,要有多少個 span,眾所周知,一天最后就是23:59:59,所以我們所需要的 span 數組為 [3, 10, 0, 6, 10, 0, 6, 10]

ps: 中間的 : 是只需要一個 span

因為布局不是我們要將的重點,所以我們就想想我們該怎么獲取我們需要的東西。比如,怎么獲取到Saturday 14 March,怎么獲取到當前時間

眾所周知啦,我們知道 js 中提供了 Date 這個對象,所以我們可以通過他可以獲取我們想要的東西,廢話不多說了,開始寫代碼吧。

新建 utils 目錄,在該目錄下新建 time.js 文件,內容如下

// 月份
const months = [
    "January","February","March","April","May","June",
    "July","August","September","October","November","December"
];
// 星期
const weekday = [
    "Sunday","Monday","Tuesday","Wednesday",
    "Thursday","Friday","Saturday"
];
// 獲取日期
function getTime() {
    const date = new Date();
    const days = date.getDate();
    const month = date.getMonth();
    const day = date.getDay();
    const hours = toDou(date.getHours());
    const minutes = toDou(date.getMinutes());
    const seconds = toDou(date.getSeconds());
    return {
        date: `${weekday[day]} ${days} ${months[month]}`,
        time: `${hours}:${minutes}:${seconds}`
    };
}
// 轉成兩位 eg: 6 => 06
function toDou(str) {
    const num = ~~str;
    return num > 9 ? num : "0" + str;
}

// 測試一下我們寫的方法,上線記得注釋掉
// console.log(getTime()) // {date: "Saturday 14 March", time: "18:53:40"}

export default getTime

通過上面代碼,我們就得到我們需要的格式啦,接下來就是寫布局啦,但這里不是我們的重點,所以略過

<template>
    <div
        class="time-container"
        :class="{ dark: isDark }"
        @click="toggleClass"
        :date="date"
    >
        <div class="time">
            <template v-for="(str, idx) in time">
                <div
                    class="time-num"
                    v-if="str !== ':'"
                    :style="numStyle[idx]"
                    :key="idx"
                >
                    <span
                        v-for="(i, spanIdx) in haveSpan[idx]"
                        :key="spanIdx"
                        >{{ i - 1 }}</span
                    >
                    <span>0</span>
                </div>
                <div class="time-dist" v-else :key="idx">
                    <span>{{ str }}</span>
                </div>
            </template>
        </div>
    </div>
</template>

<script>
import getTime from "../utils/time";
// 設置樣式
function setStyle(val) {
    return `transform: translateY(-${~~val * 100}%)`;
}
// 每個字的樣式
function numStyle(time) {
    return time.split("").map(val => setStyle(val));
}
export default {
    name: "TimeScreen",
    data() {
        const { time,date } = getTime();
        return {
            isDark: 0,
            time,
            date,
            numStyle: numStyle(time)
        };
    },
    methods: {
        // 切換樣式
        toggleClass() {
            this.isDark = !this.isDark;
        },
    },
    created() {
        // 判單有多少個Span
        // 比較小時數最多24小時,所以第一位最多是3個,0、1、2 
        // 這里使用 freeze 是因為這個值已經固定,沒有必要進行數據劫持
        this.haveSpan = Object.freeze([3, 10, 0, 6, 10, 0, 6, 10]);
    }
};
</script>
<style lang="scss" scoped>
%flexCenter {
    display: flex;
    justify-content: center;
    align-items: center;
}
$timeColor: #d9d4d0;
$white: #fff;
.time-container {
    background: $white;
    color: $timeColor;
    position: absolute;
    width: 100%;
    height: 100%;
    max-width: 540px;
    top: 0;
    left: 50%;
    transform: translateX(-50%);
    &.dark {
        background: #000;
        color: $white;
    }
    &::after {
        content: attr(date);
        position: absolute;
        color: $timeColor;
        font-size: 18px;
        line-height: 1;
        transform: rotate(90deg);
        bottom: 20%;
        left: -48px;
    }
    @extend %flexCenter;
    .time {
        font-size: 70px;
        transform: rotate(90deg);
        position: relative;
        height: 106px;
        line-height: 106px;
        overflow: hidden;
        @extend %flexCenter;

        .time-num {
            position: relative;
            width: 100%;
            height: 100%;
            text-align: center;
            text-shadow: 0 0 2px $white;
            transition: 0.5s all;
            span {
                display: block;
            }
        }
        .time-dist {
            padding-bottom: 15px;
            margin: 0 10px;
        }
    }
}
</style>

寫到這里,其實基本的樣子已經出來了,這里我們用到了 attr 函數,用來回選擇元素的屬性值,這個小技巧在一些場景很用用哦。

這里我們多加了一個 <span>0</span> 這里主要是為了無縫,那么我們如何做到無縫呢?即,當我們滾動最低下的時候,在500ms之內讓動畫取消。

ps: 這里的 500ms 是因為動畫設置了 500ms,所以需要用 1s - 500ms 得出來的500ms

既然知道了原理,那么我們就開始寫我們的代碼了

首先定義一下清空動畫之后的樣式,即

// 清除樣式
const style = "transform: translateY(0%);transition:0s all";

那么什么時候清空呢,前面也說了,當滾動到最下面的時候,也就是當 time 這個字符串某個為 0 的時候,我們就要清空了,所以

this.time.split("").forEach((val, idx) => {
    // 當 val 為 0 時,說明已經滾到最底下,這里需要清除動畫,並讓他回到最頂上來實現無縫
    if (val == 0) {
        if (this.numStyle[idx] !== style) {
            // 500ms后清除當前這個span的動畫
            this.removeAnimate(idx);
            // 設置樣式
            this.numStyle[idx] = setStyle(this.haveSpan[idx]);
        }
    } else {
        this.numStyle[idx] = setStyle(val);
    }
})

// 清除動畫
removeAnimate(idx) {
    setTimeout(() => {
        this.numStyle[idx] = style;
        this.numStyle = [...this.numStyle];
    }, 500);
}

最后,就是寫一個簡單的定時器啦,我想這應該難不倒各位小伙伴啦,所以我就不詳解啦,就貼一下代碼

<template>
    <div
        class="time-container"
        :class="{ dark: isDark }"
        @click="toggleClass"
        :date="date"
    >
        <div class="time">
            <template v-for="(str, idx) in time">
                <div
                    class="time-num"
                    v-if="str !== ':'"
                    :style="numStyle[idx]"
                    :key="idx"
                >
                    <span
                        v-for="(i, spanIdx) in haveSpan[idx]"
                        :key="spanIdx"
                        >{{ i - 1 }}</span
                    >
                    <span>0</span>
                </div>
                <div class="time-dist" v-else :key="idx">
                    <span>{{ str }}</span>
                </div>
            </template>
        </div>
    </div>
</template>

<script>
import getTime from "../utils/time";

// 清除樣式
const style = "transform: translateY(0%);transition:0s all";

// 設置樣式
function setStyle(val) {
    return `transform: translateY(-${~~val * 100}%)`;
}
// 每個字的樣式
function numStyle(time) {
    // 這里等於0則清除樣式,避免0點時,有奇怪的bug
    return time.split("").map(val => (val == "0" ? style : setStyle(val)));
}

export default {
    name: "TimeScreen",
    data() {
        // 獲取時間
        let { time, date } = getTime();
        return {
            isDark: 0,
            time,
            date,
            numStyle: numStyle(time)
        };
    },
    methods: {
        // 更新樣式
        updateStyle() {
            this.time.split("").forEach((val, idx) => {
                if (val == 0) {
                    if (this.numStyle[idx] !== style) {
                        this.removeAnimate(idx);
                        this.numStyle[idx] = setStyle(this.haveSpan[idx]);
                    }
                } else {
                    this.numStyle[idx] = setStyle(val);
                }
            });
        },
        // 切換樣式
        toggleClass() {
            this.isDark = !this.isDark;
        },
        // 清除樣式
        removeAnimate(idx) {
            setTimeout(() => {
                this.numStyle[idx] = style;
                this.numStyle = [...this.numStyle];
            }, 500);
        },
        // 每秒更新時間
        updateTime() {
            const { time, date } = getTime();
            this.time = time;
            this.date = date;
            this.updateStyle();
        }
    },
    created() {
        // 判單有多少個Span
        // 比較小時數最多24小時,所以第一位最多是3個,0、1、2
        this.haveSpan = Object.freeze([3, 10, 0, 6, 10, 0, 6, 10]);
        // 定時器
        this.timer = null;
    },
    mounted() {
        // 觸發定時器
        this.timer = setInterval(this.updateTime, 1000);
    },
    destroyed() {
        // 清除定時器
        clearInterval(this.timer);
    }
};
</script>

<style lang="scss" scoped>
%flexCenter {
    display: flex;
    justify-content: center;
    align-items: center;
}
$timeColor: #d9d4d0;
$white: #fff;
.time-container {
    background: $white;
    color: $timeColor;
    position: absolute;
    width: 100%;
    height: 100%;
    max-width: 540px;
    top: 0;
    left: 50%;
    transform: translateX(-50%);
    &.dark {
        background: #000;
        color: $white;
    }
    &::after {
        content: attr(date);
        position: absolute;
        color: $timeColor;
        font-size: 18px;
        line-height: 1;
        transform: rotate(90deg);
        bottom: 20%;
        left: -48px;
    }
    @extend %flexCenter;
    .time {
        font-size: 70px;
        transform: rotate(90deg);
        position: relative;
        height: 106px;
        line-height: 106px;
        overflow: hidden;
        @extend %flexCenter;

        .time-num {
            position: relative;
            width: 100%;
            height: 100%;
            text-align: center;
            text-shadow: 0 0 2px $white;
            transition: 0.5s all;
            span {
                display: block;
            }
        }
        .time-dist {
            padding-bottom: 15px;
            margin: 0 10px;
        }
    }
}
</style>

最后的最后

感謝各位觀眾老爺的觀看啦O(∩_∩)O,希望大家可以一起進步


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2026 CODEPRJ.COM