HTML例假周期树【女朋友生理期记录】

!!!页面样式参考CSDN博主【涔涔OVER】 HTML+PHP女友经期记录情侣月经生理周期记录 博文中页面效果


初始页面效果

在这里插入图片描述

运行效果展示

Menstrual_cycle

一、存储说明

1.本案例采用浏览器LocalStorage本地存储实现,相关数据内容的存储,刷新页面数据不会丢失

提示:数据不可跨浏览器获取,不同浏览器之间本地存储空间相互独立

提示:删除数据,点击页面右上方删除按钮,点击后出现弹窗,该操作可删除所有记录数据,删除后无法恢复!

2.为了运行便捷,建议将所有代码放到一个.html文件,只需直接点击html文件运行即可。

二、数据记录限制说明

  1. 【开始时间】选择限制

第一次记录(页面中不存在例假记录)例假数据时,【开始时间】的选择为无限制;当页面中存在例假记录时,【开始时间】选择限制为不早于页面中已有例假记录的时间

  1. 【结束时间】选择限制

记录录入顺序限制为先选择【开始时间】,后选择【结束时间】。根据正常经期时间为7天以内的原则,【结束时间】选择限制为当前选择的【开始时间】往后推7天以内

  1. 【经期情况记录】选填

记录有效字符为除空格外120字以内,生成记录时自动去除文本空格。

三、具体说明

  1. 周期树根据记录开始时间从上到下有序创建及排列。可点击页面右上方按钮,滚动到页面顶部或底部。

  2. 例假周期为自动计算。运算规则:上一条记录的开始时间与当前记录的开始时间的时间差。注:第一条记录因无前序记录无法推算显示为 斜杠 /

  3. 异常周期判定提示。根据正常例假周期时间为21~35天,若例假周期天数小于21或大于35判定为异常周期,则提醒显示为红色方框。

  4. 新增记录时【经期记录】若不填,生成的例假记录则显示为灰色不可点击的——无—— 字样,若填写了【经期记录】,生成的例假记录显示为蓝色可点击的 ——查看—— 字样,点击显示当前经期记录的文本框(不可编辑)(默认隐藏)。

见下图

在这里插入图片描述

四、代码部分

1.HTML

    <div class="title">Menstrual Cycle Tree</div>
 <!-- 操作按钮 -->
    <div class="controller">
        <button class="toTop">↑顶部</button>
        <button class="toBottom">底部↓</button>
        <button class="add">新增记录</button>
        <button class="del">删除记录</button>
    </div>
    <!-- 新增记录滑窗 -->
    <div class="info_add">
        <div>
            <span>开始日期*</span>
            <input type="date" class="startTime">
        </div>
        <div>
            <span>结束日期*</span>
            <input type="date" class="endTime">
        </div>
        <div>
            <textarea name="" id="" placeholder="经期情况记录(选填)最多120个字符" maxlength="120"></textarea>
        </div>
        <p class="text_num">0/120字</p>
        <button class="data_add">添加</button>
        <button class="content_clean">清空</button>
    </div>
    <!-- 删除记录滑窗 -->
    <div class="info_del">
        确定要删除所有记录吗?删除后无法恢复哦~
        <button class="sure_del">确定(8)</button>
    </div>
    <!-- 周期树 -->
    <div class="Menstrual_tree">
        <!-- 周期树干 -->
        <div class="center_trunk"></div>
        <!-- 周期数据 -->
        <div class="data_box"></div>
    </div>

