【综合案例】原生JS实现购物商城

  • Post author:
  • Post category:其他




资料链接和启动方法:https://pan.baidu.com/s/1Ka2xcPmsL31MpQhqNnZlCw 提取码:1115



一、案例说明



1、目录结构

在这里插入图片描述



2、conf文件夹

在这里插入图片描述



3、用户名密码的正则和ajax的封装

import { confg } from "../cof/config.js";
// 1. 正则封装
function test(reg) {
    return function (str) {
        return reg.test(str);
    };
}

const testName = test(confg.nameReg);
const testPwd = test(confg.pwdReg);

// 2. 请求封装
function objToStr(obj) {
    let str = "";
    for (let k in obj) {
        str += `${k}=${obj[k]}&`;
    }
    str = str.slice(0, str.length - 1);
    return str;
}
function createAjax(url) {
    let baseUrl = url;

    function ajax(options) {
        if (options.url === undefined) {
            throw new Error("您没有传递 url, url 为 必传");
        }

        if (
            !(
                /^(GET|POST)$/i.test(options.method) ||
                options.method === undefined
            )
        ) {
            throw new Error("method 目前仅支持 post 或者 get");
        }

        if (
            !(options.async === undefined || typeof options.async === "boolean")
        ) {
            throw new Error("async 目前仅支持 ture 或者 false");
        }

        const optionsDataType = Object.prototype.toString.call(options.data);
        if (
            !(
                optionsDataType === "[object Object]" ||
                optionsDataType === "[object String]" ||
                optionsDataType === "[object Undefined]"
            )
        ) {
            throw new Error("data 目前仅支持 字符串或者 对象");
        }

        const headersType = Object.prototype.toString.call(options.headers);
        if (
            !(
                headersType === "[object Undefined]" ||
                headersType === "[object Object]"
            )
        ) {
            throw new Error("header 暂时仅支持 对象格式");
        }

        if (
            !(
                options.dataType === undefined ||
                /^(string|json)$/.test(options.dataType)
            )
        ) {
            throw new Error("dataType 目前仅支持 'string' 或者 'json'");
        }

        const _options = {
            url: baseUrl + options.url,
            method: options.method || "GET",
            async: options.async ?? true,
            data: options.data || "",
            headers: {
                "content-type": "application/x-www-form-urlencoded",
                ...options.headers,
            },
            dataType: options.dataType || "string",
        };

        if (!(typeof _options.data === "string")) {
            _options.data = objToStr(_options.data);
        }

        if (/^GET$/i.test(_options.method)) {
            _options.url = _options.url + "?" + _options.data;
        }

        const p = new Promise(function (res, rej) {
            const xhr = new XMLHttpRequest();
            xhr.open(_options.method, _options.url, _options.async);
            xhr.onload = function () {
                try {
                    if (_options.dataType === "string") {
                        res({
                            code: 1,
                            info: xhr.responseText,
                        });
                    } else {
                        res({
                            code: 1,
                            info: JSON.parse(xhr.responseText),
                        });
                    }
                } catch (error) {
                    res({
                        code: 0,
                        info: xhr.responseText,
                    });
                }
            };

            if (/^POST$/i.test(_options.method)) {
                xhr.setRequestHeader(
                    "content-type",
                    _options.headers["content-type"]
                );
            }

            if (_options.headers.authorization) {
                xhr.setRequestHeader(
                    "authorization",
                    _options.headers.authorization
                );
            }
            /^POST$/i.test(_options.method)
                ? xhr.send(_options.data)
                : xhr.send();
        });

        return p;
    }

    return ajax;
}

const ajax = createAjax(confg.baseUrl);

export const utils = {
    testName,
    testPwd,
    ajax,
};



二、登录页的实现

在这里插入图片描述



1、案例效果

在这里插入图片描述



2、登录页逻辑


  1. 采集用户信息

    —点击登录时

  2. 验证信息

    • 非空验证
    • 正则校验

  3. 把用户名和密码发送给后端

    —> 根据后端返回结果, 做不同的事

    • 跳转首页
    • 提示用账号密码错误



3、接口文档


  • 请求地址



    /users/login

  • 请求方式



    post

  • 携带参数



    application/x-www-form-urlencoded 格式传递


    在这里插入图片描述

  • 响应数据



    根据你的用户名和密码返回登录状态


    在这里插入图片描述



4、代码实现



  • HTML代码

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
    <link rel="stylesheet" href="../css/login.css">
</head>
<body>
    <h1>登录页</h1>
    <div class="box">
        <span>用户名密码错误, 请重试 ! ^_^</span>
        <label>
            用户名: <input class="name" type="text">
        </label>
        <label>
            密码: <input class="pwd" type="text">
        </label>
        <button>登录</button>
        <a href="./register.html">没有账号, 请进入注册</a>
    </div>
    <script src="../js/login.js" type="module"></script>
</body>
</html>


  • CSS代码

* {
    margin: 0;
    padding: 0;
}

h1 {
    width: 100%;
    height: 80px;
    display: flex;
    justify-content: center;
    align-items: center;
    background-color: skyblue;
}

.box {
    width: 600px;
    display: flex;
    flex-direction: column;
    padding: 20px;
    border: 3px solid pink;
    border-radius: 15px;
    margin: 30px auto;
    padding-top: 50px;
    position: relative;
}

.box>label {
    height: 50px;
    font-size: 22px;
}

.box>label>input {
    padding-left: 20px;
    font-size: 22px;
}

.box>button {
    font-size: 22px;
}

.box>span {
    position: absolute;
    left: 50%;
    transform: translateX(-50%);
    top: 10px;
    color: red;
    display: none;
}

.box>span.active {
    display: block;
}


  • JS代码

// 导入公共方法
import { utils } from "../utils/utils.js";
const { testName, testPwd, ajax } = utils;

// 获取标签对象
const oBtn = document.querySelector("button");
const nameInp = document.querySelector(".name");
const pwdInp = document.querySelector(".pwd");
const errBox = document.querySelector("span");

