本文主要介绍:
1、Vue3的Composition-API相关用法
2、如何在Vue3使用Swiper
3、json-server:本地模拟接口快速开发
4、全局状态数据的处理
5、一些常见坑的解决方案
.......
本教程主要用作 Vue3+TS+vite 项目开发规范和流程的展示,由于项目代码量不小,这里仅通过第一个楼层开发过程来进行讲解。
页面结构分为:顶部banner、楼层区域
顶部banner
顶部banner主要有:左侧浮窗、背景图和右侧按钮区域
左侧浮窗图片、跳转链接和背景图均由后端配置,右侧按钮分别是活动说明、分享的按钮
// src/views/Home.vue
<template>
<!-- 页面中所有内容 -->
<section class="page_container">
<!-- 头部 banner -->
<header>
<div class="header_btn header_btn--desc" @click="hClickNav('desc')">
<img class="header_Nav--img" src="../assets/desc.png" alt="" />
</div>
<div class="header_btn header_btn--share" @click="hClickNav('share')">
<img class="header_Nav--img" src="../assets/share.png" alt="" />
</div>
<template v-for="e in floorData">
<!-- banner图 -->
<img
:key="e.MODULE_NAME"
v-if="e.MODULE_NAME === 'banner'"
class="header_banner--img"
:src="e.MODULE_IMAGE"
alt=""
/>
<!-- 浮窗 -->
<div
v-if="e.MODULE_NAME === 'flutter'"
class="header_btn header_btn--eManager"
:key="e.MODULE_NAME"
@click="jumpLink(e.MODULE_LINK)"
>
<img class="eManag_btn--img" :src="e.MODULE_IMAGE" alt="" />
</div>
</template>
</header>
<!-- 页面主体 -->
<main>
<!-- 所有楼层组件 -->
<component
:is="floorType(e.MODULE_NAME)"
:floorData="e"
v-for="e in floorData"
:key="e.MODULE_NAME"
/>
</main>
<!-- 页面底部 -->
<footer></footer>
</section>
<!-- 弹窗遮罩 -->
<div class="popup_mask" v-show="popType"></div>
<!-- 弹窗部分 -->
<div class="popup popup_suc" v-if="popType === 'suc'">
<div class="popup_title">这是成功弹窗</div>
</div>
<div class="popup popup_fail" v-if="popType === 'fail'">
<div class="popup_title">这是失败弹窗</div>
</div>
</template>
<script lang="ts">
import {
defineComponent,
getCurrentInstance,
onMounted,
reactive,
toRefs,
} from "vue";
import VIP from "@/components/floor/VIP.vue";
// 根据楼层ID渲染楼层组件
const floorType = (floorID: string) => {
let floorName = "";
switch (floorID) {
case "vip":
// 会员楼层
floorName = "VIP";
break;
// 其他楼层...
default:
break;
}
return floorName;
};
export default defineComponent({
components: { VIP },
setup() {
// 响应式数据
const state = reactive<any>({
pageData: {},
floorData: [],
});
const _this = getCurrentInstance()!;
// 获取全局方法
const { jumpLink, setState, $API, toast } = _this.appContext.config.globalProperties;
// 点击banner导航
const hClickNav = (type: string) => {
switch (type) {
case "desc":
// 点击查看详情按钮
const bannerFloor = state.floorData.find(
(e: any) => e.MODULE_NAME === "banner"
);
if (bannerFloor) jumpLink(bannerFloor.MODULE_LINK);
break;
case "share":
// 点击分享按钮
alert("调用平台分享方法");
break;
default:
break;
}
};
onMounted(() => {
$API.homeInit().then((res: any) => {
if (res.code === 1000) {
const { showModules, ...pageData } = res.data;
// 获取页面数据、楼层数据
state.pageData = pageData;
state.floorData = showModules;
}
});
});
// 使用toRefs处理reactive数据后,在解包reactive数据不会丢失响应式
return { ...toRefs(state), floorType, hClickNav };
},
});
</script>
<style scoped lang="scss">
section {
background-color: #fc6657;
}
main {
margin-top: -332px;
}
.popup_mask {
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
background-color: rgba(0, 0, 0, 0.7);
}
.popup {
position: absolute;
top: 50%;
left: 50%;
right: 50%;
bottom: 50%;
transform: translate(-50%, -50%);
.popup_title {
color: red;
}
}
header {
position: relative;
.header_banner--img {
width: 750px;
}
.header_btn--desc {
position: absolute;
top: 50px;
right: 50px;
width: 110px;
height: 50px;
line-height: 50px;
text-align: center;
font-size: 20px;
border-radius: 25px;
color: #fff;
}
.header_btn {
width: 60px;
}
.header_btn--desc {
position: absolute;
top: 222px;
right: 0;
}
.header_btn--share {
position: absolute;
top: 302px;
right: 0;
}
.header_Nav--img {
width: 60px;
}
.header_btn--eManager {
position: absolute;
width: 90px;
left: 0;
right: auto;
top: 300px;
}
.eManag_btn--img {
width: 90px;
}
}
</style>
VIP楼层部分
业务需求
1、VIP楼层用于会员身份展示,尊享会员 和 生活服务 两个会员板块要做成 轮播图 形式不可循环。
2、会员开通后滑块左上角显示 已开通 角标,两个会员哪个开通页面初始就展示哪一个,都开通或未开通展示第一个。页面初始10秒内 ,用户没有手动切换展示下一个会员板块,就展示另一个会员板块。
3、会员未开通后滑块右上角显示 解锁权益 ,会员开通后显示 解锁更多 。如果所有会员都开通了,隐藏右上角的按钮
4、点击会员滑块的 解锁权益 按钮,拉起开通会员的弹窗,会员开通成功后 解锁权益 按钮变为 解锁更多 或者 隐藏掉。
5、没有会员滑块下方的 卡券展示 做成循环轮播效果
6、卡券有多个展示状态:解锁权益、立即领取、去使用、去看看
- 解锁权益:卡券所属的会员未开通,卡券状态为解锁权益,点击卡券拉起办理会员弹窗
- 立即领取:点击卡券后,调取卡券领取接口,领取成功卡券状态变为去使用,领取失败给予弱提示
- 去使用:点击卡券跳转卡券领取页面
- 去看看:页面滚动到限量抢楼层
开发设计
将所有楼层抽离到src/component/floor
中、会员滑块的卡券轮播图也单独抽离出来(src/component
)方便复用跟维护
安装配置Swiper
安装Swiper6.x版本:
npm install swiper@6.x
VIP楼层
Swiper6.X版本,swiper只具备核心功能,例如其他自动播放、指示器模块都需要单独安装
// src/component/floor/VIP.vue
<template>
<div class="container">
<Swiper
:initialSlide="swiperOptions.initialSlide"
:autoplay="swiperOptions.autoplay"
:observer="swiperOptions.observer"
:observeParents="swiperOptions.observeParents"
:observeSlideChildren="swiperOptions.observeSlideChildren"
v-if="floorData"
@swiper="setSwiper"
@slideChange="slideChange"
>
<template v-for="e in floorData.vipLists">
<!-- 尊享会员 -->
<SwiperSlide
v-if="e.SPECIAL_FLAG === 'ZXVIP'"
class="zxMember_slide"
:key="e.SPECIAL_FLAG"
>
<div class="banner_slide--content">
<div class="banner_slide--wrap">
<!-- 未开通会员 -->
<img
src="../../assets/opened.png"
alt=""
class="opened_flag--img"
v-if="e.OPEN_FLAG === '1'"
/>
<!-- 已开通会员 -->
<span class="banner_slide--text" v-if="e.OPEN_FLAG === '1'"
>点击领取您的专属权益</span
>
<span class="banner_slide--text" v-else>300+ 权益优惠购</span>
<img :src="e.MODULE_IMAGE" alt="" class="banner_slide_img" />
<div class="banner_slide--btn">
<img
src="../../assets/zxBtnOpen.png"
alt=""
class="banner_slide--img"
v-if="
e.OPEN_FLAG === '1' &&
floorData.vipLists.some((e) => e.OPEN_FLAG !== '1')
"
@click="toggleSlide()"
/>
<img
src="../../assets/zxBtnNotOpen.png"
alt=""
class="banner_slide--img"
v-if="e.OPEN_FLAG !== '1'"
@click="setState({ popType: 'handleZXMember' })"
/>
</div>
<div class="banner_coupon--container">
<!-- <CouponSwiper
:couponList="e.couponList"
:isMember="e.OPEN_FLAG === '1'"
:memberType="'ZXVIP'"
/> -->
</div>
</div>
</div>
</SwiperSlide>
<!-- 生活服务会员 -->
<SwiperSlide
class="shMember_slide"
v-if="e.SPECIAL_FLAG === 'LIFEVIP'"
:key="e.SPECIAL_FLAG"
>
<div class="banner_slide--content">
<div class="banner_slide--wrap">
<img
v-if="e.OPEN_FLAG === '1'"
src="../../assets/opened.png"
alt=""
class="opened_flag--img"
/>
<span class="banner_slide--text" v-if="e.OPEN_FLAG === '1'"
>点击领取您的专属权益</span
>
<span class="banner_slide--text" v-else>300+ 权益优惠购</span>
<img :src="e.MODULE_IMAGE" alt="" class="banner_slide_img" />
<div class="banner_slide--btn">
<img
src="../../assets/zxBtnOpen.png"
alt=""
class="banner_slide--img"
v-if="
e.OPEN_FLAG === '1' &&
floorData.vipLists.some((e) => e.OPEN_FLAG !== '1')
"
@click="toggleSlide()"
/>
<img
src="../../assets/zxBtnNotOpen.png"
alt=""
class="banner_slide--img"
v-if="e.OPEN_FLAG !== '1'"
@click="setState({ popType: 'handleSHMember' })"
/>
</div>
<div class="banner_coupon--container">
<!-- <CouponSwiper
:couponList="e.couponList"
:isMember="e.OPEN_FLAG === '1'"
:memberType="'SHVIP'"
/> -->
</div>
</div>
</div>
</SwiperSlide>
</template>
</Swiper>
</div>
</template>
<script setup lang="ts">
import { getCurrentInstance, reactive, Ref, ref, toRefs } from "vue";
// 导入swiper组件
import { Swiper, SwiperSlide } from "swiper/vue";
// swiper类型
import SwiperCore, { Autoplay, Swiper as SwiperInstance } from "swiper";
// 使用组件
SwiperCore.use([Autoplay]);
const props = defineProps<{ floorData: any }>();
// 将props变为ref数据 这样解构赋值不会丢失响应式
const { floorData } = toRefs(props);
// 绑定swiper实例
let mySwiper: Ref<SwiperInstance> = ref(null!);
const setSwiper = (swiper: SwiperInstance) => (mySwiper.value = swiper);
// swiper配置模块
const modules = [Autoplay];
// 获取组件实例
const _this = getCurrentInstance();
// swiper展示的slider发生变化
const slideChange = (a: SwiperInstance) => {
// 10s轮播/手动轮播,如果自动轮播开启,关闭自动轮播
if (mySwiper.value.autoplay.running) mySwiper.value.autoplay.stop();
};
// 切换展示会员轮播图
const toggleSlide = () => {
const NextSlideIndex = mySwiper.value.realIndex === 0 ? 1 : 0;
mySwiper.value.slideTo(NextSlideIndex);
};
// Swiper配置对象
const swiperOptions = reactive({
// swiper初始化展示slider
initialSlide: 0,
// 监听swiper发生变化重新初始化
observer: true,
observeParents: true,
observeSlideChildren: true,
// 拖动不会切换slider的类名
noSwipingClass: "couponSwiper",
// 自动轮播
autoplay: {
// 10s轮播
delay: 10000,
},
});
// 已开通的下标
const index: number = floorData.value.vipLists.findIndex(
(e) => e.OPEN_FLAG === "1"
);
// 初始展示slider
if (index !== -1) swiperOptions.initialSlide = index;
</script>
<style scoped lang="scss">
.container {
padding-bottom: 20px;
}
.banner-pagination {
left: 50% !important;
bottom: 30px;
transform: translateX(-50%);
:deep(.swiper-pagination-bullet) {
width: 12px;
height: 12px;
background-color: #ffffff;
opacity: 0.8;
}
:deep(.swiper-pagination-bullet-active) {
background-color: #fc6657;
}
}
.banner_slide--content {
position: relative;
}
.banner_slide--wrap {
position: relative;
width: 722px;
margin-left: 5px;
.opened_flag--img {
position: absolute;
top: 19px;
left: 200px;
width: 80px;
}
.banner_slide--text {
position: absolute;
top: 60px;
left: 41px;
font-family: FZLTHJW--GB1-0;
font-size: 20px;
color: #8c3c10;
}
.banner_slide--btn {
position: absolute;
top: 23px;
right: 20px;
width: 146px;
}
.banner_slide--img {
width: 722px;
}
}
.shMember_slide {
.opened_flag--img {
left: 280px;
}
.banner_slide--text {
color: #f00055;
}
:deep(.swiper-pagination-bullet-active) {
background-color: #f00055;
}
}
.banner_coupon--container {
position: absolute;
top: 100px;
right: 23px;
width: 660px;
height: 220px;
overflow: hidden;
}
</style>
// src/views/Home.vue
// 引入VIP楼层组件
import VIP from "@/components/floor/VIP.vue";
export default defineComponent({
components: { VIP },
......
})
VIP楼层的卡券轮播模块
VIP卡券列表以轮播图形式展示,单独封装成组件方便复用
// src/components/CouponSwiper.vue
<template>
<Swiper
:loop="swiperOptions.loop"
:observer="swiperOptions.swiperOptions"
:observeParents="swiperOptions.observeParents"
:observeSlideChildren="swiperOptions.observeSlideChildren"
:slidesPerView="swiperOptions.slidesPerView"
:pagination="swiperOptions.pagination"
class="couponSwiper"
@click="hClickSwiperContainer"
>
<SwiperSlide v-for="(e, i) in couponList" :key="i">
<div class="slide_coupon--item" :data-index="i">
<div class="slide_coupon--wrap">
<img :src="e.IMAGE" alt="" class="slide_coupon--img" />
<div v-if="!isMember" class="slide_coupon--text">解锁权益</div>
<div v-else-if="e.GET_FLAG === 'toGet'" class="slide_coupon--text">
立即领取
</div>
<div v-else-if="e.GET_FLAG === 'got'" class="slide_coupon--text">
已使用
</div>
<div v-else-if="e.GET_FLAG === 'toJump'" class="slide_coupon--text">
去使用
</div>
<div
v-else-if="e.GET_FLAG === 'acceptting'"
class="slide_coupon--text"
>
受理中
</div>
<div v-else-if="e.GET_FLAG === 'toLook'" class="slide_coupon--text">
去看看
</div>
</div>
</div>
</SwiperSlide>
<div class="swiper-pagination pagination_wrap" slot="pagination"></div>
</Swiper>
</template>
// 使用setup语法糖,声明的变量会自动暴露到模板中,导入的子组件会自动注册
<script setup lang="ts">
import { Swiper, SwiperSlide } from "swiper/vue";
import SwiperCore, { Pagination, Swiper as SwiperInstance } from "swiper";
import { getCurrentInstance, inject, reactive, Ref, ref } from "vue";
// 引入分页器模块样式
import "swiper/components/pagination/pagination.scss";
/**couponList: 卡券列表
* memberType:会员类型
* isMember:是否开通会员
*/
const props = defineProps(["couponList", "isMember", "memberType"]);
// 跨多层级组件通讯使用注入,scrollToLimitGet:滚动到限量抢楼层
const scrollToLimitGet = inject<Function>("scrollToLimitGet");
// swiper挂载分页器模块
SwiperCore.use([Pagination]);
// swiper配置项
const swiperOptions = reactive({
// 监测发生变化是否重新初始化swiper
observer: true,
observeParents: true,
observeSlideChildren: true,
// 每个slider展示几个元素
slidesPerView: 2.5,
// 是否循环展示
loop: true,
pagination: {
el: ".swiper-pagination",
},
});
// 由于部分机型Vivo swiper的事件失效 只能用事件委托来做点击卡券处理逻辑
// 点击swiper容器
const hClickSwiperContainer = () => {
const node = getTargetNode(event?.target as HTMLElement);
// 未找到目标节点
if (!node) return;
// 获取目标节点 data-index 属性
const {
dataset: { index },
} = node;
// 获取点击卡券的数据
const couponData = props.couponList[index!];
hClickCoupon(couponData);
};
const getTargetNode = (
node: HTMLElement | undefined | null
): HTMLElement | null => {
// 递归BODY,仍未找到目标节点
if (!node || node.nodeName.toUpperCase() === "BODY") return null;
// 找到目标节点
if (node.className === "slide_coupon--item") return node;
// 未找到目标节点继续向上层找
return getTargetNode(node.parentNode as HTMLElement);
};
// Vue全局属性、方法
const globalProperties =
getCurrentInstance()?.appContext.config.globalProperties;
const hClickCoupon = (coupon: any) => {
// 未开通会员 拉起办理会员弹窗
if (!props.isMember)
return globalProperties?.setState({
popType:
props.memberType === "ZXVIP" ? "handleZXMember" : "handleSHMember",
});
switch (coupon.GET_FLAG) {
// 卡券为立即领取状态
case "toGet":
getCoupon(coupon);
break;
// 卡券为去使用状态
case "toJump":
globalProperties?.jumpLink(coupon.LINK);
break;
// 卡券为去看看状态
case "toLook":
(scrollToLimitGet as Function)();
break;
default:
break;
}
};
// 调取领取卡券接口
const getCoupon = (coupon: any) => {
globalProperties?.$API
.getPrize({ getFlag: "VIP", prizeNo: coupon.PRIZE_NO })
.then((res: response) => {
if (res.code === 1000) {
// 领取成功,卡券变为去使用
coupon.GET_FLAG = "toJump";
} else if (res.code === 1320) {
// 领取成功,卡券变为受理中
coupon.GET_FLAG = "acceptting";
} else {
// 领取事失败 全局弱提示方法
globalProperties.toast("当前活动太火爆啦,请稍后再试~");
}
});
};
</script>
<style scoped lang="scss">
.swiper-container {
overflow: visible;
}
.slide_coupon--item {
display: flex;
justify-content: center;
width: 270px;
height: 180px;
// overflow: hidden;
.slide_coupon--wrap {
position: relative;
margin-top: 27px;
width: 254px;
}
.slide_coupon--img {
width: 254px;
}
.slide_coupon--text {
position: absolute;
top: 0;
right: 6px;
height: 120px;
width: 70px;
display: flex;
align-items: center;
justify-content: center;
writing-mode: vertical-rl;
writing-mode: tb-rl;
font-family: FZLTHJW--GB1-0;
font-size: 22px;
color: #e70842;
}
}
:deep(.pagination_wrap) {
bottom: -33px !important;
left: 50% !important;
transform: translateX(-50%);
z-index: 100;
width: auto !important;
.swiper-pagination-bullet {
width: 12px;
height: 12px;
background-color: #ffffff;
opacity: 0.9;
}
.swiper-pagination-bullet-active {
background-color: #8a4621;
}
}
</style>
// VIP楼层组件挂载CouponSwiper(src/components/floor/VIP.vue)
<template>
<SwiperSlide
v-if="e.SPECIAL_FLAG === 'ZXVIP'"
class="zxMember_slide"
:key="e.SPECIAL_FLAG"
>
<div class="banner_slide--content">
<div class="banner_slide--wrap">
// 挂载CouponSwiper组件
<div class="banner_coupon--container">
<CouponSwiper
:couponList="e.couponList"
:isMember="e.OPEN_FLAG === '1'"
:memberType="'ZXVIP'"
/>
</div>
</div>
</div>
</SwiperSlide>
<SwiperSlide
v-if="e.SPECIAL_FLAG === 'LIFEVIP'"
class="shMember_slide"
:key="e.SPECIAL_FLAG"
>
<div class="banner_slide--content">
<div class="banner_slide--wrap">
// 挂载CouponSwiper组件
<div class="banner_coupon--container">
<CouponSwiper
:couponList="e.couponList"
:isMember="e.OPEN_FLAG === '1'"
:memberType="'ZXVIP'"
/>
</div>
</div>
</div>
</SwiperSlide>
</template>
<script setup lang="ts">
import CouponSwiper from "@/components/CouponSwiper.vue";
......
</script>
新增抢券、办理会员接口(src/api/index.ts)
// 抢券接口
const getPrize = (params = {}) => {
return httpGet({
url: `${BASEURL}getPrize.do?t=${date}`,
params
})
}
// 办理会员接口
const openMember = (params = {}) => {
return httpGet({
url: `${BASEURL}order.do?t=${date}`,
params
})
}
export default { homeInit, getPrize, openMember }
// src/utiils/data.json
{
// 抢券接口
"getPrize.do": {
"code": 1000,
"data": {
"SHOW_TITLE": "铁粉朋友,为您送啥第3周打卡福利",
"oneClickGetAll": [
{
"MODULE_NAME": "oneClickGetAll",
"IMAGE": "https://m.sd.10086.cn/act_service/resources/1532/images/GDcar.png",
"TITLE": "高德打车券",
"PRIZE_NO": "ONO002",
"STATUS": "1"
},
{
"MODULE_NAME": "oneClickGetAll",
"IMAGE": "https://m.sd.10086.cn/act_service/resources/1532/images/1GBN.png",
"TITLE": "1GB通用流量 新人首单礼",
"PRIZE_NO": "ONO004",
"STATUS": "1"
},
{
"MODULE_NAME": "oneClickGetAll",
"IMAGE": "https://m.sd.10086.cn/act_service/resources/1532/images/1GBYYX.png",
"TITLE": "1GB流量兑换券",
"PRIZE_NO": "ONO006",
"STATUS": "0"
}
]
}
},
// 办理会员接口
"order.do": {
"code": 1000,
"data": {
"couponList": [
{
"MODULE_NAME": "oneClickGetAll",
"IMAGE": "https://m.sd.10086.cn/act_service/resources/1532/images/pinduoduo.png",
"TITLE": "拼多多40元红包",
"PRIZE_NO": "ONO001"
},
{
"MODULE_NAME": "oneClickGetAll",
"IMAGE": "https://m.sd.10086.cn/act_service/resources/1532/images/GDcar.png",
"TITLE": "高德打车券",
"PRIZE_NO": "ONO002"
},
{
"MODULE_NAME": "oneClickGetAll",
"IMAGE": "https://m.sd.10086.cn/act_service/resources/1532/images/1GBN.png",
"TITLE": "1GB通用流量 新人首单礼",
"PRIZE_NO": "ONO004"
},
{
"MODULE_NAME": "oneClickGetAll",
"IMAGE": "https://m.sd.10086.cn/act_service/resources/1532/images/1GBYYX.png",
"TITLE": "1GB流量兑换券",
"PRIZE_NO": "ONO006",
"SUB_MODULE": "zwCoupon"
},
{
"MODULE_NAME": "oneClickGetAll",
"IMAGE": "https://m.sd.10086.cn/act_service/resources/1532/images/o5y.png",
"TITLE": "5元话费满赠券",
"PRIZE_NO": "ONO005"
},
{
"MODULE_NAME": "oneClickGetAll",
"IMAGE": "https://m.sd.10086.cn/act_service/resources/1532/images/o2y.png",
"TITLE": "2元话费满赠券",
"PRIZE_NO": "ONO003"
},
{
"G_ID": "20200422zx8",
"HONEY": "1",
"IMG": "https://m.sd.10086.cn/sd_act_service/resources/1728/images/popup_1gbtyll20210615.png",
"MAIN_NAME": "zx_1gb4",
"NAME": "1GB通用流量",
"QUOTA": "ll",
"REMAKE": "20000",
"STOCK_S": "ZT"
},
{
"G_ID": "20200422zx8",
"HONEY": "1",
"IMG": "https://m.sd.10086.cn/sd_act_service/resources/1728/images/popup_1gbtyll20210615.png",
"MAIN_NAME": "zx_1gb4",
"NAME": "1GB通用流量",
"QUOTA": "ll",
"REMAKE": "20000",
"STOCK_S": "ZT"
}
]
}
}
}
Home新增弹窗(src/views/Home.vue)
<template>
<!-- 弹窗部分 -->
<!-- 办理尊享会员、生活服务弹窗 -->
<div
class="popup pop_handleMember"
v-show="popType === 'handleZXMember' || popType === 'handleSHMember'"
>
<img src="../assets/popBg1.png" alt="" class="pop_img" />
<div class="pop_title" v-show="popType === 'handleZXMember'">
开通尊享会员 <span class="pop_subTitle">3元/月</span>
</div>
<div class="pop_title" v-show="popType === 'handleSHMember'">
开通生活服务会员 <span class="pop_subTitle">10元/月</span>
</div>
<div class="pop_desc">即可秒杀大额话费</div>
<div
class="pop_btn"
@click="handleMember(popType === 'handleZXMember' ? 'ZXVIP' : 'LIFEVIP')"
>
<img src="../assets/ceertainHandle.png" alt="" class="pop_btn--img" />
</div>
<img
src="../assets/closeBtn.png"
alt=""
class="pop_close--img"
@click="closePop"
/>
</div>
<!-- 办理会员成功 -->
<div
class="popup pop_memberSuc"
v-if="popType === 'openMemberSuc' || popType === 'chooseOpenMemberSuc'"
>
<img src="../assets/openSuc.png" alt="" class="pop_img" />
<div class="pop_title"> 恭喜,办理成功!</div>
<div class="pop_subTitle">快来领取权益吧</div>
<div
class="pop_btn"
@click="
setState({
popType:
popType === 'chooseOpenMemberSuc' ? 'oneKeyGetChoose' : 'oneKeyGet',
})
"
>
<img src="../assets/oneKeyGet.png" alt="" class="pop_btn--img" />
</div>
<img
src="../assets/closeBtn.png"
alt=""
class="pop_close--img"
@click="closePop"
/>
</div>
</template>
<script lang="ts">
setup() {
const _this = getCurrentInstance()!;
// 获取全局 跳转方法
const { jumpLink, setState, $API, toast } =
_this.appContext.config.globalProperties;
onMounted(() => {
$API.homeInit().then((res: any) => {
if (res.code === 1000) {
const { showModules, ...pageData } = res.data;
// 获取页面数据、楼层数据
state.pageData = pageData;
state.floorData = showModules;
}
});
});
// 关闭弹窗
const closePop = () => {
setState({ popType: null });
};
// 办理会员
const handleMember = (openFlag: string, popType: string | undefined) => {
$API.openMember({ openFlag }).then((res: response) => {
// 办理成功
if (res.code === 1000) {
// 获取会员楼层
const vipFloor = state.floorData.find(
(e: any) => e.MODULE_NAME === "vip"
);
// 获取会员模块、尊享/美食生活 修改被开通的模块状态
const memberModule = vipFloor?.vipLists.find(
(e: any) => e.SPECIAL_FLAG === openFlag
);
if (memberModule) memberModule.OPEN_FLAG = "1";
// 修改用户是否开通会员的状态
if (state.pageData.vipFlag !== "1") state.pageData.vipFlag = "1";
// 区分 通过即将办理成功弹窗拉起 还是通过点击VIP楼层办理拉起
setState({
popType:
popType === "upcoming" ? "chooseOpenMemberSuc" : "openMemberSuc",
popData: res.data,
});
} else if (res.code === 1310) {
setState({ popType: "charge" });
} else if (res.code === 21005) {
toast("抱歉,活动已下线~");
} else {
// 办理失败
setState({ popType: "openMemberFail" });
}
});
};
// 使用toRefs处理reactive数据后,在解包reactive数据不会丢失响应式
return { ...toRefs(state), floorType, handleMember, hClickNav, closePop };
}
</script>
<style scoped lang="scss">
main {
margin-top: -332px;
}
.popup_mask {
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
background-color: rgba(0, 0, 0, 0.7);
z-index: 5;
}
.popup {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 100;
font-family: FZLTHJW--GB1-0;
.pop_title {
position: absolute;
top: 24px;
left: 50%;
transform: translateX(-50%);
font-size: 48px;
font-weight: bold;
letter-spacing: 1px;
color: #ff1c44;
}
.pop_punchGetEquity--subTitle {
position: absolute;
top: 91px;
left: 50%;
transform: translateX(-50%);
font-size: 32px;
color: #ff1c44;
white-space: nowrap;
}
.pop_punchGetEquity--tip {
position: absolute;
bottom: 119px;
left: 50%;
transform: translateX(-50%);
font-size: 24px;
letter-spacing: 1px;
white-space: nowrap;
color: #ffffff;
}
.pop_couponWrap {
position: absolute;
top: 145px;
left: 50%;
transform: translateX(-50%);
width: 645px;
height: 250px;
}
.pop_couponWrap--item {
position: absolute;
top: 0;
left: 0;
width: 215px;
height: 225px;
transition: transform 0.15s;
text-align: center;
&:nth-child(2) {
transform: translateX(100%);
}
&:last-child {
transform: translateX(200%);
}
}
.pop_couponWrap--img {
display: inline-block;
width: 184px;
}
.pop_punchGetEquity--btn {
position: absolute;
bottom: 24px;
left: 50%;
width: 322px;
transform: translateX(-50%);
}
.pop_close--img {
position: absolute;
bottom: -100px;
left: 50%;
transform: translateX(-50%);
width: 72px;
height: 72px;
}
}
.pop_handleMember {
width: 531px;
.pop_img {
width: 531px;
}
.pop_title,
.pop_desc {
position: absolute;
top: 276px;
left: 50%;
transform: translateX(-50%);
font-family: FZLTHJW--GB1-0;
font-size: 36px;
font-weight: bold;
color: #ffffff;
display: flex;
white-space: nowrap;
}
.pop_subTitle {
margin-left: 25px;
}
.pop_desc {
top: 357px;
font-size: 32px;
font-weight: normal;
}
.pop_btn {
position: absolute;
bottom: 63px;
left: 50%;
transform: translateX(-50%);
width: 366px;
}
.pop_btn--img {
width: 366px;
}
}
.pop_memberSuc,
.pop_memberFail,
.pop_appoint,
.pop_limitAppoint,
.pop_charge {
width: 531px;
font-family: FZLTHJW--GB1-0;
font-size: 36px;
.pop_img {
width: 531px;
}
.pop_title {
font-weight: bold;
}
.pop_title,
.pop_subTitle {
top: 270px;
font-size: 36px;
color: #ffffff;
white-space: nowrap;
}
.pop_subTitle {
position: absolute;
top: 350px;
left: 0;
width: 100%;
text-align: center;
}
.pop_btn {
position: absolute;
bottom: 63px;
left: 50%;
transform: translateX(-50%);
width: 366px;
}
.pop_btn--img {
width: 366px;
}
}
.pop_memberSuc {
.pop_title {
padding-left: 20px;
}
}
</style>
其他楼层也是遵循这个设计模式
1、所有楼层抽离成组件
2、全局工具函数放入globalProperties
中,这样任何组件模板中可直接使用
3、全局状态数据映射成计算属性(保持响应式),并全局混入到组件中(慎重使用全局混入)
开发中可能会遇到的坑
模板中无法识别全局的数据(混入/globalProperties)
我们前面通过混入挂载的vuex-state数据、globalProperties挂载全局数据在模板中直接使用可能会遇到TS报错
Vue3提供了一个自定义全局属性的接口ComponentCustomProperties
,通过这个接口的拓展解决这个问题。
// src/store/iindex.ts
declare module '@vue/runtime-core' {
interface ComponentCustomProperties {
popType: string;
popData: any;
setState: (arg0: object) => void
}
}
// src/utils/tool.ts
declare module '@vue/runtime-core' {
interface ComponentCustomProperties {
insertCode: (arg: void) => void;
jumpLink: (url: string) => void;
toast: (content: string) => void;
}
}
模板中无法识别TS语法
可以在声明props数据的地方声明数据的类型
// src/components/floor/VIP.vue
<script setup lang="ts">
...
SwiperCore.use([Autoplay]);
// 声明props数据类型
const props = defineProps<{ floorData: { vipLists: any[] } }>();
....
</script>
项目打包部署在服务器页面空白
vite打包后的路径默认是根路径,如果是因为资源路径问题导致的页面空白。大概率是因为你忘记配置打包后的资源路径,你只需要在vite.config.js配置里写上 base: './'
即可轻松解决。
// vite.config.js
export default defineConfig({
// 构建时资源已相对路径引入
base: './'
})