feat: 完成见素起名小程序核心功能

- 实现 AI 起名功能(Kimi API 接入)
- 添加用户收藏功能(MySQL 数据库)
- 实现海报生成与分享
- 添加音效和触觉反馈
- 配置生产环境部署(WAR 包 + Nginx)
- 支持多种起名模式(经典、诗词、自然、现代)
- 实现分批加载优化体验
This commit is contained in:
王鹏
2026-04-17 15:34:51 +08:00
parent 1a749cdf71
commit be1f5722ab
136 changed files with 3322 additions and 420 deletions

View File

@@ -1,6 +1,18 @@
const EXIT_THRESHOLD = 80;
const FAST_SWIPE_VELOCITY = 0.5;
// 加载文案库
const LOADING_QUOTES = [
"正在翻阅《古今集成》...",
"于诗书中寻觅意境...",
"聆听平仄的韵律...",
"在字里行间游走...",
"采撷一缕清风入名...",
"品味楚辞的芬芳...",
"捕捉诗经的灵光...",
"与古人隔空对话..."
];
Page({
isDragging: false,
isExiting: false,
@@ -8,6 +20,7 @@ Page({
startX: 0,
startY: 0,
lastX: 0,
loadingQuoteTimer: null,
data: {
nameList: [],
@@ -15,6 +28,10 @@ Page({
isLoading: true,
isFlipped: false,
keyword: '清冷',
mode: 'classic',
modeName: '拾遗',
surname: '',
loadingQuote: '正在翻阅《古今集成》...',
// 动画控制
translateX: 0,
@@ -24,30 +41,197 @@ Page({
cardKey: 0,
collectedNames: [],
showCollection: false
showCollection: false,
// 海报相关
showPoster: false,
posterNameChars: []
},
onLoad(options) {
const keyword = options.keyword || this.data.keyword;
this.setData({ keyword });
this.fetchNames(keyword);
// 解码 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);
},
fetchNames(keyword) {
this.setData({ isLoading: true });
wx.showLoading({ title: '见素正在感悟...', mask: true });
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: 'http://localhost:8080/api/names/generate',
data: { keyword },
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) {
this.setData({ nameList: res.data, currentIndex: 0, isLoading: false, cardKey: this.data.cardKey + 1 });
// 清除定时器
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 {
wx.showToast({ title: '意境未达,请重试', icon: 'none' });
this.handleFetchError('意境未达,请重试');
}
},
fail: () => wx.showToast({ title: '网络疏离,请检查后端', icon: 'none' }),
complete: () => { this.setData({ isLoading: false }); wx.hideLoading(); }
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)'
});
},
@@ -74,7 +258,7 @@ Page({
if (this.isExiting) return;
const deltaX = e.touches[0].clientX - this.startX;
// 只有移动超过5px才真正判定为拖拽
// 只有移动超过5px才真正判定为"拖拽"
if (Math.abs(deltaX) > 5) {
this.isDragging = true;
}
@@ -110,6 +294,12 @@ Page({
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,
@@ -138,19 +328,53 @@ Page({
const direction = this.data.translateX > 0 ? 'like' : 'dislike';
if (direction === 'like') {
this.handleLike();
} else {
this.handleCardExit(direction);
}
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);
}
this.handleCardExit('like');
},
// 同步收藏到后端
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() {
@@ -170,7 +394,15 @@ Page({
title: '见素时刻',
content: '这一波灵感已尽,是否再求几名?',
confirmText: '再求', cancelText: '返回',
success: (res) => { if (res.confirm) this.fetchNames(this.data.keyword); }
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;
@@ -179,7 +411,10 @@ Page({
},
toggleCollectionView() {
this.setData({ showCollection: !this.data.showCollection });
// 跳转到 profile 页面
wx.navigateTo({
url: '/pages/profile/profile'
});
},
onDeleteCollected(e) {
@@ -190,53 +425,76 @@ Page({
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 });
const query = wx.createSelectorQuery().in(this);
query.select('#posterCanvas')
.fields({ node: true, size: true })
.exec((res) => {
const canvas = res[0].node;
const ctx = canvas.getContext('2d');
const dpr = wx.getSystemInfoSync().pixelRatio;
canvas.width = 750 * dpr;
canvas.height = 1334 * dpr;
ctx.scale(dpr, dpr);
ctx.fillStyle = '#FFFFFF';
ctx.fillRect(0, 0, 750, 1334);
ctx.fillStyle = '#2D2D2D';
ctx.font = '300 120px serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
const name = card.name;
const charArray = name.split('');
const charHeight = 140;
const startY = 667 - (charArray.length * charHeight) / 2 + charHeight / 2;
charArray.forEach((char, index) => ctx.fillText(char, 375, startY + index * charHeight));
const sealChar = this.data.keyword.substring(0, 1) || '素';
ctx.fillStyle = '#B22222';
ctx.fillRect(100, 100, 80, 80);
ctx.fillStyle = '#FFFFFF';
ctx.font = '40px serif';
ctx.fillText(sealChar, 140, 140);
const randomNum = Math.floor(Math.random() * 9000) + 1000;
ctx.fillStyle = '#D0D0D0';
ctx.font = '24px sans-serif';
ctx.fillText(`见素第 ${randomNum} 号灵感`, 375, 1200);
setTimeout(() => {
// 使用 snapshot 组件截图
wx.nextTick(() => {
const query = wx.createSelectorQuery().in(this);
query.select('#posterArea')
.node()
.exec((res) => {
const node = res[0].node;
// 使用 snapshot 截图
wx.canvasToTempFilePath({
canvas: canvas,
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.showShareImageMenu({ path: res.tempFilePath });
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' });
}
});
}, 300);
});
});
});
},
// 分享海报
sharePoster() {
wx.showShareImageMenu({
path: '/images/share.png'
});
},
// 阻止事件冒泡
preventBubble() {
// 什么都不做,只是阻止事件冒泡
}
});