电商网站 - 从数据库添加/删除产品。第三部分视频教程代码文章,你可能会觉得有用

2025-06-10

电商网站 - 在数据库中添加/删除产品。(三)

视频教程

代码

您可能觉得有用的文章

你好,希望你一切顺利。在上一篇博文中,我们制作了登录/退出功能和卖家控制面板,用户可以申请成为卖家,并可以使用自己的控制面板管理产品。在今天的博文中,我们将制作一个产品添加功能,这是一个非常重要的功能。我们将学习如何在将表单存储到数据库之前进行验证,如何从前端上传图片到AWS,以及添加、删除产品、编辑产品等等。

如果你还没看过之前的片段,现在就观看

如果您想观看演示或完整的编码教程视频以更好地理解,您可以观看下面的教程。

视频教程

如果您能订阅我的 YouTube 频道来支持我,我将不胜感激。

访问全栈电子商务网站视频系列源代码下载图片

代码

在开始编码之前,您可以先看一下文件夹结构。由于代码文件太多,我甚至无法像在博客中那样设计文件结构。不过您可以看看下面的截图。

捕获

捕获2

捕获3

Capture4

那么,开始编码吧。首先,让我们创建/add-product到服务addProduct.html页面的路由。

服务器.js
// add product
app.get('/add-product', (req, res) => {
    res.sendFile(path.join(staticPath, "addProduct.html"));
})
Enter fullscreen mode Exit fullscreen mode

确保在404路线之前添加此路线,正如我之前所说,如果在路线之后添加任何路线404,您将始终获得404页面。

之后,让我们制作添加产品页面,在那里我们可以填写产品详细信息。

添加产品页面 - 设计

首先,从 HTML 5 模板开始,制作元素、loader所有CSS 和 JS 文件。alertlink

<head>
    // other head tags
    <link rel="stylesheet" href="css/signup.css">
    <link rel="stylesheet" href="css/addProduct.css">
</head>
<body>
    <img src="img/loader.gif" class="loader" alt="">

    <div class="alert-box">
        <img src="img/error.png" class="alert-img" alt="">
        <p class="alert-msg"></p>
    </div>

    <script src="js/token.js"></script>
    <script src="js/addProduct.js"></script>
</body>
Enter fullscreen mode Exit fullscreen mode

当然,完成基本结构后就可以制作形状了。

<img src="img/dark-logo.png" class="logo" alt="">

<div class="form">
    <input type="text" id="product-name" placeholder="product name">
    <input type="text" id="short-des" placeholder="short line about the product">
    <textarea id="des" placeholder="detail description"></textarea>

    <!-- product image -->
    <div class="product-info">
        <div class="product-image"><p class="text">product image</p></div>
        <div class="upload-image-sec">
            <!-- upload inputs -->
            <p class="text"><img src="img/camera.png" alt="">upload image</p>
            <div class="upload-catalouge">
                <input type="file" class="fileupload" id="first-file-upload-btn" hidden>
                <label for="first-file-upload-btn" class="upload-image"></label>
                <input type="file" class="fileupload" id="second-file-upload-btn" hidden>
                <label for="second-file-upload-btn" class="upload-image"></label>
                <input type="file" class="fileupload" id="third-file-upload-btn" hidden>
                <label for="third-file-upload-btn" class="upload-image"></label>
                <input type="file" class="fileupload" id="fourth-file-upload-btn" hidden>
                <label for="fourth-file-upload-btn" class="upload-image"></label>
            </div>
        </div>
        <div class="select-sizes">
            <p class="text">size available</p>
            <div class="sizes">
                <input type="checkbox" class="size-checkbox" id="xs" value="xs">
                <input type="checkbox" class="size-checkbox" id="s" value="s">
                <input type="checkbox" class="size-checkbox" id="m" value="m">
                <input type="checkbox" class="size-checkbox" id="l" value="l">
                <input type="checkbox" class="size-checkbox" id="xl" value="xl">
                <input type="checkbox" class="size-checkbox" id="xxl" value="xxl">
                <input type="checkbox" class="size-checkbox" id="xxxl" value="xxxl">
            </div>
        </div>
    </div>

    <div class="product-price">
        <input type="number" id="actual-price" placeholder="actual price">
        <input type="number" id="discount" placeholder="discount percentage">
        <input type="number" id="sell-price" placeholder="selling price">
    </div>

    <input type="number" id="stock" min="20" placeholder="item in sstocks (minimum 20)">

    <textarea id="tags" placeholder="Enter categories here, for example - Men, Jeans, Blue Jeans, Rough jeans (you sholud add men or women at start)"></textarea>

    <input type="checkbox" class="checkbox" id="tac" checked>
    <label for="tac">clothing take 30% from your total sell</label>

    <div class="buttons">
        <button class="btn" id="add-btn">add product</button>
        <button class="btn" id="save-btn">save draft</button>
    </div>