// 给button添加点击事件 
oBtn.addEventListener('click', async function(){
    // 采集用户输入的用户名和密码
    const nameVal = nameInp.value;
    const pwdVal = pwdInp.value;

    // 验证用户信息 --- 非空校验
    // if(nameVal === '' || pwdVal === '')
    if(!nameVal || !pwdVal) {
        return alert("请填写用户名或密码");
    }

    // 验证用户信息 --- 正则验证
    // if (testName(nameVal) === false  || testPwd(pwdVal) === false) {
    if(!testName(nameVal) || !testPwd(pwdVal)) {
        return alert("您的用户名密码, 不符合规则, 请重新填写");
    }

    // 想后端返发送请求
    const res = await ajax({
        method: "POST",
        url: "/users/login",
        data: `username=${nameVal}&password=${pwdVal}`,
        dataType: 'json'
    });
    console.log(res);
    
    if (res.code === 0) {
        errBox.classList.add("active");
    } else {
        window.localStorage.setItem("token", res.info.token);
        window.localStorage.setItem("id", res.info.user.id);

        // 1. 先拿到跳转前存储的路径
        const page = window.sessionStorage.getItem('page');
        // 2. 清除存储的路径
        window.sessionStorage.removeItem('page');

        window.location.href = page || "./index.html";
    }
})



5、返回信息显示



  • 登录失败



    在这里插入图片描述



  • 登录成功



    在这里插入图片描述



  • token



    在这里插入图片描述



三、首页的实现



1、案例效果



  • 没有登录前



    在这里插入图片描述


  • 登录后



    在这里插入图片描述



2、首页的逻辑



  • 分析原因

  1. http是一个

    无状态请求

    ,每次请求之间没有任何关联
  2. 刚刚登陆成功, 并立马跳转到首页, 此时发送获取用户详情的请求,在服务端看来, 是

    两个独立

    的请求
  3. 所以我们需要一个东西, 来

    证明我们刚刚登陆成功了


  • 解决方法

  1. 有一个 叫做

    token

    的东西, 是服务端给我们的, 注意

    有过期时间
  2. 当我们请求的时候, 我把

    用户账号和密码给到服务端

    , 然后

    服务端会生成一个token信息
  3. 我们后续发送请求时, 携带上这个

    token

    ,服务端就能直到我们刚刚登陆成功、
  4. token是后端根据我们信息生成一个只属于我们自己的

    加密的文本


  • 代码逻辑

  1. 判断token和id都正常存在
  2. 向服务端发请求,根据请求结果, 展示不同的页面



3、接口文档


  • 注意

    :

    登录后方可查看

  • 请求地址



    /users/info

  • 请求方式



    get

  • 携带参数



    支持restful风格 localhost:8888/users/info/:id

    在这里插入图片描述

  • 响应数据

    在这里插入图片描述



4、代码实现



  • HTML代码

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
    <link rel="stylesheet" href="../css/index.css">
</head>
<body>
    <h1>
        首页
        <p class="off"><a href="./login.html">您好, 请登录</a></p>
        <p class="on">
            您好, <span>用户名</span>
            <a href="./self.html">个人中心</a>
        </p>
    </h1>
    <div style="font-size: 40px;">
        <a href="./list.html">商品列表</a>
    </div>
    <script src="../js/index.js" type="module"></script>
</body>
</html>


  • CSS代码

* {
    margin: 0;
    padding: 0;
}

h1 {
    width: 100%;
    height: 80px;
    background-color: skyblue;
    display: flex;
    justify-content: center;
    align-items: center;
    position: relative;
}

h1>p {
    font-size: 20px;
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
    right: 50px;
    display: none;
}

h1>p.active {
    display: block;
}

h1>p>span {
    color: red;
}


  • JS代码

// 导入公共方法
import { utils } from "../utils/utils.js";
const { ajax } = utils;

// 获取元素
const offBox = document.querySelector(".off");
const onBox = document.querySelector(".on");

test();
async function test() {
    const token = window.localStorage.getItem("token");
    const id = window.localStorage.getItem("id");

    if (!token || !id) {
        // 展示请登录
        offBox.classList.add("active");
        onBox.classList.remove("active");
        return alert("您的token 或者 id 为空, 请先登录");
    }

    // 如果运行这个位置, 证明token和id都存在
    let res = await ajax({
        url: "/users/info",
        data: `id=${id}`,
        headers: {
            authorization: token,
        },
        dataType: "json",
    });
    console.log(res);
    if (res.code == 1) {
        if (res.info.code === 1) {
            offBox.classList.remove("active");
            onBox.classList.add("active");
            console.log(res);

            onBox.firstElementChild.innerHTML = res.info.info.nickname;
        } else {
            window.location.href = './login.html'
        }
    } else {
        // 可能是token过期, 或者token是伪造的
        offBox.classList.add("active");
        onBox.classList.remove("active");
    }
}



5、返回信息显示

在这里插入图片描述



四、个人中心



1、案例效果

在这里插入图片描述



2、个人页的逻辑

  1. 这个页面, 能随便进入吗?

    • 判断当前

      是否登录 (token)
  2. 请求用户信息

    渲染页面(users/info)
  3. 修改用户信息后, 点击修改



3、接口文档



  • 页面渲染的接口文档


  • 注意

    :

    登录后方可查看

  • 请求地址



    /users/info

  • 请求方式



    get

  • 携带参数



    支持restful风格 localhost:8888/users/info/:id

    在这里插入图片描述

  • 响应数据

    在这里插入图片描述


  • 修改个人信息的接口文档


  • 注意:


    登录后方可修改

  • 请求地址



    /users/update

  • 请求方式



    post

  • 携带参数


    在这里插入图片描述

  • 响应数据

    在这里插入图片描述



4、代码实现



  • HTML代码

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
    <link rel="stylesheet" href="../css/self.css">
