vue 模拟商城 商品列表

新建vue项目

 

main.js

import Vue from 'vue'
import App from './App.vue'
import store from "@/store";
import router from "@/router";
import '@/utils/vant-ui';
import '@/styles/common.css'

Vue.config.productionTip = false;

new Vue({
  render: h => h(App),
  router,
  store,
}).$mount('#app');

app.vue

<template>
  <div id="app">
    <router-view></router-view>
  </div>
</template>

<script>

export default {
  name: 'App'
}
</script>

<style>

</style>

search list.vue

<template>
    <div class="search">
        <van-nav-bar title="商品列表" fixed left-arrow @click-left="$router.go(-1)"></van-nav-bar>

        <van-search
        readonly
        shape="round"
        background="#ffffff"
        :value="querySearch || '搜索商品'"
        show-action
        @click="$router.push('/search')">
            <template #action>
                <van-icon class="tool" name="apps-o"></van-icon>
            </template>
        </van-search>

        <!-- 排序按钮       -->
        <div class="sort-btns">
            <div class="srot-item">综合</div>
            <div class="srot-item">销量</div>
            <div class="srot-item">价格</div>
        </div>

        <div class="goods-list">
            <goods-item v-for="item in proList" :key="item.goods_id" :item="item"></goods-item>
        </div>
    </div>
</template>

<script>
    import GoodsItem from "@/components/GoodsItem";
    import { getProductList } from '@/api/product';
    export default {
        name: "ListIndex",
        data(){
          return {
              page:1,
              proList:[]
          }
        },
        components:{
            GoodsItem
        },
        computed:{
            //获取地址栏的搜索关键字
            querySearch(){
                return this.$route.query.search;
            }
        },
        async created(){
            const { data:{ list }} = await getProductList({
               goodsName: this.querySearch,
               page: this.page
            });

           this.proList = list.data;
        }
    }
</script>

<style scoped>

    .search{
        padding-top: 46px;

    }

    .search ::v-deep .van-icon-arrow-left{
        color: #333333;
    }

    .search .tool{
        font-size: 24px;
        height: 40px;
        line-height: 40px;
    }

    .sort-btns{
        display: flex;
        height: 36px;
        line-height: 36px;
    }

    .sort-btns .srot-item{
        text-align: center;
        flex: 1;
        font-size: 16px;
    }

    .goods-list{
        background-color: #f6f6f6;
    }
</style>

search index.vue

<template>
    <div class="search">
        <van-nav-bar title="商品搜索" left-arrow @click-left="$router.go(-1)"></van-nav-bar>

        <van-search v-model="search" show-action placeholder="请输入搜索关键字">
            <template #action>
                <div @click="goSearch(search)">搜索</div>
            </template>
        </van-search>

        <!-- 搜索历史       -->
        <div class="search-history" v-if="history.length >0 ">
            <div class="title">
                <span>最近搜索</span>
                <van-icon name="delete-o" size="16" @click="clear"/>
            </div>
            <div class="list">
                <div class="list-item" v-for="item in history" :key="item" @click="goSearch(item)">{{ item}}</div>
            </div>
        </div>
    </div>
</template>

<script>
    import { getHistoryList, setHistoryList} from '@/utils/storage'
    export default {
        name: "SearchIndex",
        data(){
            return {
                search: '', //输入框的值
                history: getHistoryList() //历史记录
            }
        },
        methods:{
            goSearch(key){
                //查下标
                const index = this.history.indexOf(key);
                if(index !== -1){
                    //存在相同项,将原有关键词删除
                    this.history.splice(index,1);
                }
                //在把关键词添加在首位
                this.history.unshift(key);
                this.search = '';
                //存储本地化
                setHistoryList(this.history);
                //跳转到搜索列表页
                this.$router.push(`/searchList?search=${key}`);
            },
            //清空按钮
            clear(){
                this.history = [];
                //清除本地化
                setHistoryList([]);
            }
        }
    }
</script>

