2026-02-28 05:40:21 +08:00
|
|
|
|
<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>
|
|
|
|
|
|
|
2026-03-03 15:33:50 +08:00
|
|
|
|
<!-- 立即打卡按钮:今日已打卡则显示灰色不可点 -->
|
|
|
|
|
|
<view
|
|
|
|
|
|
class="checkin-btn"
|
|
|
|
|
|
:class="{ 'checkin-btn-disabled': todaySigned }"
|
|
|
|
|
|
@click="handleCheckin"
|
|
|
|
|
|
>
|
|
|
|
|
|
<text>{{ todaySigned ? '今日已打卡' : '立即打卡' }}</text>
|
2026-02-28 05:40:21 +08:00
|
|
|
|
</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 {
|
2026-03-03 15:33:50 +08:00
|
|
|
|
todaySigned: false,
|
2026-04-12 09:31:00 +08:00
|
|
|
|
/** 防止 loadCheckinData 与打卡后刷新并发时,旧 user/info 响应覆盖新积分 */
|
|
|
|
|
|
_userInfoFetchGen: 0,
|
|
|
|
|
|
/** 打卡流程进行中:禁止 onShow 触发的 loadCheckinData 用「打卡前」发出的 user/info 写 currentPoints */
|
|
|
|
|
|
_suppressStalePointsLoad: false,
|
|
|
|
|
|
/** 防止连点导致多次打卡请求与积分展示乱序 */
|
|
|
|
|
|
_checkinSubmitting: false,
|
|
|
|
|
|
currentPoints: 0,
|
2026-02-28 05:40:21 +08:00
|
|
|
|
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() {
|
2026-03-03 15:33:50 +08:00
|
|
|
|
const { checkLogin, toLogin } = require('@/libs/login.js');
|
|
|
|
|
|
if (!checkLogin()) {
|
|
|
|
|
|
toLogin();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-02-28 05:40:21 +08:00
|
|
|
|
this.loadCheckinData();
|
|
|
|
|
|
},
|
|
|
|
|
|
onShow() {
|
2026-04-12 09:31:00 +08:00
|
|
|
|
// 页面显示时刷新数据(打卡请求进行中时跳过,避免与 handleCheckin 内刷新竞态)
|
|
|
|
|
|
if (this._checkinSubmitting) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-02-28 05:40:21 +08:00
|
|
|
|
this.loadCheckinData();
|
|
|
|
|
|
},
|
|
|
|
|
|
methods: {
|
2026-04-12 09:31:00 +08:00
|
|
|
|
async loadCheckinData(options = {}) {
|
|
|
|
|
|
const includePointsRefresh = !!options.includePointsRefresh;
|
|
|
|
|
|
const skipPoints = includePointsRefresh
|
|
|
|
|
|
? false
|
|
|
|
|
|
: (!!options.skipPoints || this._suppressStalePointsLoad);
|
|
|
|
|
|
const pointsGen = skipPoints ? null : ++this._userInfoFetchGen;
|
2026-02-28 05:40:21 +08:00
|
|
|
|
try {
|
2026-04-12 09:31:00 +08:00
|
|
|
|
const { getCheckinStreak, getCheckinTasks } = await import('@/api/tool.js');
|
|
|
|
|
|
const { getSignGet, getFrontUserInfo } = await import('@/api/user.js');
|
2026-03-03 15:33:50 +08:00
|
|
|
|
|
2026-04-12 09:31:00 +08:00
|
|
|
|
const [userInfoRes, streakRes, tasksRes, signRes] = await Promise.all([
|
|
|
|
|
|
skipPoints ? Promise.resolve(null) : getFrontUserInfo().catch(() => null),
|
2026-02-28 05:40:21 +08:00
|
|
|
|
getCheckinStreak().catch(() => ({ data: { streakDays: 7, currentStreak: 0 } })),
|
2026-03-03 15:33:50 +08:00
|
|
|
|
getCheckinTasks().catch(() => ({ data: { tasks: [] } })),
|
|
|
|
|
|
getSignGet().catch(() => ({ data: { today: false } }))
|
2026-02-28 05:40:21 +08:00
|
|
|
|
]);
|
2026-03-03 15:33:50 +08:00
|
|
|
|
|
|
|
|
|
|
if (signRes && signRes.data && signRes.data.today) {
|
|
|
|
|
|
this.todaySigned = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-12 09:31:00 +08:00
|
|
|
|
// BUG-001-A:打卡请求进行中时,勿用并行的 user/info 回写积分(避免先显示旧分再跳变);显式 includePointsRefresh 时仍刷新
|
|
|
|
|
|
const allowPointsFromThisLoad =
|
|
|
|
|
|
!this._checkinSubmitting || includePointsRefresh;
|
|
|
|
|
|
if (!skipPoints && userInfoRes && pointsGen === this._userInfoFetchGen && allowPointsFromThisLoad) {
|
|
|
|
|
|
const p = this._parsePointsFromUserInfo(userInfoRes);
|
|
|
|
|
|
if (p != null && !Number.isNaN(Number(p))) {
|
|
|
|
|
|
this.currentPoints = Number(p);
|
|
|
|
|
|
}
|
2026-02-28 05:40:21 +08:00
|
|
|
|
}
|
2026-03-03 15:33:50 +08:00
|
|
|
|
|
2026-02-28 05:40:21 +08:00
|
|
|
|
if (streakRes.data) {
|
|
|
|
|
|
const streak = streakRes.data.currentStreak || 0;
|
|
|
|
|
|
this.streakDays = Array.from({ length: 7 }, (_, i) => ({
|
|
|
|
|
|
day: i + 1,
|
|
|
|
|
|
completed: i < streak,
|
2026-03-03 15:33:50 +08:00
|
|
|
|
special: i === 6
|
2026-02-28 05:40:21 +08:00
|
|
|
|
}));
|
|
|
|
|
|
}
|
2026-03-03 15:33:50 +08:00
|
|
|
|
|
2026-02-28 05:40:21 +08:00
|
|
|
|
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'
|
|
|
|
|
|
})
|
|
|
|
|
|
},
|
2026-04-12 09:31:00 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* BUG-001-A:打卡 API 成功前不得修改 currentPoints(禁止本地 +30 或与跳转前的乐观更新)。
|
|
|
|
|
|
* BUG-001-B:仅 GET /api/front/user/info(getFrontUserInfo)解析账户 integral;打卡接口返回的 integral 为当日奖励分值,绝非总积分。
|
|
|
|
|
|
*/
|
2026-03-03 15:33:50 +08:00
|
|
|
|
async handleCheckin() {
|
2026-04-12 09:31:00 +08:00
|
|
|
|
if (this._checkinSubmitting) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-03-03 15:33:50 +08:00
|
|
|
|
if (this.todaySigned) {
|
|
|
|
|
|
uni.showToast({ title: '今日已打卡', icon: 'none' });
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-04-12 09:31:00 +08:00
|
|
|
|
this._checkinSubmitting = true;
|
|
|
|
|
|
this._suppressStalePointsLoad = true;
|
|
|
|
|
|
const pointsBeforeCheckin = Number(this.currentPoints) || 0;
|
|
|
|
|
|
const { userCheckin, userCheckinDaily, getFrontUserInfo } = await import('@/api/user.js');
|
|
|
|
|
|
uni.showLoading({ title: '打卡中...', mask: true });
|
2026-03-03 15:33:50 +08:00
|
|
|
|
try {
|
2026-04-12 09:31:00 +08:00
|
|
|
|
// 1) /api/front/user/checkin,失败时回退 /api/front/user/sign/integral;二者均不读取 body 更新积分
|
|
|
|
|
|
await this._requestCheckin(userCheckin, userCheckinDaily);
|
|
|
|
|
|
// 2) 成功后必须 GET user/info,用服务端账户积分更新展示
|
|
|
|
|
|
let refreshed = await this._refreshCurrentPointsFromUserInfo(getFrontUserInfo).catch((err) => {
|
|
|
|
|
|
console.warn('打卡后刷新积分失败:', err);
|
|
|
|
|
|
return false;
|
|
|
|
|
|
});
|
|
|
|
|
|
if (refreshed && Number(this.currentPoints) === pointsBeforeCheckin) {
|
|
|
|
|
|
await new Promise((r) => setTimeout(r, 250));
|
|
|
|
|
|
refreshed =
|
|
|
|
|
|
(await this._refreshCurrentPointsFromUserInfo(getFrontUserInfo).catch(() => false)) || refreshed;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!refreshed) {
|
|
|
|
|
|
await this.loadCheckinData({ includePointsRefresh: true });
|
2026-03-03 15:33:50 +08:00
|
|
|
|
}
|
2026-03-07 22:26:37 +08:00
|
|
|
|
this.todaySigned = true;
|
2026-04-12 09:31:00 +08:00
|
|
|
|
await this.loadCheckinData({ skipPoints: true });
|
|
|
|
|
|
uni.navigateTo({
|
|
|
|
|
|
url: '/pages/tool/checkin-publish'
|
|
|
|
|
|
});
|
2026-03-03 15:33:50 +08:00
|
|
|
|
} catch (e) {
|
|
|
|
|
|
const msg = (typeof e === 'string' ? e : (e && (e.message || e.msg))) || '打卡失败';
|
|
|
|
|
|
if (msg.includes('今日已签到') || msg.includes('不可重复')) {
|
|
|
|
|
|
this.todaySigned = true;
|
2026-04-12 09:31:00 +08:00
|
|
|
|
await this.loadCheckinData({ skipPoints: true });
|
|
|
|
|
|
} else {
|
|
|
|
|
|
await this.loadCheckinData({ includePointsRefresh: true }).catch(() => {});
|
2026-03-03 15:33:50 +08:00
|
|
|
|
}
|
|
|
|
|
|
uni.showToast({ title: msg, icon: 'none' });
|
|
|
|
|
|
return;
|
2026-04-12 09:31:00 +08:00
|
|
|
|
} finally {
|
|
|
|
|
|
this._suppressStalePointsLoad = false;
|
|
|
|
|
|
this._checkinSubmitting = false;
|
|
|
|
|
|
uni.hideLoading();
|
2026-03-03 15:33:50 +08:00
|
|
|
|
}
|
2026-02-28 05:40:21 +08:00
|
|
|
|
},
|
2026-04-12 09:31:00 +08:00
|
|
|
|
/** 优先 GET user/checkin,路由类失败时回退 user/sign/integral;返回值勿用于 currentPoints(data 为签到档位配置,integral 为奖励分非余额)。 */
|
|
|
|
|
|
async _requestCheckin(userCheckin, userCheckinDaily) {
|
2026-03-09 18:56:53 +08:00
|
|
|
|
try {
|
2026-04-12 09:31:00 +08:00
|
|
|
|
await userCheckin();
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
const msg = String((typeof err === 'string' ? err : (err && (err.message || err.msg))) || '');
|
|
|
|
|
|
const m = msg.toLowerCase();
|
|
|
|
|
|
// CRMEB 前端对 code=404 常 reject 文案「没有找到相关数据」,不含数字 404,须一并识别才能回退到 sign/integral
|
|
|
|
|
|
const needFallback =
|
|
|
|
|
|
m.includes('404') ||
|
|
|
|
|
|
m.includes('405') ||
|
|
|
|
|
|
m.includes('not found') ||
|
|
|
|
|
|
m.includes('no static resource') ||
|
|
|
|
|
|
msg.includes('没有找到相关数据') ||
|
|
|
|
|
|
msg.includes('找不到');
|
|
|
|
|
|
if (!needFallback) throw err;
|
|
|
|
|
|
await userCheckinDaily();
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
async _refreshCurrentPointsFromUserInfo(getFrontUserInfoFn) {
|
|
|
|
|
|
const gen = ++this._userInfoFetchGen;
|
|
|
|
|
|
const getInfo =
|
|
|
|
|
|
typeof getFrontUserInfoFn === 'function'
|
|
|
|
|
|
? getFrontUserInfoFn
|
|
|
|
|
|
: (await import('@/api/user.js')).getFrontUserInfo;
|
|
|
|
|
|
const infoRes = await getInfo({ _: Date.now() });
|
|
|
|
|
|
if (gen !== this._userInfoFetchGen) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
const serverPoints = this._parsePointsFromUserInfo(infoRes);
|
|
|
|
|
|
if (serverPoints != null && !Number.isNaN(Number(serverPoints))) {
|
|
|
|
|
|
this.currentPoints = Number(serverPoints);
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
return false;
|
|
|
|
|
|
},
|
|
|
|
|
|
_parsePointsFromUserInfo(infoRes) {
|
|
|
|
|
|
if (!infoRes) return null;
|
|
|
|
|
|
const payload = infoRes.data;
|
|
|
|
|
|
const d = (payload && payload.data != null && typeof payload === 'object') ? payload.data : payload;
|
|
|
|
|
|
if (d == null || typeof d !== 'object') return null;
|
|
|
|
|
|
// 勿把打卡/签到接口的 SystemGroupDataSignConfigVo(含 day + integral 奖励)当成个人中心 integral
|
|
|
|
|
|
if (d.day != null && d.nickname == null && d.nowMoney == null) {
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
const v = d.integral ?? d.points ?? d.totalPoints ?? (d.user && (d.user.integral ?? d.user.points ?? d.user.totalPoints));
|
|
|
|
|
|
return v != null && v !== '' ? v : null;
|
2026-03-09 18:56:53 +08:00
|
|
|
|
},
|
2026-02-28 05:40:21 +08:00
|
|
|
|
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);
|
2026-03-03 15:33:50 +08:00
|
|
|
|
|
2026-02-28 05:40:21 +08:00
|
|
|
|
text {
|
|
|
|
|
|
font-size: 32rpx;
|
|
|
|
|
|
color: #ffffff;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-03 15:33:50 +08:00
|
|
|
|
.checkin-btn-disabled {
|
|
|
|
|
|
background: linear-gradient(135deg, #c4c4c4 0%, #e0e0e0 100%);
|
|
|
|
|
|
box-shadow: none;
|
|
|
|
|
|
opacity: 0.9;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-28 05:40:21 +08:00
|
|
|
|
/* 通用区块样式 */
|
|
|
|
|
|
.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>
|