2.CSS

      * {
            margin: 0;
            padding: 0;
            border: 0;
            list-style: none;
        }
        html {
            scroll-behavior: smooth;
        }
        body {
            background: linear-gradient(to right, rgb(143, 141, 252) 40%, rgb(142, 199, 248) 100%);
            overflow-x: hidden;
        }
        .title {
            width: 30%;
            font-size: 40px;
            color: #fff;
            font-family: "Comic Sans MS";
            position: fixed;
            left: 0;
            text-align: center;
            pointer-events: none;
        }
        .controller {
            top: 0;
            right: 0;
            width: 30%;
            height: 50px;
            position: fixed;
            border-bottom-right-radius: 35px;
            border-bottom-left-radius: 35px;
            overflow: hidden;
        }
        .controller button {
            background: #fff;
            width: 25%;
            height: 100%;
            cursor: pointer;
            box-sizing: border-box;
            float: left;
            letter-spacing: 2px;
        }
        .controller button:hover {
            background: #f4ebeb;
        }
        /* 周期树 */
        .Menstrual_tree {
            width: 70%;
            overflow: hidden;
            position: relative;
            padding-bottom: 200px;
        }
        .Menstrual_data {
            width: 350px;
            height: 100px;
            background: rgb(255, 249, 249);
            display: flex;
            justify-content: space-between;
            align-items: center;
            padding: 5px 10px;
            border-radius: 5px;
            position: relative;
            margin-left: 100px;
            margin-top: 40px;
        }
        .Menstrual_data ul {
            width: 30%;
            height: 80px;
            background: #fff;
            border-radius: 5px;
            box-shadow: 0 0 5px 0 #ccc;
        }
        .Menstrual_data li {
            width: 100%;
            height: 30px;
            line-height: 30px;
            text-align: center;
        }
        .Menstrual_data span {
            font-weight: 600;
            color: rgb(250, 46, 148);
        }
        .Menstrual_data ul li:first-child {
            font-size: 17px;
            margin-top: 10px;
            font-weight: 600;
        }
        .Menstrual_data ul li:nth-child(2) {
            font-size: 15px;
        }
        .Menstrual_data .start_time {
            width: 85px;
            height: 25px;
            position: absolute;
            left: 10px;
            top: -18px;
            background: orange;
            font-size: 14px;
            line-height: 25px;
            text-align: center;
            border-radius: 5px;
            color: #fff;
        }
        .Menstrual_data .content {
            width: 100%;
            height: 100%;
            position: absolute;
            top: 115px;
            left: 0;
            background: #ffffff86;
            border-radius: 5px;
            font-size: 13px;
            letter-spacing: 1px;
            box-sizing: border-box;
            padding: 5px;
            line-height: 1.5;
            pointer-events: none;
            opacity: 0;
            transition: all .5s;
        }
        /* 周期树干 */
        .center_trunk {
            width: 50px;
            position: absolute;
            top: 0;
            left: 500px;
        }
        .trunk_line {
            width: 20px;
            height: 150px;
        }
        .trunk_line .line {
            width: 6px;
            height: 140px;
            background: rgb(255, 255, 255, .9);
            margin: 0 auto;
        }
        .trunk_line .lozenge {
            width: 20px;
            height: 20px;
            clip-path: polygon(50% 0, 100% 50%, 50% 100%, 0 50%);
            background: #fff;
            margin: -5px auto;
        }
        /* 新增记录页 */
        .info_add,
        .info_del {
            width: 30%;
            position: fixed;
            right: -30%;
            top: 250px;
            color: #fff;
            padding-bottom: 50px;
            transition: all .5s;
        }
        .info_add div:not(:nth-child(3)) {
            width: 50%;
            margin: 50px 0;
            float: left;
            display: flex;
            justify-content: center;
            align-items: center;
            flex-direction: column;
        }
        .info_add span {
            float: left;
            width: 80%;
            text-align: left;
            margin-bottom: 10px;
        }
        textarea {
            width: 90%;
            height: 150px;
            display: block;
            border-radius: 5px;
            padding: 5px 5px;
            outline: none;
            margin: 0 auto;
            resize: none;
        }
        .text_num {
            float: left;
            width: 90%;
            margin-top: 10px;
            margin-left: 5%;
            text-align: right;
        }
        .info_add button {
            width: 40%;
            height: 50px;
            background: #fff;
            box-shadow: 0 0 5px 0 #ccc;
            font-size: 15px;
            display: block;
            border-radius: 10px;
            float: left;
            margin-left: 6.66%;
            margin-top: 60px;
            cursor: pointer;
        }
        button:hover {
            background: #fbfbfbdb;
        }
        input[type="date"] {
            width: 80%;
            height: 40px;
            outline: none;
            border-radius: 10px;
            cursor: pointer;
            background: #fff;
            text-align: center;
            padding-right: 10px;
            font-size: 20px;
        }
        /* 删除记录页 */
        .info_del {
            font-size: 20px;
            color: red;
            text-align: center;
            padding-top: 150px;
            font-family: "楷体";
        }
        .info_del button {
            width: 25%;
            height: 40px;
            background: #fff;
            box-shadow: 0 0 5px 0 #ccc;
            font-size: 15px;
            display: block;
            border-radius: 10px;
            cursor: pointer;
            margin: 60px auto;
            pointer-events: none;
        }
        ::-webkit-datetime-edit-year-field {
            color: rgb(5, 192, 220);
        }
        ::-webkit-datetime-edit-month-field {
            color: rgb(240, 196, 52);
        }
        ::-webkit-datetime-edit-day-field {
            color: chocolate;
        }
        /* 动态添加/移除类 */
        .titlePosition {
            top: 150px;
            left: 70%;
        }
        .titleOpacity {
            opacity: 0;
        }
        .marginLeft {
            margin-left: 550px;
        }
        .width {
            right: 0;
        }
        .warning {
            border: 2px solid red;
        }
        .des_click {
            pointer-events: auto !important;
            opacity: 1 !important;
        }
        .Menstrual_data .desLight {
            color: cornflowerblue;
            cursor: pointer;
        }
        .Menstrual_data .desNone {
            pointer-events: none;
            color: #ccccccf3;
        }
        @media screen and (min-device-width:1800px) {
            .center_trunk {
                left: 600px;
            }
            .Menstrual_data {
                margin-left: 200px;
            }
            .marginLeft {
                margin-left: 650px;
            }
        }