<style scoped>

    .search .searchBtn{
        background-color: #fa2209;
        color: #ffffff;
    }

    .search ::v-deep .van-search__action{
        background-color: #c21401;
        color: #ffffff;
        padding: 0 20px;
        border-radius: 0 5px 5px 0;
        margin-right: 10px;
    }

    .search ::v-deep .van-icon-arrow-left{
        color: #333333;
    }

    .search-history .title{
        height: 40px;
        line-height: 40px;
        font-size: 14px;
        display: flex;
        justify-content: space-between;
        align-items: center;
        padding: 0 15px;
    }

    .list{
        display: flex;
        justify-content: flex-start;
        flex-wrap: wrap;
        padding: 0 10px;
        gap: 5%;
    }

    .list-item{
        width: 30%;
        text-align: center;
        padding: 7px;
        line-height: 15px;
        border-radius: 50px;
        background-color: #ffffff;
        font-size: 13px;
        border: 1px solid #efefef;
        overflow: hidden;
        white-space: nowrap;
        text-overflow: ellipsis;
        margin-bottom: 10px;
    }
</style>

productDetails index.vue

<template>
    <div>productDetails</div>
</template>

<script>
    export default {
        name: "ProductDetailsIndex"
    }
</script>

<style scoped>

</style>

pay index.vue

<template>
    <div>我是Pay</div>
</template>

<script>
    export default {
        name: "PayIndex"
    }
</script>

<style scoped>

</style>

myorder index.vue

<template>
    <div>我是MyOrder</div>
</template>

<script>
    export default {
        name: "MyOrderIndex"
    }
</script>

<style scoped>

</style>

login index.vue

<template>
    <div class="login">
        <!-- 头部       -->
        <van-nav-bar
            title="会员登录"
            left-arrow
            @click-left="$router.go(-1)"></van-nav-bar>
        <!--  底部      -->
        <div class="container">
            <div class="title">
                <h3>手机号登录</h3>
                <p>未注册的手机号登录后自动注册</p>
            </div>

            <div class="form">
                <div class="form-item">
                    <input type="text" v-model="mobile" class="inp" placeholder="请输入手机号码" maxlength="11">
                </div>
                <div class="form-item">
                    <input type="text" v-model="picCode" class="inp" placeholder="请输入图形验证码" max="5">
                    <img v-if="picUrl" :src="picUrl" alt="" @click="getPicCode">
                </div>
                <div class="form-item">
                    <input type="text" v-model="smsCode" class="inp" placeholder="请输入短信验证码" >
                    <button @click="handleCode">
                        {{ currentSecond === totalSecond ? '获取验证码' : currentSecond + '秒之后重新发送'}}
                    </button>
                </div>
            </div>

            <div class="login-btn" @click="handleLogin">登录</div>
        </div>
    </div>
</template>