</head>
<body>
    <h1>个人页
        <p>
            <a href="./index.html">回到首页</a>
            <a href="./rpwd.html">; 修改密码</a>
        </p>
    </h1>
    <div class="box">
        <label>
            用户名: <input class="name" type="text" disabled>
        </label>
        <label>
            用户年龄: <input class="age" type="text">
        </label>
        <label>
            用户昵称: <input class="nickname" type="text">
        </label>
        <label>
            用户性别: <select id="sel">
                <option value="">请选择</option>
                <option value=""></option>
                <option value=""></option>
            </select>
        </label>
        <button>确认修改</button>
    </div>
    <script src="../js/self.js" type="module"></script>
</body>
</html>


  • CSS代码

* {
    margin: 0;
    padding: 0;
}

h1 {
    width: 100%;
    height: 80px;
    display: flex;
    justify-content: space-evenly;
    align-items: center;
    background-color: skyblue;
}

.box {
    width: 600px;
    display: flex;
    flex-direction: column;
    padding: 20px;
    border: 3px solid pink;
    border-radius: 15px;
    margin: 30px auto;
    padding-top: 50px;
    position: relative;
}

.box > label {
    height: 50px;
    font-size: 22px;
}

.box > label > input {
    padding-left: 20px;
    font-size: 22px;
}

.box > button {
    font-size: 22px;
}

.box > span {
    position: absolute;
    left: 50%;
    transform: translateX(-50%);
    top: 10px;
    color: red;
    display: none;
}

.box > span.active {
    display: block;
}
.box select {
    font-size: 20px;
    padding-left: 15px;
}



  • JS代码

// 导入公共方法
import { utils } from "../utils/utils.js";
const { ajax } = utils;

// 获取DOM节点
const nameInp = document.querySelector(".name");
const ageInp = document.querySelector(".age");
const nickInp = document.querySelector(".nickname");
const selBox = document.querySelector("#sel");
const btn = document.querySelector("button");

// 获取到 token 与 id
const token = window.localStorage.getItem("token");
const id = window.localStorage.getItem("id");

test();
async function test() {
    // 0. 必须登陆状态, 才能进入页面
    if (!token || !id) {
        if (confirm("您当前没有登陆, 点击确定跳转登录页")) {
            window.sessionStorage.setItem("page", window.location.href);
            window.location.href = "./login.html";
        }
    }

    // 1. 确保登陆过后, 拿到用户信息并渲染页面
    let { info } = await ajax({
        url: "/users/info",
        data: `id=${id}`,
        dataType: "json",
        headers: {
            authorization: token,
        },
    });

    console.log(info);
    if (info.code === 1) {
        // 页面渲染
        nameInp.value = info.info.username;
        ageInp.value = info.info.age;
        nickInp.value = info.info.nickname;
        selBox.value = info.info.gender;
    } else if (info.code === 401 || info.code === 0) {
        window.location.href = "./login.html";
    }
}


// 2. 修改用户信息, 发送请求
btn.onclick = async function () {
    // 2.1 用户信息收集
    const age = ageInp.value;
    const gender = selBox.value;
    const nickname = nickInp.value;

    // console.log(age, gender, nickname)
    if (!age || !gender || !nickname) {
        alert("请输入年龄昵称,以及性别后再次修改");
        return;
    }
    // 2.2 拿到用户信息 发送请求
    let { info } = await ajax({
        url: "/users/update",
        method: "POST",
        data: { id, age, gender, nickname },
        dataType: "json",
        headers: {
            authorization: token,
        },
    });

    if (info.code == 1) {
        alert("用户信息修改成功");
    }
};



5、返回信息显示

在这里插入图片描述



五、修改密码



1、案例效果

在这里插入图片描述



2、修改密码的逻辑

  1. 思考:能随便进吗?

    • 必须是

      登陆状态的 (token)
  2. 前端验证


    • 不能为空

    • 正则验证
    • 验证

      新密码与重复新密码必须相同
  3. 满足上述三点, 发送请求



3、接口文档


  • 注意

    :

    登录后方可修改

  • 请求地址



    /users/rpwd

  • 请求方式



    post

  • 携带参数


    在这里插入图片描述

  • 响应数据



    修改密码成功后, 会自动注销当前登录状态, 需要重新登录

    在这里插入图片描述



4、代码实现



  • HTML

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
    <link rel="stylesheet" href="../css/self.css">
</head>
<body>
    <h1>修改密码
        <p>
            <a href="./index.html">回到首页</a>
            <a href="./rpwd.html">; 修改密码</a>
        </p>
    </h1>

    <div class="box">
        <label>
            旧密码: <input class="oldpwd" type="text">
        </label>
        <label>
            新密码: <input class="newpwd" type="text">
        </label>
        <label>
            重复新密码: <input class="rnewpwd" type="text">
        </label>
        <button>确认修改</button>
    </div>
    <script src="../js/rpwd.js" type="module"></script>
</body>
</html>


  • CSS代码

* {
    margin: 0;
    padding: 0;
}

h1 {
    width: 100%;
    height: 80px;
    display: flex;
    justify-content: space-evenly;
    align-items: center;
    background-color: skyblue;
}

.box {
    width: 600px;
    display: flex;
    flex-direction: column;
    padding: 20px;
    border: 3px solid pink;
    border-radius: 15px;
    margin: 30px auto;
    padding-top: 50px;
    position: relative;
}

.box > label {
    height: 50px;
    font-size: 22px;
}

.box > label > input {
    padding-left: 20px;
    font-size: 22px;
}

.box > button {
    font-size: 22px;
}

.box > span {
    position: absolute;
    left: 50%;
    transform: translateX(-50%);
    top: 10px;
    color: red;
    display: none;
}

.box > span.active {
    display: block;
}
.box select {
    font-size: 20px;
    padding-left: 15px;
}


  • JS代码

import { utils } from "../utils/utils.js";
const { ajax, testPwd } = utils;

// 0. 获取元素
const oBtn = document.querySelector("button");
const oldpwd = document.querySelector(".oldpwd");
const newpwd = document.querySelector(".newpwd");
const rnewpwd = document.querySelector(".rnewpwd");

const token = window.localStorage.getItem("token");
const id = window.localStorage.getItem("id");

