Files
JianSu-Naming/miniprogram/pages/index/index.js
王鹏 be1f5722ab feat: 完成见素起名小程序核心功能
- 实现 AI 起名功能(Kimi API 接入)
- 添加用户收藏功能(MySQL 数据库)
- 实现海报生成与分享
- 添加音效和触觉反馈
- 配置生产环境部署(WAR 包 + Nginx)
- 支持多种起名模式(经典、诗词、自然、现代)
- 实现分批加载优化体验
2026-04-17 15:34:51 +08:00

501 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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() {
// 什么都不做,只是阻止事件冒泡
}
});