<script>
    //按需导入
    import { getRandomPicCode, getMsgCode, doLogin } from '@/api/login'
    //import { Toast } from 'vant'

    export default {
        name: "LoginIndex",
        data(){
          return {
              picKey:'', //请求后台时的图形验证码唯一标识
              picUrl:'', //存储请求渲染的图片地址,
              totalSecond: 60, //总秒数
              currentSecond: 60, //当前数秒
              timer: null, //定时器
              mobile:'', //手机号
              picCode:'', //用户输入的图形验证码
              smsCode:'' //短信验证码
          }
        },
        created(){
            //页面一加载,就请求验证码
            this.getPicCode()
        },
        methods:{
            //获取图形验证码
            async getPicCode(){
                //展开分解
                const { data:{ base64, key }} = await getRandomPicCode();
                this.picUrl = base64;  //存储地址
                this.picKey = key; // 存储唯一标识
                //Toast('获取图形验证码成功!');
                this.$toast('获取验证码成功');
            },
            //获取短信验证码
            async handleCode(){
                //校验没通过,不发送短信验证码
                if(!this.validFn()){
                    return;
                }

                //当前没有开启定时器,且totalSecond==CurrentSecond时,
                if (!this.timer && this.currentSecond === this.totalSecond){
                    //发送请求,
                    const res = await getMsgCode(this.picCode,this.picKey,this.mobile);
                    //console.log(res);
                    this.$toast('短信发送成功,请注意查收!' + res.message);

                    this.timer = setInterval( ()=>{
                        console.log('正在倒计时...');
                        //开始数秒
                        this.currentSecond--;
                        //如果当前秒 = 0时,关闭清除定时器,恢复当前秒
                        if(this.currentSecond <= 0){
                            clearInterval(this.timer);
                            this.timer = null;
                            this.currentSecond = this.totalSecond;
                        }
                    },1000);
                }
            },
            //校验 手机号和图形验证码是否合法
            validFn(){
                //手机号正则表达式
                if(!(/^1[3-9]\d{9}$/.test(this.mobile))){
                    this.$toast('请输入正确的手机号码');
                    return false;
                }
                //图形验证码正则表达式
                if(!(/^\w{4}$/.test(this.picCode))){
                    this.$toast('请输入正确的图形验证码');
                    return  false;
                }
                return true;
            },
            //登录
            async handleLogin(){
                if(!this.validFn()){
                    return;
                }

                if(!(/^\d{6}$/.test(this.smsCode))){
                    this.$toast('请输入正确的短信验证码');
                    return;
                }

                const res = await doLogin(this.mobile,this.smsCode);
                //console.log(res);
                if(res.status === 200){
                    //存入vuex
                    this.$store.commit('user/setUserInfo',res.data);
                    this.$toast(res.message);
                    await this.$router.push('/');
                }
            }
        },
        destroyed(){
            //离开页面,清除定时器
            clearInterval(this.timer);
        }

    }
</script>