test();
function test() {
    if (!token || !id) {
        window.sessionStorage.setItem("page", window.location.href);
        window.location.href = "./login.html";
    }
}

oBtn.addEventListener('click', async function () {
    // 收集用户输入的信息
    const oldPassword = oldpwd.value;
    const newPassword = newpwd.value;
    const rNewPassword = rnewpwd.value;

    // 1. 验证密码不能为空
    if (!oldPassword || !newPassword || !rNewPassword) {
        return alert("密码不能为空");
    }

    // 2. 正则校验
    if (!testPwd(oldPassword) || !testPwd(newPassword) || !testPwd(rNewPassword)) {
        return alert("请正确填写密码");
    }

    // 3. 新密码与重复新密码 必须相同
    if (newPassword !== rNewPassword) {
        return alert("新密码与重复新密码 必须相同");
    }

    let { info } = await ajax({
        url: "/users/rpwd",
        method: "POST",
        data: { id, oldPassword, newPassword, rNewPassword },
        dataType: 'json',
        headers: {
            authorization: token
        }
    });

    if (info.code === 1) {
        if (confirm('修改密码成功, 已经注销登录状态, 点击确定, 跳转登录页 ^_^')) {
            // window.sessionStorage.setItem("page", window.location.href);
            window.location.href = "./login.html";
        }
    }
    console.log(info)
})



六、注册新用户



1、案例效果

在这里插入图片描述



2、注册新用户的逻辑

  1. 思考:需要登陆状态吗?


    • 不需要登陆状态
  2. 点击事件内


    • 收集用户信息

    • 非空校验
    • 正则校验:

      密码与重复密码必须相同
    • 发送注册请求


      成功:提示用户注册成功; 跳转到登录页



      失败:可能就用户名重复, 提示用户重新输入用户名



3、接口文档


  • 请求地址



    /users/register

  • 请求方式



    post

  • 携带参数



    application/x-www-form-urlencoded 格式传递

    在这里插入图片描述

  • 响应数据

    在这里插入图片描述



4、代码实现



  • HTML代码

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
    <link rel="stylesheet" href="../css/self.css">
</head>
<body>
    <h1>注册页</h1>
    <div class="box">
        <label>
            用户名: <input class="username" type="text">
        </label>
        <label>
            密码: <input class="pwd" type="text">
        </label>
        <label>
            重复密码: <input class="rpwd" type="text">
        </label>
        <label>
            用户昵称: <input class="nickname" type="text">
        </label>
        <label>已有账号, <a href="./login.html">直接登录</a></label>
        <button>注册</button>
    </div>
    <script src="../js/register.js" type="module"></script>
</body>
</html>


  • CSS代码

* {
    margin: 0;
    padding: 0;
}

h1 {
    width: 100%;
    height: 80px;
    display: flex;
    justify-content: space-evenly;
    align-items: center;
    background-color: skyblue;
}

.box {
    width: 600px;
    display: flex;
    flex-direction: column;
    padding: 20px;
    border: 3px solid pink;
    border-radius: 15px;
    margin: 30px auto;
    padding-top: 50px;
    position: relative;
}

.box > label {
    height: 50px;
    font-size: 22px;
}

.box > label > input {
    padding-left: 20px;
    font-size: 22px;
}

.box > button {
    font-size: 22px;
}

.box > span {
    position: absolute;
    left: 50%;
    transform: translateX(-50%);
    top: 10px;
    color: red;
    display: none;
}

.box > span.active {
    display: block;
}
.box select {
    font-size: 20px;
    padding-left: 15px;
}


  • JS代码

// 导入公共方法
import { utils } from "../utils/utils.js";
const { testName, testPwd, ajax } = utils;

// 获取 DOM 节点
const btn = document.querySelector("button");
const usernameInp = document.querySelector(".username");
const pwdInp = document.querySelector(".pwd");
const rpwdInp = document.querySelector(".rpwd");
const nicknameInp = document.querySelector(".nickname");

btn.onclick = async function () {
    // 收集用户信息
    const username = usernameInp.value;
    const password = pwdInp.value;
    const rpassword = rpwdInp.value;
    const nickname = nicknameInp.value;

    // 非空校验
    if (!username || !password || !rpassword || !nickname) {
        alert("请输入用户名、密码、重复密码和昵称");
        return;
    }

    // 3. 正则校验
    if (!testName(username) || !testPwd(password) || !testPwd(rpassword)) {
        alert("请按照格式输入用户名或密码");
        return;
    }
    // 密码与重复密码必须相同
    if (password !== rpassword) {
        alert("密码与重复密码不相同");
        return;
    }

    let { info } = await ajax({
        url: "/users/register",
        method: "POST",
        data: { username, password, rpassword, nickname },
        dataType: "json",
    });

    if (info.code === 1) {
        alert("注册成功, 请跳转登录页登录");
    } else {
        alert("注册失败, 用户名重复, 请更改用户名重新注册");
    }
};



5、服务器数据

在这里插入图片描述



七、商品列表



1、案例效果

在这里插入图片描述



2、商品列表的逻辑

  • 渲染分类
  • 渲染商品列表
  • 切换分类
  • 筛选切换
  • 折扣切换
  • 排序切换
  • 页码切换
  • 每页展示数据切换
  • 搜索内容
  • 切换分类后, 要求页码回归到第一页
  • 点击商品(图片)进入商品详情页(页面自由飞翔, 要求能够拿到对应的商品数据即可, 方式不限)



3、接口文档



获取购物车列表


  • 请求地址



    /goods/list

  • 请求方式



    get

  • 携带参数


    在这里插入图片描述

  • 响应数据


    在这里插入图片描述



加入购物车的接口文档

  • 请求地址:

    /cart/add
  • 请求方式:

    post
  • 携带参数:
    在这里插入图片描述
  • 响应数据
    在这里插入图片描述



4、代码实现



  • HTML代码

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <link rel="stylesheet" href="../css/base.css">
    <link rel="stylesheet" href="../css/index.css">
    <link rel="stylesheet" href="../css/list.css">