</div>
Enter fullscreen mode Exit fullscreen mode

这一下子就有很多 HTML,您可以参考视频来获取分步指南,因为我们在博客中主要关注 Javascript,但如果您对任何部分有任何疑问,请随时在讨论中询问我。

添加产品.css
body{
    display: block;
    padding: 0 10vw;
}

.logo{
    margin: 20px auto 50px;
}

input, textarea{
    font-weight: 500;
}

input:not(input[type="checkbox"]){
    width: 100%;
}

textarea{
    width: 100%;
    height: 270px;
    resize: none;
    padding: 10px 20px;
}

.product-info{
    width: 100%;
    height: 500px;
    display: grid;
    grid-template-columns: .75fr 1.25fr;
    grid-template-rows: repeat(2, 1fr);
    grid-gap: 20px;
    margin-bottom: 20px;
}

.product-image{
    display: flex;
    justify-content: center;
    align-items: center;
    background: #fff;
    background-size: cover;
    border-radius: 10px;
    grid-row: span 2;
    text-shadow: 0 0 10px #fff;
}

.text{
    text-transform: capitalize;
    color: #383838;
    font-size: 20px;
    font-weight: 500;
}

.upload-image-sec, .select-sizes{
    background: #fff;
    border-radius: 10px;
    padding: 20px;
}

.text img{
    height: 20px;
    margin-right: 10px;
}

.upload-catalouge{
    width: 100%;
    margin: 20px 0;
    display: grid;
    grid-template-columns: repeat(4, 100px);
    grid-gap: 10px;
}

.upload-image{
    width: 100%;
    height: 100px;
    background: #f5f5f5;
    cursor: pointer;
    background-size: cover;
}

.upload-image:hover{
    background: rgba(0, 0, 0, 0.2);
    background-size: cover;
}


.sizes{
    margin-top: 30px;
}

.size-checkbox{
    -webkit-appearance: none;
    width: 100px;
    height: 40px;
    border-radius: 5px;
    border: 1px solid #383838;
    cursor: pointer;
    margin-bottom: 10px;
    margin-right: 10px;
    position: relative;
    color: #383838;
}

.size-checkbox::after{
    content: attr(value);
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    font-size: 16px;
    text-transform: uppercase;
}

.size-checkbox:checked{
    background: #383838;
    color: #fff;
}

input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button{
    -webkit-appearance: none;
    margin: 0;
}

.product-price{
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    grid-gap: 20px;
}

.product-price input{
    margin: 0;
}

.buttons{
    margin: 20px 0 50px;
}

.btn{
    padding: 10px 30px;
    text-transform: capitalize;
    color: #fff;
    background: #383838;
    border-radius: 5px;
    border: none;
    outline: none;
    margin-right: 10px;
    cursor: pointer;
}

#save-btn{
    background: #a9a9a9;
}
Enter fullscreen mode Exit fullscreen mode

你可能会注意到一个新的 CSS 选择器input::-webkit-outer-spin-button。如果是这样,那么它就是简单的选择输入箭头按钮。在本例中,我们想要隐藏number输入的箭头。这就是我使用它的原因。

input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button{
    -webkit-appearance: none;
    margin: 0;
}
Enter fullscreen mode Exit fullscreen mode

如果您从一开始就关注该系列,请对signpu.css文件进行一些小的更改。

input[type="text"],
input[type="password"],
input[type="email"],
input[type="number"], // add this new line
textarea{
    // properties
}
Enter fullscreen mode Exit fullscreen mode

或者您可以简单地用它替换整个选择器。

input:not(input[type="checkbox"]),
textarea{
    // properties
}
Enter fullscreen mode Exit fullscreen mode
输出

屏幕截图-localhost_3000-2021.10.04-19_18_16

太棒了!现在,让表单具备功能性。

表单提交

在将表单提交到后端之前,我们必须使用大量的 JS 来验证表单并向元素添加特定的触发器。

首先,由于这仅适用于卖家,因此首先检查用户访问页面时是否已登录。当然,如果用户未登录,则将用户重定向到登录页面。

let user = JSON.parse(sessionStorage.user || null);
let loader = document.querySelector('.loader');

// checknig user is logged in or not
window.onload = () => {
    if(user){
        if(!compareToken(user.authToken, user.email)){
            location.replace('/login');
        }
    } else{
        location.replace('/login');
    }
}
Enter fullscreen mode Exit fullscreen mode

