Files
msh-system/msh_single_uniapp/pages/tool/checkin.vue
scottpan 6f2dc27fbc chore: update pom.xml Lombok config and deploy settings
- Update Maven compiler plugin to support Lombok annotation processing
- Add deploy.conf for automated deployment
- Update backend models and controllers
- Update frontend pages and API
2026-03-04 12:21:29 +08:00

624 lines
14 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 class="checkin-page">
<!-- 积分规则按钮 -->
<view class="rules-btn-top" @tap="showRules">
<text>积分规则</text>
</view>
<!-- 打卡状态卡片 -->
<view class="checkin-card">
<!-- 积分显示 -->
<view class="points-header">
<text class="star-icon"></text>
<text class="points-text">{{ currentPoints }} 积分</text>
</view>
<!-- 连续打卡天数 -->
<view class="streak-container">
<view
class="streak-item"
v-for="(item, index) in streakDays"
:key="index"
>
<view
class="streak-circle"
:class="{
completed: item.completed,
pending: !item.completed && !item.special,
special: item.special
}"
>
<text v-if="item.completed" class="check-icon"></text>
<text v-else-if="item.special" class="crown-icon">👑</text>
<text v-else class="circle-icon"></text>
</view>
<text class="day-number">{{ item.day }}</text>
</view>
</view>
<!-- 立即打卡按钮今日已打卡则显示灰色不可点 -->
<view
class="checkin-btn"
:class="{ 'checkin-btn-disabled': todaySigned }"
@click="handleCheckin"
>
<text>{{ todaySigned ? '今日已打卡' : '立即打卡' }}</text>
</view>
</view>
<!-- 打卡任务 -->
<view class="section">
<view class="section-title">打卡任务</view>
<view class="task-list">
<view
class="task-item"
v-for="(task, index) in taskList"
:key="index"
>
<view class="task-icon" :style="{ background: task.iconBg }">
<text>{{ task.icon }}</text>
</view>
<view class="task-content">
<view class="task-header">
<text class="task-name">{{ task.name }}</text>
<view class="points-badge">
<text>+{{ task.points }}</text>
</view>
</view>
<text class="task-desc">{{ task.desc }}</text>
</view>
<view class="task-action-btn" @click="handleTask(task)">
<text>去完成</text>
</view>
</view>
</view>
</view>
<!-- 积分兑换 -->
<view class="section">
<view class="section-title">积分兑换</view>
<view class="exchange-list">
<view
class="exchange-item"
v-for="(item, index) in exchangeList"
:key="index"
>
<view class="exchange-icon" :style="{ background: item.iconBg }">
<text>{{ item.icon }}</text>
</view>
<view class="exchange-content">
<text class="exchange-name">{{ item.name }}</text>
<text class="exchange-desc">{{ item.desc }}</text>
</view>
<view class="exchange-btn" @click="handleExchange(item)">
<text>立即兑换</text>
</view>
</view>
</view>
</view>
<!-- 提示信息 -->
<view class="tip-banner">
<text class="tip-icon">💡</text>
<text class="tip-text">连续打卡7天可获得额外奖励完成每日任务还能获得更多积分哦</text>
</view>
<!-- 底部安全距离 -->
<view class="safe-bottom"></view>
</view>
</template>
<script>
export default {
data() {
return {
todaySigned: false,
currentPoints: 522,
streakDays: [
{ day: 1, completed: false, special: false },
{ day: 2, completed: false, special: false },
{ day: 3, completed: false, special: false },
{ day: 4, completed: false, special: false },
{ day: 5, completed: false, special: false },
{ day: 6, completed: false, special: false },
{ day: 7, completed: false, special: true }
],
taskList: [
{
id: 1,
name: '饮食记录上传',
desc: '上传饮食照片赚取积分',
points: 20,
icon: '📸',
iconBg: 'linear-gradient(135deg, #fff4f0 0%, #ffe8e0 100%)'
},
{
id: 2,
name: '打卡视频制作',
desc: '制作打卡视频分享动态',
points: 50,
icon: '🎬',
iconBg: 'linear-gradient(135deg, #fff4f0 0%, #ffe8e0 100%)'
},
{
id: 3,
name: '内容分享',
desc: '分享内容至社交平台',
points: 30,
icon: '📤',
iconBg: 'linear-gradient(135deg, #fff4f0 0%, #ffe8e0 100%)'
}
],
exchangeList: [
{
id: 1,
name: '微信红包兑换',
desc: '100积分 = 5元红包',
icon: '💰',
iconBg: 'linear-gradient(135deg, #fef3c7 0%, #fde68a 100%)'
},
{
id: 2,
name: '米面油',
desc: '300积分 = 10斤大米/5升油',
icon: '🌾',
iconBg: 'linear-gradient(135deg, #fef3c7 0%, #fde68a 100%)'
},
{
id: 3,
name: '零食礼盒',
desc: '200积分 = 1箱零食',
icon: '🎁',
iconBg: 'linear-gradient(135deg, #fef3c7 0%, #fde68a 100%)'
}
]
}
},
onLoad() {
const { checkLogin, toLogin } = require('@/libs/login.js');
if (!checkLogin()) {
toLogin();
return;
}
this.loadCheckinData();
},
onShow() {
// 页面显示时刷新数据
this.loadCheckinData();
},
methods: {
async loadCheckinData() {
try {
const { getUserPoints, getCheckinStreak, getCheckinTasks } = await import('@/api/tool.js');
const { getSignGet } = await import('@/api/user.js');
const [pointsRes, streakRes, tasksRes, signRes] = await Promise.all([
getUserPoints().catch(() => ({ data: { points: 0 } })),
getCheckinStreak().catch(() => ({ data: { streakDays: 7, currentStreak: 0 } })),
getCheckinTasks().catch(() => ({ data: { tasks: [] } })),
getSignGet().catch(() => ({ data: { today: false } }))
]);
if (signRes && signRes.data && signRes.data.today) {
this.todaySigned = true;
}
if (pointsRes.data) {
this.currentPoints = pointsRes.data.totalPoints ?? pointsRes.data.points ?? 0;
}
if (streakRes.data) {
const streak = streakRes.data.currentStreak || 0;
this.streakDays = Array.from({ length: 7 }, (_, i) => ({
day: i + 1,
completed: i < streak,
special: i === 6
}));
}
if (tasksRes.data && tasksRes.data.tasks && tasksRes.data.tasks.length > 0) {
this.taskList = tasksRes.data.tasks.map(task => ({
id: task.id || task.taskId,
title: task.title || task.name,
desc: task.desc || task.description || '',
points: task.points || task.reward || 0,
completed: task.completed || task.status === 'done',
icon: task.icon || '📝'
}));
}
} catch (error) {
console.error('加载打卡数据失败:', error);
}
},
showRules() {
uni.navigateTo({
url: '/pages/tool/points-rules'
})
},
async handleCheckin() {
if (this.todaySigned) {
uni.showToast({ title: '今日已打卡', icon: 'none' });
return;
}
try {
const { setSignIntegral, getUserInfo } = await import('@/api/user.js');
const { getUserPoints } = await import('@/api/tool.js');
// 子问题 A不得在 API 返回成功前修改 currentPoints避免打卡前积分提前跳变
// 打卡接口GET /api/front/user/sign/integralsetSignIntegral
await setSignIntegral();
this.todaySigned = true;
// 子问题 B仅用服务端返回的积分更新 currentPoints禁止前端本地 +30
// 先 GET /api/front/usergetUserInfo刷新用户信息取 integral 赋给 currentPoints
const userRes = await getUserInfo();
if (userRes && userRes.data && (userRes.data.integral != null || userRes.data.points != null)) {
this.currentPoints = userRes.data.integral ?? userRes.data.points ?? 0;
} else {
const pointsRes = await getUserPoints();
if (pointsRes && pointsRes.data) {
const serverPoints = pointsRes.data.totalPoints ?? pointsRes.data.points ?? pointsRes.data.availablePoints ?? 0;
this.currentPoints = serverPoints;
}
}
} catch (e) {
const msg = (typeof e === 'string' ? e : (e && (e.message || e.msg))) || '打卡失败';
if (msg.includes('今日已签到') || msg.includes('不可重复')) {
this.todaySigned = true;
}
uni.showToast({ title: msg, icon: 'none' });
return;
}
uni.navigateTo({
url: '/pages/tool/checkin-publish'
});
},
getTodayIndex() {
// 根据连续打卡数据计算当前是第几天
// streakDays 中已有 completed 状态,找第一个未完成的位置即为今天
const index = this.streakDays.findIndex(day => !day.completed)
return index >= 0 ? index : this.streakDays.length - 1
},
handleTask(task) {
switch (task.id) {
case 1: // 饮食记录上传
uni.navigateTo({
url: '/pages/tool/checkin-publish'
})
break
case 2: // 打卡视频制作
uni.navigateTo({
url: '/pages/tool/checkin-publish?type=video'
})
break
case 3: // 内容分享
uni.navigateTo({
url: '/pages/tool/invite-rewards'
})
break
default:
uni.showToast({
title: '功能开发中',
icon: 'none'
})
}
},
handleExchange(item) {
uni.navigateTo({
url: '/pages/tool/welcome-gift'
})
}
}
}
</script>
<style lang="scss" scoped>
.checkin-page {
min-height: 100vh;
background: linear-gradient(180deg, #fafbfc 0%, #f0f2f5 100%);
padding-bottom: 20rpx;
}
/* 积分规则按钮 */
.rules-btn-top {
position: fixed;
top: calc(var(--status-bar-height, 0) + 8rpx);
right: 60rpx;
z-index: 100;
background: rgba(255, 255, 255, 0.75);
border-radius: 20rpx;
padding: 12rpx 24rpx;
box-shadow: 2rpx 4rpx 2rpx rgba(33, 33, 33, 0.3);
text {
font-size: 24rpx;
color: #ff6b35;
font-weight: 500;
}
}
/* 打卡状态卡片 */
.checkin-card {
background: #ffffff;
border-radius: 48rpx;
padding: 40rpx;
margin: 32rpx 32rpx 0;
box-shadow: 0 16rpx 64rpx rgba(0, 0, 0, 0.06);
}
.points-header {
display: flex;
align-items: center;
justify-content: center;
gap: 16rpx;
margin-bottom: 40rpx;
.star-icon {
font-size: 36rpx;
}
.points-text {
font-size: 28rpx;
color: #b45309;
font-weight: 500;
}
}
.streak-container {
display: flex;
justify-content: space-between;
margin-bottom: 40rpx;
padding: 0 8rpx;
}
.streak-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 16rpx;
flex: 1;
}
.streak-circle {
width: 76rpx;
height: 76rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 8rpx 12rpx -2rpx rgba(0, 0, 0, 0.1), 0 4rpx 8rpx -2rpx rgba(0, 0, 0, 0.1);
&.completed {
background: linear-gradient(135deg, #ff6b35 0%, #e85a2a 100%);
.check-icon {
font-size: 36rpx;
color: #ffffff;
}
}
&.pending {
background: linear-gradient(135deg, #f3f4f6 0%, #e5e7eb 100%);
.circle-icon {
font-size: 36rpx;
color: #9fa5c0;
}
}
&.special {
background: linear-gradient(135deg, #fbbf24 0%, #f59e0b 100%);
.crown-icon {
font-size: 32rpx;
}
}
}
.day-number {
font-size: 24rpx;
color: #6b7280;
}
.checkin-btn {
background: linear-gradient(135deg, #ff6b35 0%, #ff7a4a 50%, #ff6b35 100%);
border-radius: 32rpx;
padding: 20rpx;
text-align: center;
box-shadow: 0 16rpx 48rpx rgba(255, 107, 53, 0.35);
text {
font-size: 32rpx;
color: #ffffff;
font-weight: 500;
}
}
.checkin-btn-disabled {
background: linear-gradient(135deg, #c4c4c4 0%, #e0e0e0 100%);
box-shadow: none;
opacity: 0.9;
}
/* 通用区块样式 */
.section {
margin: 48rpx 32rpx 0;
.section-title {
font-size: 28rpx;
color: #6b7280;
margin-bottom: 32rpx;
padding-left: 8rpx;
}
}
/* 打卡任务列表 */
.task-list {
display: flex;
flex-direction: column;
gap: 24rpx;
}
.task-item {
background: #ffffff;
border-radius: 32rpx;
padding: 40rpx;
display: flex;
align-items: center;
gap: 32rpx;
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.04);
}
.task-icon {
width: 96rpx;
height: 96rpx;
border-radius: 32rpx;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
box-shadow: inset 0 4rpx 8rpx rgba(0, 0, 0, 0.05);
text {
font-size: 48rpx;
}
}
.task-content {
flex: 1;
display: flex;
flex-direction: column;
gap: 8rpx;
}
.task-header {
display: flex;
align-items: center;
gap: 16rpx;
.task-name {
font-size: 30rpx;
color: #2e3e5c;
font-weight: 500;
}
.points-badge {
background: #fff4f0;
border-radius: 40rpx;
padding: 4rpx 16rpx;
text {
font-size: 24rpx;
color: #ff6b35;
}
}
}
.task-desc {
font-size: 24rpx;
color: #9fa5c0;
}
.task-action-btn {
background: linear-gradient(135deg, #ff6b35 0%, #e85a2a 100%);
border-radius: 24rpx;
padding: 16rpx 32rpx;
box-shadow: 0 8rpx 12rpx -2rpx rgba(0, 0, 0, 0.1), 0 4rpx 8rpx -2rpx rgba(0, 0, 0, 0.1);
text {
font-size: 24rpx;
color: #ffffff;
}
}
/* 积分兑换列表 */
.exchange-list {
display: flex;
flex-direction: column;
gap: 24rpx;
}
.exchange-item {
background: #ffffff;
border-radius: 32rpx;
padding: 40rpx;
display: flex;
align-items: center;
gap: 32rpx;
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.04);
}
.exchange-icon {
width: 96rpx;
height: 96rpx;
border-radius: 32rpx;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
box-shadow: inset 0 4rpx 8rpx rgba(0, 0, 0, 0.05);
text {
font-size: 48rpx;
}
}
.exchange-content {
flex: 1;
display: flex;
flex-direction: column;
gap: 8rpx;
.exchange-name {
font-size: 30rpx;
color: #2e3e5c;
font-weight: 500;
}
.exchange-desc {
font-size: 24rpx;
color: #9fa5c0;
line-height: 1.6;
}
}
.exchange-btn {
background: linear-gradient(135deg, #fbbf24 0%, #f59e0b 100%);
border-radius: 24rpx;
padding: 16rpx 32rpx;
box-shadow: 0 8rpx 12rpx -2rpx rgba(0, 0, 0, 0.1), 0 4rpx 8rpx -2rpx rgba(0, 0, 0, 0.1);
text {
font-size: 24rpx;
color: #ffffff;
}
}
/* 提示信息 */
.tip-banner {
background: linear-gradient(135deg, #fef3c7 0%, #fef9e7 50%, #fef3c7 100%);
border: 1rpx solid rgba(253, 230, 138, 0.5);
border-radius: 32rpx;
padding: 32rpx;
margin: 48rpx 32rpx 0;
display: flex;
gap: 24rpx;
align-items: flex-start;
.tip-icon {
font-size: 36rpx;
flex-shrink: 0;
}
.tip-text {
font-size: 24rpx;
color: #92400e;
line-height: 1.6;
flex: 1;
}
}
/* 底部安全距离 */
.safe-bottom {
height: 40rpx;
}
</style>