整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

js和css基础知识总结


.em字体设置

body {font-size:100%;}
h1 {font-size:3.75em;}
h2 {font-size:2.5em;}
p {font-size:0.875em;}
复制代码

2.背景图标居右

.aa{
    background-image: url(arrow.png)no-repeat right center;
    background-image:url(nav-bar.jpg);
    background-repeat:no-repeat;
    background-position:right center;
}
复制代码

3.文本框超出部分显示省略号:

overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;    
复制代码


我自己是一名从事了多年开发的web前端老程序员,目前辞职在做自己的web前端私人定制课程,今年我花了一个月整理了一份最适合2020年学习的web前端学习干货,各种框架都有整理,送给每一位前端小伙伴,想要获取的可以关注我的头条号并在后台私信我:前端,即可免费获取。

一蒙版出现禁止页面滚动

1 window.onscroll=function(){
    document.body.scrollTop = 0
};

2 $('html,body').animate({scrollTop:'0'},100);
$(".tan").bind('touchmove',function(e){  //禁止弹出框出来时进行滑动 
    e.preventDefault();
});
3 document.body.style.overflow='hidden';
若键盘点击的话,就要加上:
var move=function(e){
    e.preventDefault && e.preventDefault();
    e.returnValue=false;
    e.stopPropagation && e.stopPropagation();
    return false;
}
var keyFunc=function(e){
    if(37<=e.keyCode && e.keyCode<=40){
        return move(e);
    }
}
document.body.onkeydown=keyFunc;

复制代码

二、按钮点击事件

var button=$(':button');
button.on('click',function(){
    button.css('background-color','white');
    $(this).css('background-color','#FB3336');
})
复制代码

三、安卓手机里,h5页面没有充满body导致左右滑动的问题

首先声明一下:

<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no"/>
然后
html,body {width:100%;height:100%;overflow-x:hidden;}

复制代码

四、关于页面左右滚动的问题

1.<body scoll=no> 全禁止
2.<body style="overflow:scroll;overflow-y:hidden"> 禁止纵向滚动条
3.<body style="overflow:scroll;overflow-x:hidden"> 禁止纵向滚动条
4.overflow属性: 检索或设置当对象的内容超过其指定高度及宽度时如何显示内容
5.overflow: auto; 在需要时内容会自动添加滚动条
6.overflow: scroll; 总是显示滚动条
7.overflow-x: hidden; 禁止横向的滚动条
8.overflow-y: scroll; 总是显示纵向滚动条
复制代码

五、 content && header之间的亮条怎么消除

.mui-bar-nav{
    -webkit-box-shadow: none;
    box-shadow: none;
}
复制代码

六、删除选项

$(".shanchu").click(function(){
    $(this).parent().remove()
})
复制代码

七、表单提交按钮时,鼠标放在上面显示小手的方法:

需要对元素属性的css的cursor进行设置
1、default    默认光标(通常是一个箭头)
2、auto   默认。浏览器设置的光标。 
3、crosshair   光标呈现为十字线。    
4、pointer    光标呈现为指示链接的指针(一只手)    
5、move    此光标指示某对象可被移动。    
6、e-resize    此光标指示矩形框的边缘可被向右(东)移动。    
7、ne-resize    此光标指示矩形框的边缘可被向上及向右移动(北/东)。    
8、nw-resize    此光标指示矩形框的边缘可被向上及向左移动(北/西)。    
9、n-resize    此光标指示矩形框的边缘可被向上(北)移动。    
10、se-resize    此光标指示矩形框的边缘可被向下及向右移动(南/东)。    
11、sw-resize    此光标指示矩形框的边缘可被向下及向左移动(南/西)。  
12、s-resize    此光标指示矩形框的边缘可被向下移动(南)。    
13、w-resize    此光标指示矩形框的边缘可被向左移动(西)。    
14、text    此光标指示文本。    
15、wait    此光标指示程序正忙(通常是一只表或沙漏)。    
16、help    此光标指示可用的帮助(通常是一个问号或一个气球)。    
要实现鼠标移上去显示手形、需要在你的提交按钮上增加css cursor属性,并将它的值设置为pointer;
如下:<input type="submit" name="submit" value="发布留言" class="subimt" onclick="display_alert()" style="cursor:pointer" />
复制代码

八、怎么清除table里面tbody的内容

$("#test tbody").html("");
复制代码

九、动态获取表格的行数

var bv=$("#tabd tr").length-1;
$("#sp4").html(bv);     //动态的获取注数
复制代码

十、多个按钮点击变色,再点击还原

$(".jixuan  input[type=button]").toggle(function(){
    $(this).css("background-color","yellow");
    $(this).css("cursor","pointer")
    },function(){
    $(this).css("background-color","white");
    $(this).css("cursor","pointer");
}) 
复制代码

十一、单选按钮顾名思义用于单选的场合,例如,性别,职业的选择等,语法如下:

<input type="radio" name="gender" value="男" checked />
常用属性迅美科技整理如下:
1.type="radio"
type属性设置为radio,表示产生单一选择的按钮,让用户单击选择;
2.name="gender"
radio组件的名称,name属性值相同的radio组件会视为同一组radio组件,而同一组内只能有一个radio组件被选择;
3.value="男"
radio组件的值,当表单被提交时,已选择的radio组件的value值,就会被发送进行下一步处理, radio组件的value属性设置的值 
无法从外观上看出,所以必须在radio组件旁边添加文字,此处的文字只是让用户了解此组件的意思.
4.checked
设置radio组件为已选择,同一组radio组件的name性情值必须要相同
复制代码

十二、网页中,公共头部和侧边栏的引用

1、<?php include("header.html");?>
2、使用ssi技术页面生成shtml文件,只用在头部文件位置加入<!--#include file="header.htm" -->,
然后修改的时候只要修改header.htm文件就可以了。使用shtml的好处是对搜索引擎比较友好,需要处理的文件在服务器端完成的,
不会加重访问者的浏览器负担。
复制代码

十三、锚点链接上下定位偏移解决

1、JS解决的方法

if (window.location.hash.indexOf('#') >= 0) {
    $('html,body').animate({
    scrollTop: ($(window.location.hash).offset().top - 50) + "px"
    },
    300);
}; //主要修复评论定位不准确BUG
$('#comments a[href^=#][href!=#]').click(function() {
    var target = document.getElementById(this.hash.slice(1));
    if (!target) return;
    var targetOffset = $(target).offset().top - 50;
    $('html,body').animate({
        scrollTop: targetOffset
    },
    300);
    return false;
}); //主要修复评论定位不准确BUG
复制代码

2、解决办法

能用css自然不想用js解决,因为在加载方面,css总是先加载,并且速度很快。

typecho的评论HTML结构是这样的:

<li id="comment-277" class="comment-body comment-child comment-level-odd comment-even comment-by-author">
我们给comment-body加上css

.comment-body {
    position: relative;
    padding-top: 50px;
    margin-top: -50px;
}
/*修复评论跳转定位问题*/
完美兼容chrome和Firefox,其他浏览器未测试。
复制代码

十四、蒙版弹出禁止蒙版后面的内容滚动

.ovfHiden{overflow: hidden;height: 100%;}
$('.bzh .l1 a').click(function(){
    $(".baok").show();
    $(".baod").show();
    $('html,body').addClass('ovfHiden');
});
$('.baod .img1').click(function(){
$('html,body').removeClass('ovfHiden');
    $(".baok").hide();
    $(".baod").hide();
});
复制代码

十五、获取复选框点击的次数

$("#compute").click(function(){
    $('input').live('click',function(){ 
    //alert($('input:checked').length); 
    $("#show").html($('input:checked').length);
    });
});
复制代码

十六、Tab选项卡切换

1.js

$('footer ul li').click(function(){
    var index = $(this).index();
    $(this).attr('class',"content").siblings('ul li').attr('class','ss');
    $('.content').eq(index).show(200).siblings('.content').hide();
    });

$('.ka ul li').click(function(){
    var index = $(this).index();
    $(this).attr('class',"zi").siblings('ul li').attr('class','ll');
    $(this).parent().next().find(".zi").hide(). eq(index).show();
});
复制代码

2.html

<div class="carindex-cnt">
        <ul class="nav">
            <li>续保方案</li>
            <li>热销方案</li>
            <li>自定义方案</li>
        </ul>
        <div class="tabcontent">
            <div class="zi">
                <p class="altp">此方案为您上一年的投保记录</p>
                <ul class="xiur">
                    <li>
                        <label for="saveType2">交强险</label>
                        <div  class="right-cnt">
                            <input type="text" class="coverage" disabled="disabled" value="不投保"/>
                            <ul class="datas" style="display: none;">
                                <li ref="1">投保</li>
                                <li ref="2">不投保</li>
                            </ul>
                        </div>
                    </li>
                    <li>
                        <label for="saveType2">商业险</label>
                        <div  class="right-cnt">
                            <input type="text" class="coverage" disabled="disabled" value="不投保"/>
                            <ul class="datas" style="display: none;">
                                <li ref="1">投保</li>
                                <li ref="2">不投保</li>
                            </ul>
                        </div>
                    </li>
                </ul>
                <p class="title">商业主险</p>
                <ul class="xiur">
                    <li>
                        <span>车辆损失险</span>
                        <label for="abatement0" class="labels">
                            <input class="mui-checkbox checkbox-green" type="checkbox" name="abatement" >
                        </label>
                        <div  class="right-cnt">
                            <input type="text" class="coverage" disabled="disabled" value="不投保"/>
                            <ul class="datas" style="display: none;">
                                <li ref="1">投保</li>
                                <li ref="2">不投保</li>
                            </ul>
                        </div>
                    </li>
                  
                </ul>
            </div>
            <div class="zi" style="display: none">
                <ul class="xiur">
                    <li>
                        <label for="saveType2">交强险</label>
                        <div  class="right-cnt">
                            <input type="text" class="coverage" disabled="disabled" value="不投保"/>
                            <ul class="datas" style="display: none;">
                                <li ref="1">投保</li>
                                <li ref="2">不投保</li>
                            </ul>
                        </div>
                    </li>
                    <li>
                        <label for="saveType2">商业险</label>
                        <div  class="right-cnt">
                            <input type="text" class="coverage" disabled="disabled" value="不投保"/>
                            <ul class="datas" style="display: none;">
                                <li ref="1">投保</li>
                                <li ref="2">不投保</li>
                            </ul>
                        </div>
                    </li>
                </ul>
            </div>
        </div>
</div>  
复制代码

3.js

$('.nav li').click(function () {
    var index = $(this).index();
    $(this).parent().next().find(".zi").hide().eq(index).show();
})

复制代码

十七、form表为空时,提交按钮禁用

$(function(){            
    $('.main button').click(function(){
        if(($('.ip1').val() !="") && ($('.ip2').val() !="")){
            $('.main button').css('background','#ff8100');
            $('.main button').attr('disabled', true);
            }else{
            $('.main button').css('background','#D0D0D0');
            $('.main button').attr('disabled', false);
        }
    })
})
复制代码

十八、上拉事件和下拉事件

$(window).scroll(function(){
    var scrollTop = $(this).scrollTop();               //滚动条距离顶部的高度
    var scrollHeight =$(document).height();                   //当前页面的总高度
    var windowHeight = $(this).height();                   //当前可视的页面高度
    if(scrollTop + windowHeight >= scrollHeight){    //距离顶部+当前高度 >=文档总高度 即代表滑动到底部
        alert("上拉加载,要在这调用啥方法?");
    }else if(scrollTop<=0){         //滚动条距离顶部的高度小于等于0
        alert("下拉刷新,要在这调用啥方法?");
    }
});                                                          ——>移动端

$(function(){    
    $(window).scroll(function() {  
        var scrollTop = $(this).scrollTop(),scrollHeight = $(document).height(),windowHeight = $(this).height();  
        var positionValue = (scrollTop + windowHeight) - scrollHeight;  
        if (positionValue == 0) {  
            //do something  
        }  
    });  
});  
复制代码

十九、左滑和右滑事件

var obj;
var startx;
var starty;
var overx;
var overy;
    for(var i=1;i<=$("li").length;i++){          //为每个li标签添加事件
    obj = document.getElementById(i);       //获取this元素
    evenlistener(obj);      //调用evenlistener函数并将dom元素传入,为该元素绑定事件
}

function evenlistener(obj){
    obj.addEventListener('touchstart', function(event) {        //touchstart事件,当鼠标点击屏幕时触发
    startx = event.touches[0].clientX;              //获取当前点击位置x坐标
    starty = event.touches[0].clientY;              //获取当前点击位置y坐标
    $(".sdf").text("x:"+startx+",y:"+starty+"")     //赋值到页面显示
    } , false);         //false参数,设置事件处理机制的优先顺序,具体不多说,true优先false
    obj.addEventListener('touchmove', function(event) {         //touchmove事件,当鼠标在屏幕移动时触发
    overx = event.touches[0].clientX;           //获取当前点击位置x坐标
    overy = event.touches[0].clientY;           //获取当前点击位置y坐标
    var $this = $(this);            //将dom对象转化为jq对象,由于项目用到jquery,直接使用其animate方法

    if(startx-overx>10){         //左滑动判断,当左滑动的距离大于开始的距离10进入
    $($this).animate({marginLeft:"-55px"},150);         //实现左滑动效果
    }else if(overx-startx>10){       //右滑动判断,当右滑动的距离大于开始的距离10进入
    $($this).animate({marginLeft:"0px"},150);           //恢复
    }
} , false);
    obj.addEventListener('touchend', function(event) {          //touchend事件,当鼠标离开屏幕时触发,项目中无用到,举例
    $(".sf").text("x:"+overx+",y:"+overy+"")
    } , false);
}
复制代码

