V1.4: 首页沉浸式桌面 + 积分广告奖励系统

This commit is contained in:
huanglimeng 2026-02-09 21:13:05 +08:00
parent af8c17cf72
commit c58f6faa3a
7 changed files with 1048 additions and 92 deletions

View File

@ -1,13 +1,213 @@
const { getPoints, checkDailyReward, canWatchAd, getTodayAdCount, rewardFromAd, AD_REWARD_CONFIG } = require('../../utils/pointsManager');
const { getDailyAdvice } = require('../../utils/dailyAdvice');
const { getDailyArticle, getCategories } = require('../../utils/knowledgeData');
Page({
data: {
currentPoints: 0,
dailyAdvice: '',
dailyArticle: {
id: '',
title: '',
summary: '',
category: '',
categoryName: '',
type: 'local'
},
energyVisible: false,
knowledgeVisible: false
},
onLoad: function () {
// 加载每日内容
this.loadDailyContent();
},
onShow: function () {
// 检查每日登录奖励
const rewardResult = checkDailyReward();
// 如果获得了奖励,显示提示
if (rewardResult.rewarded) {
wx.showToast({
title: rewardResult.message,
icon: 'success',
duration: 2000
});
}
// 刷新积分显示
this.setData({
currentPoints: getPoints()
});
},
// 加载每日内容
loadDailyContent: function () {
try {
// 获取今日建议
const advice = getDailyAdvice();
// 获取今日文章
const article = getDailyArticle();
// 获取分类名称
const categories = getCategories();
const category = categories.find(c => c.id === article.category);
this.setData({
dailyAdvice: advice,
dailyArticle: Object.assign({}, article, {
categoryName: category ? category.name : '塔罗知识'
})
});
console.log('[首页] 每日内容加载成功');
} catch (error) {
console.error('[首页] 每日内容加载失败:', error);
}
},
// 显示能量卡浮层
showEnergyOverlay: function () {
this.setData({
energyVisible: true
});
},
// 隐藏能量卡浮层
hideEnergyOverlay: function () {
this.setData({
energyVisible: false
});
},
// 显示知识卡半屏
showKnowledgePanel: function () {
this.setData({
knowledgeVisible: true
});
},
// 隐藏知识卡半屏
hideKnowledgePanel: function () {
this.setData({
knowledgeVisible: false
});
},
// 阻止事件冒泡
stopPropagation: function () {
// 空函数,用于阻止点击事件冒泡
},
// 跳转到塔罗占卜
goToTarot: function () {
wx.navigateTo({
url: '/pages/index/index'
})
});
},
// 跳转到知识模块
goToKnowledge: function () {
wx.navigateTo({
url: '/pages/knowledge/index'
})
});
},
// 跳转到文章详情
goToArticle: function () {
const article = this.data.dailyArticle;
// 先关闭半屏
this.setData({
knowledgeVisible: false
});
// 延迟跳转,等待动画完成
setTimeout(() => {
if (article.type === 'web') {
// 外链文章,跳转到 webview
wx.navigateTo({
url: `/pages/webview/index?url=${encodeURIComponent(article.url)}&title=${encodeURIComponent(article.title)}`
});
} else {
// 本地文章,跳转到文章详情页
wx.navigateTo({
url: `/pages/knowledge/article?id=${article.id}`
});
}
}, 300);
},
// 显示积分说明
showPointsInfo: function () {
try {
console.log('[首页] 点击积分徽章');
const remaining = AD_REWARD_CONFIG.DAILY_LIMIT - getTodayAdCount();
const canWatch = canWatchAd();
console.log('[首页] 剩余广告次数:', remaining);
console.log('[首页] 是否可以观看:', canWatch);
console.log('[首页] 当前积分:', this.data.currentPoints);
const lines = [
'当前积分:' + this.data.currentPoints,
'',
'每次占卜会消耗积分,不同牌阵消耗不同。',
'每日首次登录可获得 +3 积分奖励。',
'',
'看广告可获得积分(今日剩余 ' + remaining + ' 次)'
];
const content = lines.join('\n');
console.log('[首页] 弹窗内容:', content);
console.log('[首页] 准备调用 wx.showModal');
wx.showModal({
title: '积分说明',
content: content,
confirmText: canWatch ? '看广告' : '知道了',
cancelText: '关闭',
success: (res) => {
console.log('[首页] 弹窗回调:', res);
if (res.confirm && canWatch) {
this.watchAdForPoints();
}
},
fail: (err) => {
console.error('[首页] 弹窗失败:', err);
}
});
console.log('[首页] wx.showModal 已调用');
} catch (error) {
console.error('[首页] showPointsInfo 错误:', error);
}
},
// 观看广告获取积分(模拟)
watchAdForPoints: function () {
const result = rewardFromAd();
if (result.success) {
// 刷新积分显示
this.setData({
currentPoints: getPoints()
});
// 显示奖励提示
wx.showToast({
title: result.message,
icon: 'success',
duration: 2000
});
} else {
// 显示失败提示
wx.showToast({
title: result.message,
icon: 'none',
duration: 2000
});
}
}
})
});