</head>
<body>
    <h1>
        商品列表
        <p class="active"><a href="./cart.html">去到购物车; </a><a href="./index.html">回到首页</a></p>
    </h1>
    <div class="filterBox container">
        <div class="cateBox box">
            <p>分类 : </p>
            <ul></ul>
        </div>
        <div class="saleBox box">
            <p>筛选 : </p>
            <ul>
                <li class="saleItem active" data-sale="">全部</li>
                <li class="saleItem" data-sale="hot">热销</li>
                <li class="saleItem" data-sale="sale">折扣</li>
            </ul>
        </div>
        <div class="numberBox box">
            <p>折扣 : </p>
            <ul>
                <li class="numberItem active" data-number="10">全部</li>
                <li class="numberItem" data-number="5">5</li>
                <li class="numberItem" data-number="6">6</li>
                <li class="numberItem" data-number="7">7</li>
                <li class="numberItem" data-number="8">8</li>
                <li class="numberItem" data-number="9">9</li>
            </ul>
        </div>
        <div class="searchBox box">
            <p>搜索 : </p>
            <input class="search" type="text">
        </div>
        <div class="sortBox box">
            <p>排序 : </p>
            <ul>
                <li class="sortItem active" data-type="id" data-method="ASC">综合升序</li>
                <li class="sortItem" data-type="id" data-method="DESC">综合降序</li>
                <li class="sortItem" data-type="price" data-method="ASC">价格升序</li>
                <li class="sortItem" data-type="price" data-method="DESC">价格降序</li>
                <li class="sortItem" data-type="sale" data-method="ASC">折扣升序</li>
                <li class="sortItem" data-type="sale" data-method="DESC">折扣降序</li>
            </ul>
        </div>
    </div>
    <div class="pagination container">
        <span class="prev disable">上一页</span>
        <span class="total">1 / 100</span>
        <span class="next">下一页</span>
        <select>
            <option value="12">12</option>
            <option value="16">16</option>
            <option value="20">20</option>
            <option value="24">24</option>
        </select>
    </div>
    <ul class="list container">
        <li>
            <div class="show">
                <img src="" alt="">
                <span class="hot">热销</span>
                <span class="sale">折扣</span>
            </div>
            <p class="title">ashjdkgashjdg</p>
            <p class="price">
                <span class="current">¥ 80.00</span>
                <span class="origin">¥ 100.00</span>
            </p>
            <button>加入购物车</button>
        </li>
    </ul>
    <script src="../js/list.js" type="module"></script>
</body>
</html>


  • CSS代码

.filterBox {
    border: 1px solid #333;
    padding: 20px;
}

.filterBox > .box {
    font-size: 20px;
    font-weight: 400;
}

.filterBox > .box {
    display: flex;
    margin-bottom: 10px;
}

.filterBox > .box > p {
    width: 150px;
    text-align: right;
    padding-right: 30px;
    box-sizing: border-box;
}

.filterBox > .box > ul {
    flex: 1;
    display: flex;
    flex-wrap: wrap;
}

.filterBox > .box > ul > li {
    padding: 5px 10px;
    cursor: pointer;
    margin: 5px 10px;
}

.filterBox > .box > ul > li.active {
    background-color: skyblue;
    color: #fff;
}

.filterBox > .box > input {
    width: 220px;
    padding: 5px 0 5px 20px;
    font-size: 20px;
}

.pagination {
    border: 1px solid #333;
    margin: 10px auto;
    font-size: 22px;
    font-weight: 400;
    height: 50px;
    display: flex;
    align-items: center;
    padding: 0 10px;
}

.pagination > span {
    padding: 5px 10px;
    margin: 0 15px;
}

.pagination > span.prev,
.pagination > span.next {
    background-color: skyblue;
}

.pagination > span.disable {
    background-color: #ccc;
    color: #fff;
    cursor: not-allowed;
}

.pagination > select {
    padding: 0 0 0 20px;
    font-size: 22px;
}

.list {
    display: flex;
    flex-wrap: wrap;
    justify-content: space-between;
}

.list > li {
    height: 480px;
    width: 290px;
    border: 1px solid #333;
    margin-bottom: 10px;
    display: flex;
    flex-direction: column;
    justify-content: space-between;
}

.list > li > .show {
    width: 290px;
    height: 290px;
    border-bottom: 1px solid #333;
    box-sizing: border-box;
    padding: 5px;
    position: relative;
}

.list > li > .show > span {
    padding: 10px 20px;
    background-color: red;
    color: #fff;
    position: absolute;
    right: 0;
    top: 0;
    font-size: 20px;
}

.list > li > .show > span.sale {
    background-color: orange;
    right: 90px;
}

.list > li > p.title {
    overflow: hidden;
    text-overflow: ellipsis;
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
}

.list > li > p.price {
    font-size: 26px;
    color: red;
    font-weight: 600;
    margin: 10px;
}

.list > li > p.price > .origin {
    color: #ccc;
    text-decoration: line-through;
    font-size: 20px;
}

.list > li > * {
    pointer-events: none;
    /*
        该元素永远不会成为鼠标事件的 target
        但是,当其后代元素的 pointer-events 属性指定其他值时,
        鼠标事件可以指向后代元素
    */
}

.list > li > button {
    padding: 10px 0;
    font-size: 22px;
    pointer-events: all;
}


  • JS代码

// 0. 准备全局变量
let totalNum = 0;
const id = window.localStorage.getItem("id");
const token = window.localStorage.getItem("token");

// 1. 渲染分类列表
async function getCategory() {
    let { info } = await ajax({
        url: "/goods/category",
        dataType: "json",
    });

    cateBoxUl.innerHTML = info.list.reduce((prev, item) => {
        return (prev += `
            <li class='cate_box_item'>${item}</li>
        `);
    }, "<li class='cate_box_item active'>全部</li>");
}
getCategory();

// 2. 渲染商品列表

// 全局的参数
const data = {
    current: 1,
    pagesize: 12,
    search: "",
    filter: "",
    saleType: 10,
    sortType: "id",
    sortMethod: "ASC",
    category: "",
};

