App.vue作为程序的入口,一般只充当程序的入口
1 2 3 4
| <template> <router-view/> </template>
|
在路由中,该部分的组件为Layout,即这层存放网页各处都需要的布局组件
在Layout文件夹下新建index.vue
和components文件夹,index.vue文件使用各种组件,指明路由关系
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| <template> <layout-nav /> <layout-header /> <layout-fixed /> <router-view/> <layout-footer /> </template>
<script setup> import LayoutFooter from './components/LayoutFooter.vue'; import LayoutHeader from './components/LayoutHeader.vue'; import LayoutNav from './components/LayoutNav.vue'; import LayoutFixed from './components/LayoutFixed.vue';
import { useCategoryStore } from '@/stores/category' import { onMounted } from 'vue'
const categoryStore = useCategoryStore() onMounted(() => categoryStore.getCategory()) </script>
|
onMounted()
函数是vue3提供的生命周期钩子,当该组件挂载在DOM上时开始执行某些逻辑
导航栏
导航栏在页面的最上方,用于快速跳转到某些页面。此处只给某些地方配置了路由,实际开发中视情况而定。
在components文件夹下新建LayoutNav.vue
文件,具体代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80
| <script setup> import { useUserStore } from '@/stores/user' import { useRouter } from 'vue-router'
const userStore = useUserStore() const router = useRouter() const confirm = () => { userStore.clearUserInfo() router.push('/login') } </script>
<template> <nav class="app-topnav"> <div class="container"> <ul> <template v-if="userStore.userInfo.token"> <li> <a href="javascript:;"> <i class=" iconfont icon-user"></i>{{ userStore.userInfo.account }} </a> </li> <li> <el-popconfirm @confirm="confirm" title="确认退出吗?" confirm-button-text="确认" cancel-button-text="取消"> <template #reference> <a href="javascript:;">退出登录</a> </template> </el-popconfirm> </li> <li><a href="javascript:;" @click="$router.push('/member/order')">我的订单</a></li> <li><a href="javascript:;" @click="$router.push('/member')">会员中心</a></li> </template> <template v-else> <li><a href="javascript:;" @click="$router.push('/login')">请先登录</a></li> <li><a href="javascript:;">帮助中心</a></li> <li><a href="javascript:;">关于我们</a></li> </template> </ul> </div> </nav> </template>
<style scoped lang="scss"> .app-topnav { background: #333; position: relative;
ul { display: flex; height: 53px; justify-content: flex-end; align-items: center;
li { a { padding: 0 15px; color: #cdcdcd; line-height: 1; display: inline-block;
i { font-size: 14px; margin-right: 2px; }
&:hover { color: $xtxColor; } }
~li { a { border-left: 2px solid #666; } } } } } </style>
|
在该组件中,导航栏以表格的形式存在,当用户信息存在时显示客户相关操作,否则显示登录操作
为什么此处的空连接是href="javascript:;"
而非href=""
或者省略?href=""
可能会在点击时跳转到当前页面的根路径。省略该属性时链接可能会失去默认的样式或行为。
一级导航
该部分用于跳转到各部分功能中。在该电商平台上,则是对应不同的购物模块。
在components文件夹下新建LayoutHeader.vue
文件,具体代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137
| <script setup> import { useCategoryStore } from '@/stores/category' import HeaderCart from './HeaderCart.vue' const categoryStore = useCategoryStore() </script>
<template> <header class='app-header'> <div class="container"> <h1 class="logo"> <RouterLink to="/">小兔鲜</RouterLink> </h1>
<ul class="app-header-nav"> <li class="home"> <RouterLink to="/">首页</RouterLink> </li> <li class="home" v-for="item in categoryStore.categoryList" :key="item.id"> <RouterLink active-class="active" :to="`/category/${item.id}`"> {{ item.name }} </RouterLink> </li> </ul>
<div class="search"> <i class="iconfont icon-search"></i> <input type="text" placeholder="搜一搜"> </div>
<HeaderCart /> </div> </header> </template>
<style scoped lang='scss'> .app-header { background: white;
.container { display: flex; align-items: center; }
.logo { width: 200px;
a { display: block; height: 132px; width: 100%; text-indent: -9999px; background: url('@/assets/images/logo.png') no-repeat center 18px / contain; } }
.app-header-nav { width: 820px; display: flex; padding-left: 40px; position: relative; z-index: 998;
li { margin-right: 40px; width: 38px; text-align: center;
a { font-size: 16px; line-height: 32px; height: 32px; display: inline-block;
&:hover { color: $xtxColor; border-bottom: 1px solid $xtxColor; } }
.active { color: $xtxColor; border-bottom: 1px solid $xtxColor; } } }
.search { width: 170px; height: 32px; position: relative; border-bottom: 1px solid #e7e7e7; line-height: 32px;
.icon-search { font-size: 18px; margin-left: 5px; }
input { width: 140px; padding-left: 5px; color: #666; } }
.cart { width: 50px;
.curr { height: 32px; line-height: 32px; text-align: center; position: relative; display: block;
.icon-cart { font-size: 22px; }
em { font-style: normal; position: absolute; right: 0; top: 0; padding: 1px 6px; line-height: 1; background: $helpColor; color: #fff; font-size: 12px; border-radius: 10px; font-family: Arial; } } } } </style>
|
在该部分中,logo部分显示应用图标,并提供跳转至首页的功能。其他的各类别信息则从服务器抓取。由于类别信息其他文件也需要,所以这里使用了Store来储存,该部分需参阅Store相关文件的定义。购物车组件显示用户的购物车信息,该部分在购物车组件部分会有说明。

吸顶的一级导航
为了方便用户快速跳转所需类别,一般会有两种选择:固定一级导航栏在页面顶端,或者提供回到顶部按钮。此处使用的是前者。
在components文件夹下新建LayoutFixed.vue
文件,具体代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182
| <script setup> import { useScroll } from '@vueuse/core' import { useCategoryStore } from '@/stores/category'
const { y } = useScroll(window) const categoryStore = useCategoryStore() </script>
<template> <div class="app-header-sticky" :class="{ show: y > 78 }"> <div class="container"> <h1 class="logo"> <router-link to="/"></router-link> </h1> <ul class="app-header-nav"> <li class="home"> <RouterLink to="/">首页</RouterLink> </li> <li class="home" v-for="item in categoryStore.categoryList" :key="item.id"> <router-link :to="`/category/${item.id}`" active-class="active">{{ item.name }}</router-link> </li> </ul> <div class="search"> <i class="iconfont icon-search"></i> <input type="text" placeholder="搜一搜"> </div> </div> </div> </template> <style scoped lang='scss'> .app-header-sticky { width: 100%; height: 80px; position: fixed; left: 0; top: 0; z-index: 999; background-color: #fff; border-bottom: 1px solid #e4e4e4; transform: translateY(-100%); opacity: 0;
&.show { transition: all 0.3s linear; transform: none; opacity: 1; }
.container { display: flex; align-items: center; }
.logo { width: 200px; height: 80px; background: url("@/assets/images/logo.png") no-repeat right 2px; background-size: 160px auto; }
.right { width: 220px; display: flex; text-align: center; padding-left: 40px; border-left: 2px solid $xtxColor;
a { width: 38px; margin-right: 40px; font-size: 16px; line-height: 1;
&:hover { color: $xtxColor; } } } }
.app-header { background: #fff;
.container { display: flex; align-items: center; }
.logo { width: 200px;
a { display: block; height: 132px; width: 100%; text-indent: -9999px; background: url('@/assets/images/logo.png') no-repeat center 18px / contain; } }
.search { width: 170px; height: 32px; position: relative; border-bottom: 1px solid #e7e7e7; line-height: 32px;
.icon-search { font-size: 18px; margin-left: 5px; }
input { width: 140px; padding-left: 5px; color: #666; } }
.cart { width: 50px;
.curr { height: 32px; line-height: 32px; text-align: center; position: relative; display: block;
.icon-cart { font-size: 22px; }
em { font-style: normal; position: absolute; right: 0; top: 0; padding: 1px 6px; line-height: 1; background: $helpColor; color: #fff; font-size: 12px; border-radius: 10px; font-family: Arial; } } } }
.app-header-nav { width: 820px; display: flex; padding-left: 40px; position: relative; z-index: 998;
li { margin-right: 40px; width: 38px; text-align: center;
a { font-size: 16px; line-height: 32px; height: 32px; display: inline-block;
&:hover { color: $xtxColor; border-bottom: 1px solid $xtxColor; } }
.active { color: $xtxColor; border-bottom: 1px solid $xtxColor; } } } </style>
|
相比一级导航,这里的吸顶版本又有什么区别?此处使用了vueuse中的useScroll方法,获得页面的滚动位置和状态。当状态符合时(此处为y > 78),为相应的类名做出改变,从而改变后面的scss样式。后面的scss样式分两种,当在页面顶端时,组件移出到页面外(y轴偏移为-1)并完全透明。满足条件后,组件线性移动到页面顶端(通过特定属性清除y轴偏移)并解除透明。

页尾
网页的页尾一般用于放置版权信息、备案声明和相关链接。在components下新建LayoutFooter.vue
文件,代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229
| <template> <footer class="app_footer"> <div class="contact"> <div class="container"> <dl> <dt>客户服务</dt> <dd><i class="iconfont icon-kefu"></i> 在线客服</dd> <dd><i class="iconfont icon-question"></i> 问题反馈</dd> </dl> <dl> <dt>关注我们</dt> <dd><i class="iconfont icon-iconfontweixin"></i> 公众号</dd> <dd><i class="iconfont icon-weibo"></i> 微博</dd> </dl> <dl> <dt>下载APP</dt> <dd class="qrcode"><img src="@/assets/images/qrcode.jpg" /></dd> <dd class="download"> <span>扫描二维码</span> <span>立马下载APP</span> <a href="javascript:;">下载页面</a> </dd> </dl> <dl> <dt>服务热线</dt> <dd class="hotline">400-0000-000 <small>周一至周日 8:00-18:00</small></dd> </dl> </div> </div> <div class="extra"> <div class="container"> <div class="slogan"> <a href="javascript:;"> <i class="iconfont icon-footer01"></i> <span>价格亲民</span> </a> <a href="javascript:;"> <i class="iconfont icon-footer02"></i> <span>物流快捷</span> </a> <a href="javascript:;"> <i class="iconfont icon-footer03"></i> <span>品质新鲜</span> </a> </div> <div class="copyright"> <p> <a href="javascript:;">关于我们</a> <a href="javascript:;">帮助中心</a> <a href="javascript:;">售后服务</a> <a href="javascript:;">配送与验收</a> <a href="javascript:;">商务合作</a> <a href="javascript:;">搜索推荐</a> <a href="javascript:;">友情链接</a> </p> <p>CopyRight © 小兔鲜儿</p> </div> </div> </div> </footer> </template>
<style scoped lang='scss'> .app_footer { overflow: hidden; background-color: #f5f5f5; padding-top: 20px;
.contact { background: #fff;
.container { padding: 60px 0 40px 25px; display: flex; }
dl { height: 190px; text-align: center; padding: 0 72px; border-right: 1px solid #f2f2f2; color: #999;
&:first-child { padding-left: 0; }
&:last-child { border-right: none; padding-right: 0; } }
dt { line-height: 1; font-size: 18px; }
dd { margin: 36px 12px 0 0; float: left; width: 92px; height: 92px; padding-top: 10px; border: 1px solid #ededed;
.iconfont { font-size: 36px; display: block; color: #666; }
&:hover { .iconfont { color: $xtxColor; } }
&:last-child { margin-right: 0; }
.download { padding-top: 5px; font-size: 14px; width: auto; height: auto; border: none;
span { display: block; }
a { display: block; line-height: 1; padding: 10px 25px; margin-top: 5px; color: #fff; border-radius: 2px; background-color: $xtxColor; } }
}
.qrcode img { width: 92px; height: 92px; border: 1px solid #ededed; }
.hotline { padding-top: 20px; font-size: 22px; color: #666; width: auto; height: auto; border: none; } small { display: block; font-size: 15px; color: #999; } }
.extra { background-color: #333; }
.slogan { height: 100px; line-height: 58px; padding: 60px 100px; border-bottom: 1px solid #434343; display: flex; justify-content: space-between;
a { height: 58px; line-height: 58px; color: #fff; font-size: 28px;
i { font-size: 50px; vertical-align: middle; margin-right: 10px; font-weight: 100; }
span { vertical-align: middle; text-shadow: 0 0 1px #333; } } }
.copyright { height: 170px; padding-top: 40px; text-align: center; color: #999; font-size: 15px;
p { line-height: 1; margin-bottom: 20px; }
a { color: #999; line-height: 1; padding: 0 10px; border-right: 1px solid #999;
&:last-child { border-right: none; } } } } </style>
|

Store的定义与接口的调用
在apis文件夹下新建文件category.js
,用于存放相关接口。代码如下
1 2 3 4 5 6 7 8 9 10
| import request from "@/utils/http"
export function getCategoryAPI(id) { return request({ url:'/category', params: { id } }) }
|
这是在axios较为标准的创建请求方法。此处的request为axios相关文件在导出时使用的别名。使用的get方法作为默认方法,因此此处没有指定method。url指定向后端发起请求的地址。params即传递的参数。此处axios请求以函数的方式export到其他地方,方便复用。
在stores文件夹下新建文件category.js
,代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| import { ref } from 'vue' import { defineStore } from 'pinia' import { getCategoryAPI } from "@/apis/layout"
export const useCategoryStore = defineStore("category", () => { const categoryList = ref([]) const getCategory = async () => { const res = await getCategoryAPI() categoryList.value = res.result }
return { categoryList, getCategory } })
|
action部分是一个比较标准的调用接口方法。该函数声明为异步函数,配合await在Promise被解决或拒绝后才执行相关操作,即在接收到数据后进行赋值,并在最后返回相关数据。该接口定义在Store中,Store的名字为category。