View File

@ -1,29 +1,67 @@
<view class="container">
<!-- 顶部引导文案 -->
<view class="header-text">
<text class="line-one">今天,</text>
<text class="line-two">你想从哪里获得指引?</text>
<view class="tarot-table">
<!-- 右上角积分徽章 -->
<view class="points-badge" bindtap="showPointsInfo">
<text class="badge-icon">🎯</text>
<text class="badge-value">{{currentPoints}}</text>
</view>
<!-- 选择区域 -->
<view class="selection-area">
<!-- 塔罗入口 -->
<view class="direction-card" bindtap="goToTarot" hover-class="card-hover">
<view class="card-icon">🔮</view>
<view class="card-content">
<text class="card-title">塔罗指引</text>
<text class="card-subtitle">探索潜意识的答案</text>
<!-- 中央主牌堆 -->
<view class="main-deck" bindtap="goToTarot" hover-class="deck-hover">
<view class="deck-cards">
<view class="card-stack card-1"></view>
<view class="card-stack card-2"></view>
<view class="card-stack card-3"></view>
</view>
<text class="deck-title">开始抽牌</text>
</view>
<!-- 左侧今日能量卡 -->
<view class="energy-card side-card" bindtap="showEnergyOverlay" hover-class="card-hover">
<text class="card-icon">💫</text>
<text class="card-label">今日能量</text>
</view>
<!-- 右侧今日知识卡 -->
<view class="knowledge-card side-card" bindtap="showKnowledgePanel" hover-class="card-hover">
<text class="card-icon">✨</text>
<text class="card-label">今日知识</text>
</view>
<!-- 底部功能抽屉 -->
<view class="function-drawer">
<view class="drawer-item" bindtap="goToTarot" hover-class="drawer-hover">
<text class="drawer-icon">🎴</text>
<text class="drawer-text">常用牌阵</text>
</view>
<view class="drawer-item" bindtap="goToKnowledge" hover-class="drawer-hover">
<text class="drawer-icon">📚</text>
<text class="drawer-text">学点塔罗</text>
</view>
<view class="drawer-item" bindtap="showPointsInfo" hover-class="drawer-hover">
<text class="drawer-icon">🎯</text>
<text class="drawer-text">积分说明</text>
</view>
</view>
<!-- 能量卡浮层 -->
<view class="overlay {{energyVisible ? 'show' : ''}}" bindtap="hideEnergyOverlay">
<view class="overlay-content energy-content" catchtap="stopPropagation">
<text class="overlay-title">💫 今日能量</text>
<text class="overlay-text">{{dailyAdvice}}</text>
<view class="overlay-close" bindtap="hideEnergyOverlay">关闭</view>
</view>
</view>
<!-- 知识卡半屏 -->
<view class="panel {{knowledgeVisible ? 'show' : ''}}" bindtap="hideKnowledgePanel">
<view class="panel-content" catchtap="stopPropagation">
<text class="panel-title">✨ 今日小知识</text>
<text class="panel-article-title">{{dailyArticle.title}}</text>
<text class="panel-summary">{{dailyArticle.summary}}</text>
<view class="panel-actions">
<view class="panel-btn" bindtap="goToArticle">查看详情</view>
<view class="panel-btn secondary" bindtap="hideKnowledgePanel">关闭</view>
</view>
</view>
<!-- 知识模块入口 -->
<view class="direction-card" bindtap="goToKnowledge" hover-class="card-hover">
<view class="card-icon">📚</view>
<view class="card-content">
<text class="card-title">学点塔罗</text>
<text class="card-subtitle">了解塔罗基础知识</text>
</view>
</view>
</view>
</view>

View File