// 请求数据渲染页面
async function getList() {
    let { info } = await ajax({
        url: "/goods/list",
        // data: data
        data,
        dataType: "json",
    });

    // 保存总页码
    totalNum = info.total;
    // 修改页面 页码展示
    total.innerHTML = `${data.current} / ${info.total}`;

    // 修改按钮样式
    if (data.current > 1) {
        prev.classList.remove("disable");
    }
    if (data.current === info.total) {
        next.classList.add("disable");
    }
    if (data.current === 1) {
        prev.classList.add("disable");
    }
    if (data.current !== info.total) {
        next.classList.remove("disable");
    }

    // 商品列表渲染
    listBox.innerHTML = info.list.reduce((prev, item) => {
        return (prev += `
            <li class="list-item" data-goods_id="${item.goods_id}">
                <div class="show">
                    <img src="${item.img_big_logo}" alt="">
                    ${item.is_hot ? '<span class="hot">热销</span>' : ""}
                    ${item.is_sale ? '<span class="sale">折扣</span>' : ""}
                </div>
                <p class="title">${item.title}</p>
                <p class="price">
                    <span class="current">¥ ${item.current_price}</span>
                    <span class="origin">¥ ${item.price}</span>
                </p>
                <button>加入购物车</button>
            </li>
        `);
    }, "");
}
getList();

// 事件委托---切换分类;筛选;折扣;排序
filterBox.onclick = function (e) {
    // 1. 点击分类
    if (
        e.target.className === "cate_box_item" ||
        e.target.className === "cate_box_item active"
    ) {
        removeClass(e);

        data.category = e.target.innerText === "全部" ? "" : e.target.innerText;
        data.current = 1;

        getList();
    }

    // 点击筛选
    if (e.target.className === "saleItem") {
        removeClass(e);

        // console.log(e.target.dataset.sale)
        data.filter = e.target.dataset.sale;
        data.current = 1;

        getList();
    }

    // 点击 折扣
    if (e.target.className === "numberItem") {
        removeClass(e);

        data.saleType = e.target.dataset.number;
        data.current = 1;

        getList();
    }

    // 点击排序
    if (e.target.className === "sortItem") {
        removeClass(e);

        data.sortType = e.target.dataset.type;
        data.sortMethod = e.target.dataset.method;
        data.current = 1;

        getList();
    }
};
// 排他; 修改样式
function removeClass(e) {
    // 获取到自己父级的所有子级, 并放到数组内
    const list = [...e.target.parentElement.children];
    // 遍历数组, 给数组内所有元素, 取消 active 类名
    list.forEach((item) => item.classList.remove("active"));
    // 给自身添加类名
    e.target.classList.add("active");
}
// 模糊搜索
searchBox.oninput = function () {
    // 1. 拿到用户输入的值
    const inpVal = this.value;
    // 2. 改变参数
    data.search = inpVal;
    data.current = 1;
    // 3. 发送请求
    getList();
};

// 上一页
prev.onclick = function () {
    if (data.current === 1) return;
    data.current -= 1;
    getList();
};

// 下一页
next.onclick = function () {
    if (data.current === totalNum) return;
    data.current += 1;
    getList();
};

// 切换每页展示数据
selBox.onchange = function () {
    data.pagesize = this.value;
    getList();
};

// 点击商品
listBox.onclick = async function (e) {
    if (e.target.className === "list-item") {
        // 拿到商品ID
        window.sessionStorage.setItem("goods_id", e.target.dataset.goods_id);

        // 跳转商品详情页面
        window.location.href = "./detail.html";
    }

    // 点击加入购物车
    if (e.target.nodeName == "BUTTON") {
        console.log("点击按钮, 发请求, 加入购物车");

        // 如果我们现在没有 用户ID 需要跳转登录
        // console.log(id, token)
        if (!id || !token) {
            window.sessionStorage.setItem("page", window.location.href);
            window.location.href = "./login.html";
        }
        const goodsId = e.target.parentElement.dataset.goods_id;

        // 商品 ID 和 用户 ID 和 token 都有了
        let { info } = await ajax({
            url: "/cart/add",
            method: "POST",
            data: { id, goodsId },
            headers: { authorization: token },
            dataType: "json",
        });
        // console.log(info);
        if (info.code === 1) {
            alert(info.message);
        } else {
            // alert('登陆状态过期,  请重新登陆')
            if (confirm("登陆状态过期,  点击确定跳转登录页")) {
                window.sessionStorage.setItem("page", window.location.href);
                window.location.href = "./login.html";
            }
        }
    }
};



八、商品详情页



1、案例效果

在这里插入图片描述



2、接口文档



获取商品详细信息


  • 请求地址



    localhost:8888/goods/item

  • 请求方式



    get

  • 携带参数



    支持 restful 风格 localhost:8888/goods/item/:id

    在这里插入图片描述

  • 响应数据



    如果该商品存在, 即为该商品的详细信息

    在这里插入图片描述



4、代码实现



  • HTML代码

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <link rel="stylesheet" href="../css/base.css">
  <link rel="stylesheet" href="../css/index.css">
  <link rel="stylesheet" href="../css/detail.css">
</head>
<body>
  <h1>
    商品详情
    <p class="active"><a href="./list.html">继续购物</a><a href="./cart.html">去到购物车</a><a href="./index.html">回到首页</a></p>
  </h1>
  <div class="content container">
    <div class="left">
      <div class="show">
        <img src="" alt="">
      </div>
      <ul class="list">
        <li></li>
        <li></li>
      </ul>
    </div>
    <div class="right">
      <div class="title">asjhdgj</div>
      <div class="price">¥ 100.00</div>
      <p class="size">
        <span>XS</span>
        <span>S</span>
        <span>M</span>
        <span>L</span>
        <span>XL</span>
      </p>
      <p class="btns">
        <button>加入购物车</button>
        <button>立即结算</button>
      </p>
    </div>
  </div>
  <div class="desc container">
    a
  </div>
  <script src="../js/detail.js" type="module"></script>