<style scoped>

    .container{
        padding: 49px 29px;
    }

    .container .title{
        margin-bottom: 20px;

    }

    .title h3{
        font-size: 26px;
        font-weight: normal;
    }

    .title p{
        line-height: 40px;
        font-size: 14px;
        color: #b8b8b8;
    }

    .form-item{
        border-bottom: 1px solid #f3f1f2;
        padding: 8px;
        margin-bottom: 14px;
        display: flex;
        align-items: center;

    }

    .form-item .inp{
        display: block;
        border:none;
        outline: none;
        height: 32px;
        font-size: 14px;
        flex: 1;
    }

    .form-item img{
        width: 94px;
        height: 31px;
    }

    .form-item button{
        height: 31px;
        border: none;
        font-size: 13px;
        color: #cea26a;
        background-color: transparent;
        padding-right: 9px;
    }

    .login-btn{
        width: 100%;
        height: 42px;
        margin-top: 39px;
        background: linear-gradient(90deg,#ecb53c,#ff9211,#ff9211);
        color: #ffffff;
        border-radius: 39px;
        box-shadow: 0 10px 20px 0 rgba(0,0,0,.1);
        letter-spacing: 2px;
        display: flex;
        justify-content: center;
        align-items: center;
    }
</style>

layout user.vue

<template>
    <div>我是user</div>
</template>

<script>
    export default {
        name: "UserPage"
    }
</script>

<style scoped>

</style>

layout index.vue

<template>
    <div class="layout-index">
        <!-- 二级路由出口,二级组件展示的位置   -->
        <router-view></router-view>

        <van-tabbar route active-color="#ee0a24" inactive-color="#000">
            <van-tabbar-item to="/home" icon="wap-home-o">首页</van-tabbar-item>
            <van-tabbar-item to="/category" icon="apps-o">分类页</van-tabbar-item>
            <van-tabbar-item to="/cart" icon="shopping-cart-o">购物车</van-tabbar-item>
            <van-tabbar-item to="/user" icon="user-o">我的</van-tabbar-item>
        </van-tabbar>
    </div>
</template>

<script>
    export default {
        name: "LayoutIndex"
    }
</script>

<style scoped>

</style>

layout home.vue

<template>
    <div class="home">
        <!-- 导航条       -->
        <van-nav-bar title="模拟商城" fixed/>


        <!-- 搜索框       -->
        <van-search readonly shape="round" background="#f1f1f2"
                    placeholder="请在此输入搜索关键词" @click="$router.push('/search')" />


        <!-- 轮播图       -->
        <van-swipe class="my-swipe" :autoplay="3000" indicator-color="white">
            <van-swipe-item v-for="item in bannerList" :key="item.imgUrl">
                <img :src="item.imgUrl" alt="">
            </van-swipe-item>
        </van-swipe>

        <!--导航        -->
        <van-grid column-num="5" icon-size="40">
            <van-grid-item v-for="item in navList" :key="item.imgUrl"
            :icon = "item.imgUrl"
            :text="item.text"
            @click="$router.push('/category')"></van-grid-item>
        </van-grid>

        <!-- 主会场       -->
        <div class="main">
            <img src="" alt="">
        </div>

        <!--  猜你喜欢      -->
        <div class="guess">
            <p class="guess-title">--猜你喜欢--</p>
            <div class="goods-list">
                <GoodsItem v-for="item in productList" :key="item.goods_id" :item="item"></GoodsItem>
            </div>
        </div>


    </div>
</template>

<script>
    import GoodsItem from "@/components/GoodsItem";
    import { getHomeData } from '@/api/home'
    export default {
        name: "HomePage",
        data(){
          return{
              bannerList:[], //轮播图
              navList:[], // 导航
              productList:[], //商品
          }
        },
        components:{
            GoodsItem
        },
        async created(){
            const { data:{ pageData }} = await getHomeData();
            console.log(pageData);
            this.bannerList = pageData.items[1].data;
            this.navList = pageData.items[3].data;
            this.productList = pageData.items[6].data;

        }
    }
</script>

<style scoped>

    .home {
        padding-top: 100px;
        padding-bottom: 50px;

    }

    .van-nav-bar{
        z-index: 999;
        background-color: #c21401;

    }

    .home .van-nav-bar__title{
        color: #ffffff;
    }

    .van-search{
        position: fixed;
        width: 100%;
        top: 46px;
        z-index: 999;
    }

    .my-swipe .van-swipe-item{
        height: 185px;
        color: #ffffff;
        font-size: 20px;
        text-align: center;
        background-color: #39a9ed;
    }

    .my-swipe .van-swipe-item img{
        width: 100%;
        height: 185px;
    }


    /*主会场*/
    .main img{
        display: block;
        width: 100%;
    }

    /*猜你喜欢*/
    .guess .guess-title{
        height: 40px;
        line-height: 40px;
        text-align: center;
    }

    /*商品样式*/
    .goods-list{
        background-color: #f6f6f6;
    }
</style>

layout category.vue

<template>
    <div>我是category</div>
</template>

<script>
    export default {
        name: "CategoryPage"
    }
</script>

<style scoped>

</style>

layout cart.vue

<template>
    <div>我是cart</div>
</template>

<script>
    export default {
        name: "CartPage"
    }
</script>

<style scoped>

</style>

vant-ui.js

import Vue from 'vue'

//Vant组件库,全部导入方式
// npm i vant@latest-v2 -s
// import Vant from 'vant'
// import 'vant/lib/index.css'
// Vue.use(Vant);

//按需导入
import {
    Button,
    Tabbar,
    TabbarItem,
    NavBar,
    Toast,
    Search,
    Swipe,
    SwipeItem,
    Grid,
    GridItem,
    Icon
} from 'vant'

//注册使用
Vue.use(Button);
//底部菜单栏
Vue.use(Tabbar);
Vue.use(TabbarItem);
//登录页顶部导航栏
Vue.use(NavBar);
//轻提示
Vue.use(Toast);
//搜索框
Vue.use(Search);
//轮播图
Vue.use(Swipe);
Vue.use(SwipeItem);
//网格
Vue.use(Grid);
Vue.use(GridItem);
//图标
Vue.use(Icon);

storage.js

//约定一个通用的键名
const INFO_KEY = 'xx_shopping_info';
//搜索关键字的键名
const HISTORY_KEY = 'xx_history_list';

//获取个人登录信息
export const getInfo = () => {
    const defaultObj = { token:'',userId:''};
    const result = localStorage.getItem(INFO_KEY);
    return result ? JSON.parse(result) : defaultObj;
};

//设置个人信息
export const setInfo = (obj) => {
    localStorage.setItem(INFO_KEY,JSON.stringify(obj));
};

//删除个人信息
export const removeInfo = () => {
    localStorage.removeItem(INFO_KEY);
};

//获取搜索历史
export const getHistoryList = () => {
    const result = localStorage.getItem(HISTORY_KEY);
    return result ? JSON.parse(result) : [];
};

//设置搜索历史
export const setHistoryList = (arr) => {
    localStorage.setItem(HISTORY_KEY,JSON.stringify(arr));
};




request.js

import axios from "axios";
import { Toast } from 'vant'
//创建 axios 实例,将来对创建出来的实例,进行自定义配置
//好处,不会污染原始的axios
const instance = axios.create({
   baseURL:'http://cba.itlike.com/public/index.php?s=/api/',
   timeout:5000
});

//自定义配置 请求-响应 拦截器
//添加请求拦截器
instance.interceptors.request.use(function (config) {
   //在发送请求之前做些什么
    // 开启loading,禁止背景点击,节流处理,防止用户多次无效触发
    Toast.loading({
       message:'加载中...',
       forbidClick:true, //是否禁止点击背景功能
       loadingType: "spinner", //配置loading图标
       duration: 0, //图标不会自动消失
    });
   return config;
},function (error) {
   //对请求错误做些什么
   return Promise.reject(error);
});

//添加响应拦截器
instance.interceptors.response.use(function (response) {
    //2xx 范围内的状态码都会触发该函数
    //对响应数据做点什么
    const res = response.data;
    if(res.status !== 200){
        //提示
        Toast(res.message);
        //抛出一个错误的Promise
        return Promise.reject(res.message);
    }
    else{
        //正确情况下,清除loading效果
        Toast.clear();
    }

    return res;
},function (error) {
    //超出2xx 范围的状态码都会触发该函数
    //对响应错误做点什么
    return Promise.reject(error);
});


//导出配置好的实例
export default instance;

common.js

/*重置默认样式*/
* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

/*文字溢出省略号*/
.text-ellipsis-2{
    overflow: hidden;
    -webkit-line-clamp:2;
    text-overflow: ellipsis;
    display: -webkit-box;
    -webkit-box-orient:vertical;
}

/*
添加导航的通用样式
 */
.van-nav-bar .van-nav-bar__arrow{
    color: #333333;
}

store index.js

import Vue from 'vue'
import Vuex from 'vuex'
import user from "@/store/modules/user";

Vue.use(Vuex);

const store = new Vuex.Store({
    getters:{
        //获取tosken
        token(state){
            return state.user.userInfo.token;
        }
    },
    modules:{
        user
    }
});

export default store;

store user.js

import { getInfo, setInfo } from '@/utils/storage'

export default {
    //开启命名空间
    namespaced:true,
    state(){
        return {
            //个人权证相关
            userInfo: getInfo()
        }
    },
    mutations:{
        //所有mutation的参数都是state
        setUserInfo(state,obj){
            state.userInfo = obj;
            setInfo(obj);
        }
    },
    actions:{

    },
    getters:{

    }
}

router index.js

import Vue from 'vue'
import VueRouter from 'vue-router'
import Login from '@/views/login'
import Layout from '@/views/layout'
import MyOrder from '@/views/myorder'
import Pay from '@/views/pay'
import ProductDetails from '@/views/productdetails'
import Search from '@/views/search'
import SearchList from '@/views/search/list'
import Home from "@/views/layout/home";
import Cart from "@/views/layout/cart";
import Category from "@/views/layout/category";
import User from "@/views/layout/user";
import store from "@/store";

Vue.use(VueRouter);

const router = new VueRouter({
    routes:[
        { path:'/login',component:Login},
        {
            path:'/',
            component: Layout,
            redirect:'/home',
            children:[
                { path:'/home',component:Home},
                { path:'/category',component:Category},
                { path:'/cart',component: Cart},
                { path:'/user',component:User}
            ]
        },
        { path:'/search',component:Search},
        { path:'/searchList',component: SearchList},
        { path:'/productDetails/:id',component:ProductDetails},
        { path:'/pay',component:Pay},
        { path:'/myOrder',component:MyOrder}
    ]
});

//全局前置导航守卫
// 所有的路径在其被真正访问之前,解析渲染对应的组件页面前,
// 都会完整经过全部前置守卫,只有全局前置守卫放行了,
//才会到达对应的页面
// to 到哪里去
// from 从哪里来得
// next 是否放行 next()直接放行,next(路径),进行拦截
//定义一个数组,专用存放用户需要拦截的页面路径
const authUrl = ['/pay','/myOrder'];

router.beforeEach((to, from, next) => {
    //看to.path 是否在authUrl中出现
    if(!authUrl.includes(to.path)){
        //非权限页面,无须登录,直接放行
        next();
        return;
    }
    //是权限页面,需要判断token
    const token = store.getters.token;
    if(token){
        next();
    }
    else{
        next('/login');
    }
});

export default router;

goodsitem.vue

<template>

    <div class="goods-item" v-if="item.goods_id" @click="$router.push(`/productDetails/${item.goods_id}`)">
        <div class="left">
            <img :src="item.goods_image" alt="">
        </div>
        <div class="right">
            <p class="tit text-ellipsis-2">
                {{ item.goods_name }}
            </p>
            <p class="count">{{ item.goods_sales }}</p>
            <p class="price">
                <span class="new">¥{{ item.goods_price_min }}</span>
                <span class="old">¥{{ item.goods_price_max }}</span>
            </p>
        </div>
    </div>
</template>

<script>
    export default {
        name: "GoodsItem",
        props:{
            item:{
                type: Object,
                //默认值
                default: () => {
                    return {}
                }
            }
        }
    }
</script>

<style scoped>
    .goods-item{
        height: 148px;
        margin-bottom: 6px;
        padding: 10px;
        background-color: #ffffff;
        display: flex;
    }

    .goods-item .left{
        width: 127px;

    }

    .left img{
        display: block;
        width: 100%;
    }

    .goods-item .right{
        flex: 1;
        font-size: 14px;
        line-height: 1.3;
        padding: 10px;
        display: flex;
        flex-direction: column;
        justify-content: space-evenly;
    }

    .right .count{
        color: #999999;
        font-size: 12px;
    }

    .right .price{
        color: #999999;
        font-size: 16px;
    }

    .right .new{
        color: #f03c3c;
        margin-right: 10px;
    }

    .right .old{
        text-decoration: line-through;
        font-size: 12px;
    }
</style>

api product.js

import requset from '@/utils/request'

//获取搜索商品列表的数据
export const getProductList =  (obj) => {
    const { categoryId, goodsName, page} = obj;
    return requset.get('/goods/list',{
        params:{
            categoryId,
            goodsName,
            page
        }
    })
};

api login.js

//用于存放所有登录有关的请求接口
import request from '@/utils/request'

//1 获取图形验证码
 export const getRandomPicCode = () =>{
    return request.get('/captcha/image');
 };

//2 获取短信验证码
export const getMsgCode = (captchaCode,captchaKey,mobile) => {
  return request.post('/captcha/sendSmsCaptcha',{
      form:{
          captchaCode, //输入的验证码
          captchaKey, //
          mobile //手机号
      }
  });
};

//3. 登录接口
export const doLogin = (mobile,smsCode) => {
    return request.post('/passport/login',{
        form:{
            isParty:false, //是否第三方登录
            partyData:{}, //第三方的登录信息
            mobile, //手机号
            smsCode // 短信验证码
        }
    })
};

api home.js

import  request from '@/utils/request'

//获取首页数据
export const getHomeData = () => {
    return request.get('/page/detail',{
        params:{
            pageId:0
        }
    })
};


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

虾米大王

有你的支持,我会更有动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值