vue3移动端自定义日历定位打卡功能

686 阅读3分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第8天,点击查看活动详情

“金秋十月,我要连续30天更文,做劳模,拿手机摄影神器!点击查看活动详情 “即可成功参与

最近的需求是实现类似钉钉打卡的功能,今天实现页面的构建,后续提供交互逻辑,

日历组件

html结构:

<template>
    <div>
        <div class="top-title">
            <div><span @click="lastMonth" class="link"></span></div>
            <div><span>{{year}}年{{month}}月</span></div>
            <div><span @click="nextMonth" class="link"></span></div>
        </div>
        <div class="containerBox">
            <div v-for="(item,index) in weeks" :key="index">{{ item }}</div>
        </div>
        <div class="containerBox" style="padding: 1vh 1vh 3vh 1vh;">
            <div v-for="(item,index) in data" :key="index">
                <!-- 当前日期 -->
                <div v-if="compareToNow(item) === 0" style="color: #fff" class="nowDay">{{ item.date }}</div>
                <!-- 当前以后的日期 -->
                <div v-if="compareToNow(item) === 1">{{ item.date }}
                </div>
                <!-- 当前以前的日期 -->
                <div v-if="compareToNow(item) === -1" class="otherDay">
                    <div>{{ item.date }}</div>
                    <!--  <div class="date-desc">补卡</div> -->
                </div>
            </div>
        </div>
    </div>
</template>

js核心代码:

import { ref } from 'vue';
//打卡日历
const now = new Date()
const weeks = ["日", "一", "二", "三", "四", "五", "六"]
const year: any = ref('')
const month: any = ref('')
const date: any = ref('')
const firstDay: any = ref('')
const data: any = ref([])
const getNow = () => {
    year.value = now.getFullYear()
    month.value = now.getMonth() + 1;
    date.value = now.getDate();
    now.setDate(1);
    firstDay.value = now.getDay();
    initData();
}
const getMonthDay = (month: any) => {
    if ([1, 3, 5, 7, 8, 10, 12].includes(month)) {
        return 31
    } else if ([4, 6, 9, 11].includes(month)) {
        return 30
    } else if (month === 2) {
        //  判断当年是否为闰年
        if (
            (year.value % 4 === 0 && year.value % 100 !== 0) ||
            year.value % 400 === 0
        ) {
            return 29
        } else {
            return 28
        }
    }
}
const initData = () => {
    data.value = []
    let days: any = getMonthDay(month.value);
    for (let i = 0; i < firstDay.value; i++) {
        data.value.push({
            year: "",
            month: "",
            date: "",
        });
    }
    for (let i = 0; i < days; i++) {
        data.value.push(
            {
                year: year.value,
                month: month.value,
                date: i + 1,
            }
        );
    }

}
const lastMonth = () => {
    now.setMonth(now.getMonth() - 1);
    getNow();
}
const nextMonth = () => {
    now.setMonth(now.getMonth() + 1);
    getNow();
}
const compareToNow = (item: any) => {
    // console.log(item, 'item')
    if (item.year && item.month && item.date) {
        let date1 = new Date();
        date1.setFullYear(item.year)
        date1.setMonth(item.month - 1)
        date1.setDate(item.date)
        date1.setHours(0)
        date1.setMinutes(0)
        date1.setSeconds(0)
        let now = new Date();
        now.setHours(0)
        now.setMinutes(0)
        now.setSeconds(0)
        if (date1.getTime() > now.getTime()) {
            return 1
        } else if (date1.getTime() === now.getTime()) {
            return 0
        } else if (date1.getTime() < now.getTime()) {
            return -1
        }
    }
}
getNow()

scc样式:

//日历
.top-title {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    grid-auto-rows: 50px;
    grid-gap: 1rem;
    background-color: #FFFFFF;
    // border-bottom: 1px solid #cccccc;
    line-height: 50px;

}

.containerBox {
    display: grid;
    grid-template-columns: repeat(7, 1fr);
    grid-auto-rows: 40px;
    grid-gap: 1rem;
    background-color: #FFFFFF;
    line-height: 40px;
    font-size: 16px;

    div {
        text-align: center;
    }

    .nowDay {
        background: #4d89ff;
        border-radius: 50%;
    }
}

.today {
    background-size: 95% 95%;
    position: relative;
}

.otherDay {
    background-size: 95% 95%;
    position: relative;
    color: #ccc;
}

.link {
    font-size: 20px;
    color: #2d8cf0;
}

.date-desc {
    display: block;
    position: absolute;
    top: 6.8vw;
    left: 1.5vw;
    font-size: 2.3vw;
    color: green;
}

实现的效果如图:

image.png

ps:可以直接将日历封装成组件后续直接引入

将日历和定位综合起来