</body>
</html>


  • CSS代码

.desc {
  margin-top: 30px;
}

.content {
  display: flex;
  justify-content: space-between;
}

.content > .left {
  width: 450px;
  height: 600px;
  margin-right: 15px;
  display: flex;
  flex-direction: column;
  border: 1px solid #333;
}

.content > .left > .show {
  width: 450px;
  height: 450px;
  border-bottom: 1px solid #333;
}

.content > .left > .list {
  flex: 1;
  align-items: center;
  display: flex;
}

.content > .left > .list > li {
  width: 70px;
  height: 70px;
  border: 1px solid #333;
  margin-left: 20px;
}

.content > .right {
  flex: 1;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  box-sizing: border-box;
  padding: 10px;
}

.content > .right > .title {
  font-weight: 700;
  font-size: 22px;
}

.content > .right > .price {
  font-size: 60px;
  color: red;
}

.content > .right > .size {
  display: flex;
}

.content > .right > .size > span {
  padding: 5px 10px;
  height: 30px;
  border: 1px solid #333;
  border-right: none
}

.content > .right > .size > span:last-child {
  border-right: 1px solid #333;
  border-radius: 0 10px 10px 0;
}

.content > .right > .size > span:first-child {
  border-radius: 10px 0 0 10px;
}

.content > .right > .btns {
  display: flex;
  justify-content: space-between;
}

.content > .right > .btns > button {
  width: 45%;
  height: 50px;
  font-size: 26px;
  border: none;
  background-color: lightgreen;
  color: #fff;
}

.content > .right > .btns > button:first-child {
  background-color: lightblue;
}


  • JS代码

// 导入公共方法
import { utils } from "../utils/utils.js";
const { ajax } = utils;

//  获取DOM节点
const content = document.querySelector(".content");
const desc = document.querySelector(".desc");

const id = window.sessionStorage.getItem("goods_id");

// console.log(id)
if (!id) {
    // 没有 商品 ID 跳转 商品详情页
    window.location.href = "./list.html";
}

// 发送请求
async function getItem() {
    const { info } = await ajax({
        url: "/goods/item",
        data: { id },
        dataType: "json",
    });

    console.log(info.info);

    content.innerHTML = `
        <div class="left">
            <div class="show">
                <img src="${info.info.img_big_logo}" alt="">
            </div>
            <ul class="list">
                <li></li>
                <li></li>
            </ul>
        </div>
        <div class="right">
            <div class="title">${info.info.title}</div>
            <div class="price">¥ ${info.info.current_price}</div>
            <p class="size">
                <span>XS</span>
                <span>S</span>
                <span>M</span>
                <span>L</span>
                <span>XL</span>
            </p>
            <p class="btns">
                <button>加入购物车</button>
                <button>立即结算</button>
            </p>
        </div>
    `;

    desc.innerHTML = info.info.goods_introduce
}
getItem();



九、购物车的操作



1、案例效果

在这里插入图片描述



2、代码实现



这里对购物车操作的接口文档还挺多,就不一一列举了



  • HTML代码

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
    <link rel="stylesheet" href="../css/cart.css">
</head>
<body>
    <div class="header">页面顶部</div>
    <div class="content">
        <div class="top">
            <input type="checkbox"> 全选
        </div>
        <!-- 动态生成 -->
        <ul></ul>
        <div class="bottom">
            <div class="totalNum">
                总件数 : 3
            </div>
            <div class="btns">
                <button>清空购物车</button>
                <button>去结算</button>
                <button>删除所有已选中</button>
            </div>
            <div class="totalPrice">
                总价格 : ¥ <span>100.00</span>
            </div>
        </div>
    </div>
    <div class="footer">页面底部</div>

    <script src="../js/cart.js" type="module"></script>
</body>
</html>


  • CSS代码

* {
    margin: 0;
    padding: 0;
}

ul,ol,li {
    list-style: none;
}

.header,.footer {
    width: 1200px;
    height: 100px;
    background-color: skyblue;
    color: #fff;
    font-size: 50px;
    display: flex;
    justify-content: center;
    align-items: center;
    margin: 0 auto;
}

.footer {
    height: 400px;
}

.content {
    width: 1200px;
    margin: 0 auto;
    padding: 10px 0;
}

.content > .top,
.content > .bottom {
    height: 50px;
    background-color: pink;
    display: flex;
    align-items: center;
}

.content > .bottom {
    justify-content: space-between;
    box-sizing: border-box;
    padding: 0 10px;
}

.content > .bottom > .totalPrice > span {
    font-size: 20px;
    color: red;
}

.content > .bottom > .btns > button {
    font-size: 18px;
    padding: 5px 10px;
    cursor: pointer;
}

.content > .top > input {
    width: 30px;
    height: 30px;
    margin: 0 15px 0 50px;
}

.content > ul {
    padding-top: 10px;
}

.content > ul > li {
    width: 100%;
    border: 1px solid #333;
    box-sizing: border-box;
    height: 100px;
    margin-bottom: 10px;

    display: flex;
}

.content > ul > li > div {
    display: flex;
    justify-content: center;
    align-items: center;
    border-right: 1px solid #333;
}

.content > ul > li > div:last-child {
    border: none;
}

.content > ul > li > .show,
.content > ul > li > .status {
    width: 100px;
}

.content > ul > li > .status > input {
    width: 30px;
    height: 30px;
}

.content > ul > li > .show > img {
    width: 100%;
    height: 100%;
    display: block;
}

.content > ul > li > .price,
.content > ul > li > .sub {
    width: 200px;
    color: red;
    font-size: 20px;
}

.content > ul > li > .title {
    width: 300px;
    align-items: flex-start;
    justify-content: flex-start;
    box-sizing: border-box;
    padding: 5px;
}

.content > ul > li > .number {
    width: 230px;
}

.content > ul > li > .number > input {
    width: 50px;
    height: 30px;
    text-align: center;
    margin: 0 5px;
    border: none;
    outline: none;
    font-size: 18px;
}

