Files
msh-system/msh_single_uniapp/pages/tool/checkin.vue
scottpan f692c75f7b feat: 更新前端多个页面和后端服务
- 前端: 更新AI营养师、计算器、打卡、食物详情等页面
- 前端: 更新食物百科、知识详情、营养知识页面
- 前端: 更新社区首页
- 后端: 更新ToolKieAIServiceImpl服务
- API: 更新models-api.js和user.js
2026-03-07 22:26:37 +08:00

632 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, getFrontUserInfo, getUserInfo } = await import('@/api/user.js');
const { getUserPoints } = await import('@/api/tool.js');
// 子问题 A不得在 API 返回成功前修改 currentPoints避免打卡前积分提前跳变
// 打卡接口GET /api/front/user/sign/integralsetSignIntegral等同于 checkin 签到
await setSignIntegral();
// 子问题 B仅在打卡成功后用服务端数据更新积分。先 GET /api/front/user/info 刷新用户积分,禁止硬编码 +30
let userRes = null;
try {
userRes = await getFrontUserInfo(); // GET /api/front/user/info
} catch (_) {
userRes = await getUserInfo().catch(() => null);
}
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;
}
}
// 积分已从服务端更新后再更新打卡状态并跳转,避免“已打卡”与积分不同步
this.todaySigned = true;
} 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>