feat(uniapp_v2): gate sign-in on 签到广告 article view

Before triggering setSignIntegral, fetch the random required article;
when one is returned, show a fullscreen overlay with title, image, and
rich-text content (loaded via getArticleDetails) plus a 10s countdown
gating the confirm button. Falls through to direct sign-in when the
endpoint returns null or fails.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
danaisuiyuan
2026-05-10 15:48:13 +08:00
parent b3a1dabf87
commit f4e8dab88a
2 changed files with 216 additions and 4 deletions

View File

@@ -157,6 +157,14 @@ export function getSignCalendar(data) {
return request.get('sign/calendar', data) return request.get('sign/calendar', data)
} }
/**
* 签到前置阅读:随机返回"签到广告"分类下一条文章
* 后端无该分类或分类下没有可用文章时 data 为 null前端可跳过门槛直接签到
*/
export function getSignRequiredArticle() {
return request.get('sign/required_article')
}
/** /**
* 活动状态 * 活动状态
* *

View File

@@ -100,6 +100,29 @@
<base-calendar v-if="calendarVisible" :yearMonth="targetDate" :dataSource="signData" @dateChange="getSignCalendar" @clickChange="clickSign"></base-calendar> <base-calendar v-if="calendarVisible" :yearMonth="targetDate" :dataSource="signData" @dateChange="getSignCalendar" @clickChange="clickSign"></base-calendar>
</view> </view>
</uni-popup> </uni-popup>
<view v-if="signAdVisible" class="sign-ad-overlay" :catchtouchmove="true">
<view class="sign-ad-header">
<view class="sign-ad-close" @click="closeSignAd">
<text class="iconfont icon-ic_Xempty"></text>
</view>
<view class="sign-ad-header-title">签到广告</view>
<view class="sign-ad-header-spacer"></view>
</view>
<scroll-view scroll-y class="sign-ad-scroll">
<view class="sign-ad-body">
<view class="sign-ad-article-title">{{ signAdArticle.title }}</view>
<image v-if="signAdArticle.image" :src="signAdArticle.image" mode="widthFix" class="sign-ad-image"></image>
<rich-text v-if="signAdArticle.content" :nodes="signAdArticle.content" class="sign-ad-content"></rich-text>
<view v-else class="sign-ad-loading">文章加载中</view>
</view>
</scroll-view>
<view class="sign-ad-footer">
<view v-if="signAdCountdown > 0" class="sign-ad-tip">浏览满 {{ signAdCountdown }} 秒后可签到</view>
<view class="sign-ad-btn confirm" :class="{ disabled: signAdCountdown > 0 }" @click="confirmSignAfterAd">
{{ signAdCountdown > 0 ? `阅读中… ${signAdCountdown}s` : '我已阅读,立即签到' }}
</view>
</view>
</view>
<home></home> <home></home>
</view> </view>
</template> </template>
@@ -114,8 +137,10 @@
setSignIntegral, setSignIntegral,
signRemind, signRemind,
getSignList, getSignList,
getSignCalendar getSignCalendar,
getSignRequiredArticle
} from '@/api/user.js'; } from '@/api/user.js';
import { getArticleDetails } from '@/api/api.js';
import BaseCalendar from '@/components/BaseCalendar.vue'; import BaseCalendar from '@/components/BaseCalendar.vue';
import emptyPage from '@/components/emptyPage.vue'; import emptyPage from '@/components/emptyPage.vue';
import { import {
@@ -150,6 +175,11 @@
isScrolling: false, isScrolling: false,
calendarVisible: false, calendarVisible: false,
pageScrollStatus:false, pageScrollStatus:false,
// 签到前置阅读门槛
signAdVisible: false,
signAdArticle: { id: 0, title: '', image: '', content: '' },
signAdCountdown: 0,
signAdTimer: null,
}; };
}, },
computed: mapGetters(['isLogin']), computed: mapGetters(['isLogin']),
@@ -283,12 +313,71 @@
}); });
}, },
goSign: function(e) { goSign: function(e) {
let that = this, if (this.userInfo.is_day_sgin)
sum_sgin_day = that.userInfo.sum_sgin_day;
if (that.userInfo.is_day_sgin)
return this.$util.Tips({ return this.$util.Tips({
title: '您今日已签到!' title: '您今日已签到!'
}); });
// 先取签到前置阅读文章;后端无文章/分类则跳过门槛
getSignRequiredArticle().then(res => {
const article = res && res.data ? res.data : null;
if (article && article.id) {
this.signAdArticle = {
id: article.id,
title: article.title || '',
image: article.image || '',
content: ''
};
this.signAdVisible = true;
this.startSignAdCountdown(Number(article.view_required_seconds) || 10);
this.fetchSignAdContent(article.id);
} else {
this.doSignIntegral();
}
}).catch(() => {
// 拉文章失败兜底直接签到
this.doSignIntegral();
});
},
fetchSignAdContent(id) {
getArticleDetails(id).then(res => {
const data = res && res.data ? res.data : {};
this.signAdArticle = {
...this.signAdArticle,
title: data.title || this.signAdArticle.title,
content: data.content || ''
};
}).catch(() => {
// 仅富文本拉取失败模态保持显示content 留空
});
},
startSignAdCountdown(seconds) {
this.clearSignAdTimer();
this.signAdCountdown = seconds;
this.signAdTimer = setInterval(() => {
this.signAdCountdown -= 1;
if (this.signAdCountdown <= 0) {
this.clearSignAdTimer();
}
}, 1000);
},
clearSignAdTimer() {
if (this.signAdTimer) {
clearInterval(this.signAdTimer);
this.signAdTimer = null;
}
},
closeSignAd() {
this.clearSignAdTimer();
this.signAdCountdown = 0;
this.signAdVisible = false;
},
confirmSignAfterAd() {
if (this.signAdCountdown > 0) return;
this.closeSignAd();
this.doSignIntegral();
},
doSignIntegral() {
const that = this;
setSignIntegral() setSignIntegral()
.then((res) => { .then((res) => {
this.$util.Tips({ this.$util.Tips({
@@ -724,4 +813,119 @@
transform: translateY(0); transform: translateY(0);
} }
} }
.sign-ad-overlay {
position: fixed;
left: 0;
top: 0;
right: 0;
bottom: 0;
z-index: 9999;
background: #fff;
display: flex;
flex-direction: column;
}
.sign-ad-header {
flex: 0 0 auto;
display: flex;
align-items: center;
height: 88rpx;
padding: 0 24rpx;
border-bottom: 1rpx solid #eee;
}
.sign-ad-close {
width: 56rpx;
height: 56rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 36rpx;
color: #333;
}
.sign-ad-header-title {
flex: 1;
text-align: center;
font-size: 32rpx;
font-weight: 600;
color: #222;
}
.sign-ad-header-spacer {
width: 56rpx;
height: 56rpx;
}
.sign-ad-scroll {
flex: 1 1 auto;
min-height: 0;
}
.sign-ad-body {
padding: 32rpx 32rpx 40rpx;
}
.sign-ad-article-title {
font-size: 36rpx;
font-weight: 600;
color: #222;
line-height: 1.4;
margin-bottom: 24rpx;
}
.sign-ad-image {
width: 100%;
max-height: 600rpx;
border-radius: 12rpx;
margin-bottom: 24rpx;
}
.sign-ad-content {
font-size: 28rpx;
color: #333;
line-height: 1.7;
word-break: break-all;
}
.sign-ad-loading {
text-align: center;
font-size: 26rpx;
color: #999;
padding: 40rpx 0;
}
.sign-ad-footer {
flex: 0 0 auto;
padding: 16rpx 32rpx 32rpx;
background: #fff;
border-top: 1rpx solid #eee;
padding-bottom: calc(32rpx + constant(safe-area-inset-bottom));
padding-bottom: calc(32rpx + env(safe-area-inset-bottom));
}
.sign-ad-tip {
text-align: center;
font-size: 26rpx;
color: #FAAD14;
margin-bottom: 16rpx;
}
.sign-ad-btn {
height: 88rpx;
line-height: 88rpx;
text-align: center;
border-radius: 44rpx;
font-size: 30rpx;
}
.sign-ad-btn.confirm {
background: linear-gradient(90deg, #FF7E30, #FAAD14);
color: #fff;
}
.sign-ad-btn.confirm.disabled {
opacity: 0.55;
}
</style> </style>