xmall商城项目实战
课程目标
- 运用vue+vue-router+vuex+element-ui搭建网站
- 对项目进行需求分析和模块划分以及功能划分
- 实现首页+全部+品牌周边页面渲染
- 查看商品详情页制作、商品排序以及分页功能实现
- 使用token+jwt实现网站用户登录退出 (后台)
- 使用meta元信息实现路由权限控制
- 实现加入购物车、图片懒加载功能
- 实现数据持久化存储用户数据和购物车数据
- 项目优化以及如何打包上线整个流程
项目初始化
-
vue create xmall_front
-
项目目录如下
-
cd xmall_front
npm run server
//访问https://localhost:8080
效果如下:
安装依赖
-
安装sass:
npm install -D sass-loader node-sass
现在是:npm install -D sass-loader sass
地址:https://cli.vuejs.org/zh/guide/css.html#预处理器 -
安装element-ui第三方组件库:
vue add element
-
安装图片懒加载插件:
npm i vue-lazyload -S
安装vant解决后期懒加载警告图片显示不了问题:npm i vant@2.12.47 -S
-
安装请求库:
npm i axios -S
-
注意版本问题
:
jwt-token原理:
- 安装 模拟后端数据一些对应模块
npm i jsonwebtoken -S
npm i cors -S
npm i body-parse -S
npm i express -S
路由配置
import Vue from 'vue'
import VueRouter from 'vue-router'
// import Index from "../views/Index";
// import Login from '../views/Login'
// import Home from '../views/Home'
// import Goods from '../views/Goods'
// import Thanks from '../views/Thanks'
// 解决路由命名冲突的方法
const routerPush = VueRouter.prototype.push
VueRouter.prototype.push = function push(location) {
return routerPush.call(this, location).catch(error => error)
}
// 异步组件加载,控制台中通过查看source查看到js分成很多小文件加载(而不是一个大的app.js加载),减轻资源消耗问题
const Index = ()=> import('@/views/Index')
const Login = ()=> import('@/views/Login')
const Home = ()=> import('@/views/Home')
const Goods = ()=> import('@/views/Goods')
const Thanks = ()=> import('@/views/Thanks')
Vue.use(VueRouter)
const routes = [
{
path: '/',
name: 'Home',
redirect: '/home',
component: Index,
children:[
{
path: '/home',
name: 'Home',
component: Home,
},
{
path: '/goods',
name: 'Goods',
component: Goods,
},
{
path: 'goodsDetail',
name: 'GoodsDetail',
component: ()=>import('@/views/GoodsDetail')
},
{
path: 'thanks',
name: 'Thanks',
component: Thanks,
},
{
path: 'user',
name: 'User',
component: ()=>import('@/views/User'),
meta:{
auth:true
}
},
]
},
{
path: '/login',
name: 'Login',
component: Login,
}
]
const router = new VueRouter({
mode: 'history', // 去除路由的#
base: process.env.BASE_URL,
routes
})
export default router
组件模板样例
common/Mheader.vue
<template>
<div class="header-box">
<!-- mjj1015481875 -->
<div>
<header class="w">
<div class="w-box">
<div class="nav-logo">
<h1>
<router-link to="/" title="商城官网">Xmall商城</router-link>
</h1>
</div>
<div class="right-box">
<div class="nav-list">
<el-input
placeholder="请输入商品信息"
suffix-icon="el-icon-search"
v-model="productInfo"
minlength="1"
maxlength="100"
></el-input>
<router-link to="/goods">全部商品</router-link>
<router-link to="/thanks">捐赠</router-link>
</div>
<div class="nav-aside">
<!-- 用户 -->
<div class="user pr">
<router-link to="/user">个人中心</router-link>
<div class="nav-user-wrapper pa" v-if="login">
<div class="nav-user-list">
<ul>
<!-- 头像 -->
<li class="nav-user-avatar">
<div>
<span class="avatar" :style="{backgroundImage:'url('+userInfo.file+')'}"></span>
</div>
<p class="name">{{userInfo.username}}</p>
</li>
<li>
<router-link to="/user/orderList">我的订单</router-link>
</li>
<li>
<router-link to="/user/information">账号资料</router-link>
</li>
<li>
<router-link to="/user/addressList">收货地址</router-link>
</li>
<li>
<router-link to="/user/support">售后服务</router-link>
</li>
<li>
<router-link to="/user/coupon">我的优惠</router-link>
</li>
<li>
<a href="javascript:;" @click="logout">退出</a>
</li>
</ul>
</div>
</div>
</div>
<!-- 购物车 -->
<!--鼠标悬浮方法-->
<div
class="shop pr"
@mouseenter="cartShowState(true)"
@mouseleave="cartShowState(false)"
>
<router-link to="/cart"></router-link>
<span class="cart-num">
<i class="num" :class="{no:totalNum == 0}">{{totalNum}}</i>
</span>
<!-- 购物车显示 -->
<div class="nav-user-wrapper pa active" v-show="showCart">
<div class="nav-user-list">
<div class="full">
<div class="nav-cart-items">
<ul>
<li class="clearfix" v-for="(goods,index) in cartList" :key="index">
<div class="cart-item">
<div class="cart-item-inner">
<a>
<div class="item-thumb">
<img :src="goods.productImageBig">
</div>
<div class="item-desc">
<div class="cart-cell">
<h4>
<a href>{{goods.productName}}</a>
</h4>
<!-- <p class="attrs"><span>白色</span></p>-->
<h6>
<span class="price-icon">¥</span>
<span class="price-num">{{goods.salePrice}}</span>
<span class="item-num">x {{goods.productNum}}</span>
</h6>
</div>
</div>
</a>
<div class="del-btn del">删除</div>
</div>
</div>
</li>
</ul>
</div>
<!-- 总件数 -->
<div class="nav-cart-total">
<p>
共
<strong>{{totalNum}}</strong> 件商品
</p>
<h5>
合计:
<span class="price-icon">¥</span>
<span class="price-num">{{totalPrice}}</span>
</h5>
<h6>
<el-button type="danger">去购物车</el-button>
</h6>
</div>
</div>
<div style="height: 313px;text-align: center" class="cart-con" v-if='!totalNum'>
<p>您的购物车竟然是空的!</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</header>
<slot name="nav">
<div class="nav-sub">
<div class="nav-sub-bg"></div>
<div class="nav-sub-wrapper">
<div class="w">
<el-breadcrumb separator-class="el-icon-arrow-right">
<el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
<el-breadcrumb-item :to="{path:'/goods'}">全部</el-breadcrumb-item>
<el-breadcrumb-item :to="{path:'/goods?cid=1184'}">品牌周边</el-breadcrumb-item>
<el-breadcrumb-item :to="{path:'/thanks'}">捐赠名单</el-breadcrumb-item>
</el-breadcrumb>
</div>
</div>
</div>
</slot>
</div>
</div>
</template>
<script>
import {mapState, mapMutations} from 'vuex'
import {getStore, removeStore, setStore} from '../utils/storage'
export default {
data(){
return{
productInfo: '',
}
},
computed:{
...mapState(['login', 'userInfo', 'cartList', 'showCart']),
totalNum(){
return this.cartList && this.cartList.reduce((total, item)=>{
total += item.productNum;
return total
}, 0)
},
totalPrice(){
return this.cartList && this.cartList.reduce((total, item)=>{
total += item.productNum*item.salePrice;
return total
}, 0)
},
},
async mounted() {
if(this.login){
// 解决网页初始化刷新丢失购物车数据的问题
const res = await this.$http.post('/api/cartList', {userId:getStore('id')})
if(res.data.success === true){
setStore('buyCart', res.data.cartList.cartList); // 解决导航栏小数字与本地buyCart数量不符问题
this.INITBUYCART();
}
}else {
this.INITBUYCART();
}
},
methods:{
...mapMutations(['SHOWCART', 'INITBUYCART']),
cartShowState(status){
this.SHOWCART({
showCart:status
})
},
logout(){
// 注销登录
removeStore('token');
removeStore('userinfo');
removeStore('id');
removeStore('buyCart');
window.location.href = '/';
}
},
created() {}
};
</script>
<style lang="scss" scoped>
@import "../assets/style/theme";
@import "../assets/style/mixin";
.w-box .nav-list .el-input {
margin-right: 20px;
}
.header-box {
background: $head-bgc;
background-image: -webkit-linear-gradient(#000, #121212);
background-image: linear-gradient(#000, #121212);
width: 100%;
}
header {
height: 100px;
z-index: 30;
position: relative;
}
.w-box {
display: flex;
justify-content: space-between;
align-items: center;
height: 100%;
// position: relative;
h1 {
height: 100%;
display: flex;
align-items: center;
> a {
background: url(/static/images/global-logo-red@2x.png) no-repeat 50%;
background-size: cover;
display: block;
@include wh(50px, 40px);
text-indent: -9999px;
background-position: 0 0;
}
}
.nav-list {
display: flex;
justify-content: center;
align-items: center;
margin-right: 22px;
.el-input {
width: 305px;
}
a {
width: 110px;
color: #c8c8c8;
display: block;
font-size: 14px;
padding: 0 25px;
&:hover {
color: #fff;
}
}
a:nth-child(2) {
// width: 5vw;
margin-left: -10px;
}
// a:nth-child(3){
// width: 5vw;
// }
}
.nav-aside {
position: relative;
&:before {
background: #333;
background: hsla(0, 0%, 100%, 0.2);
content: " ";
@include wh(1px, 13px);
overflow: hidden;
// position: absolute;
display: flex;
align-items: center;
// top: 4px;
left: 0;
}
&.fixed {
width: 262px;
position: fixed;
left: 50%;
top: 19px;
margin-left: 451px;
margin-top: 0;
z-index: 32;
top: -40px;
-webkit-transform: translate3d(0, 59px, 0);
transform: translate3d(0, 59px, 0);
-webkit-transition: -webkit-transform 0.3s
cubic-bezier(0.165, 0.84, 0.44, 1);
transition: transform 0.3s cubic-bezier(0.165, 0.84, 0.44, 1);
.user {
&:hover {
a:before {
background-position: -215px 0;
}
}
}
.shop {
&:hover {
a:before {
background-position: -210px -22px;
}
}
}
}
}
.right-box {
display: flex;
}
.nav-aside {
display: flex;
align-items: center;
}
// 用户
.user {
margin-left: 41px;
width: 36px;
&:hover {
a:before {
background-position: -5px 0;
}
.nav-user-wrapper {
top: 18px;
visibility: visible;
opacity: 1;
-webkit-transition: opacity 0.15s ease-out;
transition: opacity 0.15s ease-out;
}
}
> a {
position: relative;
@include wh(36px, 20px);
display: block;
text-indent: -9999px;
&:before {
content: " ";
position: absolute;
left: 8px;
top: 0;
@include wh(20px);
background: url(/static/images/account-icon@2x.32d87deb02b3d1c3cc5bcff0c26314ac.png) -155px
0;
background-size: 240px 107px;
transition: none;
}
}
li + li {
text-align: center;
position: relative;
border-top: 1px solid #f5f5f5;
line-height: 44px;
height: 44px;
color: #616161;
font-size: 12px;
&:hover {
background: #fafafa;
}
a {
display: block;
color: #616161;
}
}
.nav-user-avatar {
> div {
position: relative;
margin: 0 auto 8px;
@include wh(46px);
text-align: center;
&:before {
content: "";
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
border-radius: 50%;
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.06);
}
.avatar {
border-radius: 50%;
display: block;
@include wh(100%);
background-repeat: no-repeat;
background-size: contain;
}
}
.name {
margin-bottom: 16px;
font-size: 12px;
line-height: 1.5;
text-align: center;
color: #757575;
}
}
.nav-user-wrapper {
width: 168px;
transform: translate(-50%);
left: 50%;
}
.nav-user-list {
width: 168px;
&:before {
left: 50%;
}
}
}
.shop {
position: relative;
float: left;
margin-left: 21px;
width: 61px;
z-index: 99;
&:hover {
a:before {
content: " ";
background-position: 0 -22px;
}
}
.nav-user-wrapper.active {
top: 18px;
visibility: visible;
opacity: 1;
-webkit-transition: opacity 0.15s ease-out;
transition: opacity 0.15s ease-out;
}
> a {
position: absolute;
left: 0;
top: 0;
bottom: 0;
display: block;
right: 0;
z-index: 1;
&:before {
display: block;
@include wh(30px, 100%);
content: " ";
background: url(/static/images/account-icon@2x.32d87deb02b3d1c3cc5bcff0c26314ac.png)
0 -22px;
background-size: 240px 107px;
background-position: -150px -22px;
}
}
.cart-num {
position: relative;
display: block;
margin-left: 31px;
margin-top: -1px;
min-width: 30px;
text-indent: 0;
line-height: 20px;
> i {
background: #eb746b;
background-image: -webkit-linear-gradient(#eb746b, #e25147);
background-image: linear-gradient(#eb746b, #e25147);
box-shadow: inset 0 0 1px hsla(0, 0%, 100%, 0.15),
0 1px 2px hsla(0, 0%, 100%, 0.15);
text-align: center;
font-style: normal;
display: inline-block;
@include wh(20px);
line-height: 20px;
border-radius: 10px;
color: #fff;
font-size: 12px;
&.no {
background: #969696;
background-image: -webkit-linear-gradient(#a4a4a4, #909090);
background-image: linear-gradient(#a4a4a4, #909090);
box-shadow: inset 0 0 1px #838383, 0 1px 2px #838383;
}
}
}
.nav-user-wrapper {
right: 0;
width: 360px;
.nav-user-list {
&:before {
right: 34px;
}
}
}
.nav-user-list {
padding: 0;
width: 100%;
.full {
border-radius: 8px;
overflow: hidden;
}
.nav-cart-items {
max-height: 363px;
overflow-x: hidden;
overflow-y: auto;
}
.cart-item {
height: 120px;
width: 100%;
overflow: hidden;
border-top: 1px solid #f0f0f0;
&:hover {
background: #fcfcfc;
.del {
display: block;
}
}
}
li:first-child .cart-item:first-child {
border-top: none;
border-radius: 8px 8px 0 0;
overflow: hidden;
}
.cart-item-inner {
padding: 20px;
position: relative;
}
.item-thumb {
position: relative;
float: left;
width: 80px;
height: 80px;
border-radius: 3px;
&:before {
content: "";
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
z-index: 2;
border: 1px solid #f0f0f0;
border: 0 solid transparent;
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.06);
border-radius: 3px;
}
img {
display: block;
@include wh(80px, 80px);
border-radius: 3px;
overflow: hidden;
}
}
.item-desc {
margin-left: 98px;
display: table;
@include wh(205px, 80px);
h4 {
color: #000;
width: 185px;
overflow: hidden;
word-break: keep-all;
white-space: nowrap;
text-overflow: ellipsis;
font-size: 14px;
line-height: 16px;
margin-bottom: 10px;
}
.attrs span {
position: relative;
display: inline-block;
margin-right: 20px;
font-size: 14px;
line-height: 14px;
color: #999;
}
.attrs span:last-child {
margin-right: 0;
}
h6 {
color: #cacaca;
font-size: 12px;
line-height: 14px;
margin-top: 20px;
span {
display: inline-block;
font-weight: 700;
color: #cacaca;
}
.price-icon,
.price-num {
color: #d44d44;
}
.price-num {
margin-left: 5px;
font-size: 14px;
}
.item-num {
margin-left: 10px;
}
}
}
.cart-cell {
display: table-cell;
vertical-align: middle;
}
.del {
display: none;
overflow: hidden;
position: absolute;
right: 20px;
top: 50%;
transform: translateY(-50%);
}
}
.nav-cart-total {
box-sizing: content-box;
position: relative;
padding: 20px;
height: 40px;
background: #fafafa;
border-top: 1px solid #f0f0f0;
border-radius: 0 0 8px 8px;
box-shadow: inset 0 -1px 0 hsla(0, 0%, 100%, 0.5),
0 -3px 8px rgba(0, 0, 0, 0.04);
background: -webkit-linear-gradient(#fafafa, #f5f5f5);
background: linear-gradient(#fafafa, #f5f5f5);
p {
margin-bottom: 4px;
line-height: 16px;
font-size: 12px;
color: #c1c1c1;
}
h5 {
line-height: 20px;
font-size: 14px;
color: #6f6f6f;
span {
font-size: 18px;
color: #de4037;
display: inline-block;
font-weight: 700;
}
span:first-child {
font-size: 12px;
margin-right: 5px;
}
}
h6 {
position: absolute;
right: 20px;
top: 20px;
width: 108px;
}
}
}
}
@media (max-height: 780px) {
.nav-cart-items {
max-height: 423px !important;
}
}
@media (max-height: 900px) {
.nav-cart-items {
max-height: 544px !important;
}
}
@media (max-height: 1080px) {
.nav-cart-items {
max-height: 620px !important;
}
}
// 用户信息弹出
.nav-user-wrapper {
position: absolute;
z-index: 30;
padding-top: 18px;
opacity: 0;
visibility: hidden;
top: -3000px;
.nav-user-list {
position: relative;
padding-top: 20px;
background: #fff;
border: 1px solid #d6d6d6;
border-color: rgba(0, 0, 0, 0.08);
border-radius: 8px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15);
z-index: 10;
&:before {
position: absolute;
content: " ";
background: url(/static/images/account-icon@2x.32d87deb02b3d1c3cc5bcff0c26314ac.png)
no-repeat -49px -43px;
background-size: 240px 107px;
@include wh(20px, 8px);
top: -8px;
margin-left: -10px;
}
}
}
.nav-sub {
position: relative;
z-index: 20;
height: 90px;
background: #f7f7f7;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.04);
&.fixed {
position: fixed;
z-index: 21;
height: 60px;
top: 0;
left: 0;
right: 0;
border-bottom: 1px solid #dadada;
background-image: -webkit-linear-gradient(#fff, #f1f1f1);
background-image: linear-gradient(#fff, #f1f1f1);
}
.nav-sub-wrapper {
padding: 31px 0;
height: 90px;
position: relative;
&.fixed {
padding: 0;
height: 100%;
display: flex;
align-items: center;
}
&:after {
content: " ";
position: absolute;
top: 89px;
left: 50%;
margin-left: -610px;
width: 1220px;
background: #000;
height: 1px;
display: none;
opacity: 0;
-webkit-transition: opacity 0.3s ease-in;
transition: opacity 0.3s ease-in;
}
}
.w {
display: flex;
justify-content: space-between;
}
.nav-list2 {
height: 28px;
line-height: 28px;
display: flex;
align-items: center;
height: 100%;
li:first-child {
padding-left: 0;
a {
padding-left: 10px;
}
}
li {
position: relative;
float: left;
padding-left: 2px;
a {
display: block;
padding: 0 10px;
color: #666;
&.active {
font-weight: bold;
}
}
a:hover {
color: #5683ea;
}
}
li:before {
content: " ";
position: absolute;
left: 0;
top: 13px;
width: 2px;
height: 2px;
background: #bdbdbd;
}
}
}
@media (min-width: 1px) {
.nav-sub .nav-sub-wrapper:after {
display: block;
}
}
.cart-con {
/*display: flex;*/
text-align: center;
position: relative;
p {
padding-top: 185px;
color: #333333;
font-size: 16px;
}
}
.cart-con:before {
position: absolute;
content: " ";
left: 50%;
transform: translate(-50%, -70%);
top: 50%;
width: 76px;
height: 62px;
background: url("/static/images/cart-empty-new.png") no-repeat;
background-size: cover;
}
</style>
APP.vue
<template>
<div id="app">
<router-view class="main"></router-view>
</div>
</template>
<script>
export default {
name: 'app',
components: {
}
}
</script>
<style lang="scss">
@import "assets/style/index";
#app{
height: 100%;
}
.main{
background-color: #ededed;
overflow: hidden;
width: 100%;
}
</style>
Index.vue
<template>
<div>
<m-header></m-header>
<!-- 子路由的出口-->
<router-view></router-view>
</div>
</template>
<script>
import MHeader from '@/common/MHeader';
export default {
components: {
MHeader,
},
}
</script>
<style lang="scss" scoped>
</style>
src/components/Mshelf.vue
<template>
<div class="gray-box">
<div class="title">
<h2>{{title}}</h2>
</div>
<div>
<!-- 具名插槽 -->
<slot name='content'></slot>
</div>
</div>
</template>
<script>
export default {
props:['title']
};
</script>
<style lang="scss" scoped>
.gray-box {
position: relative;
margin-bottom: 30px;
overflow: hidden;
background: #fff;
border-radius: 8px;
border: 1px solid #dcdcdc;
border-color: rgba(0, 0, 0, 0.14);
box-shadow: 0 3px 8px -6px rgba(0, 0, 0, 0.1);
.title {
padding-left: 30px;
position: relative;
z-index: 10;
height: 60px;
padding: 0 10px 0 24px;
border-bottom: 1px solid #d4d4d4;
border-radius: 8px 8px 0 0;
box-shadow: rgba(0, 0, 0, 0.06) 0 1px 7px;
background: #f3f3f3;
background: -webkit-linear-gradient(#fbfbfb, #ececec);
background: linear-gradient(#fbfbfb, #ececec);
line-height: 60px;
font-size: 18px;
color: #333;
display: flex;
justify-content: space-between;
align-items: center;
h2 {
font-size: 18px;
font-weight: 400;
color: #626262;
display: inline-block;
}
}
}
</style>
src/components/MallGoods.vue
<template>
<el-row class="good-item">
<el-col>
<el-card :body-style="{padding: 0}">
<div class="good-img">
<a>
<img v-lazy="goods.productImageBig" alt> <!--图片懒加载-->
</a>
</div>
<h6 class="good-title">{{goods.productName}}</h6>
<h3 class="sub-title ellipsis">{{goods.subTitle}}</h3>
<div class="good-price pr">
<div class="ds pa">
<a href='javascript:;'>
<el-button type="default" size="medium" @click="productDetail(goods.productId)">查看详情</el-button>
</a>
<a href="javascript:;">
<el-button
type="primary"
size="medium"
@click="addCart(goods.productId, goods.salePrice, goods.productName, goods.productImageBig)"
>加入购物车</el-button>
</a>
</div>
<p>
<span style="font-size:14px">¥</span>
{{Number(goods.salePrice).toFixed(2)}}
</p>
</div>
</el-card>
</el-col>
</el-row>
</template>
<script>
import {mapState, mapMutations} from 'vuex'
import {getStore, setStore} from "../utils/storage";
export default {
props: ["goods"],
computed:{
...mapState(['login'])
},
methods:{
...mapMutations(['ADDCART', ]),
productDetail(id){
// 编程式导航
this.$router.push({path:`goodsDetail?productId=${id}`})
},
addCart(id, price, name, img){
if(this.login){
// 用户已登录
this.$http.post('/api/addCart', {
userId: getStore('id'),
productId: id,
productNum: 1
});
// 存储到后端,将当前商品存储的store的cartList中
this.ADDCART({
productId: id,
salePrice: price,
productName: name,
productImageBig: img,
})
}else {
// 用户未登录,将当前商品存储的store的cartList中
this.ADDCART({
productId: id,
salePrice: price,
productName: name,
productImageBig: img,
})
}
}
}
};
</script>
<style lang="scss" scoped>
.good-img {
display: flex;
justify-content: center;
a {
display: block;
img {
margin: 50px auto 10px;
width: 206px;
height: 206px;
display: block;
}
}
}
.good-price {
margin: 15px 0;
height: 30px;
text-align: center;
line-height: 30px;
color: #d44d44;
font-family: Arial;
font-size: 18px;
font-weight: 700;
display: flex;
justify-content: space-around;
padding-bottom: 60px;
a {
margin-right: 5px;
}
.ds {
display: none;
}
}
.good-price:hover .ds {
display: block;
}
.good-title {
line-height: 1.2;
font-size: 16px;
color: #424242;
margin: 0 auto;
padding: 0 14px;
text-align: center;
overflow: hidden;
}
h3 {
text-align: center;
line-height: 1.2;
font-size: 12px;
color: #d0d0d0;
padding: 10px;
}
.good-item {
background: #fff;
width: 25%;
transition: all 0.5s;
height: 410px;
&:hover {
transform: translateY(-3px);
box-shadow: 1px 1px 20px #999;
.good-price p {
display: none;
}
.ds {
display: flex;
}
}
}
.el-card {
border: none;
}
</style>
src/components/BuyNum.vue
<template>
<el-input-number v-model="num" @change="handleChange" :min="1" :max="10" label="描述文字"></el-input-number>
</template>
<script>
export default {
data() {
return {
num: 1
};
},
methods: {
handleChange(value) {
this.$emit('handleValue',value)
}
}
};
</script>
<style lang="scss" scoped>
</style>
views/Goods/index.vue
<template>
<div class="goods">
<div class="nav">
<div class="w">
<a @click="handlerSort(index)" :class="{active:index === isIndex}" href="javascript:;" v-for="(item,index) in navList" :key="index">{{item.title}}</a>
<div class="price-interval">
<input type="number" class="input" placeholder="价格" v-model="min">
<span style="margin: 0 5px">-</span>
<input type="number" placeholder="价格" v-model="max">
<el-button type="primary" size="small" style="margin-left: 10px;" @click="reset">确定</el-button>
</div>
</div>
</div>
<div>
<div class="goods-box w">
<MallGoods v-for="goods in allGoods" :key="goods.id" :goods="goods"></MallGoods>
</div>
<div class="w">
<el-pagination
style="float: right"
v-model:currentPage="currentPage"
v-model:page-size="pageSize"
:page-sizes="[10, 20, 30, 40]"
layout="total, sizes, prev, pager, next, jumper"
:total="totalCount"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</div>
</div>
</template>
<script>
import MallGoods from "../../components/MallGoods";
export default {
data() {
return {
max: "",
min: "",
navList:[
{'title':'综合排序'},
{'title':'价格由低到高'},
{'title':'价格由高到低'},
],
isIndex:0,
currentPage:1, // 当前页默认1
pageSize:20, // 每页数据
sort:'', // 排序
allGoods:[],
totalCount: '', // 总数据多少条显示在分页组件中
};
},
components:{
MallGoods
},
watch:{
$route:'getAllGoods' // 监听路由发生变化重新执行getAllGoods方法
},
created() {
this.getAllGoods();
},
methods:{
handleSizeChange(val){
this.pageSize = val;
this.getAllGoods();
// (`${val} items per page`)
},
handleCurrentChange (val){
this.currentPage = val;
this.getAllGoods();
// (`current page: ${val}`)
},
async getAllGoods(){
const url = this.$route.query.cid ? `api/goods/allGoods?page=${this.currentPage}&size=${this.pageSize}&sort=${this.sort}&priceGt=${this.min}&priceLte=${this.max}&cid=${this.$route.query.cid}` : `api/goods/allGoods?page=${this.currentPage}&size=${this.pageSize}&sort=${this.sort}&priceGt=${this.min}&priceLte=${this.max}`
try {
const res = await this.$http.get(url)
// (res)
this.allGoods = res.data.data;
this.totalCount = res.data.total;
}catch (e) {
(e)
}
},
// 价格排序
priceSort(v){
this.sort = v;
this.getAllGoods();
},
reset(){
this.currentPage = 1;
this.sort = '';
this.getAllGoods();
},
handlerSort(i){
this.isIndex = i; // 处理活跃标签
switch (i) {
case 0:
// 综合排序
this.reset();
break
case 1:
// 正序
this.priceSort(1);
break
case 2:
// 倒序
this.priceSort(-1);
break
}
},
}
};
</script>
<style lang="scss" scoped>
@import "../../assets/style/mixin";
@import "../../assets/style/theme";
.nav {
height: 60px;
line-height: 60px;
> div {
display: flex;
align-items: center;
a {
padding: 0 30px 0 0;
height: 100%;
@extend %block-center;
font-size: 12px;
color: #999;
&.active {
color: #5683ea;
}
&:hover {
color: #5683ea;
}
}
input {
@include wh(80px, 30px);
border: 1px solid #ccc;
}
input + input {
margin-left: 10px;
}
}
.price-interval {
padding: 0 15px;
@extend %block-center;
input[type="number"] {
border: 1px solid #ccc;
text-align: center;
background: none;
border-radius: 5px;
}
}
}
.goods-box {
overflow: hidden;
> div {
float: left;
border: 1px solid #efefef;
}
}
.no-info {
padding: 100px 0;
text-align: center;
font-size: 30px;
display: flex;
flex-direction: column;
.no-data {
align-self: center;
}
}
.img-item {
display: flex;
flex-direction: column;
}
.el-pagination {
align-self: flex-end;
margin: 3vw 10vw 2vw;
}
.section {
padding-top: 8vw;
margin-bottom: -5vw;
width: 1218px;
align-self: center;
}
.recommend {
display: flex;
> div {
flex: 1;
width: 25%;
}
}
</style>
src/GoodsDetails/index.vue
<template>
<div class="w store-content">
<div class="gray-box">
<div class="gallery-wrapper">
<div class="gallery">
<div class="thumbnail">
<ul>
<!--如果小图等于大图地址,显示on效果-->
<li v-for="(item,i) in small" :key="i" :class="{on:item===big}" @click="handleClick(item)">
<img :src='item'>
</li>
</ul>
</div>
<div class="thumb">
<div class="big">
<img :src='big'>
</div>
</div>
</div>
</div>
<!--右边-->
<div class="banner">
<div class="sku-custom-title">
<h4>{{product.productName}}</h4>
<h6>
<span>{{product.subTitle}}</span>
<span class="price">
<em>¥</em>
<i>{{Number(product.salePrice).toFixed(2)}}</i>
</span>
</h6>
</div>
<div class="num">
<span class="params-name">数量</span>
<BuyNum @handlerValue="productNum"></BuyNum>
</div>
<div class="buy">
<el-button
type="primary"
@click="addCart"
>加入购物车</el-button>
<el-button type="danger">现在购买</el-button>
</div>
</div>
</div>
<!--产品信息-->
<div class="item-info">
<Mshelf title="产品信息">
<div slot="content">
<div v-if="product.detail">
<div v-html="product.detail"></div>
</div>
<div class="no-info" v-else>
<img src="/static/images/no-data.png" alt="">
<br>该产品暂无数据提示哦
</div>
</div>
</Mshelf>
</div>
</div>
</template>
<script>
import Mshelf from "../../components/Mshelf";
import BuyNum from "../../components/BuyNum";
export default {
name: "goodsDetails",
components:{
Mshelf,
BuyNum
},
data(){
return{
product: {},
small: [],
big: ""
}
},
created() {
this.getGoodsDetail();
},
methods:{
// TODO:加入购物车
addCart(){
},
// 由子组件el-input-number通过$emit传过来的number
productNum(num){
// (num);
},
handleClick(src){
this.big = src; // 保证点击时小图地址给大图地址赋值,从而展示放大效果
},
async getGoodsDetail(){
try {
const res = await this.$http.get(`/api/goods/productDet?productId=${this.$route.query.productId}`)
// (res)
this.product = res.data
this.small = this.product.productImageSmall;
this.big = this.small[0];
}catch (e) {
(e)
}
}
}
};
</script>
<style lang="scss" scoped>
@import "../../assets/style/mixin";
.store-content {
clear: both;
width: 1220px;
min-height: 600px;
padding: 0 0 25px;
margin: 0 auto;
}
.gray-box {
display: flex;
padding: 60px;
margin: 20px 0;
.gallery-wrapper {
.gallery {
display: flex;
width: 540px;
.thumbnail {
li:first-child {
margin-top: 0px;
}
li {
@include wh(80px);
margin-top: 10px;
padding: 12px;
border: 1px solid #f0f0f0;
border: 1px solid rgba(0, 0, 0, 0.06);
border-radius: 5px;
cursor: pointer;
&.on {
padding: 10px;
border: 3px solid #ccc;
border: 3px solid rgba(0, 0, 0, 0.2);
}
img {
display: block;
@include wh(100%);
}
}
}
.thumb {
.big {
margin-left: 20px;
}
img {
display: block;
@include wh(440px);
}
}
}
}
// 右边
.banner {
width: 450px;
margin-left: 10px;
h4 {
font-size: 24px;
line-height: 1.25;
color: #000;
margin-bottom: 13px;
}
h6 {
font-size: 14px;
line-height: 1.5;
color: #bdbdbd;
display: flex;
align-items: center;
justify-content: space-between;
}
.sku-custom-title {
overflow: hidden;
padding: 8px 8px 18px 10px;
position: relative;
}
.params-name {
padding-right: 20px;
font-size: 14px;
color: #8d8d8d;
line-height: 36px;
}
.num {
padding: 29px 0 8px 10px;
border-top: 1px solid #ebebeb;
display: flex;
align-items: center;
}
.buy {
position: relative;
border-top: 1px solid #ebebeb;
padding: 30px 0 0 10px;
}
}
}
.item-info {
.gray-box {
padding: 0;
display: block;
}
.img-item {
width: 1220px;
// padding: 1vw;
text-align: center;
img {
width: 100%;
height: auto;
display: block;
}
}
}
.no-info {
padding: 200px 0;
text-align: center;
font-size: 30px;
}
.price {
display: block;
color: #d44d44;
font-weight: 700;
font-size: 16px;
line-height: 20px;
text-align: right;
i {
padding-left: 2px;
font-size: 24px;
}
}
</style>
src/Home/index.vue
<template>
<div class="home">
<!--轮播图-->
<div class="banner">
<el-carousel indicator-position="outside" height="480px">
<el-carousel-item v-for="item in banner" :key="item.id">
<img :src="item.picUrl" class="img1" v-if="item.picUrl">
<img :src="item.picUrl2" class="img2" v-if="item.picUrl2">
<img :src="item.picUrl3" class="img3" v-if="item.picUrl3">
</el-carousel-item>
</el-carousel>
</div>
<div v-for="(item,index) in homeList" :key="index">
<div class="activity-panel" v-if="item.type===1">
<!-- 仅仅要活动版块的内容 -->
<el-row>
<el-col class="content" :span="8" v-for="o in item.panelContents" :key="o.id">
<el-card :body-style="{ padding: '0px' }">
<img :src="o.picUrl" class="i">
<a href="#" class="cover-link"></a>
</el-card>
</el-col>
</el-row>
</div>
<!-- 商品title -->
<section class="w mt30 clearfix" v-if="item.type===2">
<Mshelf :title="item.name">
<div slot="content" class="hot">
<MallGoods v-for="(o,i) in item.panelContents" :key="i" :goods="o"></MallGoods>
</div>
</Mshelf>
</section>
<section class="w mt30 clearfix" v-if="item.type===3">
<Mshelf :title="item.name">
<div slot="content" class="floors">
<div
class="imgbanner"
v-for="(o,j) in item.panelContents"
:key="j"
v-if="o.type===2 || o.type===3"
>
<img :src="o.picUrl" alt="">
</div>
<MallGoods :goods='o' v-for='(o,i) in item.panelContents' :key='i' v-if='o.type===0'></MallGoods>
</div>
</Mshelf>
</section>
</div>
</div>
</template>
<script>
import Mshelf from "../../components/Mshelf";
import MallGoods from "../../components/MallGoods";
export default {
name: "index",
data(){
return{
banner:[],
homeList:[]
}
},
components:{
Mshelf,
MallGoods
},
async created() {
const res = await this.$http.get('api/goods/home');
// (res)
if(res.data.code === 200){
let result = res.data.result
this.homeList = result;
// (this.homeList)
// 获取轮播图数据
let item = result.find(item=>item.type===0);
this.banner = item.panelContents;
}
}
}
</script>
<style lang="scss" scoped>
.home {
display: flex;
flex-direction: column;
}
.no-info {
padding: 100px 0;
text-align: center;
font-size: 30px;
display: flex;
flex-direction: column;
.no-data {
align-self: center;
}
}
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.5s;
}
.fade-enter,
.fade-leave-to {
opacity: 0;
}
.page {
position: absolute;
width: 100%;
top: 470px;
z-index: 30;
.dots {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
.dot-active {
display: inline-block;
width: 15px;
height: 15px;
background-color: whitesmoke;
border-radius: 8px;
margin-right: 10px;
cursor: pointer;
}
.dot {
opacity: 0.2;
}
}
}
.activity-panel {
width: 1220px;
margin: 0 auto;
.box {
overflow: hidden;
position: relative;
z-index: 0;
margin-top: 25px;
box-sizing: border-box;
border: 1px solid rgba(0, 0, 0, 0.14);
border-radius: 8px;
background: #fff;
box-shadow: 0 3px 8px -6px rgba(0, 0, 0, 0.1);
}
.content {
float: left;
position: relative;
box-sizing: border-box;
width: 25%;
height: 200px;
text-align: center;
}
.content ::before {
position: absolute;
top: 0;
left: 0;
z-index: 1;
box-sizing: border-box;
border-left: 1px solid #f2f2f2;
border-left: 1px solid rgba(0, 0, 0, 0.1);
width: 100%;
height: 100%;
content: "";
pointer-events: none;
}
.i {
width: 305px;
height: 200px;
}
.cover-link {
cursor: pointer;
display: block;
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 4;
background: url(data:image/gif;base64,R0lGODlhAQABAIAAAP///////yH5BAEHAAEALAAAAAABAAEAAAICTAEAOw==)
repeat;
}
a {
color: #5079d9;
cursor: pointer;
transition: all 0.15s ease-out;
text-decoration: none;
}
a:hover {
box-shadow: inset 0 0 38px rgba(0, 0, 0, 0.08);
transition: all 0.15s ease;
}
}
.banner,
.banner span,
.banner div {
font-family: "Microsoft YaHei";
transition: all 0.3s;
transition-timing-function: linear;
}
.banner {
cursor: pointer;
perspective: 3000px;
position: relative;
z-index: 19;
margin: 0 auto 40px;
width: 1220px;
}
.bg {
position: relative;
width: 1220px;
height: 500px;
margin: 20px auto;
background-size: 100% 100%;
border-radius: 10px;
transform-style: preserve-3d;
transform-origin: 50% 50%;
transform: rotateY(0deg) rotateX(0deg);
& div {
position: relative;
height: 100%;
width: 100%;
}
}
.img1 {
display: block;
position: absolute;
width: 100%;
height: 100%;
top: 0;
border-radius: 10px;
}
.img2 {
display: block;
position: absolute;
width: 100%;
height: 100%;
bottom: 5px;
left: 0;
background-size: 95% 100%;
border-radius: 10px;
}
.img3 {
display: block;
position: absolute;
width: 100%;
height: 100%;
top: 0;
border-radius: 10px;
}
.a {
z-index: 20;
transform: translateZ(40px);
}
.b {
z-index: 20;
transform: translateZ(30px);
}
.c {
transform: translateZ(0px);
}
.sk_item {
width: 170px;
height: 225px;
padding: 0 14px 0 15px;
> div {
width: 100%;
}
a {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
transition: all 0.3s;
&:hover {
transform: translateY(-5px);
}
}
img {
width: 130px;
height: 130px;
margin: 17px 0;
}
.sk_item_name {
color: #999;
display: block;
max-width: 100%;
_width: 100%;
overflow: hidden;
font-size: 12px;
text-align: left;
height: 32px;
line-height: 16px;
word-wrap: break-word;
word-break: break-all;
}
.sk_item_price {
padding: 3px 0;
height: 25px;
}
.price_new {
font-size: 18px;
font-weight: 700;
margin-right: 8px;
color: #f10214;
}
.price_origin {
color: #999;
font-size: 12px;
}
}
.box {
overflow: hidden;
position: relative;
z-index: 0;
margin-top: 29px;
box-sizing: border-box;
border: 1px solid rgba(0, 0, 0, 0.14);
border-radius: 8px;
background: #fff;
box-shadow: 0 3px 8px -6px rgba(0, 0, 0, 0.1);
}
ul.box {
display: flex;
li {
flex: 1;
img {
display: block;
width: 305px;
height: 200px;
}
}
}
.mt30 {
margin-top: 30px;
}
.hot {
display: flex;
> div {
flex: 1;
width: 25%;
}
}
.floors {
width: 100%;
display: flex;
flex-wrap: wrap;
align-items: center;
.imgbanner {
width: 50%;
height: 430px;
.cover-link {
cursor: pointer;
display: block;
position: absolute;
top: 60px;
left: 0;
width: 50%;
height: 430px;
z-index: 4;
background: url(data:image/gif;base64,R0lGODlhAQABAIAAAP///////yH5BAEHAAEALAAAAAABAAEAAAICTAEAOw==)
repeat;
}
.cover-link:hover {
box-shadow: inset 0 0 38px rgba(0, 0, 0, 0.08);
transition: all 0.15s ease;
}
}
img {
display: block;
width: 100%;
height: 100%;
}
}
</style>
src/Login/index.vue
<template>
<div class="login">
<div class="box">
<span>使用账号 登录官网</span>
<el-form
:model="ruleForm"
status-icon
:rules="rules"
ref="ruleForm"
label-width="100px"
class="demo-ruleForm"
>
<el-form-item label="账号" prop="user">
<el-input type="text" v-model="ruleForm.user" autocomplete="off" placeholder="请输入账号"></el-input>
</el-form-item>
<el-form-item label="密码" prop="pwd">
<el-input type="password" v-model="ruleForm.pwd" autocomplete="off" placeholder="请输入密码"></el-input>
</el-form-item>
<div class="geetest"></div>
<el-form-item>
<el-button type="primary" @click="submitForm('ruleForm')">登录</el-button>
<el-button>返回</el-button>
</el-form-item>
</el-form>
</div>
</div>
</template>
<script>
import {getStore, removeStore, setStore} from '../../utils/storage'
export default {
data(){
let validateUser = (rule, value, callback)=>{
if(value === ''){
callback(new Error('请输入账号!'))
}else {
callback();
}
};
let validatePwd = (rule, value, callback)=>{
if(value === ''){
callback(new Error('请输入密码!'))
}else {
callback();
}
};
return{
ruleForm:{
user:'',
pwd:'',
},
rules: {
user: [{ validator: validateUser, trigger: "blur" }],
pwd: [{ validator: validatePwd, trigger: "blur" }]
},
cart:[],
}
},
mounted() {
// 缓存未登录之前当前购物车数据
this.login_addCart();
},
methods:{
login_addCart(){
let cartArr = []; // 缓存数组
let localCart = JSON.parse(getStore('buyCart'));
if(localCart && localCart.length){
localCart.forEach(item=>{
cartArr.push({
userId:getStore('id'),
productId:item.productId,
productNum: item.productNum,
})
})
}
this.cart = cartArr; // 拿到未登录时购物车数据
},
submitForm(formName){
this.$refs[formName].validate(async (valid)=>{
if(valid){
const res = await this.$http.post('/api/login', this.ruleForm);
// console.log('这是校验过的数据', res)
if(res.data.code===200){
let {username, token, id} = res.data;
// 持久化存储
setStore('token', token);
setStore('id', id)
// 在请求拦截器里给所有请求头增加authorization字段
if(this.cart && this.cart.length){
try {
// 将本地缓存的数组遍历去请求添加到购物车中
this.cart.forEach(async item=>{
item.userId = getStore('id') // 解决userId找不到从而不能向下执行拿不到res的bug
// console.log('this.cart--->', this.cart)
let res = await this.$http.post("/api/addCart", item);
// console.log('res-->', res)
if(res.data.success === true){
// .....自定制一些前段内容
console.log('添加购物车成功!')
}
removeStore('buyCart'); // 登录成功后清除本地buyCart
this.$router.push('/')
});
}catch (e) {
console.log(e)
}
}else {
await this.$router.push('/');
}
}
} else {
console.log("error submit!!");
return false;
}
});
}
}
};
</script>
<style lang="scss" scoped>
.login {
position: relative;
overflow: visible;
background: #ededed;
.box {
width: 450px;
border: 1px solid #dadada;
border-radius: 10px;
position: absolute;
top: 200px;
left: 50%;
padding: 50px 50px 50px 10px;
margin-left: -225px;
box-shadow: 0 9px 30px -6px rgba(0, 0, 0, 0.2),
0 18px 20px -10px rgba(0, 0, 0, 0.04),
0 18px 20px -10px rgba(0, 0, 0, 0.04),
0 10px 20px -10px rgba(0, 0, 0, 0.04);
text-align: center;
form {
margin-top: 30px;
}
span {
color: #333;
font-weight: 400;
}
}
}
</style>
src/Thanks/idnex.vue
<template>
<div>
<h1>Thanks</h1>
</div>
</template>
<script>
export default {
name: "index"
}
</script>
<style scoped>
</style>
src/User/index.vue
<template>
<div class="layout-container">
<m-header>
<div slot="nav"></div>
</m-header>
<div class="w">
<div class="content"></div>
</div>
</div>
</template>
<script>
import MHeader from "@/common/MHeader";
export default {
components: {
MHeader
}
};
</script>
<style lang="scss" scoped>
@import "../../assets/style/mixin";
.w {
padding-top: 40px;
}
.content {
display: flex;
height: 100%;
}
.account-sidebar {
width: 210px;
border-radius: 6px;
.avatar {
padding-top: 20px;
border-radius: 10px;
text-align: center;
img {
width: 168px;
height: 168px;
}
h5 {
font-size: 18px;
line-height: 48px;
font-weight: 700;
}
}
.account-nav {
padding-top: 15px;
li {
position: relative;
height: 48px;
border-top: 1px solid #ebebeb;
line-height: 48px;
&:hover {
a {
position: relative;
z-index: 1;
height: 50px;
background-color: #98afee;
line-height: 50px;
color: #fff;
}
}
a {
display: block;
}
&.current {
a {
position: relative;
z-index: 1;
height: 50px;
background-color: #98afee;
line-height: 50px;
color: #fff;
}
}
}
}
}
.account-content {
margin-left: 20px;
flex: 1;
}
</style>
/utils/storage.js
export const setStore = (name, content)=>{
if(!name) return;
if(typeof content !== 'string'){
content = JSON.stringify(content);
}
window.localStorage.setItem(name, content);
}
export const getStore = name =>{
if(!name) return;
return window.localStorage.getItem(name);
}
export const removeStore = name =>{
if(!name) return;
window.localStorage.removeItem(name);
}
仓库存储vuex
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
import {getStore, setStore} from '../utils/storage'
export default new Vuex.Store({
state: {
login:false, // 用户是否登录
userInfo:null, // 用户信息
cartList:[], // 加入购物车商品列表
showCart:false
},
mutations: {
// 网页初始化从本地缓存拿到购物车数据
INITBUYCART(state){
let initCart = getStore('buyCart')
if(initCart){
state.cartList = JSON.parse(initCart)
}
},
SHOWCART(state, {showCart}){
state.showCart = showCart;
},
ISLOGIN(state, info){
state.userInfo = info;
state.login = true;
// 持久化到本地
setStore('userinfo', info)
},
ADDCART(state, {productId, salePrice, productName, productImageBig, productNum=1}){
let cart = state.cartList;
let goods = {
productId, salePrice, productName, productImageBig
};
let flag = false;
// 注意:此处顺序不能颠倒,一定得先判断购物车是否有值,否则添加到购物车数量不准
if(cart.length){
// 如果购物车有值
cart.forEach(item=>{
if(item.productId === productId){
if(item.productNum >= 0){
flag = true;
item.productNum += productNum;
}
// console.log('item.productNum--->', item.productNum)
}
})
}
if(!cart.length || !flag){
// 如果购物车为空,数量为默认1
goods.productNum = productNum;
cart.push(goods)
}
// 给cartList重新赋值
state.cartList = cart;
setStore('buyCart', cart);
}
},
actions: {
},
modules: {
}
})
plugins/element.js
import Vue from 'vue'
import {
Button,
Input,
Breadcrumb,
BreadcrumbItem,
Carousel,
CarouselItem,
Col,
Card,
Row,
Pagination,
InputNumber,
Form, FormItem
} from 'element-ui'
Vue.use(Button)
Vue.use(Input)
Vue.use(Breadcrumb)
Vue.use(BreadcrumbItem)
Vue.use(Carousel)
Vue.use(CarouselItem)
Vue.use(Row)
Vue.use(Col)
Vue.use(Card)
Vue.use(Pagination)
Vue.use(InputNumber)
Vue.use(Form)
Vue.use(FormItem)
接口配置
请同学们自行封装对应的模块
初始化:npm init --yes
安装包:npm i cors body-parse express jsonwebtoken -S
配置server/app.js
const express = require('express');
const app = express();
const fs = require('fs');
/**
*
* @param {*当前页的数量} pageSize
* @param {*当前页} currentPage
* @param {*当前数组} arr
*
* 总32条
* 8
* 1 2
*/
function pagination(pageSize, currentPage, arr) {
let skipNum = (currentPage - 1) * pageSize;
let newArr = (skipNum + pageSize >= arr.length) ? arr.slice(skipNum, arr.length) : arr.slice(skipNum, skipNum + pageSize);
return newArr;
}
// 升序还是降序
/**
*
* @param {*排序的属性} attr
* @param {*true表示升序排序 false表示降序排序} rev
*/
function sortBy(attr, rev) {
if (rev === undefined) {
rev = 1;
} else {
rev = rev ? 1 : -1;
}
return function (a, b) {
a = a[attr];
b = b[attr];
if (a < b) {
return rev * -1;
}
if (a > b) {
return rev * 1;
}
return 0;
}
}
function range(arr, gt, lte) {
return arr.filter(item => item.salePrice >= gt && item.salePrice <= lte)
}
const cors = require('cors');
const jwt = require('jsonwebtoken')
const bodyParser = require('body-parser');
const cartListJSON = require('./db/cartList.json');
app.use(cors());
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }))
app.get('/api/goods/home', (req, res) => {
fs.readFile('./db/home.json', 'utf8', (err, data) => {
if (!err) {
res.json(JSON.parse(data));
}
})
})
app.get('/api/goods/allGoods', (req, res) => {
// 获取的是前端地址栏上的查询参数
const page = parseInt(req.query.page);
const size = parseInt(req.query.size);
const sort = parseInt(req.query.sort);
const gt = parseInt(req.query.priceGt);
const lte = parseInt(req.query.priceLte);
const cid = req.query.cid;
let newData = []
fs.readFile('./db/allGoods.json', 'utf8', (err, data) => {
let { result } = JSON.parse(data);
let allData = result.data;
// 分页显示
newData = pagination(size, page, allData);
if (cid === '1184') { //品牌周边
newData = allData.filter((item) => item.productName.match(RegExp(/Smartisan/)))
if (sort === 1) { //价格由低到高
newData = newData.sort(sortBy('salePrice', true))
} else if (sort === -1) { //价格由高到低
newData = newData.sort(sortBy('salePrice', false))
}
} else {
if (sort === 1) { //价格由低到高
newData = newData.sort(sortBy('salePrice', true))
} else if (sort === -1) { //价格由高到低
newData = newData.sort(sortBy('salePrice', false))
}
if (gt && lte) {
// 过滤 10~1000
newData = range(newData, gt, lte)
}
// 32
}
if (newData.length < size) {
res.json({
data: newData,
total: newData.length
})
} else {
res.json({
data: newData,
total: allData.length
})
}
})
})
// 商品详情的数据
app.get('/api/goods/productDet', (req, res) => {
const productId = req.query.productId;
console.log(productId);
fs.readFile('./db/goodsDetail.json', 'utf8', (err, data) => {
if (!err) {
let { result } = JSON.parse(data);
let newData = result.find(item => item.productId == productId)
res.json(newData)
}
})
})
// 模拟一个登陆的接口
app.post('/api/login', (req, res) => {
console.log(req.body.user);
// 登录成功获取用户名
let username = req.body.user
//一系列的操作
res.json({
// 进行加密的方法
// sing 参数一:加密的对象 参数二:加密的规则 参数三:对象
token: jwt.sign({ username: username }, 'abcd', {
// 过期时间
expiresIn: "3000s"
}),
username,
state: 1,
file: '/static/images/1570600179870.png',
code: 200,
address: null,
balance: null,
description: null,
email: null,
message: null,
phone: null,
points: null,
sex: null,
id: 62
})
})
// 登录持久化验证接口 访问这个接口的时候 一定要访问token(前端页面每切换一次,就访问一下这个接口,问一下我有没有登录/登陆过期)
// 先访问登录接口,得到token,在访问这个,看是否成功
app.post('/api/validate', function (req, res) {
let token = req.headers.authorization;
console.log(token);
// 验证token合法性 对token进行解码
jwt.verify(token, 'abcd', function (err, decode) {
if (err) {
res.json({
msg: '当前用户未登录'
})
} else {
// 证明用户已经登录
res.json({
token: jwt.sign({ username: decode.username }, 'abcd', {
// 过期时间
expiresIn: "3000s"
}),
username: decode.username,
msg: '已登录',
address: null,
balance: null,
description: null,
email: null,
file: "/static/images/1570600179870.png",
id: 62,
message: null,
phone: null,
points: null,
sex: null,
state: 1,
})
}
})
})
app.post('/api/addCart', (req, res) => {
let { userId, productId, productNum } = req.body;
fs.readFile('./db/allGoods.json', (err, data) => {
let { result } = JSON.parse(data);
if (productId && userId) {
let { cartList } = cartListJSON.result.find(item => item.id == userId)
// 找到对应的商品
let newData = result.data.find(item => item.productId == productId);
newData.limitNum = 100;
let falg = true;
if (cartList && cartList.length) {
cartList.forEach(item => {
if (item.productId == productId) {
if (item.productNum >= 1) {
falg = false;
item.productNum += parseInt(productNum);
}
}
})
}
if (!cartList.length || falg) { //购物车为空
newData.productNum = parseInt(productNum)
cartList.push(newData);
}
// 序列化
fs.writeFile('./db/cartList.json', JSON.stringify(cartListJSON), (err) => {
if (!err) {
res.json({
code: 200,
message: "success",
result: 1,
success: true,
timestamp: 1571296313981,
})
}
})
}
})
})
app.post('/api/cartList', (req, res) => {
let { userId } = req.body;
fs.readFile('./db/cartList.json', (err, data) => {
let { result } = JSON.parse(data);
let newData = result.find(item => item.id == userId);
res.json({
code: 200,
cartList: newData,
success: true,
message: 'success'
})
})
})
app.listen(3000); // 将接口改为3000
修改db文件夹的位置到当前server目录下
测试启动: nodemon app.js
浏览器访问:http://localhost:3000/api/goods/home拿到全部数据
修改项目中端口地址:http://localhost:3000/
上线部署
www.pyhonav.cn
阿里云买台服务器
登录服务器
用户名:ssh root@123.206.16.61
密码:xxxx
安装node二进制文件
node版本必须高于8,否则后面 npm install 会报错
cd /tmp/
wget https://nodejs.org/download/release/v10.15.3/node-v10.15.3-linux-x64.tar.xz
解压node
xz -d node-v10.15.3-linux-x64.tar.xz
:去除掉.xz后缀
tar -xf node-v10.15.3-linux-x64.tar
配置环境变量
ln -s /opt/node-v10.15.3-linux-x64/bin/node /usr/local/sbin/
ln -s /opt/node-v10.15.3-linux-x64/bin/npm /usr/local/sbin/
安装pm2进程管理工具
npm install pm2 -g
部署Node后端
git pull https://www.github.com/xiaomage/server
- 模拟本地文件上传到服务器
- 本地终端运行:
scp ./server.zip root@123.206.16.61:/tmp
- 服务器终端运行:
unzip server.zip && cd server && npm install && pm2 start app.js
- 本地终端运行:
开放3000端口在防火墙中:
部署Vue前端项目
前端打包文件dist:
npm run build
-
本地终端运行:
scp ./dist.zip root@123.206.16.61:/tmp
-
服务器终端运行:
unzip dist.zip
部署nginx
找到nginx的安装目录
//以我的服务器为例:nginx目录
cd /opt/ngx112/conf
vim nginx.conf
//修改配置文件如下
server {
listen 80; //端口号
server_name www.pythonav.cn; //域名
location / {
try_files $uri $uri/ /index.html; #匹配所有的路由
root /tmp/dist; //填写前端的根目录
index index.html index.htm;
}
}
输入nginx的启动命令
nginx 第一次输入是启动
nginx -s reload #平滑重启,重新读取配置文件,不重启进程
nginx -s stop
访问http://www.pythonav.cn 查看xmall商城项目
· 全网最简单!3分钟用满血DeepSeek R1开发一款AI智能客服,零代码轻松接入微信、公众号、小程
· .NET 10 首个预览版发布,跨平台开发与性能全面提升
· 《HelloGitHub》第 107 期
· 从文本到图像:SSE 如何助力 AI 内容实时呈现?(Typescript篇)
· 全程使用 AI 从 0 到 1 写了个小工具