Files
sbnews/uniapp/src/components/global-popup/global-popup.vue
T

419 lines
10 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view v-if="visible && current" class="global-popup-mask" @tap="handleMaskTap">
<view class="global-popup-wrap" @tap.stop>
<view v-if="showAutoCloseProgress" class="global-popup__progress">
<view class="global-popup__progress-inner" :style="progressStyle"></view>
</view>
<view class="global-popup">
<!-- 关闭按钮 -->
<view v-if="current.is_closable !== 0" class="global-popup__close" @tap="handleClose">
<text>×</text>
</view>
<!-- 主体内容 -->
<view class="global-popup__body" @tap="handleClick">
<image v-if="current.image" :src="current.image" class="global-popup__image" mode="widthFix" />
<view v-if="current.title || current.content" class="global-popup__text">
<text v-if="current.title" class="global-popup__title">{{ current.title }}</text>
<rich-text v-if="current.content" class="global-popup__content"
:nodes="current.content"></rich-text>
</view>
<view v-if="current.link_type !== 0" class="global-popup__btn">
<text>查看详情</text>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { ref, watch, onMounted, computed } from 'vue'
import { getPopupList, reportPopupAction, type PagePopupItem } from '@/api/popup'
import { getOrCreateDeviceId, getPlatform } from '@/utils/device'
import { useAppStore } from '@/stores/app'
const props = defineProps<{
pagePath?: string
triggerTick?: number
}>()
const appStore = useAppStore()
function getPagePathFromPages(): string {
const pages = getCurrentPages()
if (!pages.length) return ''
const page = pages[pages.length - 1] as any
let route = page.route || ''
// #ifdef H5
route = route.replace(/^\//, '')
if (route.startsWith('mobile/')) {
route = route.replace('mobile/', '')
}
// #endif
return '/' + route
}
const effectivePath = computed(() => props.pagePath || appStore.popupPagePath || getPagePathFromPages())
const effectiveTick = computed(() => props.triggerTick ?? appStore.popupTick)
const visible = ref(false)
const current = ref<PagePopupItem | null>(null)
const showAutoCloseProgress = ref(false)
const progressPct = ref(100)
let autoCloseInterval: ReturnType<typeof setInterval> | null = null
function progressColor(pct: number): string {
if (pct > 60) return '#4cd964'
if (pct > 30) return '#ff9500'
return '#ff3b30'
}
const progressStyle = computed(() => ({
width: progressPct.value + '%',
backgroundColor: progressColor(progressPct.value),
transitionDuration: '200ms',
}))
const platform = getPlatform()
const deviceId = getOrCreateDeviceId()
let autoCloseTimer: ReturnType<typeof setTimeout> | null = null
// 频率控制存储 key
const FREQ_KEY_PREFIX = 'popup_shown_'
/** 同一 App 启动周期内已展示的弹窗 ID 集合(每会话一次) */
const sessionShown = new Set<number>()
/**
* 检查频率是否允许展示
* @returns true 表示可以展示
*/
function checkFrequency(item: PagePopupItem): boolean {
const key = FREQ_KEY_PREFIX + item.id
const now = Date.now()
switch (item.frequency) {
case 1: // 每次
return true
case 2: // 每会话一次
return !sessionShown.has(item.id)
case 3: {
// 每天一次
try {
const last = uni.getStorageSync(key)
if (!last) return true
return now - Number(last) >= 24 * 60 * 60 * 1000
} catch (e) {
return true
}
}
case 4: {
// 仅一次
try {
return !uni.getStorageSync(key)
} catch (e) {
return true
}
}
default:
return true
}
}
/** 标记已展示 */
function markShown(item: PagePopupItem) {
sessionShown.add(item.id)
if (item.frequency === 3 || item.frequency === 4) {
try {
uni.setStorageSync(FREQ_KEY_PREFIX + item.id, Date.now())
} catch (e) {
/* ignore */
}
}
}
/** 拉取弹窗并展示首个匹配项 */
async function checkAndShow() {
if (!effectivePath.value) return
try {
const res: any = await getPopupList({
page_path: effectivePath.value,
platform,
})
const lists: PagePopupItem[] = res?.lists || []
if (!lists.length) return
// 取第一个频率允许展示的弹窗
const target = lists.find((it) => checkFrequency(it))
if (!target) return
const showFn = () => {
current.value = target
visible.value = true
markShown(target)
startAutoClose(target)
// 上报展示
reportPopupAction({
popup_id: target.id,
action: 1,
page_path: effectivePath.value,
device_id: deviceId,
platform,
}).catch(() => { /* ignore */ })
}
if (target.delay_seconds && target.delay_seconds > 0) {
setTimeout(showFn, target.delay_seconds * 1000)
} else {
showFn()
}
} catch (e) {
console.warn('[GlobalPopup] 拉取弹窗失败:', e)
}
}
function handleClick() {
if (!current.value) return
const item = current.value
// 上报点击
reportPopupAction({
popup_id: item.id,
action: 2,
page_path: effectivePath.value,
device_id: deviceId,
platform,
}).catch(() => { /* ignore */ })
// 跳转
if (item.link_type === 1 && item.link_url) {
// 站内
uni.navigateTo({
url: item.link_url,
fail: () => {
uni.switchTab({
url: item.link_url,
fail: () => uni.reLaunch({ url: item.link_url }),
})
},
})
closeWithoutReport()
} else if (item.link_type === 2 && item.link_url) {
// 外部 H5
// #ifdef APP-PLUS
plus.runtime.openURL(item.link_url)
// #endif
// #ifdef H5
window.open(item.link_url, '_blank')
// #endif
// #ifdef MP
uni.setClipboardData({ data: item.link_url })
uni.showToast({ title: '链接已复制', icon: 'none' })
// #endif
closeWithoutReport()
} else if (item.link_type === 3 && item.link_url) {
// 拨打电话
uni.makePhoneCall({ phoneNumber: item.link_url })
closeWithoutReport()
}
}
function handleClose() {
if (!current.value) return
clearAutoClose()
reportPopupAction({
popup_id: current.value.id,
action: 3,
page_path: effectivePath.value,
device_id: deviceId,
platform,
}).catch(() => { /* ignore */ })
closeWithoutReport()
}
function handleMaskTap() {
if (current.value && current.value.is_closable === 0) return
handleClose()
}
function closeWithoutReport() {
clearAutoClose()
visible.value = false
current.value = null
}
function closeWithReport() {
if (!current.value) return
reportPopupAction({
popup_id: current.value.id,
action: 3,
page_path: effectivePath.value,
device_id: deviceId,
platform,
}).catch(() => { /* ignore */ })
closeWithoutReport()
}
function startAutoClose(item: PagePopupItem) {
clearAutoClose()
const seconds = Number(item.auto_close_seconds || 0)
if (seconds <= 0) return
const duration = seconds * 1000
const step = 200
const decrement = (100 * step) / duration
progressPct.value = 100
showAutoCloseProgress.value = true
autoCloseInterval = setInterval(() => {
progressPct.value = Math.max(0, progressPct.value - decrement)
}, step)
autoCloseTimer = setTimeout(() => {
closeWithReport()
}, duration)
}
function clearAutoClose() {
if (autoCloseTimer) {
clearTimeout(autoCloseTimer)
autoCloseTimer = null
}
if (autoCloseInterval) {
clearInterval(autoCloseInterval)
autoCloseInterval = null
}
showAutoCloseProgress.value = false
progressPct.value = 100
}
onMounted(() => {
checkAndShow()
})
watch(effectiveTick, () => {
if (!visible.value) checkAndShow()
})
watch(effectivePath, () => {
if (!visible.value) checkAndShow()
})
</script>
<style lang="scss" scoped>
.global-popup-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.6);
display: flex;
align-items: center;
justify-content: center;
z-index: 9998;
}
.global-popup-wrap {
position: relative;
width: 580rpx;
}
.global-popup {
position: relative;
width: 100%;
background: #fff;
border-radius: 24rpx;
overflow: hidden;
animation: globalPopupIn 0.25s ease-out;
}
@keyframes globalPopupIn {
0% {
transform: scale(0.85);
opacity: 0;
}
100% {
transform: scale(1);
opacity: 1;
}
}
.global-popup__close {
position: absolute;
top: 20rpx;
right: 20rpx;
width: 64rpx;
height: 64rpx;
border-radius: 50%;
background: rgba(0, 0, 0, 0.35);
display: flex;
align-items: center;
justify-content: center;
z-index: 5;
text {
font-size: 48rpx;
color: #fff;
line-height: 1;
}
}
.global-popup__progress {
width: 100%;
height: 6rpx;
margin-bottom: 16rpx;
border-radius: 999rpx;
background: rgba(255, 255, 255, 0.3);
overflow: hidden;
}
.global-popup__progress-inner {
height: 100%;
border-radius: 999rpx;
transition-property: width, background-color;
transition-timing-function: linear;
}
.global-popup__body {
width: 100%;
}
.global-popup__image {
width: 100%;
display: block;
}
.global-popup__text {
padding: 32rpx 40rpx;
display: flex;
flex-direction: column;
}
.global-popup__title {
font-size: 32rpx;
font-weight: 600;
color: #222;
margin-bottom: 16rpx;
}
.global-popup__content {
font-size: 26rpx;
color: #666;
line-height: 1.6;
}
.global-popup__btn {
margin: 32rpx 40rpx 32rpx;
background: #0b55eb;
border-radius: 40rpx;
padding: 24rpx 0;
text-align: center;
text {
font-size: 28rpx;
color: #fff;
font-weight: 500;
}
}
</style>