文章目录
- Pinia
- 1.使用
- 2. pinia-计数器案例
- 3. getters实现
- 4. 异步action
- 5. storeToRefsx 数据解构保持响应式
- 6. pinia 调试
- 项目起步
- 1.项目初始化和git管理
- 2. 使用ElementPlus
- 3. ElementPlus 主题色定制
- 4. axios 基础配置
- 5. 路由设计
- 6. 静态资源初始化和 Error lens安装
- 7.scss自动导入
- 8. Layout静态模板结构搭建
- 9. Layout字体图标引入
- 10.Layout一级导航渲染
- 11. layout - 吸顶导航
- 12. layout - Pinia优化重复请求
- 小结
Pinia
1.使用
vue
专属状态管理库,vuex
的替代
优势:
- 提供了更简单的
API
,去掉了mutation
- 提供了组合式
API
- 去掉了
modules
,每个store
都是独立的模块 - 搭配
TS
一起使用提供可靠的类型判断
Pinia
添加到vue
项目中:
- 创建一个新
vue
项目:create init vue@latest
,装依赖,项目跑起来 - 打开
pinia
官方文档,是个小菠萝。点击开始,有个安装选项
- 我是使用
npm
安装:npm install pinia
- 按照文档使用 pinia
- 在项目中实际应用(记不住看文档使用即可
2. pinia-计数器案例
看官方文档的基础实例学习如何使用
找和vue3
语法相似的语法进行使用
- 创建一个
store( state+action )
在src
添加一个stores
文件夹,新建文件counter.js
//counter.js
// 导入一个方法 defineStore
import { defineStore } from 'pinia'
import {ref} form 'vue'
// 参数:标识 回调函数
//!!变量名字需保持规范:use+函数名
//useCounterStore是一个方法,需执行才能得到真是store实例对象
export const useCounterStore = defineStore('counter', () => {
//1.定义数据state
const count = ref(0)
// 2.定义修改数据的方法(action 同步+异步)
const increment = () => {
count.value++
}
// 3.以对象的方式return供组件使用
return {
count,
increment
}
})
- 组件使用
store
<script setup>
//1.导入use 打头的方法
import { useCounterStore } from './stores/counter';
//2.执行方法获得store实例对象
const counterStore = useCounterStore();
// console.log(counterStore) 打印看看里面是否有count和increment
</script>
<template>
<button @click="counterStore.increment">{{ counterStore.count }}</button>
</template>
3. getters实现
pinia
中的getters
直接使用computed
函数进行模拟
//counter.js
// 导入一个方法 defineStore
import { defineStore } from 'pinia'
import { computed, ref } from 'vue'
//定义并暴露一个函数useCounterStore 参数:标识 回调函数
export const useCounterStore = defineStore('counter', () => {
//1.定义数据state
const count = ref(0)
// 2.定义修改数据的方法(action 同步+异步)
const increment = () => {
count.value++
}
// -- -- getters实现 -- --
const doubleCount = computed(() => count.value * 2)
// 3.以对象的方式return供组件使用
return {
count,
increment,
doubleCount
}
})
这时useCountStore
中就有了doubleCount
这个方法了
<!--App.vue-->
<template>
<button @click="counterStore.increment">{{ counterStore.count }}</button>
{{ counterStore.doubleCount }}
</template>
4. 异步action
action
中实现异步和组件中定义数据和方法的风格完全一致
安装axios
: npm install axios
举个获取数据列表 的栗子,获取数据接口地址:http://geek.itheima.net/v1_0/channels
//counter.js
const list = ref([]) //存放列表数据
//异步action
const getList = async () => {
const res = await axios.get('http://geek.itheima.net/v1_0/channels');
}
//返回,让组件可以拿到
return{
list,
getList
}
<script setup>
//1.导入use 打头的方法
import { onMounted } from 'vue';
import { useCounterStore } from './stores/counter';
//2.执行方法获得store实例对象
const counterStore = useCounterStore();
// console.log(counterStore)
onMounted(() => {
//获取数据
counterStore.getList()
})
</script>
看一下网页的网络
给list赋值
//异步action
const getList = async () => {
const res = await axios.get('http://geek.itheima.net/v1_0/channels');
list.value = res.data.data.channels
}
渲染在页面上,使用v-for
<template>
<button @click="counterStore.increment">{{ counterStore.count }}</button>
{{ counterStore.doubleCount }}
<ul>
<li v-for="item in counterStore.list" :key="item.id">{{ item.name }}</li>
</ul>
</template>
效果:
5. storeToRefsx 数据解构保持响应式
辅助保持数据(state+getter)的响应式解构
方法可以正常解构赋值哈
const {count,doubleCount} = counterStore
这样解构是不可以的,会造成响应式丢失,也就是数据变化页面不会更新。
我们可以这样写:
const {count,doubleCount} = storeToRefs(counterStore);
6. pinia 调试
使用之前使用的devtools
调试工具
项目起步
1.项目初始化和git管理
创建并打开,将项目运行起来(按照绿色的来做):
这样说明成功
下面我们看一下 小兔鲜 需要哪些基础目录,
我们按照下面的图片在刚创建好的项目中创建文件夹。
componsables
组合函数文件夹:存放通用的函数
使用git管理项目,手动初始化
执行命令并完成手动提交
git init
git add .
git commit -m "init"
配置别名路径联想提示
编写代码,一旦输入
@/
,vscode
会立刻联想出src
所有的子目录和文件,统一文件路径,不容易出错。步骤:1.根目录新增
jsconfig.json
文件 2.添加配置项
2. 使用ElementPlus
我们在这个项目中使用了通用性组件,由ElementPlus
提供
步骤:安装 - 按需引入 - 测试组件
看文档
安装elementPlus:npm install element-plus --save
安装两个插件:npm install -D unplugin-vue-components unplugin-auto-import
安装之后我们来依照文档配置这两个插件
//vite.config.js
//按需导入element Plus插件
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [ //插件配置文件
vue(),
//elementPlus插件
AutoImport({
resolvers: [ElementPlusResolver()],
}),
Components({
resolvers: [ElementPlusResolver()],
}),
],
配置文件写好后,重启项目
做个测试,看看组件能不能使用
<template>
<el-button type="primary">elementPlus</el-button>
</template>
生效就OK
3. ElementPlus 主题色定制
小免鲜主题色和elementPlus
默认的主题色存在冲突
通过定制主题让elementPlus
的主题色和小兔鲜项目保持一致
步骤:
-
安装sass:
npm i sass -D
-
准备定制文件 :
styles/element/index.scss
/* 只需要重写你需要的即可 */
@forward 'element-plus/theme-chalk/src/common/var.scss' with (
$colors: (
'primary': (
// 主色
'base': #27ba9b,
),
'success': (
// 成功色
'base': #1dc779,
),
'warning': (
// 警告色
'base': #ffb302,
),
'danger': (
// 危险色
'base': #e26237,
),
'error': (
// 错误色
'base': #cf4444,
),
)
)
- 对
ElementPlus
样式进行覆盖:通知Element
使用scss
语言,自动导入定制的scss
文件覆盖。
//vite.config.js
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
//按需导入element Plus插件
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [ //插件配置文件
vue(),
//elementPlus插件
AutoImport({
// 1.配置elementPlus采用sass样式配色系统
resolvers: [ElementPlusResolver()],
}),
Components({
resolvers: [ElementPlusResolver({ importStyle: 'sass' })],
}),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
},
css: {
preprocessorOptions: {
scss: {
//2.自动导入定制化样式文件进行样式覆盖
additionalData: `@use "@/styles/element/index.scss" as *;`
}
}
}
})
4. axios 基础配置
安装:npm i axios
配置基础实例(统一接口实例)
在utils
创建一个http.js
//axios基础封装
import axios from "axios";
const httpInstance = axios.create({
baseURL: 'http://pcapi-xiaotuxian-front-devtest.itheima.net',
timeout: '5000' //5s
})
//拦截器,默认先这样写着,后面有需求再配置
// axios请求拦截器
instance.interceptors.request.use(config => {
return config
}, e => Promise.reject(e))
// axios响应式拦截器
instance.interceptors.response.use(res => res.data, e => {
return Promise.reject(e)
})
export default httpInstance
扩展:如果项目里面不同的业务模块需要的接口基地址不同,该如何来做?
答:
axios.create()
方法可以执行多次,每次执行就会生成一个新
的实例const http1 = axios.create({baseURL:'url1'}) const http1 = axios.create({baseURL:'url2'})
5. 路由设计
-
设计首页和登录页的路由(一级路由)
路由设计规则:找内容切换的区域,如果是页面整体切换,则为一级路由
eslintrc.cjs
配置,避免命名报错:
/* eslint-env node */
module.exports = {
root: true,
'extends': [
'plugin:vue/vue3-essential',
'eslint:recommended'
],
parserOptions: {
ecmaVersion: 'latest'
},
rules: {
'vue/multi-word-component-names':0, //不再强制要求组件命名
}
}
删除views
文件夹下的组件,创建两个新文件夹Login
和Layout
,分别创建一个index.vue
文件,写入一些代码。
<template>
<h2>我是注册页/首页</h2>
</template>
打开router
文件夹的index.js
,删掉默认的代码。导入login
和layout
组件,在routes
中配置path、component
属性
import { createRouter, createWebHistory } from 'vue-router'
import Login from '@/views/Login/index.vue'
import Layout from '@/views/Layout/index.vue'
// createRouter:创建router实例对象
// createWebHistory:创建history模式的路由
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
component: Layout
},
{
path: '
App.vue
中写入一级路由出口组件
<script setup>
import { RouterLink, RouterView } from 'vue-router'
</script>
<template>
<!-- 一级路由出口组件 -->
<RouterView />
</template>
项目运行效果:
- 设计分类页和默认
Home
页路由(二级路由)
路由设计原则:找内容切换的区域,如果是在一级路由页的内部切换,则为二级路由
和上面一样,在views
新增两个文件夹,一个Home
,一个Category
,分别创建一个index.vue
,随便写点内容
//router index.js
import { createRouter, createWebHistory } from 'vue-router'
import Login from '@/views/Login/index.vue'
import Layout from '@/views/Layout/index.vue'
import Home from '@/views/Home/index.vue'
import Category from '@/views/Category/index.vue'
// createRouter:创建router实例对象
// createWebHistory:创建history模式的路由
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
component: Layout,
children: [
{
path: '',
component: Home
},
{
path: 'category',
component: Category
}
]
},
{
path: '/login',
component: Login
}
]
})
export default router
这两个二级路由要在Layout
组件里给准备路由出口
<!--Layout index.vue-->
<template>
<h2>我是首页</h2>
<!-- 二级路由出口 -->
<RouterView />
</template>
效果:
6. 静态资源初始化和 Error lens安装
图片资源 - 把images
文件夹放到assets
目录下
样式资源 - 把common.scss
文件放到styles
目录下(这个文件在资源里面,自己拿)。
在main.js
中引入common.scss
//main.js
//引入初始化样式文件
import '@/styles/common.scss'
error lens
是一个实时提供错误警告信息的VScode
插件,方便开发,在扩展程序里搜索然后安装就可以了。
7.scss自动导入
在项目里一些组件共享的色值会以scss
变量的方式统一放到一个名为var.scss
的文件中。
正常组件中使用,需要先导入scss
文件,再使用内部的变量,比较繁琐,自动导入可以免去手动导入的步骤,直接使用内部的变量。
配置步骤:
- 新增一个
var.scss
文件,存入色值变量
$xtxColor: #27ba9b;
$helpColor: #e26237;
$sucColor: #1dc779;
$warnColor: #ffb302;
$priceColor: #cf4444;
- 通过
vite.config.js
配置自动导入文件
css: {
preprocessorOptions: {
scss: {
//2.自动导入定制化样式文件进行样式覆盖
additionalData: `
@use "@/styles/element/index.scss" as *;
@use "@/styles/var.scss" as *;
`,
}
}
}
8. Layout静态模板结构搭建
Layout
文件夹创建一个components
文件夹,创建LayoutFooter.vue、LayoutHeader.vue、LayoutNav.vue
组件。
<!--LayoutNav.vue-->
<script setup>
</script>
<template>
<nav class="app-topnav">
<div class="container">
<ul>
<template v-if="true">
<li><a href="javascript:;""><i class="iconfont icon-user"></i>周杰伦</a></li>
<li>
<el-popconfirm title="确认退出吗?" confirm-button-text="确认" cancel-button-text="取消">
<template #reference>
<a href="javascript:;">退出登录</a>
</template>
</el-popconfirm>
</li>
<li><a href="javascript:;">我的订单</a></li>
<li><a href="javascript:;">会员中心</a></li>
</template>
<template v-else>
<li><a href="javascript:;">请先登录</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;
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>
<!--LayoutHeader.vue-->
<script setup>
</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> <RouterLink to="/">居家</RouterLink> </li>
<li> <RouterLink to="/">美食</RouterLink> </li>
<li> <RouterLink to="/">服饰</RouterLink> </li>
</ul>
<div class="search">
<i class="iconfont icon-search"></i>
<input type="text" placeholder="搜一搜">
</div>
<!-- 头部购物车 -->
</div>
</header>
</template>
<style scoped lang='scss'>
.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;
}
}
.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>
<!--LayoutFooter.vue-->
<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-weixin"></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;
}
}
.qrcode {
width: 92px;
height: 92px;
padding: 7px;
border: 1px solid #ededed;
}
.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;
}
}
.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: 178px;
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>
修改一下Layout
的index.vue
<script setup>
import LayoutNav from './components/LayoutNav.vue'
import LayoutHeader from './components/LayoutHeader.vue'
import LayoutFooter from './components/LayoutFooter.vue'
</script>
<template>
<LayoutNav />
<LayoutHeader />
<RouterView />
<LayoutFooter />
</template>
效果:
9. Layout字体图标引入
这里的图标没有引入,我们使用的是阿里的字体图标库,使用 font-class
引用的方式
将这个加入到index.html
文件中
<link rel="stylesheet" href="//at.alicdn.com/t/font_2143783_iq6z4ey5vu.css">
效果:
看下面这个周杰伦旁边的小人儿
它对应的代码如下
10.Layout一级导航渲染
静态结构已经全部搭建好了,我们要使用后端接口渲染 渲染一级导航路由,也就是这:
实现步骤:
- 根据接口文档封装接口函数
- 发生请求获取数据列表
v-for
渲染页面
在apis
文件夹下创建layout.js
文件,封装接口
import httpInstance from '@/utils/http.js'
//获取目录
export function getCategoryAPI() {
return httpInstance({
url: '/home/category/head'
})
}
来到LayoutHeader
组件,引入接口
封装一个函数getCategory
,返回的是promise
对象,使用async/await
。
在挂载完成之后(onMounted
)调用函数getCategory
。
打印res
看一下请求的数据,定义一个响应式空数组categoryList
接收后台传入的数据。
将 请求 封装进 函数 中是因为方便书写请求前后的逻辑
<script setup>
import { getCategoryAPI } from '@/apis/layout'
import { onMounted, ref } from 'vue'
const categoryList = ref([])
const getCategory = async () => {
const res = await getCategoryAPI()
categoryList.value = res.result
// console.log(res)
}
onMounted(() => getCategory())
</script>
获取数据成功之后,使用v-for
渲染数据
<ul class="app-header-nav">
<li class="home" v-for="item in categoryList" :key="item.id">
<RouterLink to="/">{{ item.name }}</RouterLink>
</li>
</ul>
效果:
11. layout - 吸顶导航
需求:浏览器上下滚动过程中,如果距离顶部的滚动距离大于78px,吸顶导航显示,小于78px隐藏
步骤:
- 准备吸顶导航组件
- 获取滚动距离
- 滚动距离作判断条件控制组件盒子展示或隐藏
吸顶导航组件
<script setup>
</script>
<template>
<div class="app-header-sticky">
<div class="container">
<RouterLink class="logo" to="/" />
<!-- 导航区域 -->
<ul class="app-header-nav ">
<li class="home">
<RouterLink to="/">首页</RouterLink>
</li>
<li>
<RouterLink to="/">居家</RouterLink>
</li>
<li>
<RouterLink to="/">美食</RouterLink>
</li>
<li>
<RouterLink to="/">服饰</RouterLink>
</li>
<li>
<RouterLink to="/">母婴</RouterLink>
</li>
<li>
<RouterLink to="/">个护</RouterLink>
</li>
<li>
<RouterLink to="/">严选</RouterLink>
</li>
<li>
<RouterLink to="/">数码</RouterLink>
</li>
<li>
<RouterLink to="/">运动</RouterLink>
</li>
<li>
<RouterLink to="/">杂项</RouterLink>
</li>
</ul>
<div class="right">
<RouterLink to="/">品牌</RouterLink>
<RouterLink to="/">专题</RouterLink>
</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-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>
在Layout文件夹下的index.vue中引入这个组件,使用起来
<script setup>
import LayoutNav from './components/LayoutNav.vue'
import LayoutHeader from './components/LayoutHeader.vue'
import LayoutFooter from './components/LayoutFooter.vue'
import LayoutFixed from './LayoutFixed.vue'
</script>
<template>
<LayoutNav />
<LayoutHeader />
<RouterView />
<LayoutFooter />
<LayoutFixed />
</template>
关键样式(LayoutFixed中):
.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; //透明度为0
// 状态二:移除平移 + 完全不透明
//想让组件显示出来只需要加上class = "show" 即可
&.show {
transition: all 0.3s linear;
transform: none;
opacity: 1; //完全不透明
}
获取滚动距离,不自己写了,使用一个vueUse插件,安装一下
安装:npm i @vueuse/core
滚动使用的是useScroll
,解构的这个y就是垂直方向滚动的距离。
import { useScroll } from '@vueuse/core'
const { y } = useScroll(window)
当y>78
时,show生效,我们使用 vue 的动态类实现方式
<div class="app-header-sticky" :class="{ show: y > 78 }">
12. layout - Pinia优化重复请求
我们要把 吸顶导航 组件也转化成数据动态获取的,修改完后我们发现请求了两次数据
<!-- LayoutFixed -->
<script setup>
import { useScroll } from '@vueuse/core'
import { getCategoryAPI } from '@/apis/layout'
import { onMounted, ref } from 'vue'
const categoryList = ref([]) //目录数据列表
const { y } = useScroll(window) //获取滚动距离
const getCategory = async () => {
const res = await getCategoryAPI()
categoryList.value = res.result
// console.log(res)
}
onMounted(() => getCategory())
</script>
<template>
<div class="app-header-sticky" :class="{ show: y > 78 }">
<div class="container">
<RouterLink class="logo" to="/" />
<!-- 导航区域 -->
<ul class="app-header-nav">
<li class="home" v-for="item in categoryList" :key="item.id">
<RouterLink to="/">{{ item.name }}</RouterLink>
</li>
</ul>
<div class="right">
<RouterLink to="/">品牌</RouterLink>
<RouterLink to="/">专题</RouterLink>
</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-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>
<!-- LayoutHeader -->
<script setup>
import { getCategoryAPI } from '@/apis/layout'
import { onMounted, ref } from 'vue'
const categoryList = ref([])
const getCategory = async () => {
const res = await getCategoryAPI()
categoryList.value = res.result
// console.log(res)
}
onMounted(() => getCategory())
</script>
<template>
<header class='app-header'>
<div class="container">
<h1 class="logo">
<RouterLink to="/">小兔鲜~</RouterLink>
</h1>
<ul class="app-header-nav">
<li class="home" v-for="item in categoryList" :key="item.id">
<RouterLink to="/">{{ item.name }}</RouterLink>
</li>
</ul>
<div class="search">
<i class="iconfont icon-search"></i>
<input type="text" placeholder="搜一搜">
</div>
<!-- 头部购物车 -->
</div>
</header>
</template>
<style scoped lang='scss'>
.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;
}
}
.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>
stores
新增category.js
import { ref } from 'vue'
import { defineStore } from 'pinia'
import { getCategoryAPI } from '@/apis/layout'
export const useCategoryStore = defineStore('category', () => {
// 导航列表的数据管理
// state 导航列表数据
const categoryList = ref([])
// action 获取导航数据的方法
const getCategory = async () => {
const res = await getCategoryAPI()
categoryList.value = res.result
}
return {
categoryList,
getCategory
}
})
使用:
Login文件夹的index.vue
<script setup>
//出发获取导航列表的action
import { useCategoryStore } from '@/stores/category.js'
import { onMounted } from 'vue'
const categoryStore = useCategoryStore()
onMounted(() => categoryStore.getCategory())
</script>
删掉(注释)LoginFixed
和LoginHeader
中相关的代码
<!-- LayoutFixed -->
<script setup>
import { useScroll } from '@vueuse/core'
// import { getCategoryAPI } from '@/apis/layout'
// import { onMounted, ref } from 'vue'
// const categoryList = ref([]) //目录数据列表
const { y } = useScroll(window) //获取滚动距离
// const getCategory = async () => {
// const res = await getCategoryAPI()
// categoryList.value = res.result
// // console.log(res)
// }
// onMounted(() => getCategory())
// 使用pinia中的数据
import { useCategoryStore } from '@/stores/category.js'
const categoryStore = useCategoryStore()
</script>
<template>
<ul class="app-header-nav">
<li class="home" v-for="item in categoryStore.categoryList" :key="item.id">
<RouterLink to="/">{{ item.name }}</RouterLink>
</li>
</ul>
</template>
<script setup>
import { useCategoryStore } from '@/stores/category.js'
// import { getCategoryAPI } from '@/apis/layout'
// import { onMounted, ref } from 'vue'
// const categoryList = ref([])
// const getCategory = async () => {
// const res = await getCategoryAPI()
// categoryList.value = res.result
// // console.log(res)
// }
// onMounted(() => getCategory())
const categoryStore = useCategoryStore()
</script>
<template>
<ul class="app-header-nav">
<li class="home" v-for="item in categoryStore.categoryList" :key="item.id">
<RouterLink to="/">{{ item.name }}</RouterLink>
</li>
</ul>
</template>
OK,没问题
小结
本篇文章,主要学习了Pinia管理数据,以及Layout的相关知识
私密马赛,图片有亿点糊,我是在typra上面写的,截到csdn上就糊掉了呜呜
祝大家学习顺利!!