feat: 完成见素起名小程序核心功能
- 实现 AI 起名功能(Kimi API 接入) - 添加用户收藏功能(MySQL 数据库) - 实现海报生成与分享 - 添加音效和触觉反馈 - 配置生产环境部署(WAR 包 + Nginx) - 支持多种起名模式(经典、诗词、自然、现代) - 实现分批加载优化体验
This commit is contained in:
@@ -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() {
|
||||
// 什么都不做,只是阻止事件冒泡
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user