446 lines
18 KiB
JavaScript
446 lines
18 KiB
JavaScript
// AI 配置 (建议后续迁移至后端以保证 API Key 安全)
|
||
const DEEPSEEK_API_KEY = 'sk-f659b4c3aa954baaa6cbdbd5cae0b7d1'; // 请在此处替换您的 API Key
|
||
const DEEPSEEK_BASE_URL = 'https://api.deepseek.com/chat/completions';
|
||
|
||
// --- Prompt 模板定义 ---
|
||
|
||
// 1. 基础版模板:平静、清晰、节制
|
||
const TAROT_PROMPT_BASE = `你是一位克制、温和、不制造依赖的塔罗解读者。
|
||
你的角色不是预测未来,而是陪伴当下的人理解此刻的状态。
|
||
|
||
【解读边界】
|
||
- 不给命令,不下结论,不使用“你应该”“结果是”“一定会”
|
||
- 不预测具体事件或时间,不制造焦虑或依赖
|
||
|
||
【语言风格】
|
||
- 平静、清晰、节制
|
||
- 使用“可能 / 正在 / 也许 / 可以感受到”
|
||
- 像是轻声说明,而不是说服或劝导,避免情绪渲染与神秘化表达
|
||
|
||
【解读结构】
|
||
1. 用 1–2 句话描绘这张牌当下呈现的状态氛围
|
||
2. 结合关键词与核心含义,解释它可能对应的内在状态
|
||
3. 用一句自然收口的话结束,不引导行动
|
||
|
||
【输出要求】
|
||
- 总字数 120–180 字
|
||
- 只输出解读文本,不使用标题、编号、分段符号`;
|
||
|
||
// 2. 深夜版模板:更慢、更柔和、允许留白
|
||
const TAROT_PROMPT_NIGHT = `你是一位安静、克制、不制造依赖的塔罗解读者。
|
||
此刻的解读更像一次低声的陪伴,而不是解释或分析。
|
||
|
||
【解读边界】
|
||
- 不给建议,不下判断,不使用“你应该”“结果是”“接下来会”
|
||
- 不预测未来事件或变化,不放大情绪,不渲染痛苦
|
||
|
||
【语言风格】
|
||
- 更慢、更柔和
|
||
- 使用“也许 / 正在 / 像是 / 仿佛”
|
||
- 允许留白,不急着说清楚一切,文字像夜晚一样安静
|
||
|
||
【解读结构】
|
||
1. 先用一句贴近当下感受的描述开头
|
||
2. 围绕关键词展开,但重点放在“感受本身”
|
||
3. 用一句轻微、可停住的句子结束,不指向下一步
|
||
|
||
【输出要求】
|
||
- 总字数 100–160 字
|
||
- 不使用感叹号,不使用列表、解释性语句
|
||
- 只输出解读文本`;
|
||
|
||
Page({
|
||
data: {
|
||
todayDate: '', // 用于存储今天的日期
|
||
// 1. 状态管理: idle, focusing, flipping, revealed
|
||
state: 'idle',
|
||
currentCard: null,
|
||
isRevealed: false,
|
||
explanation: '',
|
||
cardBackImage: '/images/beimian.png',
|
||
|
||
// 动画数据
|
||
deckAnimation: {},
|
||
cardAnimation: {},
|
||
infoAnimation: {},
|
||
guideAnimation: {},
|
||
|
||
// AI 相关
|
||
isAiLoading: false,
|
||
aiExplanation: '',
|
||
|
||
// 3. 前缀文案列表(随机抽取)
|
||
prefixList: [
|
||
"今天,这张牌提醒你:",
|
||
"此刻,宇宙传递的信息:",
|
||
"这张牌想告诉你:",
|
||
"请收下这份指引:"
|
||
],
|
||
|
||
// 2. 完整的 22 张大阿尔卡那牌组(包含逆位含义)
|
||
cardList: [
|
||
{
|
||
name: "愚者",
|
||
keyword: "新的开始",
|
||
meaning: "放下恐惧,勇敢迈出第一步,世界充满无限可能。",
|
||
reversedMeaning: "也许有些太冲动了?停下来检查一下背包,确保安全再出发。",
|
||
energy: "外放",
|
||
image: "/images/yuzhe.png"
|
||
},
|
||
{
|
||
name: "魔术师",
|
||
keyword: "创造力",
|
||
meaning: "你拥有实现目标所需的一切资源,现在是行动的时候。",
|
||
reversedMeaning: "你是否低估了自己的能力?或者在犹豫不决?相信自己,工具就在手边。",
|
||
energy: "外放",
|
||
image: "/images/moshushi.png"
|
||
},
|
||
{
|
||
name: "女祭司",
|
||
keyword: "直觉",
|
||
meaning: "倾听内心的声音,答案就在你潜意识的深处。",
|
||
reversedMeaning: "暂时听不见内心的声音了吗?别急着向外寻找,先让自己静下来。",
|
||
energy: "内敛",
|
||
image: "/images/nvjishi.png"
|
||
},
|
||
{
|
||
name: "皇后",
|
||
keyword: "丰饶",
|
||
meaning: "享受生活的美好,去创造、去关爱,收获也是一种能力。",
|
||
reversedMeaning: "是否忽略了对自己的关爱?或者有些过度保护?给彼此一点呼吸的空间。",
|
||
energy: "柔和",
|
||
image: "/images/nvhuang.png"
|
||
},
|
||
{
|
||
name: "皇帝",
|
||
keyword: "秩序",
|
||
meaning: "建立规则和结构,理性地掌控局面,承担起责任。",
|
||
reversedMeaning: "有时候太固执会让人疲惫,适度放权,弹性也是一种力量。",
|
||
energy: "坚实",
|
||
image: "/images/huangdi.png"
|
||
},
|
||
{
|
||
name: "教皇",
|
||
keyword: "指引",
|
||
meaning: "寻求传统智慧的帮助,或者成为他人的精神导师。",
|
||
reversedMeaning: "不必拘泥于陈规旧矩,试着打破常规,寻找适合你自己的那条路。",
|
||
energy: "庄重",
|
||
image: "/images/jiaohuang.png"
|
||
},
|
||
{
|
||
name: "恋人",
|
||
keyword: "选择",
|
||
meaning: "跟随你的心,在关系中寻找和谐,做出忠于自我的决定。",
|
||
reversedMeaning: "现在的关系是否有些失衡?先爱自己,才能更好地爱别人。",
|
||
energy: "柔和",
|
||
image: "/images/lianren.png"
|
||
},
|
||
{
|
||
name: "战车",
|
||
keyword: "意志",
|
||
meaning: "认准目标,克服相反的力量,胜利属于坚持到底的人。",
|
||
reversedMeaning: "感觉失去了方向控制?慢下来,重新调整导航,欲速则不达。",
|
||
energy: "激进",
|
||
image: "/images/zhanche.png"
|
||
},
|
||
{
|
||
name: "力量",
|
||
keyword: "勇气",
|
||
meaning: "真正的力量是温柔的坚持,用耐心驯服内心的野兽。",
|
||
reversedMeaning: "不要怀疑自己的韧性,面对内心的软弱并不是坏事,那是温柔的开始。",
|
||
energy: "柔和",
|
||
image: "/images/liliang.png"
|
||
},
|
||
{
|
||
name: "隐士",
|
||
keyword: "内省",
|
||
meaning: "暂时远离喧嚣,向内探索,寻找属于你自己的光。",
|
||
reversedMeaning: "`是不是一个人待太久了?试着走出洞穴,和外界建立一点连接吧。",
|
||
energy: "潜沉",
|
||
image: "/images/yinzhe.png"
|
||
},
|
||
{
|
||
name: "命运之轮",
|
||
keyword: "转折",
|
||
meaning: "改变是永恒的,顺势而为,抓住出现在你面前的机会。",
|
||
reversedMeaning: "运气暂时不在你这边,但没关系,低谷正是蓄力反弹的好时机。",
|
||
energy: "流转",
|
||
image: "/images/mingyunzhilun.png"
|
||
},
|
||
{
|
||
name: "正义",
|
||
keyword: "因果",
|
||
meaning: "诚实面对真相,所有的决定都会带来相应的结果。",
|
||
reversedMeaning: "即使结果不尽如人意,也要问心无愧。对自己诚实,比什么都重要。",
|
||
energy: "平衡",
|
||
image: "/images/zhengyi.png"
|
||
},
|
||
{
|
||
name: "倒吊人",
|
||
keyword: "换位",
|
||
meaning: "换一个角度看世界,有时候牺牲是为了更大的获得。",
|
||
reversedMeaning: "挣扎只会更累,不如彻底放松,换个角度看世界,也许心结就解开了。",
|
||
energy: "停滞",
|
||
image: "/images/daodiaoren.png"
|
||
},
|
||
{
|
||
name: "死神",
|
||
keyword: "新生",
|
||
meaning: "结束是为了新的开始,彻底告别过去,才能拥抱未来。",
|
||
reversedMeaning: "还在紧抓着过去不放吗?只有腾出双手,才能接住新的礼物。",
|
||
energy: "决绝",
|
||
image: "/images/sishen.png"
|
||
},
|
||
{
|
||
name: "节制",
|
||
keyword: "平衡",
|
||
meaning: "在极端之间寻找中庸之道,融合对立,通过耐心获得疗愈。",
|
||
reversedMeaning: "是不是有些失衡了?在这忙乱的世界里,找回你内心的节奏和中点。",
|
||
energy: "融合",
|
||
image: "/images/jiezhi.png"
|
||
},
|
||
{
|
||
name: "恶魔",
|
||
keyword: "束缚",
|
||
meaning: "觉察那些控制你的欲望或习惯,唯有觉知才能带来解脱。",
|
||
reversedMeaning: "别被眼前的诱惑蒙蔽,或者感觉被束缚。其实锁链是松的,你随时可以走。",
|
||
energy: "沉重",
|
||
image: "/images/emo.png"
|
||
},
|
||
{
|
||
name: "高塔",
|
||
keyword: "崩塌",
|
||
meaning: "不要害怕突如其来的剧变,它在摧毁虚假的根基,让你重建真实。",
|
||
reversedMeaning: "废墟中反而能看清地基。既然倒了,正好可以按照你真正想要的样子重建。",
|
||
energy: "剧烈",
|
||
image: "/images/ta.png"
|
||
},
|
||
{
|
||
name: "星星",
|
||
keyword: "希望",
|
||
meaning: "黑暗之后必有星光,保持信心,未来充满治愈与灵感。",
|
||
reversedMeaning: "暂时看不见星星也没关系,它们还在那里。给自己一点信心,黎明就在前方。",
|
||
energy: "明亮",
|
||
image: "/images/xingxing.png"
|
||
},
|
||
{
|
||
name: "月亮",
|
||
keyword: "幻象",
|
||
meaning: "直面内心的不安与迷茫,并非所有事情都如表象般真实。",
|
||
reversedMeaning: "恐惧只是长长的影子,别被它吓住。随着天亮,迷雾终会散去。",
|
||
energy: "幽静",
|
||
image: "/images/yueliang.png"
|
||
},
|
||
{
|
||
name: "太阳",
|
||
keyword: "喜悦",
|
||
meaning: "快乐、成功与活力,尽情地发光热,享受当下的幸福。",
|
||
reversedMeaning: "乌云暂时遮住了阳光,但这只是暂时的。保持乐观,你心里的光谁也偷不走。",
|
||
energy: "灼热",
|
||
image: "/images/taiyang.png"
|
||
},
|
||
{
|
||
name: "审判",
|
||
keyword: "觉醒",
|
||
meaning: "听到内心的召唤,回顾过往,做出决定,获得新生。",
|
||
reversedMeaning: "不要对他人的评价太敏感,也不要自我怀疑。如果你准备好了,就出发吧。",
|
||
energy: "回响",
|
||
image: "/images/shenpan.png"
|
||
},
|
||
{
|
||
name: "世界",
|
||
keyword: "圆满",
|
||
meaning: "一段旅程的完美终点,成就感与整合,准备开始新的循环。",
|
||
reversedMeaning: "机遇还没完全成熟,再耐心一点点。检查一下还有什么细节没完成?",
|
||
energy: "圆融",
|
||
meaning: "一段旅程的完美终点,成就感与整合,准备开始新的循环。",
|
||
reversedMeaning: "机遇还没完全成熟,再耐心一点点。检查一下还有什么细节没完成?",
|
||
image: "/images/shijie.png"
|
||
}
|
||
]
|
||
},
|
||
|
||
onLoad: function () {
|
||
// 获取当前日期
|
||
const now = new Date();
|
||
const year = now.getFullYear();
|
||
const month = (now.getMonth() + 1).toString().padStart(2, '0');
|
||
const day = now.getDate().toString().padStart(2, '0');
|
||
const dateStr = `${year}-${month}-${day}`;
|
||
|
||
this.setData({
|
||
todayDate: dateStr
|
||
});
|
||
|
||
this.startIdleAnimation();
|
||
console.log('塔罗牌(完整版 22 张 + 逆位文案)已经洗好了...')
|
||
},
|
||
|
||
// --- 待机呼吸动画 ---
|
||
startIdleAnimation: function () {
|
||
const animation = wx.createAnimation({
|
||
duration: 2000,
|
||
timingFunction: 'ease-in-out',
|
||
});
|
||
|
||
const next = () => {
|
||
if (this.data.state !== 'idle' && this.data.state !== 'focusing') return;
|
||
animation.translateY(4).step();
|
||
animation.translateY(0).step();
|
||
this.setData({
|
||
deckAnimation: animation.export()
|
||
});
|
||
};
|
||
|
||
next();
|
||
this.idleTimer = setInterval(next, 4000);
|
||
},
|
||
|
||
// --- 抽一张牌:进入 focusing 状态 ---
|
||
drawCard: function () {
|
||
if (this.data.state !== 'idle' && this.data.state !== 'revealed') return;
|
||
|
||
// 停止之前的呼吸动画计时器(如果有)
|
||
if (this.idleTimer) clearInterval(this.idleTimer);
|
||
|
||
this.setData({
|
||
state: 'focusing',
|
||
currentCard: null,
|
||
isRevealed: false,
|
||
explanation: ''
|
||
});
|
||
|
||
// 引导文案淡入
|
||
const guideAnim = wx.createAnimation({ duration: 500, timingFunction: 'ease' });
|
||
guideAnim.opacity(1).step();
|
||
this.setData({ guideAnimation: guideAnim.export() });
|
||
|
||
// 停顿 500-800ms 后进入 flipping
|
||
setTimeout(() => {
|
||
this.startFlipping();
|
||
}, 800);
|
||
},
|
||
|
||
// --- 开始翻牌流程 ---
|
||
startFlipping: function () {
|
||
const allCards = this.data.cardList;
|
||
const randomIndex = Math.floor(Math.random() * allCards.length);
|
||
const selected = Object.assign({}, allCards[randomIndex]);
|
||
selected.isReversed = Math.random() >= 0.5;
|
||
|
||
const prefixes = this.data.prefixList;
|
||
const randomPrefixIndex = Math.floor(Math.random() * prefixes.length);
|
||
const selectedPrefix = prefixes[randomPrefixIndex];
|
||
|
||
this.setData({
|
||
state: 'flipping',
|
||
currentCard: selected,
|
||
explanation: `${selectedPrefix}${selected.isReversed ? selected.reversedMeaning : selected.meaning}`
|
||
});
|
||
|
||
// 卡牌抬起并旋转的动画
|
||
const cardAnim = wx.createAnimation({
|
||
duration: 500,
|
||
timingFunction: 'ease-in-out'
|
||
});
|
||
|
||
// 轻微抬起 (Y -10~-15px) 和旋转 (≤ 3°)
|
||
cardAnim.translateY(-12).rotate(Math.random() * 6 - 3).step();
|
||
this.setData({ cardAnimation: cardAnim.export() });
|
||
|
||
// 翻转动画是在 WXML 中通过 CSS 类控制的,这里保持一致
|
||
setTimeout(() => {
|
||
this.setData({ isRevealed: true });
|
||
|
||
// 翻牌完成后留白 300-500ms
|
||
setTimeout(() => {
|
||
this.setData({ state: 'revealed' });
|
||
this.showInterpretation();
|
||
}, 500);
|
||
}, 500);
|
||
},
|
||
|
||
// --- 展示解读内容 ---
|
||
showInterpretation: function () {
|
||
const infoAnim = wx.createAnimation({
|
||
duration: 800,
|
||
timingFunction: 'ease'
|
||
});
|
||
infoAnim.opacity(1).translateY(0).step();
|
||
this.setData({ infoAnimation: infoAnim.export() });
|
||
|
||
// 异步获取 AI 解读
|
||
this.fetchAiInterpretation();
|
||
},
|
||
|
||
// --- 获取 AI 解读主逻辑 (DeepSeek 直接调用版) ---
|
||
fetchAiInterpretation: function () {
|
||
const card = this.data.currentCard;
|
||
if (!card) return;
|
||
|
||
this.setData({
|
||
isAiLoading: true,
|
||
aiExplanation: ''
|
||
});
|
||
|
||
// 1. 构建结构化 Prompt
|
||
const position = card.isReversed ? '逆位' : '正位';
|
||
const core = card.isReversed ? card.reversedMeaning : card.meaning;
|
||
|
||
// 当前版本统一使用基础解读 Prompt (TAROT_PROMPT_BASE)
|
||
const systemPrompt = TAROT_PROMPT_BASE;
|
||
|
||
const userPrompt = `请基于以下信息给出解读:
|
||
牌名:${card.name}
|
||
位置:${position}
|
||
关键词:${card.keyword}
|
||
一句话核心解读:${core}
|
||
能量倾向:${card.energy}`;
|
||
|
||
console.log("正在通过 DeepSeek 生成解读...");
|
||
|
||
// 2. 调用 DeepSeek API
|
||
wx.request({
|
||
url: DEEPSEEK_BASE_URL,
|
||
method: 'POST',
|
||
header: {
|
||
'Authorization': `Bearer ${DEEPSEEK_API_KEY}`,
|
||
'Content-Type': 'application/json'
|
||
},
|
||
data: {
|
||
model: "deepseek-chat",
|
||
messages: [
|
||
{ role: "system", content: systemPrompt },
|
||
{ role: "user", content: userPrompt }
|
||
],
|
||
stream: false,
|
||
temperature: 0.7
|
||
},
|
||
timeout: 15000,
|
||
success: (res) => {
|
||
if (res.data && res.data.choices && res.data.choices[0]) {
|
||
const aiResult = res.data.choices[0].message.content;
|
||
this.setData({
|
||
aiExplanation: aiResult,
|
||
isAiLoading: false
|
||
});
|
||
} else {
|
||
console.warn("DeepSeek 返回异常,执行回退");
|
||
this.handleAiFallback();
|
||
}
|
||
},
|
||
fail: (err) => {
|
||
console.error("DeepSeek 请求失败:", err);
|
||
this.handleAiFallback();
|
||
}
|
||
});
|
||
},
|
||
|
||
// --- AI 解读失败后的回退处理 ---
|
||
handleAiFallback: function () {
|
||
this.setData({
|
||
aiExplanation: this.data.explanation,
|
||
isAiLoading: false
|
||
});
|
||
},
|
||
}) |