Files
msh-system/msh_single_uniapp/pages/tool/checkin.vue
scottpan d8d2025543 feat: T10 回归测试 Bug 修复与功能完善
修复 BUG-001 至 BUG-009 及 T10-1 至 T10-6 相关问题:
- 打卡积分显示与累加逻辑优化
- 食谱计算器 Tab 选中样式修复
- 食物百科列表图片与简介展示修复
- 食物详情页数据加载修复
- AI营养师差异化回复优化
- 健康知识/营养知识名称统一
- 饮食指南/科普文章详情页内容展示修复
- 帖子营养统计数据展示修复
- 社区帖子类型中文命名统一
- 帖子详情标签中文显示修复
- 食谱营养AI填充功能完善
- 食谱收藏/点赞功能修复

新增:
- ToolNutritionFillService 营养填充服务
- T10 回归测试用例 (Playwright)
- 知识文章数据 SQL 脚本

涉及模块:
- crmeb-common: VO/Request/Response 优化
- crmeb-service: 业务逻辑完善
- crmeb-front: API 接口扩展
- msh_single_uniapp: 前端页面修复
- tests/e2e: 回归测试用例
2026-03-05 09:35:00 +08:00

626 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避免打卡前积分提前跳变
// 子问题 B-1打卡接口为 GET /api/front/user/sign/integralsetSignIntegral与 /api/front/user/checkin 同属签到类接口
await setSignIntegral();
this.todaySigned = true;
// 子问题 B-2/B-3仅用服务端返回值更新积分禁止前端本地 +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>