任务三 工单3
本工单最终效果图:

1、用户可通过首页会员码进入各自的会员码页面,该页面有用户二维码,出示二维码,商店可扫二维码获得积分以兑换相应礼品。在“首页”界面,点击如图3.3.2处,进入“会员码”界面,按照UI设计师设计效果图进行布局。

在/pages/mine文件夹下新建member-code.vue的“会员码”界面,并在pages.json中配置相应的样式,样式如下:
{
"path" : "pages/mine/member-code",
"style" :
{
"navigationBarTitleText": "会员码",
"navigationBarTextStyle": "black",
"navigationBarBackgroundColor": "#ffffff"
}
}
在index.vue中对class为qrcode_section设置点击交互@tap="memberCode",跳转至member-code.vue,代码如下:
memberCode() {//点击会员码
if(!this.isLogin) {
uni.navigateTo({url: '/pages/login/login'})//判断用户是否登录
return
}
uni.navigateTo({
url: '/pages/mine/member-code'//跳转会员码界面
})
},

通过mapState辅助函数获取用户member信息,用于展示“用户头像”“昵称”“云鲤券”、“积分”、“余额”等信息,如图3.3.3。
mapState是一个辅助函数,用于将Vuex store中的状态映射到组件的计算属性中。通过使用mapState,可以简化代码,避免手动编写大量的计算属性。
基本用法
mapState接受一个数组或对象作为参数:
数组形式:数组中的每个字符串都会被映射为组件的计算属性。
对象形式:对象的键名是组件的计算属性名,值是从store中获取的状态。

引入二维码工具类uqrcode.js进入common文件夹下,如图3.3.4。通过canvas元素进行绘制,绑定uQRCode类的canvas-id="memberCode"。其中correctLevel为二维码的容错级别,用于控制二维码在损坏或部分遮挡时的恢复能力。容错级别越高,二维码的恢复能力越强,但同时也会增加二维码的尺寸。具体用法如下,可输出二维码如图3.3.5:

makeMemberCode(i) {
uQRCode.make({
canvasId: 'memberCode',//绑定二维码id
componentInstance: this,//组件实例
text: `memberCode${i}`,//二维码扫描的信息,这里做测试循环输出
size: uni.upx2px(350),//二维码大小
margin: 20,
backgroundColor: '#ffffff',//二维码背景色
foregroundColor: '#000000', //二维码前景色
fileType: 'jpg',//输出二维码格式
correctLevel: uQRCode.defaults.correctLevel,//二维码的容错级别
success: res => {
// console.log(res)//成功回调函数
}
})
}
为了确保二维码的安全性,团队成员采取一种有效的方法,即通过setInterval()函数来实现每30秒对二维码进行一次刷新操作。方法在一进页面就展示二维码,在uni-app生命周期的onShow()方法调用该方法,代码如下,最终效果图如图3.3.6:
onShow() {
let i = 1
this.makeMemberCode(i)
setInterval(() => {
i++
this.makeMemberCode(i)
}, 30000) //30秒换一次二维码
},
相关详细代码如下:member-code.vue
<template>
<!-- 会员码 -->
<view class="container" style="padding: 20rpx 30rpx;">
<view class="w-100 d-flex flex-column">
<!-- 头像 -->
<view class="d-flex just-content-center align-items-center">
<view class="avatar-wrapper">
<image :src="member.avatar"></image>
<view class="tag">
<image src="/static/images/mine/level.png" mode="widthFix"></image>
<view>{{ member.memberLevel }}</view>
</view>
</view>
</view>
<view class="user-box">
<!-- 昵称 -->
<view class="d-flex just-content-center text-color-assist font-size-base font-weight-bold mb-30">
{{ member.nickname }}
</view>
<view class="w-100 d-flex font-size-sm text-color-assist mb-30">
<view class="user-grid" @tap="coupons">
<view class="value">{{ member.couponNum }}</view>
<view>云鲤券</view>
</view>
<view class="user-grid" @tap="integrals">
<view class="value">{{ member.pointNum }}</view>
<view>积分</view>
</view>
<view class="user-grid" @tap="balance">
<view class="value">{{ member.balance }}</view>
<view>余额</view>
</view>
<view class="user-grid">
<view class="value">{{ member.giftBalance }}</view>
<view>礼品卡</view>
</view>
</view>
<!-- qrcode begin -->
<view class="d-flex just-content-center align-items-center"><canvas canvas-id="memberCode"
style="width: 350rpx; height: 350rpx;"></canvas></view>
<!-- qrcode end -->
<view class="d-flex just-content-center align-items-center" style="margin-bottom: 50rpx;">
<view class="font-size-sm text-color-assist">支付前出示可累计积分,会员码每30秒更新</view>
</view>
</view>
</view>
<image src="https://s3.uuu.ovh/imgs/2024/12/14/dc4fa515052c9185.png" class="w-100" mode="widthFix"></image>
</view>
</template>
<script>
import {
mapState
} from 'vuex'
import uQRCode from '@/common/uqrcode'
export default {
data() {
return {};
},
onShow() {
let i = 1
this.makeMemberCode(i)
setInterval(() => {
i++
this.makeMemberCode(i)
}, 30000) //30秒换一次二维码
},
computed: {
...mapState(['member'])
},
methods: {
makeMemberCode(i) {
uQRCode.make({
canvasId: 'memberCode', //绑定二维码id
componentInstance: this, //组件实例
text: `memberCode${i}`, //二维码扫描的信息,这里做测试循环输出
size: uni.upx2px(350), //二维码大小
margin: 20,
backgroundColor: '#ffffff', //二维码背景色
foregroundColor: '#000000', //二维码前景色
fileType: 'jpg', //输出二维码格式
correctLevel: uQRCode.defaults.correctLevel, //二维码的容错级别
success: res => {
// console.log(res)//成功回调函数
}
})
}
}
}
</script>
<style lang="scss" scoped>
.avatar-wrapper {
width: 150rpx;
height: 150rpx;
border-radius: 100%;
background-color: #ffffff;
box-shadow: 0 0 20rpx rgba($color: #000000, $alpha: 0.1);
display: flex;
align-items: center;
justify-content: center;
position: relative;
z-index: 10;
image {
width: 130rpx;
height: 130rpx;
border-radius: 100%;
}
.tag {
background-color: #ffffff;
position: absolute;
right: -30rpx;
bottom: -6rpx;
display: flex;
align-items: center;
color: $color-warning;
font-size: 22rpx;
padding: 6rpx 16rpx;
border-radius: 50rem !important;
box-shadow: 2rpx 2rpx 20rpx rgba($color: #000000, $alpha: 0.1);
image {
width: 26rpx;
}
}
}
.user-box {
width: 100%;
position: relative;
border-radius: 8rpx;
background-color: #ffffff;
margin-top: -75rpx;
padding-top: 105rpx;
padding-bottom: 75rpx;
margin-bottom: 30rpx;
box-shadow: 0 0 20rpx rgba($color: #000000, $alpha: 0.1);
}
.user-grid {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
position: relative;
&::after {
content: ' ';
position: absolute;
right: 0;
top: 0;
height: 100%;
border-right: 1rpx solid $text-color-assist;
transform: scaleX(0.2) scaleY(0.5);
}
&:nth-last-child(1)::after {
border-right: 0;
}
.value {
font-size: $font-size-base;
font-weight: bold;
margin-bottom: 10rpx;
}
}
</style>
2、会员码界面完成后,在首页几处用户信息需完善。若用户未登录状态情况下,1处提示信息为“您好,游客”,2处提示信息“登录查看积分”,不提示积分;若用户登录状态下,1处提示信息为“您好,用户昵称”,2处积分为用户真实积分。效果对比图如3.3.7和3.3.8。


通过import {mapGetters,mapState} from 'vuex'引入对应isLogin、member对象,获得是否登录状态及用户昵称和用户积分。通过三目运算符完成代码,相关重要代码如下:
<view class="intro">
<view class="greet">您好,{{isLogin? member.nickname: '游客'}}</view>
<view class="note">一杯奶茶,一口软欧包,在云鲤遇见两种美好</view>
</view>
...
...
<text class="title">我的积分</text>
<text :class="{ 'value': isLogin, 'title': isLogin }">{{isLogin? member.pointNum: '登录查看积分'}}</text>
...
...
<script>
import {mapGetters,mapState} from 'vuex'
export default {
computed:{
...mapGetters(['isLogin']),
...mapState(['member'])
}
...
...
3、在完成本任务工单的研发工作后,团队成员应使用SourceTree工具执行版本提交,以创建此工单研发代码的历史版本记录。
详细代码如下index.vue
<template>
<view class="container">
<!-- 头部布局 -->
<view class="banner">
<image src="https://s3.uuu.ovh/imgs/2024/12/01/0793d43c7403e3de.jpg" class="bg"></image>
<view class="intro">
<view class="greet">您好,{{isLogin? member.nickname: '游客'}}</view>
<view class="note">一杯奶茶,一口软欧包,在云鲤遇见两种美好</view>
</view>
</view>
<view class="content">
<!-- 中间 自取 和 外卖 -->
<view class="entrance">
<view class="item" @tap="takein">
<image src="/static/images/index/zq.png" class="icon"></image>
<view class="title">自取</view>
</view>
<view class="item" @tap="takeout">
<image src="/static/images/index/wm.png" class="icon"></image>
<view class="title">外卖</view>
</view>
</view>
<!-- 我的积分板块 -->
<view class="info">
<view class="integral_section" @tap="integrals">
<view class="top">
<text class="title">我的积分</text>
<text :class="{ 'value': isLogin, 'title': isLogin }">{{isLogin? member.pointNum: '登录查看积分'}}</text>
</view>
<view class="bottom">
进入积分商城兑换云鲤券及周边好礼
<view class="iconfont iconarrow-right"></view>
</view>
</view>
<view class="qrcode_section" @tap="memberCode">
<image src="/static/images/index/qrcode.png"></image>
<text>会员码</text>
</view>
</view>
<view class="navigators">
<!-- left 云鲤的茶商城 -->
<view class="left">
<view class="grid flex-column just-content-center">
<view class="d-flex align-items-center">
<image src="/static/images/index/csc.png" class="mark-img"></image>
<view class="font-size-sm text-color-base">云鲤的茶商城</view>
</view>
<view class="text-color-assist" style="margin-left: 40rpx; font-size: 20rpx;">优质茶礼盒,网红零食</view>
</view>
<view class="grid justify-content-end align-items-end">
<image src="/static/images/index/yzclh.png" class="yzclh-img" mode="heightFix"></image>
</view>
</view>
<!-- ringt 买茶送包 会员劵包 -->
<view class="right">
<view class="tea-activity">
<image src="/static/images/index/mcsb.png" class="mark-img"></image>
<view>买茶送包</view>
<view class="right-img">
<image src="/static/images/index/mcsb_bg.png" mode="widthFix"></image>
</view>
</view>
<view class="member-gifts">
<image src="/static/images/index/hyjb.png" class="mark-img"></image>
<view>会员劵包</view>
<view class="right-img">
<image src="/static/images/index/hyjb_bg.png" mode="widthFix"></image>
</view>
</view>
</view>
</view>
<!-- 会员新鲜事 -->
<view class="member-news">
<view class="header">
<view class="title">会员新鲜事</view>
<view class="iconfont iconRightbutton"></view>
</view>
<view class="list">
<view class="item">
<image src="https://s3.uuu.ovh/imgs/2024/12/01/91c4a34528f7b66e.jpg"></image>
<view class="title">"梅"你不行 | 霸气杨梅清爽回归</view>
</view>
</view>
</view>
</view>
</view>
</template>
<script>
import {mapGetters,mapState} from 'vuex'
export default {
data() {
return {}
},
computed:{
...mapGetters(['isLogin']),
...mapState(['member'])
},
methods: {
takein() { //点击自取
//通过SET_ORDER_TYPE方法设置orderType 为自取
this.$store.commit('SET_ORDER_TYPE', 'takein')
uni.switchTab({
url: '/pages/menu/menu'
})
},
takeout() {//点击外卖
if(!this.isLogin) {//判断是否登录
uni.navigateTo({url: '/pages/login/login'})//跳转登录页面
return
}
uni.navigateTo({
url: "/pages/address/address" //进入我的地址页面
})
},
integrals() {//点击我的积分
if(!this.isLogin) {//做登录判断
uni.navigateTo({url: '/pages/login/login'})
return
}
uni.navigateTo({
url: '/pages/integrals/integrals'
})
},
memberCode() {//点击会员码
if(!this.isLogin) {
uni.navigateTo({url: '/pages/login/login'})
return
}
uni.navigateTo({
url: '/pages/mine/member-code'
})
},
}
}
</script>
<style lang="scss" scoped>
/* 这块最后来介绍 */
/* #ifdef H5 */
page {
height: auto;
min-height: 100%;
}
/* #endif */
.banner {
position: relative;
width: 100%;
height: 600rpx;
.bg {
width: 100%;
height: 600rpx;
}
.intro {
position: absolute;
top: calc(50rpx + var(--status-bar-height));
left: 40rpx;
color: #FFFFFF;
display: flex;
flex-direction: column;
.greet {
font-size: $font-size-lg;
margin-bottom: 10rpx;
}
.note {
font-size: $font-size-sm;
}
}
}
.content {
padding: 0 30rpx;
}
.entrance {
position: relative;
margin-top: -80rpx;
margin-bottom: 30rpx;
border-radius: 10rpx;
background-color: #ffffff;
box-shadow: $box-shadow;
padding: 30rpx 0;
display: flex;
align-items: center;
justify-content: center;
.item {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
position: relative;
&:nth-child(1):after {
content: '';
position: absolute;
width: 1rpx;
background-color: #ddd;
right: 0;
height: 100%;
transform: scaleX(0.5) scaleY(0.8);
}
.icon {
width: 84rpx;
height: 84rpx;
margin: 20rpx;
}
.title {
font-size: 30rpx;
color: $text-color-base;
font-weight: 600;
}
}
}
.info {
position: relative;
margin-bottom: 30rpx;
border-radius: 10rpx;
background-color: #ffffff;
box-shadow: $box-shadow;
padding: 30rpx;
display: flex;
align-items: center;
justify-content: center;
.integral_section {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
.top {
display: flex;
align-items: center;
.title {
color: $text-color-base;
font-size: $font-size-base;
margin-right: 10rpx;
}
.value {
font-size: 44rpx;
font-weight: bold;
}
}
.bottom {
font-size: $font-size-sm;
color: $text-color-assist;
display: flex;
align-items: center;
}
}
.qrcode_section {
color: $color-primary;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: $font-size-sm;
image {
width: 40rpx;
height: 40rpx;
margin-bottom: 10rpx;
}
}
}
.navigators {
width: 100%;
margin-bottom: 20rpx;
border-radius: 10rpx;
background-color: #ffffff;
box-shadow: $box-shadow;
padding: 20rpx;
display: flex;
align-items: stretch;
.left {
width: 340rpx;
margin-right: 20rpx;
display: flex;
padding: 0 20rpx;
flex-direction: column;
font-size: $font-size-sm;
color: $text-color-base;
background-color: #F2F2E6;
.grid {
height: 50%;
display: flex;
}
}
.right {
width: 290rpx;
display: flex;
flex-direction: column;
.tea-activity,
.member-gifts {
width: 100%;
display: flex;
padding: 20rpx;
font-size: $font-size-sm;
color: $text-color-base;
align-items: center;
position: relative;
}
.tea-activity {
background-color: #FDF3F2;
margin-bottom: 20rpx;
}
.member-gifts {
background-color: #FCF6D4;
}
.right-img {
flex: 1;
position: relative;
margin-left: 20rpx;
margin-right: -20rpx;
margin-bottom: -20rpx;
display: flex;
align-items: flex-end;
image {
width: 100%;
}
}
}
.mark-img {
width: 30rpx;
height: 30rpx;
margin-right: 10rpx;
}
.yzclh-img {
height: 122.96rpx;
width: 214.86rpx;
}
}
.member-news {
width: 100%;
margin-bottom: 30rpx;
.header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20rpx 0;
.title {
font-size: $font-size-lg;
font-weight: bold;
}
.iconfont {
font-size: 52rpx;
color: $text-color-assist;
}
}
.list {
width: 100%;
display: flex;
flex-direction: column;
.item {
width: 100%;
height: 240rpx;
position: relative;
image {
width: 100%;
height: 100%;
border-radius: 8rpx;
}
.title {
position: relative;
font-size: 32rpx;
font-weight: 500;
width: 100%;
top: -70rpx;
left: 16rpx;
color: #ffffff;
}
}
}
}
</style>