从那里访问用户,sessionStorage因为我将用户存储在那里。

完成这些之后,我们开始添加动态定价。什么意思呢?就是说,我们添加一个功能,用户添加actual price一个discounted price,系统会自动将selling price精确的折扣信息填充到“价格”中。而且这个功能是可逆的。

添加产品.js
// price inputs

const actualPrice = document.querySelector('#actual-price');
const discountPercentage = document.querySelector('#discount');
const sellingPrice = document.querySelector('#sell-price');
Enter fullscreen mode Exit fullscreen mode

首先选择所有三个输入,然后添加click事件discountPercentage并进行计算。

discountPercentage.addEventListener('input', () => {
    if(discountPercentage.value > 100){
        discountPercentage.value = 90;
    } else{
        let discount = actualPrice.value * discountPercentage.value / 100;
        sellingPrice.value = actualPrice.value - discount;
    }
})
Enter fullscreen mode Exit fullscreen mode

在上面的代码中,我只是检查折扣百分比是否超过 100,然后将其设置为 90,因为没有人真的想卖免费产品,对吧?之后,只需进行简单的百分比与价值的计算并设置sellingPrice价值即可。

之后添加sellingPrice反向

sellingPrice.addEventListener('input', () => {
    let discount = (sellingPrice.value / actualPrice.value) * 100;
    discountPercentage.value = discount;
})
Enter fullscreen mode Exit fullscreen mode

太棒了!我们成功了!完成之后,让我们使用 AWS 在线存储上传的图片。要查看 AWS 设置,请参阅此处的教程

好了,我们先来看看如何在服务器端进行配置。在此之前,我们先来了解一下具体要做什么。

首先,我们在服务器中配置 AWS,然后向 AWS 发出请求,获取一个安全链接。获取到链接后,我们会将该链接发送到前端。当用户使用file输入框上传图片时,前端PUT会向服务器生成的链接发出请求,上传图片。最后,我们将该链接存储在一个数组中,以便跟踪。

因此,首先安装这两个包。

npm i aws-sdk dotenv
Enter fullscreen mode Exit fullscreen mode

aws-sdk- 当然是针对 aws 的
dotenv- 针对环境变量来保护您的凭证。

服务器.js

AWS Config

// aws config
const aws = require('aws-sdk');
const dotenv = require('dotenv');

dotenv.config();

// aws parameters
const region = "ap-south-1";
const bucketName = "ecom-website-tutorial-2";
const accessKeyId = process.env.AWS_ACCESS_KEY;
const secretAccessKey = process.env.AWS_SECRET_KEY;

aws.config.update({
    region, 
    accessKeyId, 
    secretAccessKey
})

// init s3
const s3 = new aws.S3();
Enter fullscreen mode Exit fullscreen mode

S3是我们用来存储文件的 AWS 服务。
之后,创建一个生成链接的函数来生成链接。

// generate image upload link
async function generateUrl(){
    let date = new Date();
    let id = parseInt(Math.random() * 10000000000);

    const imageName = `${id}${date.getTime()}.jpg`;

    const params = ({
        Bucket: bucketName,
        Key: imageName,
        Expires: 300, //300 ms
        ContentType: 'image/jpeg'
    })
    const uploadUrl = await s3.getSignedUrlPromise('putObject', params);
    return uploadUrl;
}
Enter fullscreen mode Exit fullscreen mode

创建一个async函数,因为我们不知道获取响应需要多长时间,而我们的其他代码都依赖于此。这getSignedUrlPromise是一个用于获取put链接的 AWS 方法。您也可以参考他们的文档。

现在只需创建一条/s3url路线,将链接传送到前端。

// get the upload link
app.get('/s3url', (req, res) => {
    generateUrl().then(url => res.json(url));
})
Enter fullscreen mode Exit fullscreen mode

太棒了!现在我们需要在前端访问它。那就开始吧。

添加产品.js

选择上传输入

// upload image handle
let uploadImages = document.querySelectorAll('.fileupload');
let imagePaths = []; // will store all uploaded images paths;
Enter fullscreen mode Exit fullscreen mode

现在查看每个上传按钮并为其添加更改事件。然后访问上传的文件。

uploadImages.forEach((fileupload, index) => {
    fileupload.addEventListener('change', () => {
        const file = fileupload.files[0];
        let imageUrl;

        if(file.type.includes('image')){
            // means user uploaded an image

        } else{
            showAlert('upload image only');
        }
    })
})
Enter fullscreen mode Exit fullscreen mode

