body {font-size:100%;}
h1 {font-size:3.75em;}
h2 {font-size:2.5em;}
p {font-size:0.875em;}
復制代碼
.aa{
background-image: url(arrow.png)no-repeat right center;
background-image:url(nav-bar.jpg);
background-repeat:no-repeat;
background-position:right center;
}
復制代碼
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');
})
復制代碼
首先聲明一下:
<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; 總是顯示縱向滾動條
復制代碼
.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" />
復制代碼
$("#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的好處是對搜索引擎比較友好,需要處理的文件在服務器端完成的,
不會加重訪問者的瀏覽器負擔。
復制代碼
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
復制代碼
能用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);
});
});
復制代碼
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();
})
復制代碼
$(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急速瀏覽器內核基于谷歌的
復制代碼
button,input type=button按鈕在IE和w3c,firefox瀏覽器區別:
1、當在IE瀏覽器下面時,button標簽按鈕,input標簽type屬性為button的按鈕是一樣的功能,不會對表單進行任何操作。
2、但是在W3C瀏覽器,如Firefox下就需要注意了,button標簽按鈕會提交表單,而input標簽type屬性為button不會對表單進行任何操作。
為button按鈕增加一個type=”button”屬性。
復制代碼
<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;
}
復制代碼
$('#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頁面里的元素
復制代碼
$('.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父級頁面元素
復制代碼
$('.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
: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()方法在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的滾動條
復制代碼
<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({
type: "post",
data:
contentType:
url:
beforeSend: function () {
if(){
}else{
};
},
success: function (data) {
alert("保存失敗");
},
error: function (data) {
alert("保存成功");
}
});
復制代碼
$.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);
}
}
});
復制代碼
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)
復制代碼
layer.msg('分配成功',{time: 1000},function () {
var index = parent.layer.getFrameIndex(window.name);
parent.layer.close(index);
})
復制代碼
window.location.reload();//刷新當前頁面
window.parent.location.reload();//刷新父級頁面
復制代碼
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
最近沒有學到什么好的技術知識點分享,不過本次給大家帶來一個實戰,那就是使用 vue.js3 + ts 完成了一個小小的備忘錄。雖然每個人的手機都自帶有備忘錄,但我更傾向于自己寫一個備忘錄,自己記筆記用,好了廢話不多說,咱們開始吧。
既然是制作一個 web 版本的備忘錄,那肯定也是要好好設計一番的,備忘錄主要包含三個頁面,如下:
其中展示頁如下圖所示:
從上圖我們可以知道展示頁包含標題和中間筆記內容展示以及底部展示由幾個備忘錄和相關操作。
新增編輯頁如下圖所示:
從上圖,我們可以看到新增編輯頁就只是變更了中間的筆記展示內容為新增/編輯表單,然后底部操作按鈕變更為保存按鈕。
筆記詳情頁如下圖所示:
詳情頁更簡單,其實就是簡單的展示即可。
也可以在線訪問該備忘錄。
雖然以上的頁面看起來都比較簡單,但其實包含了很多邏輯,下面讓我一一娓娓道來。
本次備忘錄我們用到了如下技術棧:
另外我們約定了筆記的數據結構,如下所示:
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 屬性值我們還是有很大的改造空間的,因為我們的筆記應該不止有文本內容,還有圖片鏈接,等等,但是這里我們只是考慮存儲文本內容。
接下來我們來看下源碼目錄結構如下圖所示:
可以看到我們的源碼目錄結構也很清晰,分析如下:
可以這么說,雖然這只是一個小小的實戰項目,但其實已經囊括了 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'];
到這里為止,我們的基礎項目核心就搭建好了,接下來,讓我們一步一步對每個模塊的代碼進行分析。
工具函數用到的也不多,主要分為以下幾類:
接下來,我們就按照以上五個類別來逐一分析每一個工具函數。
首先是數據類型的判斷,這里我們主要用到了是否是字符串,是否是布爾值以及是否是對象的數據類型,這里我們使用 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,我們使用 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 主要封裝了 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 存儲。
接下來,就是各個組件的實現了。
組件模塊的所有代碼都是很相似的,知識點也基本都是事件傳遞,單向數據流,監聽數據等等。因此我們只需要通過分析一個根組件的源代碼,基本就可以按照相同方式去理解其它組件。
在根組件當中,我們可以看到,我們將頁面拆分成了 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 來獲取當前是哪條備忘錄數據,然后渲染到頁面即可。
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 指令渲染即可,這里我們頁渲染了錯誤的圖標。
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 組件就是我們的新增/編輯表單元素模版,其代碼如下所示:
<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。
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)的時候才會渲染。
我將 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 組件,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。
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 的語法還是很有幫助的。總結知識點如下:
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
)
*請認真填寫需求信息,我們會在24小時內與您取得聯系。