JS 重构购物车案例 改进版(详细注释)

 

 html:

<!DOCTYPE html>
<html lang="cn">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <link rel="stylesheet" href="./style/shop.css">
    <script src="./js/shop.js"></script>
</head>

<body>
    <ul id="shopBox" class="shopBox"></ul>
    <script>
        var shopCar
        var obj = [{
            "id": 1001,
            "icon": "img/1.png",
            "name": "大白菜",
            "num": 1,
            "price": 10
        }, {
            "id": 1002,
            "icon": "img/2.png",
            "name": "胡萝卜",
            "num": 1,
            "price": 230
        }, {
            "id": 1003,
            "icon": "img/3.png",
            "name": "电视机",
            "num": 1,
            "price": 340
        }, {
            "id": 1004,
            "icon": "img/4.png",
            "name": "啤酒",
            "num": 1,
            "price": 4023
        }, {
            "id": 1005,
            "icon": "img/5.png",
            "name": "摩托车",
            "num": 1,
            "price": 5103
        }, {
            "id": 1006,
            "icon": "img/6.png",
            "name": "路由器",
            "num": 1,
            "price": 602
        }, {
            "id": 1007,
            "icon": "img/7.png",
            "name": "西红柿",
            "num": 1,
            "price": 70
        }, {
            "id": 1008,
            "icon": "img/8.png",
            "name": "苹果",
            "num": 1,
            "price": 802
        }, {
            "id": 1009,
            "icon": "img/9.png",
            "name": "书笔",
            "num": 1,
            "price": 9021
        }, {
            "id": 1010,
            "icon": "img/10.png",
            "name": "手机",
            "num": 1,
            "price": 9899
        }];
        singleCar().init(obj, shopBox);

        function singleCar() { //单例模式,只产生一个实例
            if (!shopCar) {
                shopCar = new ShopCar()
            }
            return shopCar
        }
    </script>
</body>

</html>

css

* {
    margin: 0;
    padding: 0;
}

.shopBox {
    overflow: hidden;
    width: 1400px;
    margin: 50px auto;
}

.liItem {
    float: left;
    list-style: none;
    padding: 10px;
    width: 100px;
    height: 120px;
    text-align: center;
    border: 1px solid lightcoral;
}

.liItem img {
    width: 30px;
    height: 30px;
}

.leftBtn,
.rightBtn {
    width: 30px;
    height: 30px;
    background: white;
    border: 1px solid black;
    font-size: 25px;
    line-height: 30px;
}

.text {
    width: 50px;
    height: 26px;
    display: inline-block;
    vertical-align: bottom;
    text-align: center;
}

table {
    font-size: 20px;
    width: 1000px;
    border: 1px solid lightcoral;
    border-collapse: collapse;
    margin: 50px auto;
}

.checkbox {
    width: 30px;
    height: 30px;
}

td {
    border: 1px solid lightcoral;
    text-align: center;
    vertical-align: middle;
}

td button {
    width: 100px;
    height: 40px;
}

.numBox {
    width: 100px;
    height: 30px;
    margin: auto;
    position: relative;
}

.numBox>button {
    width: 30px;
    height: 42px;
    background-color: white;
    border: 1px solid #000000;
}

.numBox>input {
    width: 40px;
    height: 40px;
    border: 1px solid #000000;
    border-left: none;
    border-right: none;
    text-align: center;
}

js