此后只需fetch从服务器获取 url,然后再次使用fetchmake PUTrequest 上传图像。

if(file.type.includes('image')){
    // means user uploaded an image
    fetch('/s3url').then(res => res.json())
    .then(url => {
        fetch(url,{
            method: 'PUT',
            headers: new Headers({'Content-Type': 'multipart/form-data'}),
            body: file
        }).then(res => {
            console.log(url)
        })
    })
}
Enter fullscreen mode Exit fullscreen mode

大功告成,图片上传成功。现在要让它对用户可见。只需使用style属性设置元素的background-image

if(file.type.includes('image')){
    // means user uploaded an image
    fetch('/s3url').then(res => res.json())
    .then(url => {
        fetch(url,{
            method: 'PUT',
            headers: new Headers({'Content-Type': 'multipart/form-data'}),
            body: file
        }).then(res => {
            imageUrl = url.split("?")[0];
            imagePaths[index] = imageUrl;
            let label = document.querySelector(`label[for=${fileupload.id}]`);
            label.style.backgroundImage = `url(${imageUrl})`;
            let productImage = document.querySelector('.product-image');
            productImage.style.backgroundImage = `url(${imageUrl})`;
        })
    })
}
Enter fullscreen mode Exit fullscreen mode
输出

Capture5

现在,还剩下什么?我知道很多东西了 XD 现在,因为我们有自定义复选框,为了跟踪存储的大小,我们必须创建一个函数来跟踪它。

// store size function
const storeSizes = () => {
    sizes = [];
    let sizeCheckBox = document.querySelectorAll('.size-checkbox');
    sizeCheckBox.forEach(item => {
        if(item.checked){
            sizes.push(item.value);
        }
    })
}
Enter fullscreen mode Exit fullscreen mode

上面的代码很简单,我想你已经明白了。现在让我们选择剩下的所有表单元素。

// form submission

const productName = document.querySelector('#product-name');
const shortLine = document.querySelector('#short-des');
const des = document.querySelector('#des');

let sizes = []; // will store all the sizes

const stock = document.querySelector('#stock');
const tags = document.querySelector('#tags');
const tac = document.querySelector('#tac');

// buttons
const addProductBtn = document.querySelector('#add-btn');
const saveDraft = document.querySelector('#save-btn');

Enter fullscreen mode Exit fullscreen mode

现在添加click事件addProductBtn和类storeSizes函数来存储大小。

addProductBtn.addEventListener('click', () => {
    storeSizes();
    // validate form
})
Enter fullscreen mode Exit fullscreen mode

好的,为了验证表单,我们将使用单独的函数。但该函数会根据验证结果返回 true 或 false。

const validateForm = () => {
    if(!productName.value.length){
        return showAlert('enter product name');
    } else if(shortLine.value.length > 100 || shortLine.value.length < 10){
        return showAlert('short description must be between 10 to 100 letters long');
    } else if(!des.value.length){
        return showAlert('enter detail description about the product');
    } else if(!imagePaths.length){ // image link array
        return showAlert('upload atleast one product image')
    } else if(!sizes.length){ // size array
        return showAlert('select at least one size');
    } else if(!actualPrice.value.length || !discount.value.length || !sellingPrice.value.length){
        return showAlert('you must add pricings');
    } else if(stock.value < 20){
        return showAlert('you should have at least 20 items in stock');
    } else if(!tags.value.length){
        return showAlert('enter few tags to help ranking your product in search');
    } else if(!tac.checked){
        return showAlert('you must agree to our terms and conditions');
    } 
    return true;
}

addProductBtn.addEventListener('click', () => {
    storeSizes();
    // validate form
    if(validateForm()){ // validateForm return true or false while doing validation

    }
})
Enter fullscreen mode Exit fullscreen mode

现在如果你注意到,不是返回validateForm而是返回,这是为什么呢,因为我不想在每个函数中都写,所以我只是把它写在函数里面。falseshowAlertreturn falseif elseshowAlert

Token.js
// alert function
const showAlert = (msg) => {
    // previous code
    return false;
}
Enter fullscreen mode Exit fullscreen mode

如果你运行代码,你会收到警报。但是,有一个问题。我们会在页面顶部收到警报。当我从底部提交表单时,因为我没有将其位置设置为fixed

注册.css
/* alert */
.alert-box{
    // previous code
    position: fixed;
    z-index: 2;
}
Enter fullscreen mode Exit fullscreen mode

我做了同样的事情loader

注册.css
.loader{
    position: fixed;
}
Enter fullscreen mode Exit fullscreen mode

