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, rotate: 0, opacity: 1, transition: 'none', cardKey: 0, collectedNames: [], showCollection: false, // 海报相关 showPoster: false, posterNameChars: [] }, 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 = 'test_openid'; // 实际应从登录获取 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); }, // 第一波:快速获取前2个名字 fetchFirstBatch(keyword, mode, surname, count) { const apiBaseUrl = getApp().globalData.apiBaseUrl; const requestData = { keyword, count: count, batch: 'first' }; if (mode) requestData.mode = mode; if (surname) requestData.surname = surname; wx.request({ url: `${apiBaseUrl}/api/names/generate`, data: requestData, success: (res) => { if (res.statusCode === 200 && res.data && res.data.length > 0) { // 清除定时器 if (this.loadingQuoteTimer) { clearInterval(this.loadingQuoteTimer); this.loadingQuoteTimer = null; } // 立即展示第一波结果 this.setData({ nameList: res.data, currentIndex: 0, isLoading: false, cardKey: this.data.cardKey + 1 }); // 静默加载剩余3个名字 this.fetchSecondBatch(keyword, mode, surname, 3); } else { this.handleFetchError('意境未达,请重试'); } }, fail: () => { this.handleFetchError('网络疏离,请检查后端'); } }); }, // 第二波:静默加载剩余名字 fetchSecondBatch(keyword, mode, surname, count) { const apiBaseUrl = getApp().globalData.apiBaseUrl; const requestData = { keyword, count: count, batch: 'second' }; if (mode) requestData.mode = mode; if (surname) requestData.surname = surname; wx.request({ url: `${apiBaseUrl}/api/names/generate`, data: requestData, success: (res) => { if (res.statusCode === 200 && res.data && res.data.length > 0) { // 平滑合并数据 const currentList = this.data.nameList; const newList = [...currentList, ...res.data]; this.setData({ nameList: newList }); console.log('第二波数据已静默加载,当前共', newList.length, '个名字'); } }, fail: (err) => { console.log('第二波加载失败(不影响已展示内容):', err); } }); }, // 处理加载错误 handleFetchError(message) { if (this.loadingQuoteTimer) { clearInterval(this.loadingQuoteTimer); this.loadingQuoteTimer = null; } this.setData({ isLoading: false }); wx.showToast({ title: message, icon: 'none' }); }, // 返回首页 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; // 只有移动超过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; } // 以下是拖拽结束后的逻辑 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 = 'test_openid'; // 实际应从登录获取 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 { wx.showModal({ title: '见素时刻', content: '这一波灵感已尽,是否再求几名?', confirmText: '再求', cancelText: '返回', success: (res) => { if (res.confirm) { this.setData({ isLoading: true }); this.startLoadingQuoteRotation(); this.fetchNamesBatch(this.data.keyword, this.data.mode, this.data.surname); } else { this.onBack(); } } }); } this.isExiting = false; }); }); }, toggleCollectionView() { // 跳转到 profile 页面 wx.navigateTo({ url: '/pages/profile/profile' }); }, 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' }); }, // 阻止事件冒泡 preventBubble() { // 什么都不做,只是阻止事件冒泡 } });