function ShopCar() {

}
ShopCar.prototype = (function() {
    //闭包存放私有变量
    return {
        shopList: null, //商品详情列表
        shoppingList: null, //购物车中的商品
        table: null, //购物车列表容器
        ADD_ITEM_EVENT: "add_item_event", //添加单个商品自定义事件
        REDUCE_ITEM_EVENT: "reduce_item_event", //减少单个商品自定义事件
        DDELETE_ITEM_EVENT: "delete_item_event", //删除单个商品自定义事件
        INPUT_ITEM_EVENT: "input_item_event", //修改商品数量自定义事件
        CHECK_ITEM_EVENT: "check_item_event", //全选自定义事件
        /*
             初始化函数
             1、获取存储中的商品列表数据
             2、如果商品购物车列表在存储器中存在,那么就将这个数据获取赋值给购物车列表
                如果不存在,创建一个空数组
             3、侦听添加商品事件,侦听删除商品事件,侦听修改商品数量事件,侦听选中商品事件
             4、循环商品列表,创建所有的商品标签
             5、根据购物车列表创建表格
         * */
        init(shopList, parentEle) {
            if (localStorage.shoppingList) { //获取本地缓存中已加入购物车的数据
                this.shoppingList = JSON.parse(localStorage.shoppingList);
            } else {
                this.shoppingList = [];
            }
            if (!this.shopList) {
                this.shopList = shopList
                this.createShopList(parentEle)
            }
            // 以下均为操作时的事件监听,采取自定义事件接收document的事件抛发,分别是:加入购物车(增加某一个商品),减少一个商品,删除某商品,输入商品数量,全选/取消全选
            document.addEventListener(this.ADD_ITEM_EVENT, this.addItemEventHandler)
            document.addEventListener(this.REDUCE_ITEM_EVENT, this.reduceItemEventHandler)
            document.addEventListener(this.DDELETE_ITEM_EVENT, this.deleteItemEventHandler)
            document.addEventListener(this.INPUT_ITEM_EVENT, this.inputItemEventHandler)
            document.addEventListener(this.CHECK_ITEM_EVENT, this.checkItemEventHandler)
        },
        createShopList(parentEle) { //创建商品详情列表
            let data = this.shopList
            for (let i = 0; i < data.length; i++) {

                let li = document.createElement("li");
                li.className = "liItem";

                let img = new Image();
                img.src = data[i].icon;
                li.appendChild(img);

                let title = document.createElement("div");
                title.textContent = data[i].name;
                li.appendChild(title);
                let price = document.createElement("span");
                price.textContent = data[i].price + "元";
                li.appendChild(price);
                let addShops = document.createElement("p");
                addShops.innerHTML = "加入购物车"
                li.appendChild(addShops)
                addShops.style.backgroundColor = "red";
                addShops.style.height = "40px";
                addShops.style.lineHeight = "40px";
                addShops.style.borderRadius = "10px";
                li.that = this //将全局this存至元素中,方便后续调用
                li.data = data[i]; //与上句同理,方便后续调用
                li.addEventListener("click", this.addItemEvent);
                parentEle.appendChild(li);
            }
            this.createTable()
        },
        createTable() { //当数据发生变化时重新渲染购物车列表初始化购物车
            if (this.table) { //初始化购物车
                this.table.remove()
                this.table = null;
            }
            if (this.shoppingList.length === 0) return;
            this.table = document.createElement("table");
            document.body.appendChild(this.table);
            let thr = document.createElement("tr");
            this.table.appendChild(thr);
            for (let prop in this.shoppingList[0]) { //创建表头,如果属性名是select,就创建全选按钮
                let th = document.createElement("th");
                if (prop === "select") {
                    let input = document.createElement("input");
                    input.type = "checkbox";
                    input.className = 'checkbox'
                    input.checked = this.checkAll(); //由于当数据修改时绑定了createTable方法,每次数据改变就会刷新全选状态
                    input.that = this //将全局this存至元素中,方便后续调用
                    input.addEventListener("change", this.checkEvent);
                    th.appendChild(input);
                } else {
                    th.textContent = prop;
                }
                thr.appendChild(th)
            }
            for (let i = 0; i < this.shoppingList.length; i++) {
                let tr = document.createElement("tr");
                this.table.appendChild(tr);
                for (let str in this.shoppingList[i]) {
                    let td = document.createElement("td");
                    td.data = this.shoppingList[i]; //将每一行数据存至td元素中,方便后续调用
                    this.selectTdType(td, this.shoppingList[i], str);
                    tr.appendChild(td);
                }
            }
            localStorage.shoppingList = JSON.stringify(this.shoppingList) //当表格渲染完毕就将数据存至本地缓存,浏览器关闭或刷新不会清除数据
        },
        selectTdType(td, data, type) { //将表格中的元素通过属性分类
            switch (type) {
                case 'select':
                    let input = document.createElement("input");
                    input.type = "checkbox";
                    input.checked = data.select;
                    input.className = 'checkbox'
                    input.that = this
                    input.addEventListener("change", this.checkEvent);
                    td.appendChild(input);
                    break;
                case 'icon':
                    let img = new Image();
                    img.src = data.icon;
                    td.appendChild(img);
                    break;
                case 'num':
                    let countNum = this.createCountNum(td, data);
                    countNum.input.value = data.num;
                    break;
                case 'deleted':
                    let btn = document.createElement("button");
                    btn.textContent = "删除";
                    td.appendChild(btn);
                    btn.that = this
                    btn.data = data;
                    btn.addEventListener("click", this.deleteItemEvent);
                    break;
                default:
                    td.textContent = data[type];
                    break;
            }
        },
        createCountNum(ele, data) { //创建数量计数器
            let div = document.createElement("div"); //这个父元素用于储存单个商品信息
            ele.appendChild(div);
            div.className = "numBox";
            let leftBtn = this.createMark(div, 'reduce') //减少商品按钮
            let input = document.createElement("input");
            input.type = "text";
            input.value = "1"; //商品初始数量
            div.appendChild(input);
            let rightBtn = this.createMark(div, 'add') //新增商品按钮
            div.input = input;
            div.data = data;
            div.that = this
            rightBtn.that = this
            rightBtn.data = data;
            leftBtn.addEventListener("click", this.reduceItemEvent);
            rightBtn.addEventListener("click", this.addItemEvent);
            input.addEventListener("input", this.inputItemEvent);
            input.addEventListener("blur", this.inputItemEvent);
            return div;
        },
        createMark(parentEle, type) { //判断增加或减少键
            let markBtn = document.createElement("button");
            markBtn.textContent = type == "add" ? '+' : '-';
            parentEle.appendChild(markBtn);
            return markBtn
        },
        addItemEvent() { //第一个事件抛发(当用户点击列表商品或点击增加键时,抛发事件给document触发addItemEventHandler方法,用于参数传递,参数通过event对象进行传递)
            let _this = this.that
            let event = new Event(_this.ADD_ITEM_EVENT);
            event.data = this.data;
            event._this = _this;
            document.dispatchEvent(event);
        },
        // 添加商品事件
        /* 
            1、如果需要添加商品是新商品时候,就创建该商品对象
            2、在表格里,使用for-in循环时,是根据对象属性创建先后来循环的
            3、因此如下,我们给对象先添加一个select属性,这样在后面的循环中,第一个就可以创建出多选框了,紧接着将商品列表对应的对象的所有属性添加到这个对象中,再添加sum属性,并且计算sum的值
        
        */
        addItemEventHandler(e) { //当用户点击列表商品或点击增加键时执行函数
            let _this = e._this
            let obj = { //这里遵循对象顺序,先定义的属性排在前面,所以将选择框放在最前定义,总价和删除键放在最后
                select: false
            };
            for (let str in e.data) {
                obj[str] = e.data[str];
            }
            obj.sum = obj.num * obj.price;
            obj.deleted = false;
            _this.searchItemById(obj.id, _this.shoppingList, obj, 'add')
            _this.createTable() //当数据发生变化时重新渲染购物车列表初始化购物车
        },
        reduceItemEvent() { //第二个事件抛发(当用户点击减少键时,抛发事件给document触发reduceItemEventHandler方法,用于参数传递,参数通过event对象进行传递)
            let _this = this.parentElement.that
            let event = new Event(_this.REDUCE_ITEM_EVENT);
            event.data = this.parentElement.data;
            event._this = _this;
            document.dispatchEvent(event);
        },
        reduceItemEventHandler(e) { //与增加同理
            let _this = e._this
            e.data.sum = e.data.num * e.data.price;
            _this.searchItemById(e.data.id, _this.shoppingList, e.data, 'reduce')
            _this.createTable() //当数据发生变化时重新渲染购物车列表初始化购物车
        },
        deleteItemEvent() { //第三个事件抛发(当用户点击删除键时,抛发事件给document触发deleteItemEventHandler方法,用于参数传递,参数通过event对象进行传递)
            let _this = this.that
            let event = new Event(_this.DDELETE_ITEM_EVENT);
            event.data = this.data;
            event._this = _this;
            document.dispatchEvent(event);
        },
        deleteItemEventHandler(e) {
            let _this = e._this
            _this.shoppingList = _this.shoppingList.filter(function(item) { //数组过滤函数,返回id属性不等于当前id的数组,即删除当前选中的对象,并重新赋值
                return item.id !== e.data.id;
            });
            _this.createTable() //当数据发生变化时重新渲染购物车列表初始化购物车
            localStorage.shoppingList = JSON.stringify(_this.shoppingList) //这里为了考虑,若删除到0条时,无法重新初始化表格(shoppingList.length==0,无法进入createTable函数)
        },
        inputItemEvent(e) { //第四个事件抛发(当用户输入数值或处于失焦时,过滤数据,并抛发事件给document触发inputItemEventHandler方法,用于参数传递,参数通过event对象进行传递)
            let _this = this.parentElement.that
            if (e.type === "input") {
                this.value = this.value.replace(/[^0-9]/g, ""); //只允许输入数字
                if (this.value === "0") { // 如果=0,就设为1
                    this.value = "1";
                }
                if (this.value.length > 2) { // 如果输入的内容大于2位,让这个值为99(最大99个)
                    this.value = "99";
                }
            } else if (e.type === "blur") {
                if (this.value.length === 0) { //  失焦时,如果什么都没有输入,也设为1
                    this.value = "1";
                }
                // 当失焦时才将事件抛发到document
                let event = new Event(_this.INPUT_ITEM_EVENT);
                event.num = this.value
                event.data = this.parentElement.data;
                event._this = _this;
                document.dispatchEvent(event);
            }
        },
        inputItemEventHandler(e) {
            let _this = e._this
            _this.shoppingList.map(function(item) { //遍历查询被修改的对象
                if (item.id === e.data.id) {
                    item.num = e.num;
                    item.sum = item.price * item.num;
                }
            });
            _this.createTable() //当数据发生变化时重新渲染购物车列表初始化购物车
        },
        checkAll() { //初始化表格时执行,查找所有选项是否都选中(全选框随其他选项框状态修改而修改)
            return this.shoppingList.filter(function(item) {
                return !item.select;
            }).length === 0; //返回true或false
        },
        checkEvent() { //第五个事件抛发(当用户选中或取消任意选项框时,抛发事件给document触发checkItemEventHandler方法,用于参数传递,参数通过event对象进行传递)
            let _this = this.that
            let data = this.parentElement.data;
            let event = new Event(_this.CHECK_ITEM_EVENT);
            event.all = !data; //若没有data,说明是全选框
            event.select = this.checked; //赋值给select属性
            event._this = _this;
            event.data = data;
            document.dispatchEvent(event);
        },
        checkItemEventHandler(e) {
            let _this = e._this
            if (e.all) { //若e.all为true说明该选项框是全选框
                _this.shoppingList.map(function(item) {
                    item.select = e.select; //其他选项框与全选框状态一致
                })
            } else {
                _this.shoppingList.map(function(item) { //单选,选中某一个(在表格初始化时执行checkAll判断是否全选)
                    if (item.id === e.data.id) {
                        item.select = e.select;
                    }
                })
            }
            _this.createTable() //当数据发生变化时重新渲染购物车列表初始化购物车
        },
        searchItemById(id, list, data, type) { //遍历查询某项商品增加或减少
            let arr = list.filter(function(item) {
                return item.id === id;
            }); //若有返回值则对某项商品操作(在1-99区间,若为0则直接删除)
            switch (type) {
                case "add":
                    if (arr.length == 0) {
                        list.push(data);
                    } else if (arr[0].num < 99) {
                        arr[0].num++;
                        arr[0].sum = arr[0].num * arr[0].price;
                    }
                    break;
                case "reduce":
                    if (arr[0].num > 1) {
                        arr[0].num--;
                        arr[0].sum = arr[0].num * arr[0].price;
                    } else {
                        this.shoppingList = list.filter(function(item) {
                            return item.id !== id;
                        });
                        this.createTable() //当数据发生变化时重新渲染购物车列表初始化购物车
                        localStorage.shoppingList = JSON.stringify(this.shoppingList)
                    }
                    break;
            }
        }
    }
}())
ShopCar.prototype.constructor = ShopCar;

完整目录结构

 

posted @ 2022-01-25 10:55  thomas_001  阅读(86)  评论(0)    收藏  举报