const EXIT_THRESHOLD = 80; const FAST_SWIPE_VELOCITY = 0.5; // 加载文案库 const LOADING_QUOTES = [ "正在翻阅《古今集成》...", "于诗书中寻觅意境...", "聆听平仄的韵律...", "在字里行间游走...", "采撷一缕清风入名...", "品味楚辞的芬芳...", "捕捉诗经的灵光...", "与古人隔空对话..." ]; Page({ isDragging: false, isExiting: false, startTime: 0, startX: 0, startY: 0, lastX: 0, loadingQuoteTimer: null, data: { nameList: [], currentIndex: 0, isLoading: true, isFlipped: false, keyword: '清冷', mode: 'classic', modeName: '拾遗', surname: '', loadingQuote: '正在翻阅《古今集成》...', // 动画控制 translateX: 0, translateY: 0, rotate: 0, opacity: 1, transition: 'none', cardKey: 0, collectedNames: [], showCollection: false, // 海报相关 showPoster: false, posterNameChars: [], // 音频相关 showMusicMenu: false, currentAmbience: 'silent', // 字源弹窗 showCharDetail: false, selectedChar: '', charDetailData: null, // AI 解析弹窗 showAIModal: false, aiContext: '', aiExplanation: '', aiLoading: false, // 结语卡片 showEndingCard: false }, onLoad(options) { // 解码 URL 参数 const keyword = options.keyword ? decodeURIComponent(options.keyword) : this.data.keyword; const mode = options.mode || 'classic'; const surname = options.surname ? decodeURIComponent(options.surname) : ''; // 模式名称映射 const modeNameMap = { 'baby': '宝宝', 'persona': '人设', 'classic': '拾遗' }; this.setData({ keyword, mode, surname, modeName: modeNameMap[mode] || '拾遗', isLoading: true }); // 启动文案轮换 this.startLoadingQuoteRotation(); // 分批加载:先请求2个,再静默加载剩余 this.fetchNamesBatch(keyword, mode, surname); }, onUnload() { // 清理定时器 if (this.loadingQuoteTimer) { clearInterval(this.loadingQuoteTimer); this.loadingQuoteTimer = null; } }, onShow() { // 每次显示页面时加载收藏列表 this.loadCollectedNames(); }, // 从后端加载收藏列表 loadCollectedNames() { const openid = getApp().getOpenid(); if (!openid) { console.log('openid 未获取到,跳过加载收藏'); return; } const apiBaseUrl = getApp().globalData.apiBaseUrl; wx.request({ url: `${apiBaseUrl}/api/favorites/list`, data: { openid }, success: (res) => { if (res.data && res.data.success) { this.setData({ collectedNames: res.data.data || [] }); } } }); }, // 启动加载文案轮换 startLoadingQuoteRotation() { let index = 0; this.loadingQuoteTimer = setInterval(() => { index = (index + 1) % LOADING_QUOTES.length; this.setData({ loadingQuote: LOADING_QUOTES[index] }); }, 2000); }, // 分批加载名字 fetchNamesBatch(keyword, mode, surname) { // 第一波:请求2个名字,快速展示 this.fetchFirstBatch(keyword, mode, surname, 2); }, // 第一波:快速获取前5个名字 fetchFirstBatch(keyword, mode, surname, count) { const apiBaseUrl = getApp().globalData.apiBaseUrl; const openid = getApp().getOpenid(); // 获取已生成的名字列表(用于去重) const existingNames = this.data.nameList.map(card => card.name).join(','); const requestData = { keyword, count: 5, batch: 'first' }; if (mode) requestData.mode = mode; if (surname) requestData.surname = surname; if (openid) requestData.openid = openid; if (existingNames) requestData.excludeNames = existingNames; wx.request({ url: `${apiBaseUrl}/api/names/generate`, data: requestData, success: (res) => { // 检查是否积分不足 if (res.data && !res.data.success) { wx.showToast({ title: res.data.message || '次数已用完', icon: 'none' }); this.setData({ isLoading: false }); return; } const nameList = res.data && res.data.data ? res.data.data : res.data; if (res.statusCode === 200 && nameList && nameList.length > 0) { // 清除定时器 if (this.loadingQuoteTimer) { clearInterval(this.loadingQuoteTimer); this.loadingQuoteTimer = null; } // 立即展示结果(5个名字) this.setData({ nameList: nameList, currentIndex: 0, isLoading: false, cardKey: this.data.cardKey + 1 }); // 呼吸震动效果 - 名字逐字显现时的节奏震动 this.breatheVibrate(nameList[0].name); console.log('已加载 5 个不重复名字:', nameList.map(n => n.name).join(', ')); } else { this.handleFetchError('意境未达,请重试'); } }, fail: () => { this.handleFetchError('网络疏离,请检查后端'); } }); }, // 处理加载错误 - 显示结语卡片 handleFetchError(message) { if (this.loadingQuoteTimer) { clearInterval(this.loadingQuoteTimer); this.loadingQuoteTimer = null; } this.setData({ isLoading: false, showEndingCard: true }); }, // 返回首页 onBack() { wx.navigateBack({ success: () => { // 返回成功后,首页的 onShow 会重置状态 } }); }, // 点击不喜欢按钮 onDislike() { if (this.isExiting) return; this.isExiting = true; getApp().playAudio('swipe'); this.setData({ translateX: -500, opacity: 0, transition: 'all 0.3s cubic-bezier(0.6, -0.28, 0.735, 0.045)' }); }, // 点击喜欢按钮 onLike() { if (this.isExiting) return; this.handleLike(); this.isExiting = true; this.setData({ translateX: 500, opacity: 0, transition: 'all 0.3s cubic-bezier(0.6, -0.28, 0.735, 0.045)' }); }, onFlip() { // 守卫:如果这是一次拖拽,isDragging 会为 true,则不执行翻转 if (this.isDragging || this.isExiting) return; getApp().playAudio('flip'); this.setData({ isFlipped: !this.data.isFlipped }); }, onTouchStart(e) { if (this.isExiting) return; this.isDragging = false; // 每次开始触摸时,都假定为点击,而非拖拽 this.startX = e.touches[0].clientX; this.lastX = this.startX; this.startY = e.touches[0].clientY; this.startTime = Date.now(); this.setData({ transition: 'none' }); }, onTouchMove(e) { if (this.isExiting) return; const deltaX = e.touches[0].clientX - this.startX; const deltaY = e.touches[0].clientY - this.startY; // 检测上划进入广场(在Y轴移动超过X轴,且向上滑动) if (Math.abs(deltaY) > Math.abs(deltaX) && deltaY < -50 && !this.isDragging) { this.isDragging = true; this.isSwipingUp = true; this.setData({ translateY: deltaY, opacity: 1 - Math.abs(deltaY) / 400, transition: 'none' }); return; } // 只有移动超过5px,才真正判定为"拖拽" if (Math.abs(deltaX) > 5) { this.isDragging = true; } // 如果不是拖拽,则不进行任何移动 if (!this.isDragging) return; this.lastX = e.touches[0].clientX; const rotate = deltaX * 0.05; const opacity = 1 - Math.abs(deltaX) / 200; this.setData({ translateX: deltaX, rotate: rotate, opacity: opacity }); }, onTouchEnd() { if (this.isExiting) return; // 如果不是拖拽(即这是一次纯点击),则 onTouchEnd 不执行任何操作,交由 onFlip 处理 if (!this.isDragging) { return; } // 处理上划进入广场 if (this.isSwipingUp) { const deltaY = this.data.translateY; if (Math.abs(deltaY) > 150) { // 上划距离足够,进入广场 this.isExiting = true; this.setData({ translateY: -800, opacity: 0, transition: 'all 0.4s cubic-bezier(0.6, -0.28, 0.735, 0.045)' }); // 延迟后跳转 setTimeout(() => { this.onGoToSquare(); // 重置状态 this.setData({ translateY: 0, opacity: 1, transition: 'none' }); this.isExiting = false; this.isDragging = false; this.isSwipingUp = false; }, 400); } else { // 上划距离不够,回弹 this.setData({ translateY: 0, opacity: 1, transition: 'all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275)' }); setTimeout(() => { this.isDragging = false; this.isSwipingUp = false; }, 100); } return; } // 以下是拖拽结束后的逻辑(左右滑动) const deltaX = this.lastX - this.startX; const deltaTime = Date.now() - this.startTime; const velocity = deltaX / deltaTime; const shouldExit = Math.abs(deltaX) > EXIT_THRESHOLD || Math.abs(velocity) > FAST_SWIPE_VELOCITY; if (shouldExit) { this.isExiting = true; const direction = deltaX > 0 ? 1 : -1; const targetX = direction * 500; // 播放滑动音效 getApp().playAudio('swipe'); // 触觉反馈 wx.vibrateShort({ type: 'light' }); this.setData({ translateX: targetX, opacity: 0, transition: 'all 0.3s cubic-bezier(0.6, -0.28, 0.735, 0.045)' }); } else { this.setData({ translateX: 0, rotate: 0, opacity: 1, transition: 'all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275)' }); } // 延迟重置 isDragging 标志位。 // 这是为了确保在系统触发 tap 事件时,isDragging 标志仍然为 true, // 从而让 onFlip 方法可以正确地忽略掉这次由拖拽产生的 tap。 setTimeout(() => { this.isDragging = false; }, 100); }, onTransitionEnd() { if (!this.isExiting) return; const direction = this.data.translateX > 0 ? 'like' : 'dislike'; if (direction === 'like') { this.handleLike(); } this.handleCardExit(direction); }, handleLike() { // 触觉反馈 - 中等震动 wx.vibrateShort({ type: 'medium' }); // 音效反馈 getApp().playAudio('success'); const card = this.data.nameList[this.data.currentIndex]; if (!this.data.collectedNames.some(item => item.name === card.name)) { this.setData({ collectedNames: [...this.data.collectedNames, card] }); // 同步到后端 this.syncFavoriteToBackend(card); } }, // 同步收藏到后端 syncFavoriteToBackend(card) { const openid = getApp().getOpenid(); if (!openid) { console.log('openid 未获取到,无法同步收藏'); return; } const apiBaseUrl = getApp().globalData.apiBaseUrl; wx.request({ url: `${apiBaseUrl}/api/favorites/add?openid=${openid}&mode=${this.data.mode}&keyword=${encodeURIComponent(this.data.keyword)}`, method: 'POST', header: { 'Content-Type': 'application/json' }, data: { name: card.name, origin: card.origin, description: card.description, tone: card.tone, score: card.score }, success: (res) => { if (res.data && res.data.success) { console.log('收藏同步成功'); } else { console.log('收藏同步失败:', res.data); } }, fail: (err) => { console.log('收藏同步失败:', err); } }); }, handleCardExit() { const nextIndex = this.data.currentIndex + 1; wx.nextTick(() => { this.setData({ transition: 'none', translateX: 0, rotate: 0, opacity: 1, isFlipped: false }, () => { if (nextIndex < this.data.nameList.length) { this.setData({ currentIndex: nextIndex, cardKey: this.data.cardKey + 1 }); } else { // 检查是否还有剩余次数 this.checkAndLoadMore(); } this.isExiting = false; }); }); }, // 检查是否还有次数,有则自动加载,无则显示结语卡片 checkAndLoadMore() { const openid = getApp().getOpenid(); if (!openid) { this.setData({ showEndingCard: true }); return; } const apiBaseUrl = getApp().globalData.apiBaseUrl; wx.request({ url: `${apiBaseUrl}/api/credits/info`, data: { openid }, success: (res) => { if (res.data && res.data.success) { const creditsInfo = res.data.data; if (creditsInfo.dailyCredits > 0) { // 还有次数,自动加载新的一批 console.log('还有次数,自动加载新的一批'); this.setData({ isLoading: true }); this.startLoadingQuoteRotation(); this.fetchFirstBatch(this.data.keyword, this.data.mode, this.data.surname, 5); } else { // 次数用完,显示结语卡片 this.setData({ showEndingCard: true }); } } else { this.setData({ showEndingCard: true }); } }, fail: () => { this.setData({ showEndingCard: true }); } }); }, toggleCollectionView() { // 跳转到 profile 页面 wx.navigateTo({ url: '/pages/profile/profile' }); }, // 跳转到灵感广场 onGoToSquare() { wx.navigateTo({ url: '/pages/square/square' }); }, onDeleteCollected(e) { const nameToDelete = e.currentTarget.dataset.name; this.setData({ collectedNames: this.data.collectedNames.filter(item => item.name !== nameToDelete) }); wx.vibrateShort({ type: 'light' }); }, // 显示海报弹窗 onSavePoster() { const card = this.data.nameList[this.data.currentIndex]; const nameChars = card.name.split(''); this.setData({ showPoster: true, posterNameChars: nameChars }); // 触觉反馈 wx.vibrateShort({ type: 'light' }); }, // 关闭海报弹窗 closePoster() { this.setData({ showPoster: false }); }, // 保存海报到相册 savePoster() { wx.showLoading({ title: '绘笔收录中...', mask: true }); // 使用 snapshot 组件截图 wx.nextTick(() => { const query = wx.createSelectorQuery().in(this); query.select('#posterArea') .node() .exec((res) => { const node = res[0].node; // 使用 snapshot 截图 wx.canvasToTempFilePath({ x: 0, y: 0, width: node.width, height: node.height, destWidth: node.width * 2, destHeight: node.height * 2, canvasId: 'posterCanvas', success: (res) => { wx.hideLoading(); wx.saveImageToPhotosAlbum({ filePath: res.tempFilePath, success: () => { wx.showToast({ title: '已保存到相册', icon: 'success' }); }, fail: () => { wx.showToast({ title: '保存失败', icon: 'none' }); } }); }, fail: () => { wx.hideLoading(); wx.showToast({ title: '绘笔受阻', icon: 'none' }); } }); }); }); }, // 分享海报 sharePoster() { wx.showShareImageMenu({ path: '/images/share.png' }); }, // 分享到灵感广场 shareToSquare() { const card = this.data.nameList[this.data.currentIndex]; const openid = getApp().getOpenid(); if (!openid) { wx.showToast({ title: '请先登录', icon: 'none' }); return; } wx.showLoading({ title: '分享中...', mask: true }); const apiBaseUrl = getApp().globalData.apiBaseUrl; // 直接提交到灵感广场(不上传图片,使用默认图片) // 将数据转换为 URL 编码格式 const formData = `name=${encodeURIComponent(card.name)}&origin=${encodeURIComponent(card.origin)}&description=${encodeURIComponent(card.description)}&keyword=${encodeURIComponent(this.data.keyword)}&mode=${encodeURIComponent(this.data.mode)}&openid=${encodeURIComponent(openid)}&imageUrl=`; wx.request({ url: `${apiBaseUrl}/api/square/posts`, method: 'POST', header: { 'Content-Type': 'application/x-www-form-urlencoded' }, data: formData, success: (res) => { wx.hideLoading(); if (res.data && res.data.success) { wx.showToast({ title: '分享成功', icon: 'success' }); this.setData({ showPoster: false }); } else { wx.showToast({ title: res.data.message || '分享失败', icon: 'none' }); } }, fail: () => { wx.hideLoading(); wx.showToast({ title: '分享失败', icon: 'none' }); } }); }, // 阻止事件冒泡 preventBubble() { // 什么都不做,只是阻止事件冒泡 }, // 点击音符按钮 onMusicTap() { this.setData({ showMusicMenu: !this.data.showMusicMenu }); }, // 阻止菜单冒泡 onMenuTap() { // 什么都不做,只是阻止事件冒泡 }, // 切换环境音 onAmbienceChange(e) { const type = e.currentTarget.dataset.type; const { currentAmbience } = this.data; // 如果点击当前正在播放的类型,则切换到静音 const newType = (type === currentAmbience) ? 'silent' : type; this.setData({ currentAmbience: newType, showMusicMenu: false }); // 播放或停止环境音 const { playAmbience, fadeOutAndStop } = require('../../utils/audio.js'); if (newType === 'silent') { fadeOutAndStop(); wx.showToast({ title: '已静音', icon: 'none' }); } else { playAmbience(newType, true); const typeName = { wind: '风铃', rain: '雨落', guqin: '古琴' }[newType]; wx.showToast({ title: `正在播放:${typeName}`, icon: 'none' }); } }, // 长按名字显示字源弹窗 onNameLongPress() { const currentName = this.data.nameList[this.data.currentIndex]; if (!currentName || !currentName.name) return; // 默认显示名字的第一个字 const firstChar = currentName.name.charAt(0); this.showCharDetail(firstChar); // 触觉反馈 wx.vibrateLong(); }, // 显示字源详情 showCharDetail(char) { // 播放水滴声(入墨效果) const { playSoundEffect } = require('../../utils/audio.js'); playSoundEffect('inkDrop', { volume: 0.5 }); this.setData({ showCharDetail: true, selectedChar: char, charDetailData: null }); // 加载字源数据 this.loadCharDetail(char); }, // 加载字源数据 loadCharDetail(char) { const apiBaseUrl = getApp().globalData.apiBaseUrl; wx.request({ url: `${apiBaseUrl}/api/char/detail`, data: { char }, success: (res) => { if (res.data && res.data.success) { this.setData({ charDetailData: res.data.data }); } }, fail: () => { // 使用本地静态数据作为备选 this.setData({ charDetailData: this.getLocalCharData(char) }); } }); }, // 本地静态字源数据(备选) getLocalCharData(char) { const charDB = { '清': { char: '清', pinyin: 'qīng', radical: '氵', strokes: 11, wuxing: '水', meaning: '水清澈透明,引申为纯净、高洁、明白', imagery: '清澈如泉,心境澄明。寓意纯净通透、高洁自持,如清水出芙蓉,天然去雕饰。', poetry: '「清水出芙蓉,天然去雕饰」——李白' }, '雅': { char: '雅', pinyin: 'yǎ', radical: '隹', strokes: 12, wuxing: '木', meaning: '正也,规范、美好、高尚', imagery: '温文尔雅,气度不凡。寓意举止得体、品味高雅,如芝兰玉树,生于庭阶。', poetry: '「雅步擢纤腰,巧笑发皓齿」——曹植' }, '若': { char: '若', pinyin: 'ruò', radical: '艹', strokes: 8, wuxing: '木', meaning: '如、像,引申为顺从、选择', imagery: '虚怀若谷,温润如玉。寓意谦逊包容、从容自在,如兰若生春阳。', poetry: '「若有人兮山之阿,被薜荔兮带女萝」——屈原' }, '溪': { char: '溪', pinyin: 'xī', radical: '氵', strokes: 13, wuxing: '水', meaning: '山间小河,水流清澈', imagery: '溪水潺潺,源远流长。寓意灵动清澈、生生不息,如溪水绕石,柔中带刚。', poetry: '「旧时茅店社林边,路转溪桥忽见」——辛弃疾' }, '云': { char: '云', pinyin: 'yún', radical: '二', strokes: 4, wuxing: '水', meaning: '水气上升凝结,引申为说、众多', imagery: '云卷云舒,自在飘逸。寓意超然物外、洒脱不羁,如行云流水,任意西东。', poetry: '「行到水穷处,坐看云起时」——王维' }, '月': { char: '月', pinyin: 'yuè', radical: '月', strokes: 4, wuxing: '木', meaning: '月亮,引申为月份、光明', imagery: '明月清风,皎洁无瑕。寓意光明磊落、清雅高洁,如月出东山,照彻千里。', poetry: '「举杯邀明月,对影成三人」——李白' } }; return charDB[char] || { char: char, pinyin: '', radical: '', strokes: '', wuxing: '', meaning: '暂无详细解析', imagery: '暂无意象分析', poetry: '' }; }, // 关闭字源弹窗 closeCharDetail() { this.setData({ showCharDetail: false, selectedChar: '', charDetailData: null }); }, // 呼吸震动 - 名字逐字显现时的节奏震动 breatheVibrate(name) { if (!name || name.length === 0) return; const chars = name.split(''); let index = 0; // 每 300ms 震动一次,模拟呼吸节奏 const vibrateInterval = setInterval(() => { if (index >= chars.length) { clearInterval(vibrateInterval); return; } // 极微弱的震动,营造意境 wx.vibrateShort({ type: 'light' }); // 播放字显现音效 const { playSoundEffect } = require('../../utils/audio.js'); playSoundEffect('char', { volume: 0.3 }); index++; }, 300); }, // 打开 AI 解析弹窗 onAskAI(e) { e.stopPropagation(); this.setData({ showAIModal: true, aiContext: '', aiExplanation: '' }); }, // 关闭 AI 解析弹窗 closeAIModal() { this.setData({ showAIModal: false, aiContext: '', aiExplanation: '' }); }, // AI 上下文输入 onAIContextInput(e) { this.setData({ aiContext: e.detail.value }); }, // 请求 AI 解析 requestAIExplanation() { const currentName = this.data.nameList[this.data.currentIndex]; if (!currentName || !currentName.name) return; this.setData({ aiLoading: true }); const apiBaseUrl = getApp().globalData.apiBaseUrl; wx.request({ url: `${apiBaseUrl}/api/names/explain`, method: 'POST', header: { 'Content-Type': 'application/json' }, data: { name: currentName.name, context: this.data.aiContext || '一般场景' }, success: (res) => { if (res.data && res.data.success) { this.setData({ aiExplanation: res.data.data, aiLoading: false }); } else { wx.showToast({ title: '解析失败', icon: 'none' }); this.setData({ aiLoading: false }); } }, fail: () => { wx.showToast({ title: '网络错误', icon: 'none' }); this.setData({ aiLoading: false }); } }); } });