到目前为止,验证部分也完成了。现在只需提交数据。但是要提交数据,首先我们需要数据,对吧?为此,我们需要另一个函数productData()来返回数据。

添加产品.js
const productData = () => {
    return data = {
        name: productName.value,
        shortDes: shortLine.value,
        des: des.value,
        images: imagePaths,
        sizes: sizes,
        actualPrice: actualPrice.value,
        discount: discountPercentage.value,
        sellPrice: sellingPrice.value,
        stock: stock.value,
        tags: tags.value,
        tac: tac.checked,
        email: user.email
    }
}
Enter fullscreen mode Exit fullscreen mode

现在,一旦我们在前端获得了数据,我们就使用我们的提交它sendData()

addProductBtn.addEventListener('click', () => {
    storeSizes();
    // validate form
    if(validateForm()){ // validateForm return true or false while doing validation
        loader.style.display = 'block';
        let data = productData();
        sendData('/add-product', data);
    }
})
Enter fullscreen mode Exit fullscreen mode

太棒了。但是我们/add-product的服务器里有 POST 路由吗?我觉得没有,我们自己做一个吧。

服务器.js
// add product
app.post('/add-product', (req, res) => {
    let { name, shortDes, des, images, sizes, actualPrice, discount, sellPrice, stock, tags, tac, email } = req.body;

    // validation
    if(!draft){
        if(!name.length){
            return res.json({'alert': 'enter product name'});
        } else if(shortDes.length > 100 || shortDes.length < 10){
            return res.json({'alert': 'short description must be between 10 to 100 letters long'});
        } else if(!des.length){
            return res.json({'alert': 'enter detail description about the product'});
        } else if(!images.length){ // image link array
            return res.json({'alert': 'upload atleast one product image'})
        } else if(!sizes.length){ // size array
            return res.json({'alert': 'select at least one size'});
        } else if(!actualPrice.length || !discount.length || !sellPrice.length){
            return res.json({'alert': 'you must add pricings'});
        } else if(stock < 20){
            return res.json({'alert': 'you should have at least 20 items in stock'});
        } else if(!tags.length){
            return res.json({'alert': 'enter few tags to help ranking your product in search'});
        } else if(!tac){
            return res.json({'alert': 'you must agree to our terms and conditions'});
        } 
    }

    // add product
    let docName = `${name.toLowerCase()}-${Math.floor(Math.random() * 5000)};
    db.collection('products').doc(docName).set(req.body)
    .then(data => {
        res.json({'product': name});
    })
    .catch(err => {
        return res.json({'alert': 'some error occurred. Try again'});
    })
})
Enter fullscreen mode Exit fullscreen mode

在上面的路由中,我只是简单地从请求中访问变量,并对数据执行验证。验证与前端相同,区别在于我们showAlert在那里返回,而在这里我们返回JSON。最后,我在产品名称后生成一个随机文档名称,并将数据添加到 Firestore。

现在只需收到产品添加的确认,我们就可以编辑文件processData()中的一点点内容token.js

token.js
const processData = (data) => {
    // previous conditions
    else if(data.product){
        location.href = '/seller';
    }
}
Enter fullscreen mode Exit fullscreen mode

所以,我们正式完成了验证,将产品添加到第一个库。干得好。博客开始有点长了。我想今天就讲到这里吧。当然,还要制作deleteedit和其他功能。请参阅视频教程。

希望你理解了所有内容。如果你有疑问或者我遗漏了什么,请在评论区告诉我。

您可能觉得有用的文章

  1. 最佳 CSS 效果
  2. 无限 CSS 加载器
  3. Disney+ 克隆版
  4. Youtube API - Youtube 克隆
  5. TMDB - Netflix 克隆

如果您能订阅我的YouTube频道,我将不胜感激。我创作了非常棒的网络内容。

源代码通过 Paypal 捐赠给我

您的捐赠真的激励我创作更多类似的精彩教程。请在Patreon上支持我,请我喝杯咖啡,或在 PayPal 上捐款。

感谢您的阅读。

鏂囩珷鏉ユ簮锛�https://dev.to/themodernweb/e-commerce-website-adding-deleting-product-from-database-part-3-18np
PREV
仅使用 HTML、CSS 和 JS 的响应式作品集视频教程 Lets Code
NEXT
CSS 组合器:关于 CSS 的一切 ~ 选择器。学习 CSS 2022 📌 什么是组合器?1. 后代选择器 2. 子选择器 ( > ) 3. 相邻兄弟选择器 ( + ) 4. 通用兄弟选择器 ( ~ ) 📌 总结