二十、各大浏览器的判断

var Sys = {};
var ua = navigator.userAgent.toLowerCase();
var s;
(s = ua.match(/rv:([\d.]+)\) like gecko/)) ? Sys.ie = s[1] :
(s = ua.match(/msie ([\d.]+)/)) ? Sys.ie = s[1] :
(s = ua.match(/firefox\/([\d.]+)/)) ? Sys.firefox = s[1] :
(s = ua.match(/chrome\/([\d.]+)/)) ? Sys.chrome = s[1] :
(s = ua.match(/opera.([\d.]+)/)) ? Sys.opera = s[1] :
(s = ua.match(/version\/([\d.]+).*safari/)) ? Sys.safari = s[1] : 0;
if (Sys.ie){
    $("*").css({fontFamily:"微软雅黑"});
};
if (window.ActiveXObject){
Sys.ie = ua.match(/msie ([\d.]+)/)[1];
if (Sys.ie<=9){
    alert('你目前的IE版本为'+Sys.ie+'版本太低,请升级!');
    location.href="http://windows.microsoft.com/zh-CN/internet-explorer/downloads/ie";
    }
}
var UA=navigator.userAgent;
if(is360se = UA.toLowerCase().indexOf('360se')>-1 ){

}else{
    $("*").css({fontFamily:"微软雅黑"});
}

360浏览器基于IE内核的,360急速浏览器内核基于谷歌的
复制代码

二十一、form表单中点击button按钮刷新问题

button,input type=button按钮在IE和w3c,firefox浏览器区别: 
1、当在IE浏览器下面时,button标签按钮,input标签type属性为button的按钮是一样的功能,不会对表单进行任何操作。 
2、但是在W3C浏览器,如Firefox下就需要注意了,button标签按钮会提交表单,而input标签type属性为button不会对表单进行任何操作。
为button按钮增加一个type=”button”属性。
复制代码

二十二、textrare文字输入提示:

<textarea name="" id="sign" maxlength=30 onKeyUp="keypress1()"></textarea>
<div class="tish">
<span id="number">0</span><span>/30</span>
</div>

function keypress1() //text输入长度处理 
{ 
    var text1=document.getElementById("sign").value; 
    var len=text1.length; 
    var show=len; 
    document.getElementById("number").innerText=show; 
} 
复制代码

二十三、iframe操作

1:父页面操作iframe子页面

