congming_huose-apk/common/components/AppTopPushNotice.vue

233 lines
5.4 KiB
Vue

<template>
<view
v-show="ts.visible && isStackTop"
class="gnp-wrap"
:class="{ 'gnp-wrap--touch': gnpTouching }"
:style="gnpWrapDragStyle"
@touchstart.stop="gnpOnTouchStart"
@touchmove.stop="gnpOnTouchMove"
@touchend.stop="gnpOnTouchEnd"
@touchcancel.stop="gnpOnTouchCancel"
>
<view class="gnp-card" :class="{ 'gnp-card--in': ts.enterActive }">
<view class="gnp-left">
<text class="gnp-time">{{ ts.displayTime }}</text>
<text class="gnp-text">{{ ts.displayText }}</text>
</view>
<view v-if="ts.displayPicture" class="gnp-icon-box">
<image class="gnp-thumb" :src="ts.displayPicture" mode="aspectFill" />
</view>
</view>
</view>
</template>
<script>
import { noticeTopState, hideTopNotice } from '@/common/utils/noticeTopShared.js'
const INDEX_ROUTE = 'pages/index/index'
const UP_SWIPE = 50
const TAP = 24
const RE_STACK_MS = 300
export default {
name: 'AppTopPushNotice',
data() {
return {
gnpTouchStartY: 0,
gnpTouchStartX: 0,
gnpDragOffset: 0,
gnpTouching: false,
_stackTick: 0,
_stackTimer: null,
}
},
computed: {
ts() {
// 依赖页面栈:非栈顶不展示本实例
// eslint-disable-next-line no-unused-expressions
this._stackTick
// 显式读 observable 字段,否则子模板不随推送更新
const {
visible,
enterActive,
displayTime,
displayText,
displayPicture,
} = noticeTopState
return { visible, enterActive, displayTime, displayText, displayPicture }
},
isStackTop() {
// eslint-disable-next-line no-unused-expressions
this._stackTick
try {
const stack = getCurrentPages()
if (!stack || !stack.length) return true
const top = stack[stack.length - 1]
return top && top.$vm === this.$root
} catch (e) {
return true
}
},
gnpWrapDragStyle() {
const o = this.gnpDragOffset
if (o === 0) {
return {}
}
return {
transform: `translateY(${o}px)`,
transition: this.gnpTouching ? 'none' : 'transform 0.2s ease-out',
}
},
},
watch: {
'ts.visible'(v) {
if (v) {
this.gnpDragOffset = 0
this.gnpTouching = false
}
},
},
mounted() {
this._stackTimer = setInterval(() => {
this._stackTick += 1
}, RE_STACK_MS)
},
beforeDestroy() {
if (this._stackTimer) {
clearInterval(this._stackTimer)
this._stackTimer = null
}
},
methods: {
gnpOnTouchStart(e) {
if (!this.ts.visible || !this.isStackTop || !e.touches || !e.touches[0]) return
this.gnpTouching = true
this.gnpTouchStartY = e.touches[0].clientY
this.gnpTouchStartX = e.touches[0].clientX
},
gnpOnTouchMove(e) {
if (!this.ts.visible || !this.gnpTouching || !e.touches || !e.touches[0]) return
const y = e.touches[0].clientY
const dy = y - this.gnpTouchStartY
if (dy < 0) {
this.gnpDragOffset = Math.max(dy, -220)
} else {
this.gnpDragOffset = 0
}
},
gnpOnTouchEnd(e) {
if (!this.ts.visible) return
const t = e.changedTouches && e.changedTouches[0]
const dragAtEnd = this.gnpDragOffset
this.gnpTouching = false
this.gnpDragOffset = 0
if (!t) return
const y = t.clientY
const x = t.clientX
const totalDy = y - this.gnpTouchStartY
const totalDx = x - this.gnpTouchStartX
if (totalDy < -UP_SWIPE || dragAtEnd < -UP_SWIPE) {
hideTopNotice()
return
}
if (Math.abs(totalDy) <= TAP && Math.abs(totalDx) <= TAP) {
hideTopNotice()
this._openNotificationTab()
}
},
gnpOnTouchCancel() {
if (!this.ts.visible) return
this.gnpTouching = false
this.gnpDragOffset = 0
},
_openNotificationTab() {
try {
const stack = getCurrentPages()
const cur = stack && stack.length ? stack[stack.length - 1] : null
const route = cur && (cur.route || (cur.$page && cur.$page.fullPath))
if (route && String(route).replace(/^\//, '') === INDEX_ROUTE) {
uni.$emit('indexOpenNotificationTab')
return
}
} catch (e) {}
uni.reLaunch({ url: '/pages/index/index?tab=2' })
},
},
}
</script>
<style scoped>
.gnp-wrap {
position: fixed;
left: 0;
right: 0;
top: 100rpx;
z-index: 10050;
padding-left: 24rpx;
padding-right: 24rpx;
padding-bottom: 8rpx;
box-sizing: border-box;
touch-action: none;
}
.gnp-wrap--touch {
z-index: 10051;
}
.gnp-card {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
background: #ffffff;
border-radius: 24rpx;
padding: 30rpx 24rpx;
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.1);
transform: translateY(-120%);
opacity: 0;
transition: transform 0.32s ease-out, opacity 0.28s ease;
}
.gnp-card--in {
transform: translateY(0);
opacity: 1;
}
.gnp-left {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
padding-right: 20rpx;
}
.gnp-time {
font-size: 24rpx;
color: #f59a23;
line-height: 1.3;
margin-bottom: 8rpx;
}
.gnp-text {
font-size: 28rpx;
color: #333333;
line-height: 1.45;
word-break: break-all;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;
line-clamp: 3;
overflow: hidden;
}
.gnp-icon-box {
width: 100rpx;
height: 100rpx;
border-radius: 20rpx;
background: #f2f2f2;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
.gnp-thumb {
width: 100%;
height: 100%;
display: block;
}
</style>