3.JS

        const dataList = []
        let cycleData = JSON.parse(localStorage.getItem('cycleData'))
        if (cycleData === null) {
            localStorage.setItem('cycleData', JSON.stringify(dataList))
            cycleData = JSON.parse(localStorage.getItem('cycleData'))
        } else {
            MenstrualDataRender()
        }
        const delSure = document.querySelector('.sure_del')
        const dataAdd = document.querySelector('.data_add')
        const startTime = document.querySelector('.startTime')
        const endTime = document.querySelector('.endTime')
        const description = document.querySelector('textarea')
        const textLength = document.querySelector('.text_num')
        const title = document.querySelector('.title')

        let hide = 0
        let delInter = 0
        document.documentElement.scrollTop = 0
        cycleData.length == 0 ? '' : title.classList.add('titlePosition')
        //新增记录按钮
        document.querySelector('.add').onclick = () => {
            startTime.value = endTime.value = description.value = ''
            textLength.innerHTML = `0/120字`
            endTime.max = endTime.min = ''
            //设置周期树下方记录不得早于已有记录
            cycleData = JSON.parse(localStorage.getItem('cycleData'))
            startTime.min = cycleData.length > 0 ? cycleData[cycleData.length - 1].start_T : ''
            document.querySelector('.width') ? document.querySelector('.width').classList.remove('width') : ''
            document.querySelector('.info_add').classList.add('width')
            hide == 1 ? document.querySelector('.width').classList.remove('width') : ''
            hide = hide == 1 ? 0 : 1
        }
        //清空记录按钮
        document.querySelector('.del').onclick = () => {
            let delTime = 8
            delSure.disabled = true
            delSure.style.pointerEvents = 'none'
            clearInterval(delInter)
            document.querySelector('.width') ? document.querySelector('.width').classList.remove('width') : ''
            document.querySelector('.info_del').classList.add('width')
            delSure.innerHTML = `确定(8)`
            delInter = setInterval(() => {
                delTime--
                if (delTime == 0) {
                    clearInterval(delInter)
                    delSure.innerHTML = `确定`
                    delSure.style.pointerEvents = 'auto'
                    delSure.disabled = false
                } else {
                    delSure.innerHTML = `确定(${delTime})`
                }
            }, 1000)
            hide == 2 ? document.querySelector('.width').classList.remove('width') : ''
            hide = hide == 2 ? 0 : 2
        }

        //清空记录页,确定按钮
        delSure.onclick = () => {
            localStorage.removeItem('cycleData')
            localStorage.setItem('cycleData', JSON.stringify(dataList))
            //重新渲染周期树
            MenstrualDataRender()
            document.querySelector('.del').click()
            titlePositionRender('del')
        }

        //新增记录页,新增按钮
        dataAdd.onclick = () => {
            if (startTime.value == '' || endTime.value == '') {
                alert('开始/结束时间都是必选信息哦~')
            } else {
                const localData = {
                    start_T: startTime.value,
                    end_T: endTime.value,
                    desc: description.value.replace(/\s*/g, "")
                }
                cycleData = JSON.parse(localStorage.getItem('cycleData'))
                cycleData.push(localData)
                localStorage.setItem('cycleData', JSON.stringify(cycleData))
                MenstrualDataRender()
                startTime.value = endTime.value = description.value = ''
                textLength.innerHTML = `0/120字`
                endTime.max = endTime.min = ''
                //设置周期树下方记录不得早于已有记录
                cycleData = JSON.parse(localStorage.getItem('cycleData'))
                startTime.min = cycleData.length > 0 ? cycleData[cycleData.length - 1].start_T : ''
                cycleData.length == 1 ? titlePositionRender('add') : ''
                windowScroll()
            }
        }

        //新增记录页,清空表单按钮
        document.querySelector('.content_clean').onclick = () => {
            startTime.value = endTime.value = description.value = ''
            textLength.innerHTML = `0/120字`
            endTime.max = endTime.min = ''
        }
        //选择开始时间,根据开始时间,限制结束时间的选择范围
        startTime.oninput = () => {
            endTime.value = ''
            endTime.min = startTime.value == '' ? '' : startTime.value
            endTime.max = iptTime(startTime)
        }
        //限制日期选择顺序
        endTime.onclick = () => {
            startTime.value == '' ? alert('请先选择——开始日期——哦~') : ''
        }

        //记录框字数记录
        description.oninput = () => {
            textLength.innerHTML = `${description.value.replace(/\s*/g, "").length}/120字`
            if (description.value.replace(/\s*/g, "").length < 120) {
                description.maxLength = description.value.length + 1
                textLength.style.color = '#fff'
            } else if (description.value.replace(/\s*/g, "").length == 120) {
                description.maxLength = description.value.length
                textLength.style.color = 'red'
            } else {
                textLength.innerHTML = `120/120字`
                textLength.style.color = 'red'
            }
        }
        //点击回到页面顶部
        document.querySelector('.toTop').onclick = () => {
            document.documentElement.scrollTop = 0
        }
        //点击滚动到页面底部
        document.querySelector('.toBottom').onclick = () => {
            windowScroll()
        }
        //周期树渲染函数
        function MenstrualDataRender() {
            let cycleData = JSON.parse(localStorage.getItem('cycleData'))
            const data = document.querySelector('.data_box')
            const tree = document.querySelector('.center_trunk')
            data.innerHTML = tree.innerHTML = ''
            cycleData.forEach((item, index) => {
                //例假周期计算
                let cycle_days = index == 0 ? '/' : MenstrualComputed(cycleData[index - 1].start_T, cycleData[index].start_T)
                //计算经期
                const duration = MenstrualComputed(item.start_T, item.end_T)
                data.innerHTML += `
                    <div class="Menstrual_data ${index % 2 == 0 ? 'marginLeft' : ''}">
                        <div class="start_time">${item.start_T}</div>
                        <ul class="${cycle_days > 35 || cycle_days < 21 ? 'warning' : ''}">
                            <li>例假周期</li>
                            <li><span class="cycle_days">${cycle_days}</span>天</li>
                        </ul>
                        <ul>
                            <li>经期</li>
                            <li><span class="duration">${duration}</span>天</li>
                        </ul>
                        <ul>
                            <li>记录</li>
                            <li class="description"><span id="descShow" class="${item.desc.length == 0 ? 'desNone' : 'desLight'}">${item.desc.length == 0 ? '无' : '查看'}</span></li>
                        </ul>
                        <p class="content">${item.desc}</p>
                    </div>
            `
                tree.innerHTML += `         
                <div class="trunk_line">
                    <div class="line" ></div>
                    <div class="lozenge"></div>
                </div>
                    `
            })
            //点击查看记录
            document.querySelectorAll('#descShow').forEach((item, index) => {
                item.onclick = () => {
                    document.querySelectorAll('.content')[index].classList.toggle('des_click')
                }
            })
        }
        //周期数计算函数
        function MenstrualComputed(s_t, e_t) {
            const start = +new Date(`${s_t} 00:00:00`)
            const end = +new Date(`${e_t} 00:00:00`)
            return parseInt((end - start) / 1000 / 3600 / 24)
        }
        //页面滚动
        function windowScroll() {
            const position = document.body.scrollHeight
            document.documentElement.scrollTop = position
        }
        //根据选择的开始日期,限制结束日期选择区间
        function iptTime(lis_E) {
            const time_1 = lis_E.value == '' ? '' : lis_E.value
            let timeStamp = +new Date(`${time_1} 00:00:00`) + (7 * 1000 * 3600 * 24)
            const date = new Date(timeStamp)
            let y = date.getFullYear()
            let m = date.getMonth() + 1
            let d = date.getDate()
            m = m < 10 ? '0' + m : m
            d = d < 10 ? '0' + d : d
            return `${y}-${m}-${d}`
        }
        //title渲染
        function titlePositionRender(operate) {
            title.style.transition = 'all .3s'
            title.classList.add('titleOpacity')
            setTimeout(() => {
                operate == 'del' ? title.classList.remove('titlePosition') : title.classList.add('titlePosition')
                setTimeout(() => {
                    title.classList.remove('titleOpacity')
                }, 300)
            }, 300)
        }

总结

如有问题或者对本案例有任何的改进建议,欢迎评论区留言讨论!