diff --git a/pages/home/home.js b/pages/home/home.js index bbb74ab..9db7097 100644 --- a/pages/home/home.js +++ b/pages/home/home.js @@ -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 + }); + } } -}) +}); diff --git a/pages/home/home.wxml b/pages/home/home.wxml index 4e82e62..a5ca5ff 100644 --- a/pages/home/home.wxml +++ b/pages/home/home.wxml @@ -1,29 +1,67 @@ - - - - 今天, - 你想从哪里获得指引? + + + + 🎯 + {{currentPoints}} - - - - - - 🔮 - - 塔罗指引 - 探索潜意识的答案 + + + + + + + + + 开始抽牌 + + + + + 💫 + 今日能量 + + + + + + 今日知识 + + + + + + 🎴 + 常用牌阵 + + + 📚 + 学点塔罗 + + + 🎯 + 积分说明 + + + + + + + 💫 今日能量 + {{dailyAdvice}} + 关闭 + + + + + + + ✨ 今日小知识 + {{dailyArticle.title}} + {{dailyArticle.summary}} + + 查看详情 + 关闭 - - - - 📚 - - 学点塔罗 - 了解塔罗基础知识 - - - diff --git a/pages/home/home.wxss b/pages/home/home.wxss index 7ae3bbe..fb50c27 100644 --- a/pages/home/home.wxss +++ b/pages/home/home.wxss @@ -1,89 +1,349 @@ page { - background-color: #1a1a2e; + background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f0f23 100%); height: 100%; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; + overflow: hidden; } -.container { - display: flex; - flex-direction: column; +/* ========== 主容器 ========== */ +.tarot-table { 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; + height: 100vh; + position: relative; display: flex; align-items: center; + justify-content: center; +} + +/* ========== 右上角积分徽章 ========== */ +.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; } diff --git a/pages/index/index.js b/pages/index/index.js index 317cdf0..e9991f6 100644 --- a/pages/index/index.js +++ b/pages/index/index.js @@ -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', diff --git a/utils/dailyAdvice.js b/utils/dailyAdvice.js new file mode 100644 index 0000000..7d97285 --- /dev/null +++ b/utils/dailyAdvice.js @@ -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} 建议列表 + */ +function getAllAdvice() { + return adviceList; +} + +module.exports = { + getDailyAdvice, + getAllAdvice +}; diff --git a/utils/knowledgeData.js b/utils/knowledgeData.js index 395d9eb..a491783 100644 --- a/utils/knowledgeData.js +++ b/utils/knowledgeData.js @@ -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 }; diff --git a/utils/pointsManager.js b/utils/pointsManager.js new file mode 100644 index 0000000..d55612d --- /dev/null +++ b/utils/pointsManager.js @@ -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 +};