$('#ifrme').load(function(){
$('#ifrme').contents().find('.baod .img1').click(function(){
    $(.ifrme').contents().find('.baod').hide();
    $('.baok',window.parent.document).hide();
    $('html,body',window.parent.document).removeClass('ovfHiden');
    });
})
* .ifrme父页面的ID为iframe的父级
    .baod .img1是iframe页面里的元素
复制代码

2:子页面操作父页面

$('.baod .bt1').click(function(){
    $('.baod').hide();
    $('.edit',window.parent.document).hide();
    $(".baok", window.parent.document).hide(); 
    $('html,body',window.parent.document).removeClass('ovfHiden');
});
*.baod .bt1子页面里的元素
window.parent.document父级窗口
.edit父级页面元素
复制代码

二十四、toggle开关切换图标或是元素的隐藏

$('.other .pg').click(function(){
    $(this).toggleClass ("pots");
    $('.below').slideToggle(300);
})

* .other .pg元素名称
pots 点击元素要切换的图标(以background()形式的图标)
.below要进行toggle的内容
复制代码

二十五、弹框居中

$(".btnDel").click(function() {  
//$(".box-mask").css({"display":"block"});  
    $(".box-mask").fadeIn(500);  
    center($(".box"));  
    //载入弹出窗口上的按钮事件  
    checkEvent($(this).parent(),            $(".btnSure"), $(".btnCancel"));  
});  *center  弹框名称

function center(obj) {  
    //obj这个参数是弹出框的整个对象  
    var screenWidth = $(window).width(), screenHeigth = $(window).height();  
    //获取屏幕宽高  
    var scollTop = $(document).scrollTop();  
    //当前窗口距离页面顶部的距离  
    var objLeft = (screenWidth - obj.width()) / 2;  
    ///弹出框距离左侧距离  
    var objTop = (screenHeigth - obj.height()) / 2 + scollTop;  
    ///弹出框距离顶部的距离  
    obj.css({  
        left:objLeft + "px",  
        top:objTop + "px"  
    });  
    obj.fadeIn(500);  
    //弹出框淡入  
    isOpen = 1;  
    //弹出框打开后这个变量置1 说明弹出框是打开装填  
    //当窗口大小发生改变时  
    $(window).resize(function() {  
        //只有isOpen状态下才执行  
        if (isOpen == 1) {  
            //重新获取数据  
            screenWidth = $(window).width();  
            screenHeigth = $(window).height();  
            var scollTop = $(document).scrollTop();  
            objLeft = (screenWidth - obj.width()) / 2;  
            var objTop = (screenHeigth - obj.height()) / 2 + scollTop;  
            obj.css({  
                left:objLeft + "px",  
                top:objTop + "px"  
            });  
            obj.fadeIn(500);  
        }  
});  
    //当滚动条发生改变的时候  
$(window).scroll(function() {  
    if (isOpen == 1) {  
        //重新获取数据  
        screenWidth = $(window).width();  
        screenHeigth = $(window).height();  
        var scollTop = $(document).scrollTop();  
        objLeft = (screenWidth - obj.width()) / 2;  
        var objTop = (screenHeigth - obj.height()) / 2 + scollTop;  
        obj.css({  
            left:objLeft + "px",  
            top:objTop + "px"  
        });  
        obj.fadeIn(500);  
    }  
});  
复制代码

二十六、css和js进行奇偶选择器

css

:nth-child(odd){background-color:#FFE4C4;}奇数行
:nth-child(even){background-color:#F0F0F0;}偶数行
复制代码

js

$("table  tr:nth-child(even)").css("background-color","#FFE4C4");    //设置偶数行的背景色
$("table  tr:nth-child(odd)").css("background-color","#F0F0F0");    //设置奇数行的背景色
复制代码

二十七、jQuery中live()使用报错,TypeError: $(...).live is not a function

jquery中的live()方法在jquery1.9及以上的版本中已被废弃了,如果使用,会抛出TypeError: $(...).live is not a function错误。

解决方法:

之前的用法:

.live(events, function)  

新方法:

.on(eventType, selector, function)

若selector不需要,可传入null


例子1:

之前:

$('#mainmenu a').live('click', function)

之后:

$('#mainmenu').on('click', 'a', function)


例子2:

之前:

$('.myButton').live('click', function)

之后(应使用距离myButton最近的节点):

$('#parentElement').on('click', ‘.myButton’, function)

若不知最近的节点,可使用如下的方法:

$('body').on('click', ‘.myButton’, function)
复制代码

二十八、iframe滚动条问题

iframe嵌入的滚动条可以用iframe里面页面的大小覆盖掉iframe的滚动条
复制代码

二十九、点击图片下载(不用新窗口打开)

<a class="downs"  style="display:'+display+'" onclick="downimg(\''+list[i].skuTieTu+'\')">下载</a>
复制代码

js方法

/**
* 图片单独下载
*/
function downimg(skuTieTu){
    console.log(skuTieTu)
    let src = skuTieTu;
    var canvas = document.createElement('canvas');
    var img = document.createElement('img');
    img.onload = function(e) {
    canvas.width = img.width;
    canvas.height = img.height;
    var context = canvas.getContext('2d');
    context.drawImage(img, 0, 0, img.width, img.height);
    canvas.getContext('2d').drawImage(img, 0, 0, img.width, img.height);
    canvas.toBlob((blob)=>{
        let link = document.createElement('a');
        link.href = window.URL.createObjectURL(blob);
        link.download = 'zzsp'; 
        link.click();  
    }, "image/jpeg");
}
img.setAttribute("crossOrigin",'Anonymous');
img.src = src;
复制代码

三十、ajax里面条件判断

$.ajax({
    type: "post",
    data: 
    contentType:
    url: 
    beforeSend: function () {
        if(){
        
        }else{
        
        };
    },
    success: function (data) {
        alert("保存失败");
    },
    error: function (data) {
        alert("保存成功");
    }
});
复制代码

三十一、ajax里面在数据请求之前加layui.load()时,请求状态必须是异步的才行( async: true)

$.ajax({
    type:"post",
    url: API,
    data: {
        'a':'project.kujiale.plan.YongliaoUser'
    },
    dataType: "json",
    async: true,
    beforeSend: function () {
        layer.load(1);
    },
    success: function(data) {   
        var item =data.data;
        list = item
        if(data.code==0){
            layer.closeAll();
            var url = '/module/designplan/searchplan/searchlist.jsp';
            layer.open({
            type: 2,
            title: "搜索方案",
            shadeClose: true,
            shade: 0.8,
            area: ['700px','500px'],
            content: [url]
            });
        }else{
            layer.msg(data.msg);
        }
    }
});

复制代码

三十二、js根据元素的属性获取到改元素其他属性的值

jquery
$("a[id=search]").attr("data-search")

原生js
document.querySelector("a[id=search]").getAttribute("data-search") //根据当前元素的属性获取该元素其他属性的值

document.querySelector("a[id=search]").text //根据当前属性获取该元素的值
document.querySelector("a[id=search]").innerText //根据当前属性获取该元素的值    
复制代码

三十三、数组对象提交时转化问题

JSON.stringify(userList)
复制代码

三十四、layui使用

1、关闭弹窗

layer.msg('分配成功',{time: 1000},function () {
    var index = parent.layer.getFrameIndex(window.name);
    parent.layer.close(index);
})
复制代码

2、关闭弹窗,刷新页面

window.location.reload();//刷新当前页面
window.parent.location.reload();//刷新父级页面
复制代码

三十五、js创建下载方式

download(data.data);

function downpdf(data){
    var link = document.createElement('a');
    link.href = data;
    link.target = '_blank';
    link.click();
    delete link;
}
复制代码

三十六、高阶函数

const isYoung = age => age < 25;

const message = msg => "He is "+ msg;

function isPersonOld(age, isYoung, message) {
    const returnMessage = isYoung(age)?message("young"):message("old");
    return returnMessage;
}

// passing functions as an arguments
    console.log(isPersonOld(13,isYoung,message))
// He is young
复制代码

递归

递归是一种函数在满足一定条件之前调用自身的技术。只要可能,最好使用递归而不是循环。你必须注意这一点,浏览器不能处理太多递归和抛出错误。
下面是一个演示递归的例子,在这个递归中,打印一个类似于楼梯的名称。我们也可以使用for循环,但只要可能,我们更喜欢递归。
复制代码
function printMyName(name, count) {
    if(count <= name.length) {
        console.log(name.substring(0,count));
        printMyName(name, ++count);
    }
}

console.log(printMyName("Bhargav", 1));

/*
    B
    Bh
    Bha
    Bhar
    Bharg
    Bharga
    Bhargav
*/

// withotu recursion
var name = "Bhargav"
var output = "";
for(let i=0; i<name.length; i++) {
    output = output + name[i];
    console.log(output);
}


作者:山水有轻音
链接:https://juejin.im/post/6873003814065012750

ue3.js 实战--备忘录的实现

最近没有学到什么好的技术知识点分享,不过本次给大家带来一个实战,那就是使用 vue.js3 + ts 完成了一个小小的备忘录。虽然每个人的手机都自带有备忘录,但我更倾向于自己写一个备忘录,自己记笔记用,好了废话不多说,咱们开始吧。

备忘录的设计

既然是制作一个 web 版本的备忘录,那肯定也是要好好设计一番的,备忘录主要包含三个页面,如下:

  1. 笔记展示页。
  2. 笔记新增/编辑页。
  3. 笔记详情页。

其中展示页如下图所示:

从上图我们可以知道展示页包含标题和中间笔记内容展示以及底部展示由几个备忘录和相关操作。

新增编辑页如下图所示:

从上图,我们可以看到新增编辑页就只是变更了中间的笔记展示内容为新增/编辑表单,然后底部操作按钮变更为保存按钮。

笔记详情页如下图所示:

详情页更简单,其实就是简单的展示即可。

也可以在线访问该备忘录。

虽然以上的页面看起来都比较简单,但其实包含了很多逻辑,下面让我一一娓娓道来。

技术知识点

本次备忘录我们用到了如下技术栈:

  1. vue3.js 基础语法以及状态管理工具 pinia 还有 vue 路由。
  2. 使用 localforage 来存储管理笔记数据(后期也可以改造成后端接口)。
  3. 封装了一个弹出框插件,以及使用到了自己写的消息提示框插件 ew-message。
  4. vite 搭建工程。

另外我们约定了笔记的数据结构,如下所示:

interface NoteDataItem extends NoteFormDataItem {
  id?: string;
  createDate?: string;
  updateDate?: string;
}

interface NoteFormDataItem {
  classification?: string;
  content?: string;
  title?: string;
}

这里可以讲解一下每个属性的含义,首先是 id 属性,这个不需多说,我们使用 uuid 来作为 id 属性值,其它如 title 则为笔记标题,content 则为笔记内容,classification 则为笔记分类,createDate 则为创建日期,updateDate 则为更新日期。

事实上 content 属性值我们还是有很大的改造空间的,因为我们的笔记应该不止有文本内容,还有图片链接,等等,但是这里我们只是考虑存储文本内容。

接下来我们来看下源码目录结构如下图所示:

可以看到我们的源码目录结构也很清晰,分析如下:

  1. 首先是 components 目录,这里主要放置我们封装的备忘录用到的组件。
  2. const 目录用来定义一些常量,比如我们这里用到了 iconfont 的很多图标,就定义在这个目录下。
  3. 然后就是 hooks 钩子函数目录。
  4. plugins 目录代表插件,这里主要是封装的弹出框插件。
  5. routes 目录,vue 路由目录。
  6. stores 目录,vue 状态管理数据。
  7. styles 目录,样式。
  8. utils 目录,工具函数。

可以这么说,虽然这只是一个小小的实战项目,但其实已经囊括了 vue 项目的基本,一个大型项目的基本骨架也就是这样慢慢从零到一累计起来的,只要掌握了本实战项目,那你的 vue.js 框架已经算是熟练使用呢。

根据实际效果,我们可以知道,整个备忘录其实整体变动不大,主要都是一些核心的小组件进行变动,比如新增编辑页面就主要变更图标还有中间内容区,再比如点击编辑多选删除的时候,也只是为数据增加一个多选框,因此,我们会在 app.vue 也就是根组件文件中定义一个 mainType 变量,用来确认当前是处于何种操作状态,并且我们还要将这种状态存储到会话存储中,以防止页面刷新时导致状态判断相关组件的显隐失效从而出现一些展示问题。

初始化项目

初始化项目很简单,其实按照官方vite文档上的说明即可初始化成功,这里不需要多说,然后我们需要安装相关依赖。如下所示:

pnpm  ew-message localforage pinia vue-router --save-dev

安装好我们需要的依赖之后,接下来按照我们最终实现的源码格式,我们删掉 app.vue 里面的一些示例代码,以及 components 目录下的 helloworld.vue 文件,新增 const,stores,routes,plugins,hooks,utils,styles 等目录。在 app.vue 同目录下新建一个 global.d.ts 用来定义备忘录笔记数据结构,如下所示:

// global.d.ts
interface NoteDataItem extends NoteFormDataItem {
  id?: string;
  createDate?: string;
  updateDate?: string;
}

interface NoteFormDataItem {
  classification?: string;
  content?: string;
  title?: string;
}

需要注意的就是这里我们为什么要区分出 NoteFormDataItem 和 NoteDataItem,这也是区分新增和编辑表单数据与最终展示的数据的结构,新增/编辑表单数据我们只需要核心的三个数据即可。

ps: 当然,这里其实我们还可以设计一个状态字段,用于判断该备忘录是否已经完成,不过这属于后续扩展,这里暂时不讲解。

接下来,我们先来定义路由并挂载到 vue 根实例上,在 routes 目录下新建一个 route.ts,里面写上如下代码:

import { createRouter, createWebHashHistory } from 'vue-router';
const routes = [
  {
    path: '/',
    name: 'index',
    component: () => import('../components/List/List.vue')
  },
  {
    path: '/detail/:uuid',
    name: 'detail',
    component: () => import('../components/Detail/Detail.vue')
  },
  {
    path: '/*',
    name: 'error',
    component: () => import('../components/Error/Error.vue')
  }
];
const router = createRouter({
  history: createWebHashHistory(),
  routes
});

router.beforeEach((to, _, next) => {
  if (to.matched.length === 0) {
    // 没有匹配到路由则跳转到404页面
    next({ name: 'error' });
  } else {
    next(); // 正常跳转到相应路由
  }
});

export default router;

这里可以解释一下,我们使用 createRouter 方法创建一个路由,这里采用的是 hash 模式而非 history 模式,同理我们定义了一个 routs 路由数组,路由数组包含了是哪个路由配置对象,分别是 path 定义路由路径,name 定义路由名字,component 用于渲染相关组件,这里包含了 3 个组件,列表组件 List,详情组件 Detail 和错误组件 Error,写上这三个组件导入的代码的同时,我们可以在 components 目录下新建这三个组件,然后我们写了一个路由导航守卫方法,在方法里根据 matched.length 属性从而判断是否匹配到相关路由,如果没有匹配到,就跳转到 404 页面,否则即正常跳转,这里的 next 方法是 beforeEach 暴露出来的参数,具体用法可以参考vue-router 官方文档。

在 main.ts 中,我们改造一下代码,如下所示:

import { createApp } from 'vue';
import { createPinia } from 'pinia';
import './styles/variable.css';
import './styles/common.css';
import App from './App.vue';
import router from './routes/route';

const pinia = createPinia();
const app = createApp(App);
app.use(router);
app.use(pinia).mount('#app');

可以看到,我们使用 app.use 方法将 pinia 和 router 都挂载到了根实例上,然后我们导入了两个样式文件 common.css 和 variable.css,这两个文件我们将创建在 styles 目录下。

由于备忘录整体样式比较简单,基本上没有什么可以多讲的知识点,唯一需要说明的就是这里的输入框元素,我们是通过 div 元素模拟的,并且我们通过 attr 属性成功将元素的 placeholder 属性的内容值渲染到标签元素中。css 样式代码如下所示:

.ew-note-textarea:focus,
.ew-note-textarea:focus::before,
.ew-note-textarea:not(:empty):before,
.ew-note-input:focus,
.ew-note-input:focus::before,
.ew-note-input:not(:empty):before {
  content: '';
}

.ew-note-textarea::before,
.ew-note-input::before {
  content: attr(placeholder);
  display: block;
  color: var(--mainTextareaPlaceholderColor--);
  letter-spacing: 2px;
}

其它都是一些基础样式没什么好说的,感兴趣的可以参考源码。

接下来,我们需要创建 2 个状态,第一个就是新增备忘录时用到的表单数据,而第二个则是我们选中数据时存储的 id 数组,这里由于组件嵌套太深,因此我们使用状态管理工具来管理表单数据和选中数据存储的 ID 数组(这个数据主要用来批量删除备忘录数据的)。在 stores 目录下新建 checkedStore.ts 和 noteStore.ts,里面的代码分别如下所示:

// checkedStore.ts
import { defineStore } from 'pinia';
export const useCheckedStore = defineStore('noteCheckedData', {
  state: () => {
    return {
      checkedData: [] as string[]
    };
  }
});
// noteStore.ts
import { defineStore } from 'pinia';
import { updateFormKeys } from '../const/keys';

export const useNoteStore = defineStore('noteFormData', {
  state: () => {
    return {
      title: '',
      classification: '',
      content: ''
    };
  },
  actions: {
    clear() {
      updateFormKeys.forEach(key => {
        this[key as keyof NoteFormDataItem] = '';
      });
    }
  }
});

可以看到,我们使用 pinia 提供的 defineStore 方法定义 2 个状态,这个方法接受 2 个参数,第一个是数据 key,第二个则是数据配置对象,配置对象中可以配置 state 以及 actions,state 即状态,actions 即行为。这里我们还只是简单的使用 pinia 来定义状态,因为我们这样定义就足够了。值得注意的就是第二个 store 里面我们定义了一个 clear 方法,顾名思义就是清空数据状态值,这里引入了一个 updateFormKeys 属性数组。它在 const 目录下的 keys.ts 中定义,代码如下所示:

// keys.ts
export const updateFormKeys = ['title', 'classification', 'content'];

到这里为止,我们的基础项目核心就搭建好了,接下来,让我们一步一步对每个模块的代码进行分析。

工具函数模块

工具函数用到的也不多,主要分为以下几类:

  1. 数据类型的判断。
  2. 创建 uuid。
  3. 回到顶部工具函数。
  4. 时间日期格式化。
  5. 操作类名工具函数。

接下来,我们就按照以上五个类别来逐一分析每一个工具函数。

数据类型的判断

首先是数据类型的判断,这里我们主要用到了是否是字符串,是否是布尔值以及是否是对象的数据类型,这里我们使用 typeof 操作符来判断数据类型,如下所示:

export const isString = <T>(value: T) => typeof value === 'string';
export const isBoolean = <T>(v: T) => typeof v === 'boolean';
export const isObject = <T>(v: T) => v && typeof v === 'object';

除此之外,还有一个判断是否是空对象的工具函数,很简单,首先判断是否是对象,然后使用 Object.keys 方法获取对象的属性,收集成为一个数组,然后判断数组的长度是否为 0 即可判断是否是空对象,代码如下所示:

export const isEmptyObject = <T>(v: T) =>
  isObject(v) && Object.keys(v as object).length === 0;

创建 uuid 工具函数

创建 uuid,我们使用 Math.random 函数取随机数,然后乘以一个几万或者几十万的数值,然后去截取,再与当前创建日期拼接起来,再拼接一个随机数,每一次拼接使用-来拼接起来,即可得到最终的 uuid,这样也能确保每次创建出来的 uuid 是唯一的。代码如下所示:

export const createUUID = () =>
  (Math.random() * 10000000).toString(16).substring(0, 4) +
  '-' +
  new Date().getTime() +
  '-' +
  Math.random().toString().substring(2, 5);

回到顶部工具函数

要实现回到顶部的逻辑,那么就需要监听事件,因此我们首先需要封装一个 on 方法,使用 element.addEventListener 来监听一个事件。代码如下所示:

export const on = (
  element: HTMLElement | Document | Element | Window,
  type: string,
  handler: EventListenerOrEventListenerObject,
  useCapture = false
) => {
  if (element && type && handler) {
    element.addEventListener(type, handler, useCapture);
  }
};

然后实现回到顶部的逻辑就是分两步,第一步就是页面滚动超出可见区域高度的时候,就出现回到顶部按钮否则就隐藏的逻辑,第二步则是点击回到顶部按钮修改滚动值为 0,这里采用定时器的方式使得滚动值是缓缓变成 0 的。基于这个思路,我们可以写出如下代码:

export const toTop = (top: HTMLElement, scrollEl?: HTMLElement) => {
  let scrollElement = scrollEl
      ? scrollEl
      : document.documentElement || document.body,
    timer: ReturnType<typeof setTimeout> | null = null,
    backTop = true;
  const onScrollHandler = () => {
    const oTop = scrollElement.scrollTop;
    // 可能有10px的偏差
    const clientHeight = Math.max(
      scrollElement?.scrollHeight - scrollElement.offsetHeight - 10,
      0
    );
    if (oTop > clientHeight) {
      top.style.visibility = 'visible';
    } else {
      top.style.visibility = 'hidden';
    }
    if (!backTop && timer) {
      clearTimeout(timer);
    }
    backTop = true;
  };
  const toTopHandler = () => {
    const oTop = scrollElement.scrollTop,
      speed = Math.floor(-oTop / 6);
    scrollElement.scrollTop = oTop + speed;
    if (oTop === 0) {
      timer && clearTimeout(timer);
      top.style.visibility = 'hidden';
      backTop = false;
    } else {
      timer = setTimeout(toTopHandler, 30);
    }
  };
  on(top, 'click', toTopHandler);
  on(scrollElement || window, 'scroll', onScrollHandler);
};

以上之所以创建一个 backTop 变量,是为了保证两个逻辑之间不起冲突。这个方法支持传入 2 个参数,第一个参数为回到顶部按钮元素,第二个参数则为滚动元素(也就是出现滚动条的元素)。这里由于我们实现的弹窗插件也用到了一些工具函数,会和这里重复,因此我们单独提取出来封装成了一个类,如下所示:

// baseUtils.ts
export default class ewWebsiteBaseUtils {
  eventType: string[];
  constructor() {
    this.eventType = this.isMobile()
      ? ['touchstart', 'touchmove', 'touchend']
      : ['mousedown', 'mousemove', 'mouseup'];
  }
  isMobile() {
    return !!navigator.userAgent.match(/(iPhone|iPod|Android|ios)/i);
  }
  $(selector: string, el: Document | HTMLElement = document) {
    return el.querySelector(selector);
  }
  $$(selector: string, el: Document | HTMLElement = document) {
    return el.querySelectorAll(selector);
  }

  getStyle(
    el: HTMLElement,
    selector: string | null | undefined = null,
    prop: string
  ) {
    const getComputedStyle = window.getComputedStyle || document.defaultView;
    return getComputedStyle(el, selector).getPropertyValue(prop);
  }
  hasClass(el: HTMLElement, className: string) {
    if (el.classList.contains) {
      return el.classList.contains(className);
    } else {
      const matchRegExp = new RegExp('(^|\\s)' + className + '(\\s|$)');
      return matchRegExp.test(el.className);
    }
  }
  handleClassName(className?: string, status?: boolean) {
    const condition = this.isBoolean(status)
      ? status
      : this.isString(className) && className;
    return condition ? ` ${className}` : '';
  }
  handleTemplate(isRender?: boolean, template?: string) {
    return this.isBoolean(isRender) &&
      isRender &&
      this.isString(template) &&
      template
      ? template
      : '';
  }

  isObject<T>(v: T) {
    return v && typeof v === 'object';
  }
  isString<T>(value: T) {
    return typeof value === 'string';
  }

  isBoolean<T>(v: T) {
    return typeof v === 'boolean';
  }

  on(
    element: HTMLElement | Document | Element | Window,
    type: string,
    handler: EventListenerOrEventListenerObject,
    useCapture = false
  ) {
    if (element && type && handler) {
      element.addEventListener(type, handler, useCapture);
    }
  }
  off(
    element: HTMLElement | Document | Element | Window,
    type: string,
    handler: EventListenerOrEventListenerObject,
    useCapture = false
  ) {
    if (element && type && handler) {
      element.removeEventListener(type, handler, useCapture);
    }
  }

  create(tagName: string) {
    return document.createElement(tagName);
  }
  createElement(str: string) {
    const element = this.create('div');
    element.innerHTML = str;
    return element.firstElementChild;
  }
  assign(target: Record<string, any>, ...args: Record<string, any>[]) {
    if (Object.assign) {
      return Object.assign(target, ...args);
    } else {
      if (target === null) {
        return;
      }
      const _ = Object(target);
      args.forEach(item => {
        if (this.isObject(item)) {
          for (let key in item) {
            if (Object.prototype.hasOwnProperty.call(item, key)) {
              _[key] = item[key];
            }
          }
        }
      });
      return _;
    }
  }
  addClass(el: HTMLElement, className: string) {
    return el.classList.add(className);
  }
  removeClass(el: HTMLElement, className: string) {
    return el.classList.remove(className);
  }
}

时间日期格式化

接下来我们就是封装一下时间日期的格式化,其实很简单,就是通过 Date 对象获取到年月日时分秒,然后改下格式即可,代码如下所示:

export const formatNumber = (n: number | string) => {
  n = n.toString();
  return n[1] ? n : '0' + n;
};
export const formatTime = (date: Date = new Date()) => {
  const year = date.getFullYear();
  const month = date.getMonth() + 1;
  const day = date.getDate();
  const hour = date.getHours();
  const minute = date.getMinutes();
  const second = date.getSeconds();
  return (
    [year, month, day].map(formatNumber).join('-') +
    ' ' +
    [hour, minute, second].map(formatNumber).join(':')
  );
};

这里有一个有意思的点,就是 formatNumber 函数当中如何确定是需要补零的呢?首先年份我们是不需要补零的,至于其它时间只有小于 10 的情况下才会补零,因此我们转成字符串,只需要判断如果第二个字符存在,代表大于 10 了,就不需要补零,否则才补零。

操作类名工具函数

操作类名函数,这个我主要用在了 svg 元素上,观察 const/icon.ts 中,我的图标是如此定义的,如下所示:

import { handleClassName } from '../utils/utils';
export const cancelIcon = (className?: string) =>
  `<svg t="1701689019983" class="cancel-icon${handleClassName(
    className
  )}" viewBox="0 0 1140 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="9471" ><path d="M474.133828 76.681372c-261.931418 0-474.133828 212.297312-474.133828 474.133828 0 261.836515 212.20241 474.133828 474.133828 474.133828s474.133828-212.297312 474.133828-474.133828C948.267655 288.978684 735.970343 76.681372 474.133828 76.681372zM521.774977 637.271548 521.774977 521.774977c-57.321223 0-203.471362 1.328638-203.471362 158.487488 0 82.28063 55.80278 150.990176 130.016682 166.838925C329.217424 830.208712 237.066914 724.487118 237.066914 595.134754c0-240.293605 245.228545-242.286562 284.708063-242.286562L521.774977 254.529008l189.330862 192.08304L521.774977 637.271548z" p-id="9472"></path></svg>`;
export const emptyDataIcon = (className?: string) =>
  `<svg t="1690278699020" class="empty-data-icon${handleClassName(
    className
  )}" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3148"><path d="M102.4 896a409.6 51.2 0 1 0 819.2 0 409.6 51.2 0 1 0-819.2 0Z" opacity=".1" p-id="3149"></path><path d="M116.736 376.832c0 8.704 6.656 15.36 15.36 15.36s15.36-6.656 15.36-15.36-6.656-15.36-15.36-15.36c-8.192 0-15.36 7.168-15.36 15.36zM926.72 832c-19.456 5.12-23.552 9.216-28.16 28.16-5.12-19.456-9.216-23.552-28.16-28.16 18.944-5.12 23.552-9.216 28.16-28.16 4.608 18.944 8.704 23.552 28.16 28.16zM202.24 323.072c-25.088 6.656-30.208 11.776-36.864 36.864-6.656-25.088-11.776-30.208-36.864-36.864 25.088-6.656 30.208-12.288 36.864-36.864 6.144 25.088 11.776 30.208 36.864 36.864zM816.64 235.008c-15.36 4.096-18.432 7.168-22.528 22.528-4.096-15.36-7.168-18.432-22.528-22.528 15.36-4.096 18.432-7.168 22.528-22.528 3.584 15.36 7.168 18.432 22.528 22.528zM882.688 156.16c-39.936 10.24-48.128 18.944-58.88 58.88-10.24-39.936-18.944-48.128-58.88-58.88 39.936-10.24 48.128-18.944 58.88-58.88 10.24 39.424 18.944 48.128 58.88 58.88z" opacity=".5" p-id="3150"></path><path d="M419.84 713.216v4.096l33.792 31.232 129.536-62.976L465.92 760.832v36.864l18.944-18.432v-0.512 0.512l18.944 18.432 100.352-122.88v-4.096z" opacity=".2" p-id="3151"></path><path d="M860.16 551.936v-1.024c0-1.024-0.512-1.536-0.512-2.56v-0.512l-110.08-287.232c-15.872-48.64-60.928-81.408-112.128-81.408H387.072c-51.2 0-96.256 32.768-112.128 81.408L164.864 547.84v0.512c-0.512 1.024-0.512 1.536-0.512 2.56V757.76c0 65.024 52.736 117.76 117.76 117.76h460.8c65.024 0 117.76-52.736 117.76-117.76v-204.8c-0.512-0.512-0.512-0.512-0.512-1.024zM303.616 271.36s0-0.512 0.512-0.512C315.392 233.984 349.184 209.92 387.072 209.92h249.856c37.888 0 71.68 24.064 83.456 60.416 0 0 0 0.512 0.512 0.512l101.888 266.24H588.8c-8.704 0-15.36 6.656-15.36 15.36 0 33.792-27.648 61.44-61.44 61.44s-61.44-27.648-61.44-61.44c0-8.704-6.656-15.36-15.36-15.36H201.728L303.616 271.36zM829.44 757.76c0 48.128-38.912 87.04-87.04 87.04H281.6c-48.128 0-87.04-38.912-87.04-87.04v-189.44h226.816c7.168 43.52 45.056 76.8 90.624 76.8s83.456-33.28 90.624-76.8H829.44v189.44z" opacity=".5" p-id="3152"></path><path d="M512 578.56c-14.336 0-25.6-11.264-25.6-25.6V501.76H253.44l83.968-219.136 0.512-1.024c7.168-21.504 26.624-35.84 49.152-35.84h249.856c22.528 0 41.984 14.336 49.152 35.84l0.512 1.024L770.56 501.76H537.6v51.2c0 14.336-11.264 25.6-25.6 25.6z" opacity=".2" p-id="3153"></path></svg>`;
export const addIcon = (className?: string) =>
  `<svg t="1697700092492" class="add-icon${handleClassName(
    className
  )}" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="900" ><path d="M560.064 149.952a48 48 0 0 0-96 0V464H150.016a48 48 0 0 0 0 96H464v313.984a48 48 0 0 0 96 0V560h314.048a48 48 0 0 0 0-96H560V149.952z" p-id="901"></path></svg>`;
export const closeIcon = (className?: string) =>
  `<svg t="1690189203554" class="close-icon${handleClassName(
    className
  )}" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2272"><path d="M504.224 470.288l207.84-207.84a16 16 0 0 1 22.608 0l11.328 11.328a16 16 0 0 1 0 22.624l-207.84 207.824 207.84 207.84a16 16 0 0 1 0 22.608l-11.328 11.328a16 16 0 0 1-22.624 0l-207.824-207.84-207.84 207.84a16 16 0 0 1-22.608 0l-11.328-11.328a16 16 0 0 1 0-22.624l207.84-207.824-207.84-207.84a16 16 0 0 1 0-22.608l11.328-11.328a16 16 0 0 1 22.624 0l207.824 207.84z" p-id="2273"></path></svg>`;
export const checkedIcon = (className?: string) =>
  `<svg t="1702382629512" class="checked-icon${handleClassName(
    className
  )}" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2321" ><path d="M969.6 208c-9.6-9.6-25.6-9.6-35.2 0l-508.8 537.6c-19.2 19.2-48 19.2-70.4 3.2l-265.6-252.8c-9.6-9.6-25.6-9.6-35.2 0-9.6 9.6-9.6 25.6 0 35.2l265.6 252.8c38.4 38.4 102.4 35.2 137.6-3.2l508.8-537.6C979.2 233.6 979.2 217.6 969.6 208z" p-id="2322"></path></svg>`;
export const editIcon = (className?: string) =>
  `<svg t="1702451742331" class="edit-icon${handleClassName(
    className
  )}" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3330"><path d="M862.709333 116.042667a32 32 0 1 1 45.248 45.248L455.445333 613.813333a32 32 0 1 1-45.258666-45.258666L862.709333 116.053333zM853.333333 448a32 32 0 0 1 64 0v352c0 64.8-52.533333 117.333333-117.333333 117.333333H224c-64.8 0-117.333333-52.533333-117.333333-117.333333V224c0-64.8 52.533333-117.333333 117.333333-117.333333h341.333333a32 32 0 0 1 0 64H224a53.333333 53.333333 0 0 0-53.333333 53.333333v576a53.333333 53.333333 0 0 0 53.333333 53.333333h576a53.333333 53.333333 0 0 0 53.333333-53.333333V448z" p-id="3331"></path></svg>`;
export const deleteIcon = (className?: string) =>
  `<svg t="1702452402229" class="delete-icon${handleClassName(
    className
  )}" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4351" ><path d="M96 128h832v64H96zM128 256h768l-89.024 704H217.024z" p-id="4352"></path><path d="M384 64h256v96h-256z" p-id="4353"></path></svg>`;
export const backIcon = (className?: string) =>
  `<svg t="1702455221301" class="back-icon${handleClassName(
    className
  )}" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5323" ><path d="M624.788992 204.047974 585.205965 164.464026 219.560038 530.185011 585.205965 895.864013 624.788992 856.280986 298.663014 530.16105Z" p-id="5324"></path></svg>`;
export const arrowRightIcon = (className?: string) =>
  `<svg t="1702456062203" class="arrow-right-icon${handleClassName(
    className
  )}" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5477"><path d="M289.301454 938.361551c8.958022 8.93551 24.607444 7.868201 34.877345-2.312672l405.886217-403.662573c5.846148-5.780657 8.581446-13.271258 8.314363-20.306488 0.331551-7.080256-2.423189-14.637372-8.270361-20.463054L324.178799 87.966471c-10.269901-10.225899-25.875321-11.248182-34.877345-2.322905-8.960069 8.946766-7.936763 24.451902 2.334161 34.666544l393.880789 391.68068L291.635615 903.68375C281.364691 913.908626 280.341385 929.423995 289.301454 938.361551z" p-id="5478"></path></svg>`;
export const saveIcon = (className?: string) =>
  `<svg t="1702465877637" class="save-icon${handleClassName(
    className
  )}" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6475"><path d="M814.805 128a51.179 51.179 0 0 1 51.179 51.179V844.01a51.179 51.179 0 0 1-51.179 51.157H201.173a51.179 51.179 0 0 1-51.178-51.157V179.179A51.179 51.179 0 0 1 201.173 128h613.654zM329.024 434.837a51.093 51.093 0 0 1-51.179-51.093V179.157h-76.672v664.854h613.76V179.179H738.22v204.48a51.179 51.179 0 0 1-51.179 51.178H329.024z m0-51.093h357.995V179.157H329.024v204.587z m357.91 204.501a25.557 25.557 0 1 1 0.085 51.072H329.024a25.536 25.536 0 1 1 0-51.072h357.91z" p-id="6476"></path></svg>`;
export const errorIcon = (className?: string) =>
  `<svg t="1702887842356" class="error-icon${handleClassName(
    className
  )}" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2350"><path d="M931.6 585.6v79c28.6-60.2 44.8-127.4 44.8-198.4C976.4 211 769.4 4 514.2 4S52 211 52 466.2c0 3.2 0.2 6.4 0.2 9.6l166-206h96.4L171.8 485.6h46.4v-54.8l99.2-154.6V668h-99.2v-82.4H67.6c43 161 170.6 287.4 332.4 328.6-10.4 26.2-40.6 89.4-90.8 100.6-62.2 14 168.8 3.4 333.6-104.6C769.4 873.6 873.6 784.4 930.2 668h-97.6v-82.4H666.4V476l166.2-206.2h94L786.2 485.6h46.4v-59l99.2-154v313zM366.2 608c-4.8-11.2-7.2-23.2-7.2-36V357.6c0-12.8 2.4-24.8 7.2-36 4.8-11.2 11.4-21 19.6-29.2 8.2-8.2 18-14.8 29.2-19.6 11.2-4.8 23.2-7.2 36-7.2h81.6c12.8 0 24.8 2.4 36 7.2 11 4.8 20.6 11.2 28.8 19.2l-88.6 129.4v-23c0-4.8-1.6-8.8-4.8-12-3.2-3.2-7.2-4.8-12-4.8s-8.8 1.6-12 4.8c-3.2 3.2-4.8 7.2-4.8 12v72L372.6 620c-2.4-3.8-4.6-7.8-6.4-12z m258.2-36c0 12.8-2.4 24.8-7.2 36-4.8 11.2-11.4 21-19.6 29.2-8.2 8.2-18 14.8-29.2 19.6-11.2 4.8-23.2 7.2-36 7.2h-81.6c-12.8 0-24.8-2.4-36-7.2-11.2-4.8-21-11.4-29.2-19.6-3.6-3.6-7-7.8-10-12l99.2-144.6v50.6c0 4.8 1.6 8.8 4.8 12 3.2 3.2 7.2 4.8 12 4.8s8.8-1.6 12-4.8c3.2-3.2 4.8-7.2 4.8-12v-99.6L601 296.4c6.6 7.4 12 15.8 16 25.2 4.8 11.2 7.2 23.2 7.2 36V572z"  p-id="2351"></path></svg>`;
export const searchIcon = (className?: string) =>
  `<svg t="1702966824556" class="search-icon${handleClassName(
    className
  )}" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3880"><path d="M624 293.92h114.96v461.04a48 48 0 0 1-48 48H244.36a48 48 0 0 1-48-48v-576a48 48 0 0 1 48-48h332v114.96a48 48 0 0 0 47.64 48z" fill="#CBECF9" p-id="3881"></path><path d="M624 293.92h114.96v410.76a48 48 0 0 1-48 48H244.36a48 48 0 0 1-48-48V178.96a48 48 0 0 1 48-48h332v114.96a48 48 0 0 0 47.64 48z" fill="#FFFFFF" p-id="3882"></path><path d="M651.04 316.88m0 28.16l0 0.04q0 28.16-28.16 28.16l-310.24 0q-28.16 0-28.16-28.16l0-0.04q0-28.16 28.16-28.16l310.24 0q28.16 0 28.16 28.16Z" fill="#90FC95" p-id="3883"></path><path d="M526.52 398.16m0 28.16l0 0.04q0 28.16-28.16 28.16l-185.72 0q-28.16 0-28.16-28.16l0-0.04q0-28.16 28.16-28.16l185.72 0q28.16 0 28.16 28.16Z" fill="#90FC95" p-id="3884"></path><path d="M480.04 479.44m0 28.16l0 0.04q0 28.16-28.16 28.16l-139.24 0q-28.16 0-28.16-28.16l0-0.04q0-28.16 28.16-28.16l139.24 0q28.16 0 28.16 28.16Z" fill="#90FC95" p-id="3885"></path><path d="M615.16 560.72m0 28.16l0 0.04q0 28.16-28.16 28.16l-274.36 0q-28.16 0-28.16-28.16l0-0.04q0-28.16 28.16-28.16l274.36 0q28.16 0 28.16 28.16Z" fill="#90FC95" p-id="3886"></path><path d="M739.16 325.6H624a48 48 0 0 1-48-48V162.64l162.96 131.28z" fill="#CBECF9" p-id="3887"></path><path d="M691.16 810.96H244.36a56 56 0 0 1-56-56v-576a56 56 0 0 1 56-56h332a8 8 0 0 1 8 8v114.96a40 40 0 0 0 40 40h114.96a8 8 0 0 1 8 8v461.04a56 56 0 0 1-56.16 56z m-446.8-672a40 40 0 0 0-40 40v576a40 40 0 0 0 40 40h446.8a40 40 0 0 0 40-40V301.92H624a56 56 0 0 1-56-56V138.96z" fill="#2FB1EA" p-id="3888"></path><path d="M739.16 293.92H624a48 48 0 0 1-48-48V130.96z" fill="#E5F5FC" p-id="3889"></path><path d="M739.16 301.92H624a56 56 0 0 1-56-56V130.96a8 8 0 0 1 13.64-5.64l163.16 162.96a8 8 0 0 1-5.64 13.64zM584 150.28v95.64a40 40 0 0 0 40 40h96zM794.68 894L628.72 728a24 24 0 0 1 33.96-33.96L828.64 860a24 24 0 0 1-33.96 33.96z" fill="#2FB1EA" p-id="3890"></path><path d="M689.92 721.36l-27.28-27.28a24 24 0 0 0-33.96 33.96l27.28 27.28a209.76 209.76 0 0 0 33.96-33.96z" fill="#1A96E2" p-id="3891"></path><path d="M526.96 592.32m-168 0a168 168 0 1 0 336 0 168 168 0 1 0-336 0Z" fill="#FFC444" p-id="3892"></path><path d="M526.96 579.08m-154.76 0a154.76 154.76 0 1 0 309.52 0 154.76 154.76 0 1 0-309.52 0Z" fill="#FFE76E" p-id="3893"></path><path d="M526.96 768.32a176 176 0 1 1 176-176 176 176 0 0 1-176 176z m0-336a160 160 0 1 0 160 160 160 160 0 0 0-160-160z" fill="#2FB1EA" p-id="3894"></path><path d="M526.96 582m-131.48 0a131.48 131.48 0 1 0 262.96 0 131.48 131.48 0 1 0-262.96 0Z" fill="#FFC444" p-id="3895"></path><path d="M526.96 592.32m-121.16 0a121.16 121.16 0 1 0 242.32 0 121.16 121.16 0 1 0-242.32 0Z" fill="#FFFFFF" p-id="3896"></path><path d="M484.2 509.4a37.56 37.56 0 0 0-10.4-25.96 121.56 121.56 0 0 0-59.24 63.72h32a37.72 37.72 0 0 0 37.64-37.76zM648 586.64a37.52 37.52 0 0 0-20.56-6.12h-221.08c-0.36 4-0.56 8-0.56 11.8A120.56 120.56 0 0 0 424 656H630.2a120.56 120.56 0 0 0 18-63.56c-0.2-2.04-0.2-3.92-0.2-5.8z" fill="#90FC95" p-id="3897"></path><path d="M526.96 721.48A129.16 129.16 0 1 1 656 592.32a129.28 129.28 0 0 1-129.04 129.16z m0-242.32A113.16 113.16 0 1 0 640 592.32a113.28 113.28 0 0 0-113.04-113.16z" fill="#2FB1EA" p-id="3898"></path><path d="M776 176m-20 0a20 20 0 1 0 40 0 20 20 0 1 0-40 0Z" fill="#D4FFD4" p-id="3899"></path><path d="M156 568m-16 0a16 16 0 1 0 32 0 16 16 0 1 0-32 0Z" fill="#D4FFD4" p-id="3900"></path><path d="M132 188m-12 0a12 12 0 1 0 24 0 12 12 0 1 0-24 0Z" fill="#D4FFD4" p-id="3901"></path><path d="M808 428m-8 0a8 8 0 1 0 16 0 8 8 0 1 0-16 0Z" fill="#D4FFD4" p-id="3902"></path><path d="M916 908m-4 0a4 4 0 1 0 8 0 4 4 0 1 0-8 0Z" fill="#D4FFD4" p-id="3903"></path><path d="M860 996m-20 0a20 20 0 1 0 40 0 20 20 0 1 0-40 0Z" fill="#FFBDBD" p-id="3904"></path><path d="M828 716m-16 0a16 16 0 1 0 32 0 16 16 0 1 0-32 0Z" fill="#FFBDBD" p-id="3905"></path><path d="M272 948m-12 0a12 12 0 1 0 24 0 12 12 0 1 0-24 0Z" fill="#FFBDBD" p-id="3906"></path><path d="M824 72m-8 0a8 8 0 1 0 16 0 8 8 0 1 0-16 0Z" fill="#FFBDBD" p-id="3907"></path><path d="M440 76m-4 0a4 4 0 1 0 8 0 4 4 0 1 0-8 0Z" fill="#FFBDBD" p-id="3908"></path><path d="M112 420m-20 0a20 20 0 1 0 40 0 20 20 0 1 0-40 0Z" fill="#BBF1FF" p-id="3909"></path><path d="M472 976m-16 0a16 16 0 1 0 32 0 16 16 0 1 0-32 0Z" fill="#BBF1FF" p-id="3910"></path><path d="M860 500m-12 0a12 12 0 1 0 24 0 12 12 0 1 0-24 0Z" fill="#BBF1FF" p-id="3911"></path><path d="M800 320m-8 0a8 8 0 1 0 16 0 8 8 0 1 0-16 0Z" fill="#BBF1FF" p-id="3912"></path><path d="M124 852m-4 0a4 4 0 1 0 8 0 4 4 0 1 0-8 0Z" fill="#BBF1FF" p-id="3913"></path><path d="M228 28m-20 0a20 20 0 1 0 40 0 20 20 0 1 0-40 0Z" fill="#FFF4C5" p-id="3914"></path><path d="M680 84m-16 0a16 16 0 1 0 32 0 16 16 0 1 0-32 0Z" fill="#FFF4C5" p-id="3915"></path><path d="M132 704m-12 0a12 12 0 1 0 24 0 12 12 0 1 0-24 0Z" fill="#FFF4C5" p-id="3916"></path><path d="M176 320m-8 0a8 8 0 1 0 16 0 8 8 0 1 0-16 0Z" fill="#FFF4C5" p-id="3917"></path><path d="M928 632m-4 0a4 4 0 1 0 8 0 4 4 0 1 0-8 0Z" fill="#FFF4C5" p-id="3918"></path></svg>`;

这也就促使了这个工具函数的诞生,那就是如果传入了类名,则需要空格区分添加进去,否则就返回空字符串,不添加即可,至于为什么要有第二个参数,那是因为在弹出框插件当中,如果是动态插入模板字符串,需要修改类名,那么就可能需要第二个布尔值。

通过以上分析,我们也就明白了这个工具函数的定义,如下所示:

export const handleClassName = (
  className?: string,
  status?: boolean
): string => {
  const condition = this.isBoolean(status)
    ? status
    : this.isString(className) && className;
  return condition ? ` ${className}` : '';
};

插件目录

弹出框插件的实现,不打算细讲,感兴趣的可以查看源码。

hooks 目录

hooks 主要封装了 2 个函数,第一个就是存储数据,第二个则是获取存储数据,代码如下所示:

import localforage from 'localforage';

export const useMemoData = async () => {
  let memoStoreCacheData: string =
    (await localforage.getItem<string>('memoData')) || '';
  let memoStoreData: Array<NoteDataItem> = [];
  try {
    memoStoreData = JSON.parse(memoStoreCacheData);
  } catch (error) {
    memoStoreData = [];
  }
  return memoStoreData;
};

export const useSetMemoData = async (
  data: Array<NoteDataItem>,
  isGetCache = true
) => {
  let memoStoreCacheData = isGetCache ? await useMemoData() : [];
  let memoStoreData: Array<NoteDataItem> = [...memoStoreCacheData, ...data];
  localforage.setItem('memoData', JSON.stringify(memoStoreData));
};

这两个 hooks 我们将在组件当中经常用到,也就是新增,编辑,删除备忘录的时候都会用到,这两个 hooks 函数的逻辑也好理解,第一个就是通过 getItem 方法获取到字符串数据然后通过 JSON.parse 解析成数组,而第二个则是实用 JSON.stringify 方法将数组转成字符串然后调用 setItem 存储。

接下来,就是各个组件的实现了。

组件模块

组件模块的所有代码都是很相似的,知识点也基本都是事件传递,单向数据流,监听数据等等。因此我们只需要通过分析一个根组件的源代码,基本就可以按照相同方式去理解其它组件。

根组件 app.vue

在根组件当中,我们可以看到,我们将页面拆分成了 3 个部分,即头部 header,中间内容 main 以及底部 footer。如下所示:

<!-- template部分 -->
<async-header
  :mainType="mainType"
  @on-back="onBackHandler"
  :memoData="memoData"
  @on-header-click="onHeaderClickHandler"
></async-header>
<async-main
  :mainType="mainType"
  :memo-data="memoData"
  @on-delete="getMemoData"
  @on-detail="onDetailHandler"
  @on-edit="onEditHandler"
  :editData="editData"
  :showCheckBox="showCheckBox"
  @on-search="onSearchHandler"
></async-main>
<async-footer
  @on-footer-click="onFooterClick"
  :mainType="mainType"
  v-if="mainType !== 'detail'"
  :totalNote="memoData.length"
  :editData="editData"
></async-footer>

其中 mainType 就是我们前面讲过的用来确定当前页面属于哪一模块,具体的值,我们定义有 add,save,detail,然后 memoData 就是我们的数据,editData 则是我们的编辑数据,它应该是一个对象,totalNote 就是总共有多少条备忘录,showCheckBox 表示是否显示多选框从而触发多选删除操作。其它就是一些事件,比如 on-back 就是点击返回按钮所执行的逻辑,on-delete 就是点击单个备忘录删除的逻辑,on-detail 点击跳转到备忘录详情的逻辑,on-edit 表示点击编辑单个备忘录的的逻辑,on-search 则是点击搜索的逻辑。

然后可以看下我们的 ts 逻辑,代码如下所示:

<script setup lang="ts">
import { defineAsyncComponent, ref, onMounted } from 'vue';
import { useRouter, useRoute } from 'vue-router';
import { useMemoData, useSetMemoData } from './hooks/useMemoData';
import { useCheckedStore } from './stores/checkedStore';
import ewMessage from 'ew-message';
import localforage from 'localforage';
import { ewConfirm } from './plugins/ewPopbox';
const AsyncHeader = defineAsyncComponent(
  () => import('./components/Header/Header.vue')
);
const AsyncMain = defineAsyncComponent(
  () => import('./components/Main/Main.vue')
);
const AsyncFooter = defineAsyncComponent(
  () => import('./components/Footer/Footer.vue')
);
const mainType = ref<string>('add');
const editData = ref<NoteDataItem>({});
const memoData = ref<NoteDataItem[]>([]);
const searchCacheMemoData = ref<NoteDataItem[]>([]);
const showCheckBox = ref(false);
const router = useRouter();
const route = useRoute();
const checkedStore = useCheckedStore();
const getMemoData = async () => {
  const memoStoreData = (await useMemoData()) || [];
  memoData.value = [...memoStoreData];
  searchCacheMemoData.value = memoData.value;
  const type = await localforage.getItem<string>('mainType');
  if (type) {
    mainType.value = type;
  }
  // 如果当前处于选中待删除状态,刷新页面后重置回未选中待删除状态
  if (type === 'delete') {
    mainType.value = 'add';
  }
};
const onBackHandler = () => {
  mainType.value = 'add';
  localforage.setItem('mainType', mainType.value);
  if (route.name === 'detail') {
    router.push({
      name: 'index'
    });
    showCheckBox.value = false;
    getMemoData();
  }
};
const onHeaderClickHandler = (v: string) => {
  const isCancel = v === 'cancel';
  showCheckBox.value = isCancel;
  mainType.value = isCancel ? 'delete' : 'add';
};
const onFooterClick = async (v: string, isClearEditData: boolean) => {
  if (v === 'editRefresh') {
    mainType.value = 'add';
  }
  if (v !== 'addRefresh') {
    mainType.value = v;
  }
  // 点击新增需要清空编辑数据
  if (isClearEditData) {
    editData.value = {};
  }
  // 新增或者编辑成功后都需要刷新列表
  if (v.toLowerCase().includes('refresh')) {
    getMemoData();
  }
  if (v === 'delete') {
    if (checkedStore.checkedData.length === 0) {
      return ewMessage.warning({
        content: '请选择需要删除的备忘录事项',
        duration: 4000
      });
    }
    ewConfirm({
      title: '温馨提示',
      content: '确定要删除这些备忘录事项吗?',
      showCancel: true,
      sure: async (ctx, e) => {
        e?.stopImmediatePropagation();
        searchCacheMemoData.value = memoData.value =
          searchCacheMemoData.value.filter(
            item => !checkedStore.checkedData.includes(item.id!)
          );
        if (memoData.value.length === 0) {
          mainType.value = 'add';
        }
        await useSetMemoData(memoData.value, false);
        // 删除完成需要清空
        checkedStore.$patch({ checkedData: [] });
        ewMessage.success({
          content: '删除成功',
          duration: 4000
        });
        ctx?.close(600);
        setTimeout(() => getMemoData(), 10);
      }
    });
  }
  localforage.setItem('mainType', mainType.value);
};

const onEditHandler = (id: string) => {
  mainType.value = 'save';
  editData.value = memoData.value.find(item => item.id === id) || {};
};

const onDetailHandler = () => {
  mainType.value = 'detail';
  localforage.setItem('mainType', mainType.value);
};

const onSearchHandler = (v: string) => {
  // if (!v) {
  //     return ewMessage.warning({
  //         content: "请输入需要搜素的内容",
  //         duration: 4000
  //     })
  // }
  const searchMemoData = searchCacheMemoData.value.filter(
    item =>
      item.content?.includes(v) ||
      item.title?.includes(v) ||
      item.classification?.includes(v)
  );
  memoData.value = searchMemoData;
};
onMounted(async () => {
  getMemoData();
});
</script>

接下来我们对以上代码逐一分析,总结下来就三步,第一步导入相关依赖或工具函数,第二步定义相关数据状态,第三步添加事件逻辑。

首先我们使用 defineAsyncComponent 这个方法来异步加载组件,这样做的好处就是懒加载组件,尽可能减少首页的渲染。然后我们会用一个 searchCacheMemoData 来缓存数据,因为我们的搜索功能需要基于缓存的数据来进行替换。然后还需要注意的就是,在多选删除或者删除数据之后,我们的缓存的数据也需要更换。其余的像 memoData,editData 等在前面我们也已经介绍过了,然后我们定义了一个 getMemoData 方法,这个方法就是获取备忘录数据的,我们通过 useMemoData 封装好的 hooks 函数来获取数据,然后在 onMounted 钩子函数中调用一次。

然后我们还需要缓存 mainType,这样能保证如果页面刷新后,当前页面所处于哪一种状态不会出现任何问题。接下来就是每一个事件的逻辑,比如 onBackHandler,在新增/编辑/详情页时会出现该按钮,点击该按钮我们就会返回到首页,如果是新增/编辑页点击返回,我们只需要修改 mainType 即可,而如果是详情页,我们就需要跳转路由,并且也需要重置 showCheckBox,还需要重新请求数据。

接下来是点击头部的图标的事件逻辑,即 onHeaderClickHandler 方法,这个比较简单,那就是在有数据的时候,会显示编辑按钮,点击编辑按钮,并且编辑按钮也变成取消按钮,同时底部就会变成删除按钮,并且会出现多选框,因此我们只需要修改 mainType 和 showCheckBox。

然后就是 onFooterClick 方法,也就是点击底部按钮的逻辑,这个方法稍微复杂一点,首先,默认情况下会是新增按钮,因此点击新增按钮的时候,下面要变成保存按钮,这是第一种情况,紧接着如果是第二种情况,那就是点击保存,点击保存也分为两种,是新增保存还是编辑保存,两者都需要刷新数据,因此我们回传了一个带 refresh 的字符串用来代表是否刷新数据,也就是重新请求 getMemoData,如果是新增的时候,我们还需要重置编辑的数据,因为点击编辑的时候,我们是赋值了 editData 的,然后就是点击删除,我们会给出一个弹窗,点击确定,就获取到选中的 id,然后根据 id 过滤掉数据并重新赋值,删除完成之后,我们给出一个提示,并且重置我们选中的 id,当然还要刷新列表请求数据。

接下来是 onEditHandler 方法,顾名思义,这个就是点击编辑的时候,在什么情况下呢?那就是单个数据会有编辑和删除项,因此也就需要这个方法了。这个方法做的事情也很简单,那就是修改 mainType 的值为 save,然后修改编辑数据。

紧接着就是 onDetailHandler 方法,这个方法就是修改 mainType 的值并存储,这个方法是底部传来的,关于详情页的跳转都在底部做了,因此在这里我们只需要修改 mainType 的值即可。

最后是我们的 onSearchHandler 方法,那就是根据搜索值过滤掉数据并修改数据即可。

多选框组件

components/CheckBox/CheckBox.vue 下是我们的多选框组件,代码如下所示:

<script setup lang="ts">
import { computed, ref } from 'vue';
import { checkedIcon } from '../../const/icon';
const emit = defineEmits(['on-change']);
const getCheckBoxIcon = computed(() =>
  checkedIcon('ew-note-checkbox-checked-icon')
);
const isChecked = ref(false);
const onClickHandler = () => {
  isChecked.value = !isChecked.value;
  emit('on-change', isChecked.value);
};
</script>
<template>
  <label
    class="ew-note-checkbox ew-note-flex-center"
    v-html="getCheckBoxIcon"
    :class="{ checked: isChecked }"
    @click="onClickHandler"
  ></label>
</template>
<style scoped>
.ew-note-checkbox {
  width: 28px;
  height: 28px;
  border-radius: 1px;
  border: 1px solid var(--ew-note-checkbox-border-color--);
  margin-right: 5px;
  cursor: pointer;
  color: var(--white--);
}

.ew-note-checkbox.checked {
  background-color: var(--ew-note-checkbox-bg-color--);
}
</style>

根据以上代码,我们可以看到组件的代码很简单,主要是用 defineEmits 定义一个事件传递给父组件,然后约定一个状态用来控制组件是否是选中状态,其余都是样式和简单的元素。

详情组件

components/Detail/Detail.vue 下就是我们的详情组件,也很简单,代码如下所示:

<script setup lang="ts">
import { useRoute } from 'vue-router';
import { computed } from 'vue';
const route = useRoute();
const props = withDefaults(defineProps<{ memoData?: NoteDataItem[] }>(), {});
const detailData = computed(
  () => props.memoData?.find(item => item.id === route.params.uuid) || {}
);
</script>
<template>
  <div class="ew-note-detail-container">
    <h1 class="ew-note-detail-title">{{ detailData?.title }}</h1>
    <div class="ew-note-detail-classification">
      {{ detailData?.classification }}
    </div>
    <div class="ew-note-detail-content">{{ detailData?.content }}</div>
    <div class="ew-note-detail-date">
      <p>创建日期: {{ detailData?.createDate }}</p>
      <p>更新日期: {{ detailData?.updateDate }}</p>
    </div>
  </div>
</template>

简单来说就是根据路由的 uuid 来获取当前是哪条备忘录数据,然后渲染到页面即可。

Error 组件

components/Error/Error.vue 代表 404 组件,如果路由未匹配到,就会渲染该组件,该组件代码也很简单,如下所示:

<script setup lang="ts">
import { errorIcon } from '../../const/icon';
import { computed } from 'vue';
const getErrorHTML = computed(
  () => `
    ${errorIcon('ew-note-error-icon')}
    <p>暂未找到该页面!</p>
    <a href="/"  class="ew-note-error-link">返回首页</a>
`
);
</script>
<template>
  <div class="ew-note-error ew-note-flex-center" v-html="getErrorHTML"></div>
</template>

使用 computed 缓存 html 子元素结构,然后使用 v-html 指令渲染即可,这里我们页渲染了错误的图标。

footer 组件

components/Footer/Footer.vue 就是对底部组件的封装,这里面的代码稍微复杂一点,我们先看所有代码如下所示:

<script setup lang="ts">
import { computed } from 'vue';
import {
  createUUID,
  formatTime,
  handleClassName,
  isEmptyObject
} from '../../utils/utils';
import { addIcon, deleteIcon, saveIcon } from '../../const/icon';
import { useNoteStore } from '../../stores/noteStore';
import { useMemoData, useSetMemoData } from '../../hooks/useMemoData';
import ewMessage from 'ew-message';

const props = defineProps({
  mainType: String,
  totalNote: Number,
  editData: Object
});
const noteStore = useNoteStore();
const emit = defineEmits(['on-footer-click']);
const getFooterBtnClassName = computed(() => props.mainType);
const getFooterIcon = computed(() => {
  if (props.mainType === 'add') {
    return addIcon('ew-note-add-btn-icon');
  } else if (props.mainType === 'delete') {
    return deleteIcon('ew-note-delete-btn-icon');
  } else {
    return saveIcon('ew-note-save-btn-icon');
  }
});
const addMemoData = async () => {
  if (!noteStore.title) {
    return ewMessage.warning({
      content: '请输入需要记录的事项标题',
      duration: 4000
    });
  }
  if (!noteStore.classification) {
    return ewMessage.warning({
      content: '请输入需要记录的事项分类',
      duration: 4000
    });
  }
  if (!noteStore.content) {
    return ewMessage.warning({
      content: '请输入需要记录的事项内容',
      duration: 4000
    });
  }
  let memoStoreData: NoteDataItem[] = [];
  memoStoreData.push({
    id: createUUID(),
    createDate: formatTime(),
    updateDate: '',
    ...noteStore.$state
  });
  await useSetMemoData(memoStoreData);
  ewMessage.success({
    content: '添加事项成功',
    duration: 2000
  });
  noteStore.clear();
};
const editMemoData = async () => {
  let memoStoreData: Array<NoteDataItem> = await useMemoData();
  memoStoreData = memoStoreData.map(item => {
    if (item.id === props.editData?.id) {
      return {
        ...props.editData,
        ...noteStore.$state,
        updateDate: formatTime()
      };
    } else {
      return item;
    }
  });
  await useSetMemoData(memoStoreData, false);
  ewMessage.success({
    content: '修改事项成功,2s后将跳转至首页',
    duration: 2000
  });
};
const onFooterClickHandler = async () => {
  if (props.mainType === 'add') {
    emit('on-footer-click', 'save', true);
  }
  if (props.mainType === 'save') {
    const isEdit = !isEmptyObject(props.editData);
    const type = isEdit ? 'editRefresh' : 'addRefresh';
    if (isEdit) {
      await editMemoData();
    } else {
      await addMemoData();
    }
    setTimeout(() => {
      emit('on-footer-click', type);
    }, 2100);
  }
  if (props.mainType === 'delete') {
    emit('on-footer-click', 'delete');
  }
};
</script>
<template>
  <footer class="ew-note-footer ew-note-flex-center">
    <h3 class="ew-note-footer-title">
      <span class="ew-note-footer-title-total">{{ props.totalNote || 0 }}</span
      >个备忘录
    </h3>
    <button
      type="button"
      :class="handleClassName(`ew-note-${getFooterBtnClassName}-btn`)"
      class="ew-note-btn"
      v-html="getFooterIcon"
      @click="onFooterClickHandler"
    ></button>
  </footer>
</template>

接下来我们来逐一分析,首先我们先分析一下 html 元素结构,很简单就包含一个标题,标题会展示有多少个备忘录数据,在前面的 app.vue 我们也能看到 totalNote 是从父组件传下来的,基于数据 memoData.length 计算而得到的结果。

然后就是按钮元素,按钮元素稍微有点复杂,其实主要是两步,因为按钮元素有保存 save 和新增 add 以及删除 delete 三种情况,因此这里我们分别设置了三个动态类名,以及渲染三个图标,不同的按钮元素,点击事件触发的逻辑也有所不同。

然后我们基于 mainType 的值来判断是触发什么逻辑,如果值是 add,代表我们点击的是新增,此时我们应该重置表单,因此需要修改 mainType 的值为 save,并向父组件抛出事件,传递 2 个参数,从前面 app.vue 我们可以知道第二个参数 boolean 值是用于清除新增表单时的数据,为什么会有这个逻辑呢?试想如果用户是点击编辑,此时赋值了编辑数据,也就渲染了编辑数据,再点击返回取消编辑,此时编辑数据是没有被重置的,然后我们再点击新增,那么就会变成编辑数据而非新增数据。

从父组件传下来主要有三个字段,即 mainType,totalNote 与 editData,点击新增和删除的逻辑还比较简单,就是向父组件抛出事件并传递相应参数即可,其余逻辑都在父组件那里处理了。

点击保存时会分成两种情况即新增保存和编辑保存,新增的时候需要判断是否有值,其实这里的校验都比较简单,只是简单判断是否输入值即可,如果未输入值,则给出提示,不执行后续逻辑,然后新建一个数组,创建一条数据,将相关值添加到数据中,最后调用 useSetMemoData 函数即可,而编辑则是获取当前的数据,根据 id 去修改相应的数据即可。

不管是什么保存,最终都需要向父组件抛出一个事件,好让父组件刷新页面数据,又或者这里还做了一个很有意思的功能,那就是新增完成,我们的页面是不会回到数据列表首页的,但是编辑完成是需要跳转的。

然后就是最开始的我们根据 mainType 来确定渲染的类名和渲染的图标,从而确定是渲染新增按钮还是保存按钮又或者是删除按钮。

Form 组件

form 组件就是我们的新增/编辑表单元素模版,其代码如下所示:

<script setup lang="ts">
import { watch } from 'vue';
import { isObject, isString } from '../../utils/utils';
import { useNoteStore } from '../../stores/noteStore';
import { updateFormKeys } from '../../const/keys';
const props = withDefaults(
  defineProps<{ editData?: Partial<NoteDataItem> }>(),
  {
    editData: undefined
  }
);

const noteStore = useNoteStore();
watch(
  () => props.editData,
  val => {
    if (isObject(val)) {
      updateFormKeys.forEach(key => {
        const value = val![key as keyof NoteDataItem];
        const store = {
          [key]: isString(value) ? value : ''
        };
        noteStore.$patch(store);
      });
    }
  },
  { immediate: true }
);

const onChangeForm = (v: Event) => {
  const target = v.target as HTMLElement;
  if (target) {
    const key = target.getAttribute('name') as keyof NoteFormDataItem;
    const value = target.textContent;
    if (key && value) {
      noteStore.$patch({
        [key]: value
      });
    }
  }
};
</script>
<template>
  <div
    contenteditable="true"
    class="ew-note-input ew-note-input-title"
    placeholder="请输入需要记录的事项标题"
    @input="onChangeForm"
    name="title"
  >
    {{ noteStore.title }}
  </div>
  <div
    contenteditable="true"
    class="ew-note-input ew-note-input-class"
    placeholder="请输入需要记录的事项分类"
    @input="onChangeForm"
    name="classification"
  >
    {{ noteStore.classification }}
  </div>
  <div
    contenteditable="true"
    class="ew-note-textarea ew-note-textarea-content"
    placeholder="请输入需要记录的事项内容"
    @input="onChangeForm"
    name="content"
  >
    {{ noteStore.content }}
  </div>
</template>

以上我们渲染了三个 div 元素并设置了 contenteditable 为 true 可以让元素像表单元素那样被编辑,然后绑定了相应的数据值,这里有一点就是我们添加了一个 name 属性,用来确定用户输入的是哪个字段的值。

然后我们监听是否有编辑数据,如果有就赋值,没有就是空值,可以看到,这里我们是通过 pinia 将表单数据放置在 store 里面的,因此这里我们使用的是 store.$patch 来赋值。

同样的我们监听三个 div 元素的 input 事件,也一样是修改 store。

Header 组件

components/Header/Header.vue 代表头部组件,其代码如下:

<script setup lang="ts">
import { backIcon, cancelIcon, editIcon } from '../../const/icon';
import { handleClassName } from '../../utils/utils';
import { ref, computed, watch } from 'vue';
const props = defineProps({
  mainType: String,
  memoData: Array
});
const emit = defineEmits(['on-back', 'on-header-click']);
const headerIconType = ref('');
const getHeaderIcon = computed(() => {
  if (headerIconType.value === 'edit') {
    return editIcon('ew-note-edit-btn-icon');
  } else if (headerIconType.value === 'cancel') {
    return cancelIcon('ew-note-cancel-btn-icon');
  } else {
    return '';
  }
});

const onBackHandler = () => {
  emit('on-back');
};

const onHeaderClick = () => {
  const val = headerIconType.value;
  if (val === '') {
    return;
  }
  headerIconType.value = val === 'edit' ? 'cancel' : 'edit';
  emit('on-header-click', headerIconType.value);
};

watch(
  [() => props.mainType, () => props.memoData],
  val => {
    const [mainType, memoData] = val;
    const noData = Array.isArray(memoData) && memoData.length;
    if (mainType === 'add' && noData) {
      headerIconType.value = 'edit';
    } else if (!noData || (mainType !== 'add' && mainType !== 'delete')) {
      headerIconType.value = '';
    }
  },
  { immediate: true }
);
</script>
<template>
  <header class="ew-note-header ew-note-flex-center">
    <button
      type="button"
      class="ew-note-btn ew-note-back-btn"
      v-html="backIcon('ew-note-back-btn-icon')"
      v-if="['save', 'detail'].includes(props.mainType!)"
      @click="onBackHandler"
    ></button>
    <h3 class="ew-note-header-title">备忘录</h3>
    <button
      type="button"
      :class="
        handleClassName(
          `ew-note-${headerIconType === 'edit' ? 'edit' : 'cancel'}-btn`
        )
      "
      class="ew-note-btn"
      v-html="getHeaderIcon"
      v-if="headerIconType"
      @click="onHeaderClick"
    ></button>
  </header>
</template>

与 footer.vue 里面的逻辑有点相似,头部主要渲染标题,返回按钮和编辑/取消按钮。这里值得说一下的就是,如果没有数据,我们是不需要渲染编辑按钮的,并且 mainType 如果不是 add(首页默认该值就是 add),同样也是不需要渲染编辑按钮,因此,这里我们监听了从父组件传来的 memoData 和 mainType 两个字段的值。

另外还有一个逻辑就是返回按钮只有在当前是详情页或者当前是新增/编辑(即 mainType 为 save)的时候才会渲染。

List 组件

我将 Main 组件还做了拆分,里面如果是渲染数据即首页的话,那么就需要用到该组件,该组件代码如下所示:

<script setup lang="ts">
import { computed, defineAsyncComponent, ref } from 'vue';
import {
  arrowRightIcon,
  deleteIcon,
  editIcon,
  emptyDataIcon
} from '../../const/icon';
import { ewConfirm } from '../../plugins/ewPopbox';
import { useMemoData, useSetMemoData } from '../../hooks/useMemoData';
import { useRouter } from 'vue-router';
import { useCheckedStore } from '../../stores/checkedStore';
const checkedStore = useCheckedStore();
const AsyncCheckBox = defineAsyncComponent(
  () => import('../CheckBox/CheckBox.vue')
);
const router = useRouter();
const emit = defineEmits(['on-delete', 'on-detail', 'on-edit']);
const props = withDefaults(
  defineProps<{
    memoData?: NoteDataItem[];
    mainType: string;
    showCheckBox: boolean;
  }>(),
  {
    mainType: 'add',
    showCheckBox: false
  }
);
const handleBtnIcon = computed(
  () => `
${deleteIcon('ew-note-main-content-list-item-delete-icon')}
${editIcon('ew-note-main-content-list-item-edit-icon')}
${arrowRightIcon('ew-note-main-content-list-item-right-icon')}
`
);

const noEmptyData = computed(
  () => `
${emptyDataIcon('ew-note-main-no-data-icon')}
<p class="ew-note-main-no-data-text">暂无数据</p>
`
);

const checkedData = ref<string[]>([]);
const onChangeHandler = (e: boolean, v: string) => {
  if (e) {
    checkedData.value.push(v);
  } else {
    checkedData.value = checkedData.value.filter(item => item !== v);
  }
  checkedStore.$patch({ checkedData: checkedData.value });
};
const toDetailHandler = (data: NoteDataItem) => {
  router.push({
    name: 'detail',
    params: {
      uuid: data.id
    }
  });
  emit('on-detail');
};
const onClickHandler = (e: Event, data: NoteDataItem) => {
  e.stopPropagation();
  const target = e.target as HTMLElement;
  if (target) {
    const newTarget =
      target.tagName.toLowerCase() === 'path' ? target?.parentElement : target;
    const classNames = (newTarget as unknown as SVGElement).classList;
    if (classNames.contains('ew-note-main-content-list-item-delete-icon')) {
      ewConfirm({
        title: '温馨提示',
        content: '确定要删除该数据吗?',
        showCancel: true,
        sure: async ctx => {
          let memoStoreData: Array<NoteDataItem> = await useMemoData();
          const memoNewStoreData = memoStoreData.filter(
            item => item.id !== data.id
          );
          await useSetMemoData(memoNewStoreData, false);
          ctx?.close(600);
          emit('on-delete');
        }
      });
    } else if (
      classNames.contains('ew-note-main-content-list-item-edit-icon')
    ) {
      emit('on-edit', data.id);
    } else {
      toDetailHandler(data);
    }
  }
};
const onGoToDetail = (e: Event, data: NoteDataItem) => {
  e.stopPropagation();
  toDetailHandler(data);
};
</script>
<template>
  <ul class="ew-note-main-content-list">
    <li
      class="ew-note-main-content-list-item"
      v-for="data in props.memoData || []"
      :key="data.id"
    >
      <async-check-box
        @on-change="onChangeHandler($event, data.id!)"
        v-if="showCheckBox"
      ></async-check-box>
      <a
        href="javascript:void 0;"
        :data-url="`/detail?uuid=${data.id}`"
        class="ew-note-main-content-list-item-link"
        rel="noopener noreferrer"
        @click="onGoToDetail($event, data)"
      >
        <p class="ew-note-main-content-list-item-title">{{ data.title }}</p>
        <p class="ew-note-main-content-list-item-date">
          <span class="ew-note-main-content-list-item-create-date"
            >创建日期:{{ data.createDate }}</span
          >
          <span class="ew-note-main-content-list-item-update-date"
            >更新日期:{{ data.updateDate }}</span
          >
        </p>
        <div
          class="ew-note-main-content-list-item-btn-group"
          v-html="handleBtnIcon"
          @click="onClickHandler($event, data)"
        ></div>
      </a>
    </li>
  </ul>
  <div
    class="ew-note-main-no-data-container ew-note-flex-center"
    v-html="noEmptyData"
    v-if="!props.memoData?.length && props.mainType === 'add'"
  ></div>
</template>

这个组件的逻辑也不多,就是单个编辑,删除,多选框选中以及跳转到详情的逻辑,点击右箭头按钮或者整个超链接元素,都需要跳转到详情,因此我们封装了一个 toDetailHandler 方法。

main 组件

接下来我们来看 main 组件,components/Main/Main.vue 下,代码如下所示:

<script setup lang="ts">
import { defineAsyncComponent, onMounted } from 'vue';
import { $, toTop } from '../../utils/utils';
const AsyncForm = defineAsyncComponent(() => import('../Form/Form.vue'));
const AsyncSearch = defineAsyncComponent(() => import('../Search/search.vue'));
const props = withDefaults(
  defineProps<{
    mainType: string;
    memoData?: NoteDataItem[];
    editData?: NoteDataItem;
    showCheckBox: boolean;
  }>(),
  {
    mainType: '',
    showCheckBox: false
  }
);

onMounted(() => {
  const topElement = $('.ew-note-to-top') as HTMLDivElement;
  const mainElement = $('.ew-note-main') as HTMLElement;
  if (topElement) {
    toTop(topElement, mainElement);
  }
});
</script>

<template>
  <main class="ew-note-main">
    <async-form
      v-if="['save', 'edit'].includes(props.mainType)"
      :editData="props.editData"
    ></async-form>
    <async-search
      @on-search="$emit('on-search', $event)"
      v-if="['add', 'delete'].includes(props.mainType)"
    ></async-search>
    <router-view
      :memoData="props.memoData"
      :mainType="props.mainType"
      @on-detail="$emit('on-detail')"
      @on-delete="$emit('on-delete')"
      @on-edit="(id: string) => $emit('on-edit', id)"
      :showCheckBox="showCheckBox"
      v-if="props.mainType !== 'save'"
    ></router-view>
    <div class="ew-note-to-top"></div>
  </main>
</template>

main 组件也就是渲染了新增/编辑表单,路由,以及回到顶部按钮,其中路由我们也将渲染组件抛出的事件继续向付组件抛出,当然这里也需要注意事件参数的传递,然后就是在 onMounted 钩子函数中,我们调用了回到顶部按钮事件相关逻辑方法 toTop。

Search.vue

components/Search/Search.vue 代码如下所示:

<script setup lang="ts">
import { ref, computed, watch } from 'vue';
import { searchIcon } from '../../const/icon';

const emit = defineEmits(['on-search']);
const searchValue = ref('');
const getSearchIcon = computed(() => searchIcon('ew-note-search-icon'));
const onSearchHandler = () => {
  emit('on-search', searchValue.value);
};
</script>
<template>
  <div class="ew-note-search">
    <input
      type="text"
      v-model="searchValue"
      placeholder="请输入您需要搜索的备忘录事项"
      class="ew-note-search-input"
      @keydown.enter="$emit('on-search', searchValue)"
    />
    <span
      v-html="getSearchIcon"
      class="ew-note-search-icon-container"
      @click="onSearchHandler"
    ></span>
  </div>
</template>
<style scoped>
.ew-note-search {
  display: flex;
  align-items: center;
  position: relative;
}

.ew-note-search-input {
  border-radius: 6px;
  padding: 8px 12px;
  width: 100%;
  display: inline-block;
  outline: none;
  border: 1px solid var(--search-border-color--);
  color: var(--search-color--);
}

.ew-note-search-input:focus {
  border-color: var(--search-focus-color--);
}

.ew-note-search-icon-container {
  position: absolute;
  right: 4px;
  display: flex;
  align-items: center;
}
</style>

也就是渲染一个搜索框和搜索图标元素,然后监听按下键盘 enter 事件和点击搜索图标,我们将事件抛出给父组件。

总结

虽然这只是一个小小的备忘录,但我们可以看到这个小小的备忘录项目几乎用到了 vue 的常用语法以及相关生态(vue 路由和 vue 状态管理工具 pinia),这对我们熟练使用 vue3 的语法还是很有帮助的。总结知识点如下:

  1. vue3 基础语法
  2. vue3 状态管理工具
  3. vue3 路由
  4. localforage 的使用
  5. 弹出框插件的实现
  6. 一些表单数据的增删改查业务逻辑

ps: 万丈高楼平地起,虽然备忘录还很简单,但是假以时日,不断的扩展更新,也一样可以算作是一个代表性的实战项目。

最后,感谢大家阅读。

标:实现一个可以安装在手机上运行的,ui效果很好的app (开饭了)。

前端框架汇总:

框架其实就是优秀的代码,帮助封装了工具、类库等,提高开发速度。

jQuery: js的库 简化DOM操作

jQuery UI : 是一个html组件库,实现PC端应用

bootStrap: 响应式 pc+移动端 偏向于PC端

angularJS: 是一个实现SPA的js的MVC框架,数据操作比较频繁

Ionic:h5的实现移动端应用程序的框架,集成了ng、cordova、ui库、uiRouter

PhoneGap/cordova:通过plugin插件的形式 提供了丰富的js API,实现原生应用程序才能调用的功能(hybridApp)

配对组合:框架之间的定位是否有相同的部分

jQuery+jQueryUI

jQuery+bootStrap

jQuery+ng 不建议

jQuery+Ionic(ng+phoneGap+ui) 不建议

jQueryUI+bootStrap 不建议

jQueryUI+ng

jQueryUI+Ionic 不建议

bootStrap+ng

bootStrap+Ionic 不建议

技术选型

①考虑生态圈

②明确框架的卖点

③根据需求,选择

④技术是否有定位有很多相似的地方,如果是,建议选其中最好的

ionic (css\ion-list$ionicLoading + uiRouter )

目的:实现移动端的app(开饭啦)

技术构成: ionic

('ui库->内容'+

'ng->数据'+

'uiRouter->处理路由'+

'打包:部署在服务器')

①完成脚手架的搭建

②模拟数据 把页面展示出来

③联调,在前端调用后端的接口进行调试(边做边测试)

④部署到生产环境

1、项目的搭建(15:45 - 16:10)

①、创建项目,添加css、js、img、tpl文件夹,添加必须引用的css、js文件,添加自定义的css、js文件以及img图片,并创建完整的引导页面kaifanla.html;

②、编写kaifanla.html文件:

定义模块ng-app=”kaifanla”


引入ionic.css以及自定义的css文件

定义用于替换模板的视图

引入ionic_bundle.js文件,并引入自定义的js文件

③、添加模板文件:添加 start/main/detail/order/myorder页面,每个页面中删掉原有内容,添加一个文字;

④、编写 kaifanla.js 文件

定义各自的控制器

定义路由:为所有模板定义路由,默认跳转到start

⑥、测试:跳转是否正常,是否有错误

2、所有静态页面的编写---》运行通畅,数据静态死数据

①start.html (16:25 - 16:40)

1.1 页面布局

1.2 实现该页面时,将通过js跳转的方法封装在控制器中(所有的代码片段都可以去用)

②main.html 模拟数据

http://ionicons.com/

可以通过ng-include包含页头和页尾

has-header has-footer --> ion-content

③detail.html 详情页

card

(9:45 - 10:10)

④order.html 表单提交页

⑤myOrder.html 个人中心页

通过grid 模拟 table 展示数据

每日一练:

完成所有的静态页面。

3、调用php页面,联调

在进行联合调试时,如果遇到了问题?

①确认请求的api接口是否正确

②请求服务器端时,确保参数的个数和类型服务要求

③服务器端返回的数据是否正确

④经常去看network(response\header)、console

条件:

①工程在c:\xampp\htdocs

②xampp的apache和mySql跑起来

③测试-》 localhost: / (不要直接在webStorm中打开)

要求:通过service创建一个自定义服务$kflHttp,

①在服务中封装一个方法sendRequest(url,func),在这个方法中有两个参数,第一个是要求的地址,第二个参数是请求成功之后要执行的处理函数;

②在sendRequest方法被调用时启动一个‘正在加载数据’的窗口,当成功的请求到服务器端的数据,关闭加载中的窗口。

①main.html

ng-model

-->

初始化模型数据:$scope.inputTxt = {kw:''};

$watch : $scope.$watch('inputTxt.kw',function(){})

方向2的数据绑定: ng-model='inputTxt.kw'

尝试: ion-infinite-scroll 上拉加载更多

第一步:要将ionInfiniteScroll放到页面底部

第二步:on-infinite

第三步:$scope.$broadcast('scroll.infiniteScrollComplete');

②detail.html (2:15 - 2:30)

发送: main->a

接受: detail

1、配置detail状态中的url /detail/:id

2、$stateParams

③order.html(14:50 - 15:20)

接收detail传递来的参数:菜品的id

点击下单,将数据一起发给服务器端,根据服务器返回的结果,将表单隐藏显示下单结果

序列化:

$HttpParamSerializerJQLike

④myOrder.html

根据手机号 去查找所有的订单

⑤设置页面

在点击设置的时候,有一个设置页面(显示一个列表:关于--点击显示自定义弹窗,退出登录--点击回到起始页)

自定义弹窗:

①创建一个自定义弹窗

②显示

注册邀请:http://t.cn/RqG1Nja

混合编程:

打开eclipse,将模拟器,新建一个Android应用,通过webView的loadUrl

wv.loadUrl('http://172.163.0.1/ionic_kaifanla/kaifanla.html');

方式1:

将前端工程的全部代码 拷贝到 assets目录

方式2:

将前端代码放在服务器,拿到服务器的url地址(不需要拷贝assets)

①确保前端代码没问题

②将代码部署在服务器

③编写java代码

//创建WebView类型的一个变量

WebView wv = newWebView(getApplicationCotnext());

//允许执行js

wv.getSettings().setJavaScriptEnabled(true);

//载入指定的页面

wv.loadUrl("http://172.173.0.100/chaptor4/webApp/kfl_ionic/kaifanla.html#/settings");

//设置内容视图

setContentView(wv);

添加网络权限:

上午:

迭代

在之前的基础上:start main detail order myOrder

加入购物车。

详情页:立即下单--》添加到购物车

中间多了购物车的标签: 跳转到购物车页面,购物车支持产品数量的编辑。

一、ZeptoJS概述

http://zeptojs.com/

what?是一个接口和JQuery比较类似的js的库,它的目标是实现一个10k以内的通用的模块化的js的库

注意事项:ZeptoJS只是实现了JQuery一部分的功能,也有自己的和移动端相关的处理。

where?

针对现代高级浏览器,在手机端用的比较多

why?

①非常轻量

②有着熟悉的api接口(开发者不需要太高的学习成本)

③非常方便的搭配其他库去使用

④核心库的性能比较好,舍弃了低版本的兼容的支持

how?

引入对应的zepto.js文件到工程

在zepto.js中内置:

zepto核心模块;包含许多方法

event通过on()& off()处理事件

ajaxXMLHttpRequest 和 JSONP 实用功能

form序列化 & 提交web表单

ie增加支持桌面的Internet Explorer 10+和Windows Phone 8。

二、ZeptoJS Core Module

2.1 操作数据的基本方法:

each/map/grep/parseJson/isFunction/isPlainObject.....

2.2 常见的选择器

id、标签、父子、后代、

class、属性($('[name=test]'))

2.3 简化DOM操作的方法

增删改查

2.3.1 增

insertAfter insertBefore append appendTo prepend prependTo

2.3.2 改

html()

css()

addClass()

width()

height()

2.3.3 删

remove

removeClass()

2.3.4 查

三、ZeptoJS Detect Module

$.os

{android: true,phone: true ,tablet: false}

$.browser

{chrome:true,version:"56.0.2924.87",webkit:true}

四、ZeptoJS Event Module

on 绑定一个事件处理程序

off 解除绑定的事件处理程序

one 第一次事件触发之后,将自动解除绑定

trigger 通过js的方式触发指定的事件(多数都是自定义事件)

练习:实现一个页面,在该页面中有2个按钮,两个按钮的id分别btn1,btn2;

要求:

①点击btn1,超过5次自动解除绑定

②点击btn2,在第3次,触发自定义的事件,弹窗显示‘自定义事件被触发了’

Zepto.js默认包含5个模块

①zepto核心模块;包含许多方法

②event通过on()& off()处理事件

③ajaxXMLHttpRequest 和 JSONP 实用功能

④form序列化 & 提交web表单

⑤ie增加支持桌面的Internet Explorer 10+和Windows Phone 8。

一、Zepto的常见模块

1、core

$(''),支持常见的选择器

$.each/map/grep/parseJson/camelCames/trim... 和数据操作相关的方法

insertAfter insertBefore append appendTo prepend prenpendTO

html/css/addClass/removeClass/width/height

remove

next/prev/parent/parents/children..

detect

$.os

$.browser

event

on

off

one

trigger

2、ajax模块

$.ajax()

$.get()

$.param() 将一个对象进行表单序列化 (在angularJS时

提供了$httpParamSerializerJQLike(object))

$.post()

练习:使用post请求,将{name:'zhangsan'}发给服务器端,服务器端接收到数据,返回一个{tip:"Hello zhangSan"}.

通过Zepto中的$.post()来实现要求。

打开apache,请求注意端口号。

3、form模块

serialize

serializeArray

submit

使用基本步骤:需要给表单元素指定name属性。

注意事项:

如果表单元素中有复选、单选框,如果没有选中的,默认在进行表单序列化时,是不会把它添加到字符串或者数组中的。

4、Touch模块

//设置阻止掉默认的滑动效果

document.addEventListener(

'touchmove',

function (event) {

event.preventDefault();

},

{passive:false}

)

Touch模块 给我们提供了两大类事件的支持

①点按类

tap/longtap/doubletap

②滑动类

swipe/swipeLeft/swipeRight/swipeUp/swipeDown

如何使用事件呢?

①绑定对应的事件

element.on('swipe',function(){})

②触发事件

5、动画模块 (fx+fxmethods)

fxmethods:

fadeIn fadeOut fadeTo fadeToggle

show hide toggle

...

fx:

animate()

二、练习Zepto的使用。

1、实现一个网页版的简历

1.1 向服务器端获取数据,将数据显示在列表中。

①获取数据

②找到列表,创建元素,插入到列表

1.2 向服务器端获取数据,将数据显示在列表中。

①获取数据

②找到id为skills的div,创建元素,插入到div

2、将pc端的项目移植到Mobile端。

2048实现混合编程的方式有很多种,phoneGap也是一种的常见的方式,它最大的特点是借助各种各样的插件来实现对于设备底层的调用。

phoneGap与cordova的关系:

最早的时候,phoneGap是一个非常流行的框架,后来被Adobe收购了,PhoneGap依然了自己的商标所有权,将核心的跨平台代码共享给了Adobe,Adobe将核心代码全部开源,形成的新的项目叫做cordova。

cordova是phoneGap的核心代码

混合编程:

①将前端代码拷贝到Android工程的assets

②将前端代码部署在服务器,通过loadUrl载入服务器的url

一、PhoneGap的概述

https://phonegap.com/ phoneGap官网

http://cordova.apache.org/ cordova官网

https://build.phonegap.com/ phoneGap提供的云端打包网址

http://phonegap-plugins.com/ 第三方的基于phoneGap的插件的列表站点

what?是一个针对移动端的,旨在通过编写前端代码来实现开发跨平台应用程序的开发框架

之前通过eclipse创建Android项目,通过混合编程的方式来打包生成安装文件(没有办法通过java来实现很多原生组件的调用,包括设备底层的调用)

*phoneGap提供了各种各样的插件,这些插件给我们提供了js的接口来实现设备低层的调用。

理念:

实现write once,run everywhere.

where? 低成本的开发跨平台应用程序(Android/iOS/WP)

why?

①免费开源

②标准化 完全遵循w3c标准

③提供了非常方便的云端打包工具,可以直接将前端代码打包生成可以安装在移动端OS的app

④低成本 即使没有学过专业的原生开发技能,也能够基于前端技术以及phoneGap所提供的核心API来实现一个能够调用底层硬件的app

ADB: Android Debug Bridge 安卓调试桥,之前在Android项目点击run as去作为Android应用去运行,背后就是靠ADB将生成的apk结尾的文件传递到模拟器中。

how?

方式1:

借助于phoneGap所建议的方式,通过两个软件,一个安装在pc,一个安装在mobile

①安装pc端的软件 phonegap-desktop

②安装移动端的软件

https://github.com/phonegap/phonegap-app-developer/tree/master/resources/release

adb install **.apk

2.1 windows+R->cmd

2.2

方式2:

通过命令行 :

//全局安装cordova

npm install -g cordova

//创建一个基于cordova 的myApp项目

cordova create MyApp

//进入到MyApp的目录

cd MyApp

//让cordova支持Android开发平台

cordova platform add android

//打包APK的安装包,运行在对应的Android设备

cordova run android

二、PhoneGap支持的事件

deviceready

pause/resume

online/offline

battery***

***button

绑定事件的方式:

document.addEventListener(

'deviceready',

function(){},

false

)