html:结构

<template>
    <div style="background-color:#4d89ff;height: 100%;">
        <div class="container">
            <header>
                <div class="goBack" @click="goBack">
                    <van-icon name="arrow-left" size="20pt" />
                </div>
                <div class="title">签到</div>
            </header>
        </div>
        <div class="rili">
        <!-- 封装的日历组件 -->
            <calendarCom />
            <div class="daka">
                <div class="box" v-if="flag" @click="qiandao">
                    <div class="data">{{ str }}</div>
                    <div class="tip">点击签到</div>
                </div>
                <div class="box" v-if="!flag">
                    <div class="suc"></div>
                    <div class="tip2">签到成功</div>
                </div>
            </div>
            <div class="adress">
                <van-icon name="location-o" />
                {{ address }}
            </div>
        </div>
    </div>

</template>

js核心代码

import { reactive, ref, onMounted } from "vue"
import { jsonp } from 'vue-jsonp'
import wx from 'weixin-js-sdk'
import { getSignature } from "@/api/request/login";
import { useRouter } from "vue-router";
import { Toast, Dialog } from 'vant';
import calendarCom from './components/calendar-com.vue'
import getwxFn from '@/utils/wx'
const router = useRouter()
const flag = ref<boolean>(true)
const address = ref<string>('获取定位中······')
const x = ref<string>('')
const y = ref<string>('')
let timer = reactive<any>(null)
let timer2 = reactive<any>(null)
let str = ref<any>('');
//点击签到
const qiandao = () => {
    clearInterval(timer2)
    timer = setTimeout(() => {
        flag.value = false
    }, 500)
}
//返回
const goBack = () => {
    clearInterval(timer)
    clearInterval(timer2)
    router.push('/home')
}
//获取定位
let signature = () => {
    wx.getLocation({
        type: 'gcj02', // 默认为wgs84的gps坐标,如果要返回直接给openLocation用的火星坐标,可传入'gcj02'
        success: (res: any) => {
            x.value = res.longitude
            y.value = res.latitude
            //逆解析
            jsonp(`https://apis.map.qq.com/ws/geocoder/v1/?location=${res.latitude},${res.longitude}`, {
                key: '', //必填
                get_poi: 1,
                output: 'jsonp'
            }).then((res) => {
                debugger
                if (res.status === 0) {
                    address.value = res.result.address_component.province + res.result.address_component.city + res.result.address_component.district + res.result.address_component.street + res.result.address_component.street_number
                } else {
                    Toast.fail(res.message);
                }
            })
        },
        fail: (err: any) => {
            console.log(err, 'faill')
        }
    })
    wx.error((err: any) => {
        throw new Error(err)
    })
}

setTimeout(() => {
    signature()
}, 1000)
onMounted(() => {
    timer2 = setInterval(() => {
        str.value = new Date().toTimeString().substring(0, 8)
    }, 1000)
    getwxFn()//这里是获取微信签名,我实现这个功能的时候封装起来了,需要看如何获取签名可看小程序官网,或翻阅之前的文章有记录下来
})

scc样式:

.container {
    width: 100%;
    height: 60px;
    background-color: rgba(75, 139, 255, 1);

    header {
        width: 100%;
        height: 44pt;
        position: relative;
        color: #fff;

        .goBack {
            position: absolute;
            left: 20pt;
            top: 50%;
            transform: translateY(-50%);
        }

        .title {
            font-size: 18pt;
            height: 44pt;
            line-height: 44pt;
        }
    }


}

//定位
:deep .van-nav-bar__content {
    background-color: #2d8cf0;
}

:deep .van-nav-bar__title {
    color: #fff;
}

:deep .van-nav-bar__text {
    color: #fff;
}

:deep .van-nav-bar .van-icon {
    color: #fff;
}

.rili {
    height: 100%;
    width: 100%;
    background-color: #fff;
    // overflow: hidden;
    border-radius: 16pt 16pt 0pt 0pt;

    .daka {
        width: 100%;
        height: 120pt;
        //margin-top: 30pt;
        position: relative;

        .box {
            width: 100pt;
            height: 100pt;
            border-radius: 50%;
            position: absolute;
            left: 50%;
            top: 50%;
            transform: translate(-50%, -50%);
            background-color: #4d89ff;
            box-shadow: 0 0 16pt #4d89ff;
            color: #fff;

            .data {
                width: 70pt;
                height: 30pt;
                margin-top: 36pt;
                margin-left: 10pt;
                font-size: 20pt;
                font-weight: 500;
            }

            .tip {
                font-size: 12pt;
            }

            .suc {
                margin-top: 24pt;
                font-size: 24pt;
            }

            .tip2 {
                margin-top: 8pt;
            }
        }
    }
}

最终效果如图:

image.png