Vue实战——饿了么(三)
公告部分
Index.vue
<template>
<div class="header">
<!-- 上面部分 -->
<div class="content-wrapper">
<!-- 商家/店铺头像 -->
<div class="avatar">
<img :src="seller.avatar" alt="">
</div>
<div class="content">
<div class="title">
<span class="brand"></span>
<span class="name">{{ seller.name }}</span>
</div>
<div class="description">
{{ seller.description }}/{{ seller.deliveryTime }}分钟送达
</div>
<div class="support" v-if="seller.supports">
<!-- pic -->
<SupportIcon :type="seller.supports[0].type" size="1" />
<span class="text">{{ seller.supports[0].description }}</span>
</div>
</div>
<div class="support-count" v-if="seller.supports">
<span class="count">{{ seller.supports.length }}个</span>
<i class="iconfont icon-youjiantou"></i>
</div>
</div>
<!-- 下面的公告部分 -->
<div class="bulletin-wrapper">
<!-- 放背景图片 -->
<span class="bulletin-title"></span>
<!-- 文字部分 -->
<span class="bulletin-text">公告</span>
<!-- 阿里巴巴取图标 > -->
<i class="iconfont icon-youjiantou"></i>
</div>
</div>
</template>
<script setup>
import SupportIcon from '@/components/support-icon/Index.vue'
defineProps({
seller: {
type: Object,
default: () => { }
}
})
</script>
<style lang="less" scoped>
@import '@/assets/variable.less';
@import '@/assets/mixin.less';
.header {
position: relative;
overflow: hidden;
color: @color-white;
background-color: @color-background-ss;
}
.content-wrapper {
display: flex;
padding: 24px 12px 18px 24px;
position: relative;
.avatar {
flex: 0 0 64px;
margin-right: 16px;
img {
width: 100%;
border-radius: 2px;
}
}
.content {
flex: 1;
.title {
display: flex;
margin-bottom: 8px;
.brand {
width: 30px;
height: 18px;
.bg-image('brand');
background-size: 100% 100%;
background-repeat: no-repeat;
}
.name {
margin-left: 6px;
font-size: @fontsize-large;
font-weight: bold;
}
}
.description {
font-size: @fontsize-small;
margin-bottom: 8px;
}
.support {
display: flex;
align-items: center;
.text {
font-size: @fontsize-small-s;
margin-left: 4px;
}
}
}
.support-count {
position: absolute;
right: 12px;
bottom: 14px;
padding: 0 8px;
height: 24px;
line-height: 24px;
text-align: center;
background-color: @color-background-sss;
border-radius: 14px;
display: flex;
align-items: center;
.count {
font-size: @fontsize-small-s;
}
.iconfont {
font-size: 8px;
margin-left: 2px;
}
}
}
.bulletin-wrapper {
display: flex;
align-items: center;
height: 28px;
padding: 0 8px;
background: @color-background-sss;
.bulletin-title {
flex: 0 0 22px;
height: 12px;
.bg-image('bulletin');
background-size: 100% 100%;
background-repeat: no-repeat;
}
.bulletin-text {
margin-left: 4px;
flex: 1;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
font-size: @fontsize-small-s;
}
.icon-youjiantou {
flex: 0 0 10px;
font-size: 8px;
}
}
</style>
- 公告部分由三部分组成,用三个span,第三个span取icon图标,然后分别做一些css样式。
- white-space: nowrap; 多的不换行
- text-overflow: ellipsis;多的做省略号
- 然后写个超出则隐藏overflow: hidden;
公告换成真实数据
<span class="bulletin-text">{{ seller.bulletin }}</span>
头部背景图+虚化
由于背景图取决于商家的头像长什么样,因此这里不能写死,还是要用到seller中的avatar变量,在css中写js,于是我们就又在header中加了一个div,类名为bg,通过绝对定位让他覆盖整个header,然后加上背景图
.bg {
position: absolute;
left: 0;
bottom: 0;
top: 0;
right: 0;
background: v-bind(bg);
background-size: 100% 100%;
z-index: -1;
}
- 在less中写变量,用到了v-bind,注意这里的背景图没加url,因此在变量中要加上url(),但是接口响应的数据不一定及时到达,因此我们又通过computed方法做计算,最后bg来承接这个变量
const bg = computed(() => {
return `url(${props.seller?.avatar})`
})
- ?:当seller加载出来之后,再去取avatar
虚化图片 此处的背景图还是太高清了,于是做一个虚化
filter: blur(10px);
头部弹出框
当我们点击头部任意位置,会出现一个全屏的弹出框
于是我们在component中添加一个组件新建文件夹header-detail,新建Index.vue,现在这个子组件需要被也引入到header中用
import HeaderDetail from '@/components/header-detail/Index.vue'
- 这个页面不能占据header的位置,因此我们通过固定定位使其脱离文档流
Index.vue
<template>
<Transition name="fade">
<div class="header-detail">
<div class="detail-wrapper">
<div class="detail-main">
<h1 class="name">{{ seller.name }}</h1>
<div class="star-wrapper">
♥♥♥♥♥
</div>
<div class="title">
<span>优惠信息</span>
</div>
<ul class="supports">
<li class="supports-item" v-for="item in seller.supports" :key="item.type">
<SupportIcon size="2" :type="item.type" />
<span>{{ item.description }}</span>
</li>
</ul>
<div class="title">
<span>商家公告</span>
</div>
<div class="bulletin">
<p class="content">
{{ seller.bulletin }}
</p>
</div>
</div>
</div>
<div class="detail-close" @click.stop="hide">
<i class="iconfont icon-close"></i>
</div>
</div>
</Transition>
</template>
<script setup>
import SupportIcon from '@/components/support-icon/Index.vue'
const emits = defineEmits(['hidden'])
const hide = () => {
// 发布事件 携带值
emits('hidden', false)
}
const props = defineProps({
seller: {
type: Object,
default: () => { }
}
})
</script>
<style lang="less" scoped>
@import '@/assets/variable.less';
.header-detail {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
background: @color-background-s;
color: @color-white;
backdrop-filter: blur(10px);
&.v-enter-active,
&.v-leave-active {
transition: opacity 0.5s ease;
}
&.v-enter-from,
&.v-leave-to {
opacity: 0;
}
.detail-wrapper {
width: 100%;
min-height: 100%;
overflow: auto;
.detail-main {
margin-top: 64px;
padding-bottom: 64px;
.name {
text-align: center;
font-size: @fontsize-large;
font-weight: bold;
line-height: 16px;
}
.star-wrapper {
padding: 2px 0;
margin-top: 8px;
text-align: center;
}
.title {
width: 80%;
margin: 28px auto 24px auto;
text-align: center;
display: flex;
align-items: center;
&::before,
&::after {
content: '';
height: 1px;
background: #fff;
flex: 1;
opacity: 0.2;
}
span {
font-size: @fontsize-medium;
padding: 0 12px;
font-weight: bold;
}
}
.supports {
width: 80%;
margin: 0 auto;
&-item {
display: flex;
margin-bottom: 12px;
padding: 0 12px;
align-items: center;
span {
font-size: @fontsize-small;
margin-left: 6px;
}
}
}
.bulletin {
width: 80%;
margin: 0 auto;
.content {
padding: 0 12px;
font-size: @fontsize-small;
line-height: 24px;
}
}
}
}
.detail-close {
position: absolute;
left: 50%;
bottom: 30px;
transform: translate(-50%, 0);
.icon-close {
font-size: @fontsize-large-xxxx;
}
}
}
</style>
父组件的Index.vue
<template>
<div class="header" @click="showDetail = true">
<!-- 上面部分 -->
<div class="content-wrapper">
<!-- 商家/店铺头像 -->
<div class="avatar">
<img :src="seller.avatar" alt="">
</div>
<div class="content">
<div class="title">
<span class="brand"></span>
<span class="name">{{ seller.name }}</span>
</div>
<div class="description">
{{ seller.description }}/{{ seller.deliveryTime }}分钟送达
</div>
<div class="support" v-if="seller.supports">
<!-- pic -->
<SupportIcon :type="seller.supports[0].type" size="1" />
<span class="text">{{ seller.supports[0].description }}</span>
</div>
</div>
<div class="support-count" v-if="seller.supports">
<span class="count">{{ seller.supports.length }}个</span>
<i class="iconfont icon-youjiantou"></i>
</div>
</div>
<!-- 下面的公告部分 -->
<div class="bulletin-wrapper">
<!-- 放背景图片 -->
<span class="bulletin-title"></span>
<!-- 文字部分 -->
<span class="bulletin-text">{{ seller.bulletin }}</span>
<!-- 阿里巴巴取图标 > -->
<i class="iconfont icon-youjiantou"></i>
</div>
<div class="bg"></div>
<HeaderDetail v-if="showDetail" @hidden="handle" :seller="seller" />
</div>
</template>
<script setup>
import { defineProps, computed, ref } from 'vue'
import SupportIcon from '@/components/support-icon/Index.vue'
import HeaderDetail from '@/components/header-detail/Index.vue'
const handle = (e) => {
// console.log(e);
showDetail.value = e
}
const props = defineProps({
seller: {
type: Object,
default: () => { }
}
})
const bg = computed(() => {
return `url(${props.seller?.avatar})`
})
let showDetail = ref(true)
</script>
<style lang="less" scoped>
@import '@/assets/variable.less';
@import '@/assets/mixin.less';
.header {
position: relative;
overflow: hidden;
color: @color-white;
background-color: @color-background-ss;
}
.content-wrapper {
display: flex;
padding: 24px 12px 18px 24px;
position: relative;
.avatar {
flex: 0 0 64px;
margin-right: 16px;
img {
width: 100%;
border-radius: 2px;
}
}
.content {
flex: 1;
.title {
display: flex;
margin-bottom: 8px;
.brand {
width: 30px;
height: 18px;
.bg-image('brand');
background-size: 100% 100%;
background-repeat: no-repeat;
}
.name {
margin-left: 6px;
font-size: @fontsize-large;
font-weight: bold;
}
}
.description {
font-size: @fontsize-small;
margin-bottom: 8px;
}
.support {
display: flex;
align-items: center;
.text {
font-size: @fontsize-small-s;
margin-left: 4px;
}
}
}
.support-count {
position: absolute;
right: 12px;
bottom: 14px;
padding: 0 8px;
height: 24px;
line-height: 24px;
text-align: center;
background-color: @color-background-sss;
border-radius: 14px;
display: flex;
align-items: center;
.count {
font-size: @fontsize-small-s;
}
.iconfont {
font-size: 8px;
margin-left: 2px;
}
}
}
.bulletin-wrapper {
display: flex;
align-items: center;
height: 28px;
padding: 0 8px;
background: @color-background-sss;
.bulletin-title {
flex: 0 0 22px;
height: 12px;
.bg-image('bulletin');
background-size: 100% 100%;
background-repeat: no-repeat;
}
.bulletin-text {
margin-left: 4px;
flex: 1;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
font-size: @fontsize-small-s;
}
.icon-youjiantou {
flex: 0 0 10px;
font-size: 8px;
}
}
.bg {
position: absolute;
left: 0;
bottom: 0;
top: 0;
right: 0;
background: v-bind(bg);
background-size: 100% 100%;
z-index: -1;
filter: blur(10px);
}
</style>
- 使用了
Transition
组件,并指定了名为fade
的过渡效果,内部包含一个具有各种详细信息的div
元素。 - 具体的transition如何使用进而添加动画,官方文档:Transition | Vue.js (vuejs.org)
- 展示了卖家的名称、星级、优惠信息、商家公告等内容。通过
v-for
指令遍历seller.supports
来动态生成支持项列表。 - 使用
defineEmits
定义了一个名为hidden
的自定义事件,并在hide
函数中触发该事件并传递值false
。这是利用了子组件向父组件传值的发布订阅机制,子组件发布一个事件,并携带数据,父组件只需要订阅这个事件@xxx 然后接收值即可。 - 注意:此处我们是为了方便切页面才把数据改成true,正常需要改回false。
let showDetail = ref(true)
这个值用于控制我们的弹出框点击关闭,关闭弹出框。开始默认为false,当点击头部之后,值更改为true
<div class="detail-close" @click.stop="hide">
<i class="iconfont icon-close"></i>
</div>
注意,点击弹出框中的关闭按钮也是点击了header部分,因此他会瞬间把值改回为true,导致没有效果,因此需要对click添加一个stop,防止扩散
。
效果
转载自:https://juejin.cn/post/7389089457734516773