.content > ul > li > .number > button {
    width: 30px;
    height: 30px;
    cursor: pointer;
}

.content > ul > li > .destory {
    flex: 1;
}

.content > ul > li > .destory > button {
    padding: 5px;
    font-size: 18px;
    cursor: pointer;
}



  • JS代码

// 导入公共方法
import { utils } from "../utils/utils.js";
const { ajax } = utils;

// 判断用户是否登录, 没有登录跳转登录页
const id = window.localStorage.getItem("id");
const token = window.localStorage.getItem("token");
if (!id || !token) {
    window.sessionStorage.setItem("page", window.location.href);
    window.location.href = "./login.html";
}

// 获取元素
var content = document.querySelector(".content");

// 准备渲染函数
async function bindHtml() {
    // 发送请求, 请求到原本的cartList
    let { info } = await ajax({
        url: "/cart/list",
        data: { id },
        headers: { authorization: token },
        dataType: "json",
    });
    let cartList = info.cart;

    var selctItem = 0;      // 存储选中商品的数量
    var selctTotalNum = 0;  // 存储选中商品的总数量
    var totalPrice = 0;     // 存储选中商品的总价
    // 1.0 找到选中商品
    cartList.forEach(function (item) {
        if (item.is_select) {
            selctItem++;
            selctTotalNum += item.cart_number;
            totalPrice += item.cart_number * item.current_price;
        }
    });

    // 1.1 查询数据, 渲染页面
    var str = `<div class="top">
        <input class="selcet_all" type="checkbox" ${
            // 选中的商品数量如果等于商品数量, 代表所有商品被选中
            selctItem === cartList.length ? "checked" : ""
        }> 全选
    </div>
    <ul>`;

    cartList.forEach(function (item) {
        str += `<li>
            <div class="status">
                <input data-id="${
                    item.goods_id
                }" class="item" type="checkbox" ${
            item.is_select ? "checked" : ""
        }>
            </div>
            <div class="show">
                <img src="${item.img_small_logo}" alt="">
            </div>
            <div class="title">
                ${item.title}
            </div>
            <div class="price">
                ¥ ${item.current_price}
            </div>
            <div class="number">
                <button class="sub_btn" data-id=${item.goods_id} data-num="${
            item.cart_number
        }">-</button>
                <input type="text" value="${item.cart_number}">
                <button class="add_btn"
                    data-id="${item.goods_id}"
                    data-num="${item.cart_number}"
                    data-goods_number="${item.goods_number}"
                >+</button>
            </div>
            <div class="sub">
                ¥ ${(item.cart_number * item.current_price).toFixed(2)}
            </div>
            <div class="destory">
                <button data-id="${item.goods_id}" class="del">删除</button>
            </div>
        </li>`;
    });

    str += `</ul>
    <div class="bottom">
        <div class="totalNum">
            总件数 : ${selctTotalNum}
        </div>
        <div class="btns">
            <button class="clear">清空购物车</button>
            <button class="go_pay" data-TotalPrice="${totalPrice}">去结算</button>
            <button class="del_item">删除所有已选中</button>
        </div>
        <div class="totalPrice">
            总价格 : ¥ <span>${totalPrice.toFixed(2)}</span>
        </div>
    </div>`;

    content.innerHTML = str;
}

// 2. 首次打开页面, 调用渲染函数
bindHtml();

// 3. 利用事件冒泡, 将所有的事件委托给统一的父级
content.onclick = async function (e) {
    // 3.1 全选按钮事件
    if (e.target.className === "selcet_all") {
        // 发送 修改 全选按钮 的请求
        await ajax({
            url: "/cart/select/all",
            method: "POST",
            data: {
                id,
                type: e.target.checked ? 1 : 0,
            },
            headers: { authorization: token },
        });

        //重新渲染视图
        bindHtml();
    }

    //清空购物车
    if (e.target.className === "clear") {
        var boo = confirm("请问您确定清空吗");
        if (boo) {
            await ajax({
                url: "/cart/clear",
                data: { id },
                headers: { authorization: token },
            });

            // 重新渲染页面
            bindHtml();
        }
    }

    // 删除已选中  (没有选中项时 禁止执行)
    if (e.target.className === "del_item") {
        var boo = confirm("请问您确定删除已选中吗");
        if (boo) {
            await ajax({
                url: "/cart/remove/select",
                data: { id },
                headers: { authorization: token },
            });

            // 重新渲染视图
            bindHtml();
        }
    }

    // 减少商品数量
    if (e.target.className === "sub_btn") {
        const goodsId = e.target.dataset.id;
        const number = e.target.dataset.num - 0 - 1;

        if (number < 1) return;

        await ajax({
            url: "/cart/number",
            method: "POST",
            data: { id, goodsId, number },
            headers: { authorization: token },
        });

        // 重新渲染视图
        bindHtml();
    }

    // 增加商品数量
    if (e.target.className === "add_btn") {
        const goodsId = e.target.dataset.id;
        const number = e.target.dataset.num - 0 + 1;
        const goods_number = e.target.dataset.goods_number - 0;

        if (number > goods_number) return;

        await ajax({
            url: "/cart/number",
            method: "POST",
            data: { id, goodsId, number },
            headers: { authorization: token },
        });

        // 重新渲染视图
        bindHtml();
    }

    // 选中商品
    if (e.target.className === "item") {
        const goodsId = e.target.dataset.id;

        await ajax({
            url: "/cart/select",
            method: "POST",
            data: { id, goodsId },
            headers: { authorization: token },
        });

        // 重新渲染视图
        bindHtml();
    }

    // 删除某一项
    if (e.target.className === "del") {
        var boo = confirm("请问您确定删除当前项吗");
        // 询问用户是否需要删除
        if (!boo) return;

        const goodsId = e.target.dataset.id - 0;

        await ajax({
            url: "/cart/remove",
            data: { id, goodsId },
            headers: { authorization: token },
        });

        // 重新渲染视图
        bindHtml();
    }
};




版权声明:本文为m0_58190023原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。