@ -1,89 +1,349 @@
page {
background-color: #1a1a2e;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f0f23 100%);
height: 100%;
overflow: hidden;
}
/* ========== 主容器 ========== */
.tarot-table {
width: 100%;
height: 100vh;
position: relative;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
align-items: center;
}
.container {
display: flex;
flex-direction: column;
width: 100%;
padding: 40px 30px;
box-sizing: border-box;
align-items: flex-start; /* 左对齐更有叙述感 */
}
/* 顶部引导文案 */
.header-text {
margin-top: 40px;
margin-bottom: 60px;
display: flex;
flex-direction: column;
}
.line-one {
color: #fff;
font-size: 28px;
font-weight: 300; /* 细一点显得高级 */
margin-bottom: 10px;
opacity: 0.9;
}
.line-two {
color: #fff;
font-size: 22px;
font-weight: 300;
opacity: 0.7; /* 稍微淡一点 */
}
/* 选择区域 */
.selection-area {
width: 100%;
display: flex;
flex-direction: column;
gap: 25px; /* 卡片之间的间距 */
}
/* 方向卡片 */
.direction-card {
background-color: rgba(255, 255, 255, 0.05); /* 极低透明度背景 */
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 16px;
padding: 25px;
/* ========== 右上角积分徽章 ========== */
.points-badge {
position: absolute;
top: 30px;
right: 20px;
background: linear-gradient(135deg, rgba(233, 69, 96, 0.25), rgba(233, 69, 96, 0.15));
border: 1px solid rgba(233, 69, 96, 0.5);
border-radius: 20px;
padding: 8px 16px;
display: flex;
align-items: center;
gap: 6px;
box-shadow: 0 4px 15px rgba(233, 69, 96, 0.2);
z-index: 2000;
cursor: pointer;
}
.badge-icon {
font-size: 16px;
}
.badge-value {
color: #e94560;
font-size: 18px;
font-weight: bold;
}
/* ========== 中央主牌堆 ========== */
.main-deck {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
gap: 25px;
z-index: 10;
}
.deck-cards {
position: relative;
width: 180px;
height: 260px;
}
.card-stack {
position: absolute;
width: 180px;
height: 260px;
background: linear-gradient(135deg, #e94560 0%, #8b3a62 50%, #4a1942 100%);
border-radius: 12px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5);
transition: all 0.3s ease;
}
/* 点击/按压时的效果 */
.card-1 {
left: 0;
top: 0;
transform: rotate(-3deg);
z-index: 1;
}
.card-2 {
left: 0;
top: 0;
transform: rotate(0deg);
z-index: 2;
}
.card-3 {
left: 0;
top: 0;
transform: rotate(3deg);
z-index: 3;
}
.deck-hover .card-1 {
transform: rotate(-5deg) translateY(-5px);
}
.deck-hover .card-2 {
transform: rotate(0deg) translateY(-8px);
}
.deck-hover .card-3 {
transform: rotate(5deg) translateY(-5px);
}
.deck-title {
color: #fff;
font-size: 20px;
font-weight: bold;
letter-spacing: 2px;
text-shadow: 0 2px 8px rgba(0, 0, 0, 0.5);
}
/* ========== 侧边卡片 ========== */
.side-card {
position: absolute;
width: 90px;
height: 120px;
background: rgba(255, 255, 255, 0.08);
border: 1px solid rgba(255, 255, 255, 0.15);
border-radius: 12px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 10px;
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.3);
transition: all 0.3s ease;
z-index: 5;
}
.card-hover {
background-color: rgba(255, 255, 255, 0.1);
transform: scale(0.98);
transform: translateY(-5px);
box-shadow: 0 12px 35px rgba(233, 69, 96, 0.3);
}
.energy-card {
left: 30px;
top: 50%;
transform: translateY(-50%);
}
.knowledge-card {
right: 30px;
top: 50%;
transform: translateY(-50%);
}
.card-icon {
font-size: 32px;
margin-right: 20px;
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.3));
}
.card-content {
.card-label {
color: rgba(255, 255, 255, 0.9);
font-size: 12px;
letter-spacing: 1px;
}
/* ========== 底部功能抽屉 ========== */
.function-drawer {
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: rgba(0, 0, 0, 0.3);
backdrop-filter: blur(10px);
border-top: 1px solid rgba(255, 255, 255, 0.1);
padding: 15px 20px 25px;
display: flex;
justify-content: space-around;
z-index: 20;
}
.drawer-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
padding: 10px 15px;
border-radius: 12px;
transition: all 0.3s ease;
}
.card-title {
color: #e94560;
.drawer-hover {
background: rgba(255, 255, 255, 0.05);
}
.drawer-icon {
font-size: 24px;
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.2));
}
.drawer-text {
color: rgba(255, 255, 255, 0.8);
font-size: 12px;
letter-spacing: 0.5px;
}
/* ========== 能量卡浮层 ========== */
.overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
opacity: 0;
pointer-events: none;
transition: all 0.3s ease;
}
.overlay.show {
background: rgba(0, 0, 0, 0.7);
opacity: 1;
pointer-events: auto;
}
.overlay-content {
background: linear-gradient(135deg, #2a2a3e 0%, #1a1a2e 100%);
border: 1px solid rgba(255, 255, 255, 0.15);
border-radius: 20px;
padding: 30px;
width: 80%;
max-width: 350px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
transform: scale(0.9);
transition: all 0.3s ease;
}
.overlay.show .overlay-content {
transform: scale(1);
}
.overlay-title {
color: #fff;
font-size: 20px;
font-weight: bold;
margin-bottom: 20px;
display: block;
text-align: center;
}
.overlay-text {
color: rgba(255, 255, 255, 0.85);
font-size: 15px;
line-height: 1.8;
display: block;
margin-bottom: 25px;
text-align: center;
}
.overlay-close {
background: linear-gradient(135deg, #e94560, #8b3a62);
color: #fff;
padding: 12px 30px;
border-radius: 25px;
text-align: center;
font-size: 14px;
font-weight: bold;
box-shadow: 0 4px 15px rgba(233, 69, 96, 0.3);
}
/* ========== 知识卡半屏 ========== */
.panel {
position: fixed;
left: 0;
right: 0;
bottom: 0;
height: 60vh;
background: rgba(0, 0, 0, 0);
z-index: 1000;
opacity: 0;
pointer-events: none;
transition: all 0.3s ease;
}
.panel.show {
background: rgba(0, 0, 0, 0.7);
opacity: 1;
pointer-events: auto;
}
.panel-content {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 55vh;
background: linear-gradient(180deg, #2a2a3e 0%, #1a1a2e 100%);
border-top-left-radius: 25px;
border-top-right-radius: 25px;
padding: 30px 25px;
box-shadow: 0 -10px 40px rgba(0, 0, 0, 0.5);
transform: translateY(100%);
transition: all 0.3s ease;
}
.panel.show .panel-content {
transform: translateY(0);
}
.panel-title {
color: #fff;
font-size: 18px;
font-weight: bold;
letter-spacing: 1px;
margin-bottom: 5px;
margin-bottom: 20px;
display: block;
}
.card-subtitle {
color: #ccc;
font-size: 12px;
opacity: 0.6;
.panel-article-title {
color: #fff;
font-size: 20px;
font-weight: bold;
margin-bottom: 15px;
display: block;
line-height: 1.4;
}
.panel-summary {
color: rgba(255, 255, 255, 0.7);
font-size: 14px;
line-height: 1.7;
display: block;
margin-bottom: 30px;
}
.panel-actions {
display: flex;
gap: 12px;
}
.panel-btn {
flex: 1;
background: linear-gradient(135deg, #e94560, #8b3a62);
color: #fff;
padding: 14px;
border-radius: 12px;
text-align: center;
font-size: 14px;
font-weight: bold;
box-shadow: 0 4px 15px rgba(233, 69, 96, 0.3);
}
.panel-btn.secondary {
background: rgba(255, 255, 255, 0.1);
box-shadow: none;
}

View File

@ -8,6 +8,7 @@ const { getQuestionType } = require('../../utils/questionType');
const { generateSpreadStory } = require('../../utils/spreadStory');
const { validateQuestion, scoreQuestion } = require('../../utils/questionFilter');
const { safeAIRequest } = require('../../utils/aiRequestManager');
const { hasEnough, deductPoints } = require('../../utils/pointsManager');
// --- Prompt 模板定义 ---
@ -158,6 +159,7 @@ const SPREADS = [
"id": "one_card_guidance",
"name": "单张指引",
"cardCount": 1,
"cost": 1,
"positions": ["当下的指引"],
"description": "为你此刻的状态提供一个温和而清晰的方向提示。",
"aiSchema": ["core_theme", "current_state", "action_advice"]
@ -166,6 +168,7 @@ const SPREADS = [
"id": "three_time_flow",
"name": "过去 · 现在 · 未来",
"cardCount": 3,
"cost": 3,
"positions": ["过去", "现在", "未来"],
"description": "帮助你理解事情的发展过程与可能走向。",
"aiSchema": ["core_theme", "current_state", "potential_influence", "action_advice"]
@ -174,6 +177,7 @@ const SPREADS = [
"id": "three_problem_solution",
"name": "问题 · 阻碍 · 建议",
"cardCount": 3,
"cost": 3,
"positions": ["问题核心", "当前阻碍", "行动建议"],
"description": "聚焦关键问题,找出当下最可行的应对方式。",
"aiSchema": ["core_theme", "current_state", "action_advice"]
@ -182,6 +186,7 @@ const SPREADS = [
"id": "two_choice_decision",
"name": "二选一抉择",
"cardCount": 4,
"cost": 4,
"positions": ["选择A的发展", "选择A的结果", "选择B的发展", "选择B的结果"],
"description": "对比两种选择的潜在走向,辅助理性决策。",
"aiSchema": ["core_theme", "potential_influence", "action_advice"]
@ -190,6 +195,7 @@ const SPREADS = [
"id": "five_situation_analysis",
"name": "现状分析",
"cardCount": 5,
"cost": 5,
"positions": ["现状", "内在因素", "外在影响", "行动方向", "可能结果"],
"description": "从内外层面拆解局势,明确下一步行动。",
"aiSchema": ["core_theme", "current_state", "potential_influence", "action_advice"]
@ -198,6 +204,7 @@ const SPREADS = [
"id": "relationship_spread",
"name": "关系洞察",
"cardCount": 5,
"cost": 5,
"positions": ["你的位置", "对方的位置", "关系现状", "隐藏影响", "未来趋势"],
"description": "理解一段关系中的互动模式与发展方向。",
"aiSchema": ["core_theme", "current_state", "potential_influence", "action_advice"]
@ -206,6 +213,7 @@ const SPREADS = [
"id": "timeline_spread",
"name": "时间之流",
"cardCount": 5,
"cost": 5,
"positions": ["远古根源", "过去影响", "当下状态", "近期发展", "未来趋势"],
"description": "追溯事件的时间线,看清发展脉络。",
"aiSchema": ["core_theme", "current_state", "potential_influence", "action_advice"]
@ -214,6 +222,7 @@ const SPREADS = [
"id": "diamond_spread",
"name": "钻石牌阵",
"cardCount": 5,
"cost": 5,
"positions": ["问题本质", "过去原因", "未来发展", "外部资源", "最佳行动"],
"description": "多角度剖析问题,找到解决之道。",
"aiSchema": ["core_theme", "current_state", "potential_influence", "action_advice"]
@ -222,6 +231,7 @@ const SPREADS = [
"id": "spiritual_guidance",
"name": "灵性指引",
"cardCount": 7,
"cost": 7,
"positions": ["当前能量", "内在阻碍", "潜在天赋", "灵性课题", "指导建议", "未来机遇", "最高指引"],
"description": "深入探索内在世界,获得灵性层面的启发。",
"aiSchema": ["core_theme", "current_state", "potential_influence", "action_advice"]
@ -230,6 +240,7 @@ const SPREADS = [
"id": "horseshoe_spread",
"name": "马蹄铁牌阵",
"cardCount": 7,
"cost": 7,
"positions": ["过去", "现在", "未来", "你的态度", "他人影响", "障碍", "最终结果"],
"description": "全面了解情况的来龙去脉与未来走向。",
"aiSchema": ["core_theme", "current_state", "potential_influence", "action_advice"]
@ -238,6 +249,7 @@ const SPREADS = [
"id": "celtic_cross",
"name": "凯尔特十字",
"cardCount": 10,
"cost": 10,
"positions": ["现状", "挑战", "根基", "过去", "可能性", "近期未来", "你的态度", "外部影响", "希望与恐惧", "最终结果"],
"description": "最经典的综合牌阵,深度解析生命议题。",
"aiSchema": ["core_theme", "current_state", "potential_influence", "action_advice"]
@ -246,6 +258,7 @@ const SPREADS = [
"id": "tree_of_life",
"name": "生命之树",
"cardCount": 10,
"cost": 10,
"positions": ["王冠", "智慧", "理解", "慈悲", "严厉", "美丽", "胜利", "荣耀", "基础", "王国"],
"description": "基于卡巴拉生命之树的深度灵性探索。",
"aiSchema": ["core_theme", "current_state", "potential_influence", "action_advice"]
@ -492,6 +505,18 @@ Page({
selectSpread: function (e) {
const id = e.currentTarget.dataset.id;
const spread = this.data.spreads.find(s => s.id === id);
// 检查积分是否足够
if (!hasEnough(spread.cost)) {
wx.showModal({
title: '积分不足',
content: `当前牌阵需要 ${spread.cost} 积分,您的积分不足,请稍后再来`,
showCancel: false,
confirmText: '知道了'
});
return;
}
this.setData({
selectedSpread: spread,
state: 'asking' // 先进入提问状态
@ -597,7 +622,7 @@ Page({
// --- 3. 确认开启 ---
confirmDraw: function () {
const { drawnCardIndices, allCards } = this.data;
const { drawnCardIndices, allCards, selectedSpread } = this.data;
const drawnCards = drawnCardIndices.map(() => {
const randomIndex = Math.floor(Math.random() * allCards.length);
const card = Object.assign({}, allCards[randomIndex]);
@ -609,6 +634,10 @@ Page({
return card;
});
// 扣除积分(只扣一次)
deductPoints(selectedSpread.cost);
console.log(`[积分系统] 占卜消耗 ${selectedSpread.cost} 积分`);
this.setData({
drawnCards,
state: 'flipping',

87
utils/dailyAdvice.js Normal file
View File

@ -0,0 +1,87 @@
/**
* 每日建议生成器
* 基于日期生成随机建议确保同一天返回相同内容
*/
const adviceList = [
// 情感类
"今天适合倾听内心的声音,答案就在你心中",
"保持开放的心态,接纳新的可能性",
"关注当下的感受,而不是未来的焦虑",
"真诚地面对自己,才能看清真相",
"给自己一些温柔,你已经做得很好了",
// 行动类
"今天是行动的好时机,迈出第一步",
"相信直觉,它会为你指引方向",
"放下犹豫,勇敢地做出选择",
"专注于你能控制的事情",
"小步前进,也是一种进步",
// 成长类
"每个挑战都是成长的机会",
"接纳不完美,这是成长的一部分",
"从过去的经验中学习,但不要被困住",
"保持好奇心,探索未知的领域",
"给自己时间,改变需要过程",
// 关系类
"真诚的沟通能化解许多误会",
"尊重他人的选择,也尊重自己的边界",
"倾听比说服更重要",
"关系需要双方的努力和理解",
"给彼此一些空间,距离产生美",
// 平静类
"深呼吸,让内心平静下来",
"放下执念,顺其自然",
"不必急于寻找答案,时机到了自然会明白",
"享受当下的宁静时刻",
"有些事情需要时间,耐心等待",
// 力量类
"你比自己想象的更强大",
"相信自己的判断力",
"困难只是暂时的,你能度过",
"你拥有改变现状的力量",
"勇敢地表达自己的需求"
];
/**
* 获取今日建议
* @returns {string} 今日建议文本
*/
function getDailyAdvice() {
try {
// 获取今天的日期字符串格式YYYY-MM-DD
const today = new Date();
const dateStr = `${today.getFullYear()}-${(today.getMonth() + 1).toString().padStart(2, '0')}-${today.getDate().toString().padStart(2, '0')}`;
// 使用日期字符串生成一个简单的哈希值作为随机种子
let hash = 0;
for (let i = 0; i < dateStr.length; i++) {
hash = ((hash << 5) - hash) + dateStr.charCodeAt(i);
hash = hash & hash; // Convert to 32bit integer
}
// 使用哈希值选择建议
const index = Math.abs(hash) % adviceList.length;
return adviceList[index];
} catch (error) {
console.error('[每日建议] 生成失败:', error);
return adviceList[0]; // 返回默认建议
}
}
/**
* 获取所有建议列表用于测试或展示
* @returns {Array<string>} 建议列表
*/
function getAllAdvice() {
return adviceList;
}
module.exports = {
getDailyAdvice,
getAllAdvice
};

View File

@ -342,9 +342,52 @@ function getArticleById(id) {
return knowledgeData.find(item => item.id === id);
}
// 获取随机文章
function getRandomArticle() {
const randomIndex = Math.floor(Math.random() * knowledgeData.length);
return knowledgeData[randomIndex];
}
// 获取随机本地文章(优先本地内容)
function getRandomLocalArticle() {
const localArticles = knowledgeData.filter(item => item.type === 'local');
if (localArticles.length === 0) {
return getRandomArticle(); // 如果没有本地文章,返回任意文章
}
const randomIndex = Math.floor(Math.random() * localArticles.length);
return localArticles[randomIndex];
}
// 基于日期获取今日文章(确保同一天返回相同文章)
function getDailyArticle() {
try {
const today = new Date();
const dateStr = `${today.getFullYear()}-${(today.getMonth() + 1).toString().padStart(2, '0')}-${today.getDate().toString().padStart(2, '0')}`;
// 使用日期字符串生成哈希值
let hash = 0;
for (let i = 0; i < dateStr.length; i++) {
hash = ((hash << 5) - hash) + dateStr.charCodeAt(i);
hash = hash & hash;
}
// 优先使用本地文章
const localArticles = knowledgeData.filter(item => item.type === 'local');
const articles = localArticles.length > 0 ? localArticles : knowledgeData;
const index = Math.abs(hash) % articles.length;
return articles[index];
} catch (error) {
console.error('[每日文章] 获取失败:', error);
return knowledgeData[0];
}
}
module.exports = {
knowledgeData,
getCategories,
getArticlesByCategory,
getArticleById
getArticleById,
getRandomArticle,
getRandomLocalArticle,
getDailyArticle
};

299
utils/pointsManager.js Normal file
View File

@ -0,0 +1,299 @@
/**
* 积分管理模块
* 提供积分的初始化查询增减校验等功能
* 使用微信小程序本地存储实现
*/
const STORAGE_KEY_POINTS = 'tarot_points';
const STORAGE_KEY_LAST_LOGIN = 'tarot_last_login';
const STORAGE_KEY_AD_DATE = 'tarot_ad_date';
const STORAGE_KEY_AD_COUNT = 'tarot_ad_count';
const DEFAULT_POINTS = 20; // 新用户默认积分
const DAILY_REWARD = 3; // 每日登录奖励
// 积分来源常量
const POINT_SOURCE = {
DAILY_LOGIN: 'daily_login',
TAROT_USE: 'tarot_use',
KNOWLEDGE_READ: 'knowledge_read',
AD_REWARD: 'ad_reward'
};
// 广告奖励配置
const AD_REWARD_CONFIG = {
REWARD_POINTS: 5, // 每次奖励积分
DAILY_LIMIT: 3 // 每日上限次数
};
/**
* 初始化积分系统
* 如果用户是首次使用设置默认积分
*/
function initPoints() {
try {
const points = wx.getStorageSync(STORAGE_KEY_POINTS);
if (points === '' || points === null || points === undefined) {
wx.setStorageSync(STORAGE_KEY_POINTS, DEFAULT_POINTS);
console.log(`[积分系统] 新用户初始化,赋予 ${DEFAULT_POINTS} 积分`);
return DEFAULT_POINTS;
}
return points;
} catch (error) {
console.error('[积分系统] 初始化失败:', error);
return DEFAULT_POINTS;
}
}
/**
* 获取当前积分
* @returns {number} 当前积分数
*/
function getPoints() {
try {
const points = wx.getStorageSync(STORAGE_KEY_POINTS);
if (points === '' || points === null || points === undefined) {
return initPoints();
}
return points;
} catch (error) {
console.error('[积分系统] 获取积分失败:', error);
return 0;
}
}
/**
* 增加积分
* @param {number} num - 要增加的积分数
* @param {string} source - 积分来源可选
* @returns {number} 增加后的积分数
*/
function addPoints(num, source = '') {
try {
if (typeof num !== 'number' || num <= 0) {
console.warn('[积分系统] 无效的增加数值:', num);
return getPoints();
}
const currentPoints = getPoints();
const newPoints = currentPoints + num;
wx.setStorageSync(STORAGE_KEY_POINTS, newPoints);
// 记录积分来源
if (source) {
console.log(`[积分系统] +${num} 积分,来源: ${source},当前: ${newPoints}`);
} else {
console.log(`[积分系统] 增加 ${num} 积分,当前: ${newPoints}`);
}
return newPoints;
} catch (error) {
console.error('[积分系统] 增加积分失败:', error);
return getPoints();
}
}
/**
* 扣除积分
* @param {number} num - 要扣除的积分数
* @returns {number} 扣除后的积分数如果积分不足则返回当前积分
*/
function deductPoints(num) {
try {
if (typeof num !== 'number' || num <= 0) {
console.warn('[积分系统] 无效的扣除数值:', num);
return getPoints();
}
const currentPoints = getPoints();
if (currentPoints < num) {
console.warn(`[积分系统] 积分不足,当前: ${currentPoints},需要: ${num}`);
return currentPoints;
}
const newPoints = currentPoints - num;
wx.setStorageSync(STORAGE_KEY_POINTS, newPoints);
console.log(`[积分系统] 扣除 ${num} 积分,剩余: ${newPoints}`);
return newPoints;
} catch (error) {
console.error('[积分系统] 扣除积分失败:', error);
return getPoints();
}
}
/**
* 检查积分是否足够
* @param {number} num - 需要的积分数
* @returns {boolean} 是否足够
*/
function hasEnough(num) {
try {
if (typeof num !== 'number' || num < 0) {
console.warn('[积分系统] 无效的检查数值:', num);
return false;
}
const currentPoints = getPoints();
return currentPoints >= num;
} catch (error) {
console.error('[积分系统] 检查积分失败:', error);
return false;
}
}
/**
* 检查并发放每日登录奖励
* @returns {object} { rewarded: boolean, points: number, message: string }
*/
function checkDailyReward() {
try {
const today = new Date().toDateString(); // 格式: "Sun Feb 09 2026"
const lastLogin = wx.getStorageSync(STORAGE_KEY_LAST_LOGIN);
// 如果是新的一天
if (lastLogin !== today) {
// 更新最后登录日期
wx.setStorageSync(STORAGE_KEY_LAST_LOGIN, today);
// 如果不是首次登录(首次登录已经有初始积分了)
if (lastLogin !== '' && lastLogin !== null && lastLogin !== undefined) {
const newPoints = addPoints(DAILY_REWARD, POINT_SOURCE.DAILY_LOGIN);
console.log(`[积分系统] 每日登录奖励 +${DAILY_REWARD} 积分`);
return {
rewarded: true,
points: newPoints,
message: `每日登录奖励 +${DAILY_REWARD} 积分`
};
} else {
// 首次登录,只记录日期,不发放奖励
console.log('[积分系统] 首次登录,已记录日期');
return {
rewarded: false,
points: getPoints(),
message: '欢迎使用塔罗占卜'
};
}
}
// 今天已经登录过了
return {
rewarded: false,
points: getPoints(),
message: '今日已签到'
};
} catch (error) {
console.error('[积分系统] 每日奖励检查失败:', error);
return {
rewarded: false,
points: getPoints(),
message: '签到检查失败'
};
}
}
/**
* 获取今日广告观看次数
* @returns {number} 今日已观看次数
*/
function getTodayAdCount() {
try {
const today = new Date().toDateString();
const lastDate = wx.getStorageSync(STORAGE_KEY_AD_DATE) || '';
// 如果不是今天,返回 0
if (lastDate !== today) {
return 0;
}
return wx.getStorageSync(STORAGE_KEY_AD_COUNT) || 0;
} catch (error) {
console.error('[广告系统] 获取广告次数失败:', error);
return 0;
}
}
/**
* 增加广告观看次数
*/
function incrementAdCount() {
try {
const today = new Date().toDateString();
const count = getTodayAdCount();
wx.setStorageSync(STORAGE_KEY_AD_DATE, today);
wx.setStorageSync(STORAGE_KEY_AD_COUNT, count + 1);
console.log(`[广告系统] 今日广告次数: ${count + 1}/${AD_REWARD_CONFIG.DAILY_LIMIT}`);
} catch (error) {
console.error('[广告系统] 增加广告次数失败:', error);
}
}
/**
* 检查是否可以观看广告
* @returns {boolean} 是否可以观看
*/
function canWatchAd() {
return getTodayAdCount() < AD_REWARD_CONFIG.DAILY_LIMIT;
}
/**
* 广告奖励积分核心方法
* 未来只需修改此函数内部逻辑UI 调用方式不变
* @returns {Object} { success, message, points, remainingCount }
*/
function rewardFromAd() {
try {
// 检查每日次数限制
if (!canWatchAd()) {
return {
success: false,
message: '今日广告次数已用完',
points: 0,
remainingCount: 0
};
}
// 🎬 未来在此处接入真实广告 SDK
// 当前版本:直接模拟成功
// 增加积分
const rewardPoints = AD_REWARD_CONFIG.REWARD_POINTS;
addPoints(rewardPoints, POINT_SOURCE.AD_REWARD);
// 增加广告次数
incrementAdCount();
const remaining = AD_REWARD_CONFIG.DAILY_LIMIT - getTodayAdCount();
return {
success: true,
message: `获得 +${rewardPoints} 积分`,
points: rewardPoints,
remainingCount: remaining
};
} catch (error) {
console.error('[广告奖励] 失败:', error);
return {
success: false,
message: '广告奖励失败',
points: 0,
remainingCount: 0
};
}
}
module.exports = {
initPoints,
getPoints,
addPoints,
deductPoints,
hasEnough,
checkDailyReward,
// 广告奖励相关
getTodayAdCount,
canWatchAd,
rewardFromAd,
// 常量导出
POINT_SOURCE,
AD_REWARD_CONFIG
};