feat: 完善见素起名小程序功能

- 添加收藏锦囊功能,支持查看和删除收藏
- 实现积分系统,每日赠送5次灵感次数
- 添加静心阅读功能,阅读15秒可获得额外次数
- 实现灵感广场,展示用户分享的名字
- 添加字源溯源组件,长按汉字查看详情
- 优化空状态和结语卡片样式统一
- 添加音频控制(静音/风铃/雨落/古琴/白噪音/森林/溪流)
- 优化名字生成逻辑,确保每次返回5个不重复名字
- 修复卡片翻转样式问题
- 移除首页动态提醒气泡
This commit is contained in:
王鹏
2026-04-18 16:56:31 +08:00
parent be1f5722ab
commit 2c47fb8f65
59 changed files with 4643 additions and 145 deletions

View File

@@ -35,6 +35,7 @@ Page({
// 动画控制
translateX: 0,
translateY: 0,
rotate: 0,
opacity: 1,
transition: 'none',
@@ -45,7 +46,25 @@ Page({
// 海报相关
showPoster: false,
posterNameChars: []
posterNameChars: [],
// 音频相关
showMusicMenu: false,
currentAmbience: 'silent',
// 字源弹窗
showCharDetail: false,
selectedChar: '',
charDetailData: null,
// AI 解析弹窗
showAIModal: false,
aiContext: '',
aiExplanation: '',
aiLoading: false,
// 结语卡片
showEndingCard: false
},
onLoad(options) {
@@ -91,7 +110,11 @@ Page({
// 从后端加载收藏列表
loadCollectedNames() {
const openid = 'test_openid'; // 实际应从登录获取
const openid = getApp().getOpenid();
if (!openid) {
console.log('openid 未获取到,跳过加载收藏');
return;
}
const apiBaseUrl = getApp().globalData.apiBaseUrl;
wx.request({
url: `${apiBaseUrl}/api/favorites/list`,
@@ -119,38 +142,55 @@ Page({
this.fetchFirstBatch(keyword, mode, surname, 2);
},
// 第一波:快速获取前2个名字
// 第一波:快速获取前5个名字
fetchFirstBatch(keyword, mode, surname, count) {
const apiBaseUrl = getApp().globalData.apiBaseUrl;
const requestData = {
keyword,
count: count,
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.statusCode === 200 && res.data && res.data.length > 0) {
// 检查是否积分不足
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;
}
// 立即展示第一波结果
this.setData({
nameList: res.data,
currentIndex: 0,
isLoading: false,
cardKey: this.data.cardKey + 1
// 立即展示结果5个名字
this.setData({
nameList: nameList,
currentIndex: 0,
isLoading: false,
cardKey: this.data.cardKey + 1
});
// 静默加载剩余3个名字
this.fetchSecondBatch(keyword, mode, surname, 3);
// 呼吸震动效果 - 名字逐字显现时的节奏震动
this.breatheVibrate(nameList[0].name);
console.log('已加载 5 个不重复名字:', nameList.map(n => n.name).join(', '));
} else {
this.handleFetchError('意境未达,请重试');
}
@@ -161,43 +201,16 @@ Page({
});
},
// 第二波:静默加载剩余名字
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' });
this.setData({
isLoading: false,
showEndingCard: true
});
},
// 返回首页
@@ -258,6 +271,20 @@ Page({
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;
@@ -283,8 +310,47 @@ Page({
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;
@@ -294,12 +360,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,
@@ -313,7 +379,7 @@ Page({
transition: 'all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275)'
});
}
// 延迟重置 isDragging 标志位。
// 这是为了确保在系统触发 tap 事件时isDragging 标志仍然为 true
// 从而让 onFlip 方法可以正确地忽略掉这次由拖拽产生的 tap。
@@ -349,7 +415,11 @@ Page({
// 同步收藏到后端
syncFavoriteToBackend(card) {
const openid = 'test_openid'; // 实际应从登录获取
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)}`,
@@ -390,26 +460,49 @@ Page({
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.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({
@@ -417,6 +510,13 @@ Page({
});
},
// 跳转到灵感广场
onGoToSquare() {
wx.navigateTo({
url: '/pages/square/square'
});
},
onDeleteCollected(e) {
const nameToDelete = e.currentTarget.dataset.name;
this.setData({
@@ -493,8 +593,302 @@ Page({
});
},
// 分享到灵感广场
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 });
}
});
}
});

View File

@@ -0,0 +1,3 @@
{
"usingComponents": {}
}

View File

@@ -18,7 +18,7 @@
<view class="card-stack" wx:if="{{!isLoading && nameList.length > 0}}">
<view
class="card-container"
style="transform: translateX({{translateX}}px) rotate({{rotate}}deg); transition: {{transition}};"
style="transform: translateX({{translateX}}px) translateY({{translateY}}px) rotate({{rotate}}deg); transition: {{transition}};"
bindtouchstart="onTouchStart"
bindtouchmove="onTouchMove"
bindtouchend="onTouchEnd"
@@ -27,7 +27,40 @@
<view class="card {{isFlipped ? 'flipped' : ''}}">
<!-- 正面:名字与诗词 -->
<view class="card-face front" bindtap="onFlip">
<text class="name">{{nameList[currentIndex].name}}</text>
<!-- 音符按钮 -->
<view class="music-btn {{currentAmbience !== 'silent' ? 'active' : ''}}" catchtap="onMusicTap">♪</view>
<!-- 音频菜单 -->
<view class="music-menu {{showMusicMenu ? 'show' : ''}}" catchtap="onMenuTap">
<view class="menu-item {{currentAmbience === 'silent' ? 'active' : ''}}" data-type="silent" catchtap="onAmbienceChange">
<text class="menu-icon">🔇</text>
<text class="menu-text">静音</text>
</view>
<view class="menu-item {{currentAmbience === 'wind' ? 'active' : ''}}" data-type="wind" catchtap="onAmbienceChange">
<text class="menu-icon">🎐</text>
<text class="menu-text">风铃</text>
</view>
<view class="menu-item {{currentAmbience === 'rain' ? 'active' : ''}}" data-type="rain" catchtap="onAmbienceChange">
<text class="menu-icon">🌧</text>
<text class="menu-text">雨落</text>
</view>
<view class="menu-item {{currentAmbience === 'guqin' ? 'active' : ''}}" data-type="guqin" catchtap="onAmbienceChange">
<text class="menu-icon">🎵</text>
<text class="menu-text">古琴</text>
</view>
<view class="menu-item {{currentAmbience === 'white' ? 'active' : ''}}" data-type="white" catchtap="onAmbienceChange">
<text class="menu-icon">🌫</text>
<text class="menu-text">白噪音</text>
</view>
<view class="menu-item {{currentAmbience === 'forest' ? 'active' : ''}}" data-type="forest" catchtap="onAmbienceChange">
<text class="menu-icon">🌲</text>
<text class="menu-text">森林</text>
</view>
<view class="menu-item {{currentAmbience === 'stream' ? 'active' : ''}}" data-type="stream" catchtap="onAmbienceChange">
<text class="menu-icon">💧</text>
<text class="menu-text">溪流</text>
</view>
</view>
<text class="name" bindlongpress="onNameLongPress">{{nameList[currentIndex].name}}</text>
<view class="poem-container">
<text class="poem">{{nameList[currentIndex].origin}}</text>
</view>
@@ -40,6 +73,10 @@
<view class="analysis-container">
<view class="tone-tag">声韵:{{nameList[currentIndex].tone}}</view>
<view class="score-tag">见素评分:{{nameList[currentIndex].score}}</view>
<view class="ai-btn" catchtap="onAskAI">
<text class="ai-icon">✦</text>
<text>问问 AI</text>
</view>
<view class="save-btn" catchtap="onSavePoster">
<text class="save-icon"></text>
<text>存为海报</text>
@@ -83,6 +120,9 @@
<view class="poster-btn save-btn" bindtap="savePoster">
<text>保存到相册</text>
</view>
<view class="poster-btn square-btn" bindtap="shareToSquare">
<text>分享到广场</text>
</view>
<view class="poster-btn share-btn" bindtap="sharePoster">
<text>分享给好友</text>
</view>
@@ -90,26 +130,53 @@
</view>
</view>
<!-- 结语卡片:灵感耗尽或加载失败时显示 -->
<view class="ending-card {{showEndingCard ? 'show' : ''}}" wx:if="{{showEndingCard}}">
<view class="ending-ink"></view>
<text class="ending-hint">此间灵感暂歇</text>
<text class="ending-subhint">或寻他处,或待重来</text>
<view class="ending-actions">
<view class="ending-btn square-btn" bindtap="onGoToSquare">
<text>进入灵感广场</text>
</view>
<view class="ending-btn back-btn" bindtap="onBack">
<text>返回重试</text>
</view>
</view>
</view>
<!-- 底部操作栏 -->
<view class="action-bar" wx:if="{{!isLoading && nameList.length > 0}}">
<view class="action-bar" wx:if="{{!isLoading && nameList.length > 0 && !showEndingCard}}">
<view class="action-btn dislike-btn" bindtap="onDislike">
<text class="action-icon">×</text>
</view>
<view class="action-btn square-btn" bindtap="onGoToSquare">
<text class="action-icon">✦</text>
</view>
<view class="action-btn like-btn" bindtap="onLike">
<text class="action-icon">♥</text>
</view>
</view>
<!-- 空状态 -->
<view class="empty-state" wx:if="{{!isLoading && nameList.length === 0}}">
<text>暂无灵感,请重试</text>
<button bindtap="onLoad" style="margin-top: 20px; font-weight: 300;">重新感悟</button>
<!-- 空状态:和结语卡片样式一致 -->
<view class="ending-card {{!isLoading && nameList.length === 0 ? 'show' : ''}}" wx:if="{{!isLoading && nameList.length === 0}}">
<view class="ending-ink"></view>
<text class="ending-hint">此间灵感暂歇</text>
<text class="ending-subhint">或寻他处,或待重来</text>
<view class="ending-actions">
<view class="ending-btn square-btn" bindtap="onGoToSquare">
<text>进入灵感广场</text>
</view>
<view class="ending-btn back-btn" bindtap="onBack">
<text>返回重试</text>
</view>
</view>
</view>
<!-- 收藏锦囊 -->
<!-- 收藏锦囊:始终显示 -->
<view class="collection-bag" bindtap="toggleCollectionView">
<text class="bag-icon">囊</text>
<view class="collection-count">{{collectedNames.length}}</view>
<view class="collection-count" wx:if="{{collectedNames.length > 0}}">{{collectedNames.length}}</view>
</view>
<!-- 收藏列表浮层 -->
@@ -130,4 +197,109 @@
</scroll-view>
</view>
</view>
<!-- 字源溯源弹窗 -->
<view class="char-detail-modal {{showCharDetail ? 'visible' : ''}}" bindtap="closeCharDetail">
<view class="char-detail-content" catchtap="preventBubble">
<view class="char-detail-close" bindtap="closeCharDetail">×</view>
<!-- 磨砂玻璃背景 -->
<view class="char-detail-glass"></view>
<!-- 篆书背景装饰 -->
<view class="char-seal-bg">
<text class="seal-char seal-1">{{selectedChar}}</text>
<text class="seal-char seal-2">{{selectedChar}}</text>
<text class="seal-char seal-3">{{selectedChar}}</text>
</view>
<!-- 大字展示 -->
<view class="char-display">
<text class="char-big">{{selectedChar}}</text>
<text class="char-pinyin" wx:if="{{charDetailData}}">{{charDetailData.pinyin}}</text>
</view>
<!-- 字源信息 -->
<scroll-view class="char-info-scroll" scroll-y wx:if="{{charDetailData}}">
<!-- 基础信息 -->
<view class="char-info-section">
<view class="char-info-item">
<text class="char-label">部首</text>
<text class="char-value">{{charDetailData.radical || '未知'}}</text>
</view>
<view class="char-info-item">
<text class="char-label">笔画</text>
<text class="char-value">{{charDetailData.strokes || '未知'}}画</text>
</view>
<view class="char-info-item">
<text class="char-label">五行</text>
<text class="char-value">{{charDetailData.wuxing || '未知'}}</text>
</view>
</view>
<!-- 本义 -->
<view class="char-section">
<view class="char-section-title">本义</view>
<text class="char-section-text">{{charDetailData.meaning || '暂无解析'}}</text>
</view>
<!-- 起名意象 -->
<view class="char-section">
<view class="char-section-title">起名意象</view>
<text class="char-section-text">{{charDetailData.imagery || '暂无解析'}}</text>
</view>
<!-- 诗词典故 -->
<view class="char-section" wx:if="{{charDetailData.poetry}}">
<view class="char-section-title">诗词典故</view>
<text class="char-section-text poetry">{{charDetailData.poetry}}</text>
</view>
</scroll-view>
<!-- 加载中 -->
<view class="char-loading" wx:if="{{!charDetailData}}">
<text>正在溯源...</text>
</view>
</view>
</view>
<!-- 问问 AI 弹窗 -->
<view class="ai-modal {{showAIModal ? 'show' : ''}}" bindtap="closeAIModal">
<view class="ai-modal-content" catchtap="preventBubble">
<!-- 磨砂玻璃背景 -->
<view class="ai-modal-glass"></view>
<!-- 关闭按钮 -->
<view class="ai-modal-close" bindtap="closeAIModal">×</view>
<!-- 标题 -->
<view class="ai-modal-header">
<text class="ai-modal-title">AI 深度解析</text>
<text class="ai-modal-subtitle">{{nameList[currentIndex].name}}</text>
</view>
<!-- 输入框 -->
<view class="ai-input-section">
<text class="ai-input-label">你想了解这个名字在什么场景下的暗示?</text>
<input class="ai-context-input" placeholder="例如:从事艺术行业、出国留学、创业..." value="{{aiContext}}" bindinput="onAIContextInput" />
</view>
<!-- 解析结果 -->
<scroll-view class="ai-result-scroll" scroll-y wx:if="{{aiExplanation}}">
<text class="ai-explanation">{{aiExplanation}}</text>
</scroll-view>
<!-- 加载中 -->
<view class="ai-loading" wx:if="{{aiLoading}}">
<text>正在思考...</text>
</view>
<!-- 操作按钮 -->
<view class="ai-actions" wx:if="{{!aiLoading}}">
<view class="ai-action-btn" bindtap="requestAIExplanation">
<text>{{aiExplanation ? '重新解析' : '开始解析'}}</text>
</view>
</view>
</view>
</view>
</view>

View File

@@ -129,9 +129,10 @@
.card-face {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
backface-visibility: hidden;
background: #FFFFFF;
border-radius: 24rpx;
box-shadow: 0 10rpx 40rpx rgba(0, 0, 0, 0.08);
@@ -139,16 +140,101 @@
flex-direction: column;
padding: 60rpx;
box-sizing: border-box;
-webkit-backface-visibility: hidden;
backface-visibility: hidden;
}
.card-face.front {
align-items: center;
justify-content: center;
-webkit-backface-visibility: hidden;
backface-visibility: hidden;
}
.card-face.back {
transform: rotateY(180deg);
justify-content: space-between;
-webkit-backface-visibility: hidden;
backface-visibility: hidden;
}
/* 音符按钮 */
.music-btn {
position: absolute;
top: 30rpx;
right: 30rpx;
width: 56rpx;
height: 56rpx;
border-radius: 50%;
background: rgba(255, 255, 255, 0.8);
display: flex;
align-items: center;
justify-content: center;
font-size: 32rpx;
color: #999;
opacity: 0.6;
transition: all 0.3s;
z-index: 10;
}
.music-btn:active {
opacity: 1;
transform: scale(0.9);
}
.music-btn.active {
color: #B22222;
opacity: 1;
}
/* 音频菜单 */
.music-menu {
position: absolute;
top: 100rpx;
right: 20rpx;
background: rgba(255, 255, 255, 0.95);
border-radius: 16rpx;
padding: 16rpx;
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.1);
opacity: 0;
visibility: hidden;
transform: translateY(-10rpx);
transition: all 0.3s;
z-index: 20;
max-height: 400rpx;
overflow-y: auto;
}
.music-menu.show {
opacity: 1;
visibility: visible;
transform: translateY(0);
}
.menu-item {
display: flex;
align-items: center;
gap: 16rpx;
padding: 16rpx 24rpx;
border-radius: 12rpx;
transition: all 0.2s;
}
.menu-item:active {
background: #F5F5F0;
}
.menu-item.active {
background: #F5F5F0;
}
.menu-icon {
font-size: 36rpx;
}
.menu-text {
font-size: 28rpx;
color: #333;
}
.name {
@@ -200,7 +286,7 @@
}
.save-btn {
margin-top: 20rpx;
margin-top: 16rpx;
padding: 16rpx 40rpx;
background: #F5F5F5;
border-radius: 30rpx;
@@ -220,6 +306,28 @@
font-size: 28rpx;
}
/* 问问 AI 按钮 */
.ai-btn {
margin-top: 16rpx;
padding: 16rpx 40rpx;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 30rpx;
display: flex;
align-items: center;
gap: 8rpx;
font-size: 24rpx;
color: #FFFFFF;
transition: all 0.2s;
}
.ai-btn:active {
opacity: 0.8;
}
.ai-icon {
font-size: 28rpx;
}
/* 底部操作栏 */
.action-bar {
display: flex;
@@ -251,6 +359,12 @@
color: #FFFFFF;
}
.action-btn.square-btn {
background: #F5F5F0;
color: #B22222;
font-size: 40rpx;
}
.action-btn:active {
transform: scale(0.9);
}
@@ -284,20 +398,106 @@
border: none !important;
}
/* 结语卡片 */
.ending-card {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: #FFFFFF;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 50;
opacity: 0;
visibility: hidden;
transition: all 0.5s ease;
}
.ending-card.show {
opacity: 1;
visibility: visible;
}
.ending-ink {
width: 120rpx;
height: 120rpx;
background: radial-gradient(circle, rgba(45,45,45,0.1) 0%, transparent 70%);
border-radius: 50%;
margin-bottom: 60rpx;
animation: inkSpread 2s ease-out;
}
@keyframes inkSpread {
from { transform: scale(0); opacity: 0; }
to { transform: scale(1); opacity: 1; }
}
.ending-hint {
font-family: "KaiTi", "STKaiti", serif;
font-size: 40rpx;
color: #2D2D2D;
letter-spacing: 8rpx;
margin-bottom: 20rpx;
}
.ending-subhint {
font-size: 28rpx;
color: #888888;
letter-spacing: 4rpx;
margin-bottom: 80rpx;
}
.ending-actions {
display: flex;
flex-direction: column;
gap: 24rpx;
align-items: center;
}
.ending-btn {
padding: 24rpx 80rpx;
border-radius: 40rpx;
font-size: 28rpx;
letter-spacing: 4rpx;
transition: all 0.2s;
}
.ending-btn.square-btn {
background: #2D2D2D;
color: #FFFFFF;
}
.ending-btn.square-btn:active {
background: #1a1a1a;
}
.ending-btn.back-btn {
background: transparent;
color: #888888;
border: 1rpx solid #E0E0E0;
}
.ending-btn.back-btn:active {
background: #F5F5F5;
}
/* 收藏锦囊 */
.collection-bag {
position: fixed;
right: 40rpx;
bottom: 200rpx;
bottom: 160rpx;
width: 100rpx;
height: 100rpx;
background: #FFFFFF;
border-radius: 50%;
box-shadow: 0 8rpx 30rpx rgba(0, 0, 0, 0.1);
box-shadow: 0 8rpx 30rpx rgba(0, 0, 0, 0.15);
display: flex;
align-items: center;
justify-content: center;
z-index: 10;
z-index: 100;
transition: transform 0.2s;
}
@@ -614,6 +814,367 @@
border: 2rpx solid rgba(255, 255, 255, 0.5);
}
.poster-btn.square-btn {
background: #B22222;
color: #FFFFFF;
}
.poster-btn.square-btn:active {
background: #8B1A1A;
}
.poster-btn.share-btn:active {
background: rgba(255, 255, 255, 0.1);
}
/* 字源溯源弹窗 */
.char-detail-modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.4);
backdrop-filter: blur(10px);
z-index: 200;
opacity: 0;
visibility: hidden;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
}
.char-detail-modal.visible {
opacity: 1;
visibility: visible;
}
.char-detail-content {
width: 85%;
max-width: 600rpx;
max-height: 80vh;
background: rgba(255, 255, 255, 0.95);
border-radius: 24rpx;
position: relative;
overflow: hidden;
transform: scale(0.9);
transition: transform 0.3s ease;
}
.char-detail-modal.visible .char-detail-content {
transform: scale(1);
}
.char-detail-close {
position: absolute;
top: 20rpx;
right: 20rpx;
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 40rpx;
color: #999;
z-index: 10;
}
.char-detail-close:active {
color: #666;
}
.char-detail-glass {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(135deg, rgba(255,255,255,0.4) 0%, rgba(255,255,255,0.1) 100%);
backdrop-filter: blur(20px);
z-index: 0;
}
/* 篆书背景装饰 */
.char-seal-bg {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
overflow: hidden;
z-index: 0;
opacity: 0.08;
}
.seal-char {
position: absolute;
font-family: "KaiTi", "STKaiti", "SimSun", serif;
font-size: 200rpx;
color: #8B4513;
font-weight: bold;
letter-spacing: 20rpx;
}
.seal-1 {
top: 10%;
left: -10%;
transform: rotate(-15deg);
}
.seal-2 {
top: 40%;
right: -15%;
transform: rotate(10deg);
}
.seal-3 {
bottom: 5%;
left: 20%;
transform: rotate(-5deg);
}
.char-display {
position: relative;
z-index: 1;
padding: 60rpx 40rpx 40rpx;
text-align: center;
background: linear-gradient(180deg, #F5F5F0 0%, rgba(245,245,240,0) 100%);
}
.char-big {
font-family: "KaiTi", "STKaiti", "Noto Serif SC", serif;
font-size: 120rpx;
color: #2D2D2D;
letter-spacing: 8rpx;
text-shadow: 2rpx 2rpx 4rpx rgba(0,0,0,0.1);
}
.char-pinyin {
font-size: 28rpx;
color: #888;
margin-top: 16rpx;
letter-spacing: 4rpx;
}
.char-info-scroll {
max-height: 50vh;
padding: 0 40rpx 40rpx;
position: relative;
z-index: 1;
}
.char-info-section {
display: flex;
justify-content: space-around;
padding: 30rpx 0;
border-bottom: 1rpx solid #E8E8E0;
margin-bottom: 30rpx;
}
.char-info-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 12rpx;
}
.char-label {
font-size: 22rpx;
color: #999;
letter-spacing: 2rpx;
}
.char-value {
font-size: 28rpx;
color: #333;
font-weight: 500;
}
.char-section {
margin-bottom: 30rpx;
}
.char-section-title {
font-size: 24rpx;
color: #B22222;
letter-spacing: 4rpx;
margin-bottom: 16rpx;
font-weight: 500;
}
.char-section-text {
font-size: 26rpx;
color: #555;
line-height: 1.8;
letter-spacing: 2rpx;
}
.char-section-text.poetry {
font-family: "KaiTi", "STKaiti", serif;
color: #666;
font-style: italic;
}
.char-loading {
padding: 60rpx;
text-align: center;
color: #999;
font-size: 26rpx;
letter-spacing: 4rpx;
}
/* 问问 AI 弹窗 */
.ai-modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 100;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
visibility: hidden;
transition: all 0.3s ease;
}
.ai-modal.show {
opacity: 1;
visibility: visible;
}
.ai-modal-content {
position: relative;
width: 80%;
max-width: 600rpx;
max-height: 70vh;
border-radius: 24rpx;
overflow: hidden;
transform: scale(0.9);
transition: transform 0.3s ease;
}
.ai-modal.show .ai-modal-content {
transform: scale(1);
}
.ai-modal-glass {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(135deg, rgba(255,255,255,0.95) 0%, rgba(250,250,250,0.9) 100%);
backdrop-filter: blur(20px);
z-index: 0;
}
.ai-modal-close {
position: absolute;
top: 20rpx;
right: 20rpx;
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 40rpx;
color: #999;
z-index: 2;
}
.ai-modal-header {
position: relative;
z-index: 1;
padding: 40rpx 40rpx 20rpx;
text-align: center;
border-bottom: 1rpx solid rgba(0,0,0,0.05);
}
.ai-modal-title {
font-size: 32rpx;
font-weight: 500;
color: #2D2D2D;
letter-spacing: 4rpx;
}
.ai-modal-subtitle {
display: block;
font-size: 48rpx;
color: #667eea;
margin-top: 12rpx;
font-family: "KaiTi", "STKaiti", serif;
}
.ai-input-section {
position: relative;
z-index: 1;
padding: 30rpx 40rpx;
}
.ai-input-label {
display: block;
font-size: 26rpx;
color: #666;
margin-bottom: 16rpx;
letter-spacing: 2rpx;
}
.ai-context-input {
width: 100%;
height: 80rpx;
padding: 0 24rpx;
background: #F5F5F5;
border-radius: 12rpx;
font-size: 26rpx;
color: #333;
box-sizing: border-box;
}
.ai-result-scroll {
position: relative;
z-index: 1;
max-height: 300rpx;
padding: 0 40rpx;
margin-bottom: 20rpx;
}
.ai-explanation {
font-size: 28rpx;
color: #444;
line-height: 1.8;
letter-spacing: 2rpx;
text-align: justify;
}
.ai-loading {
padding: 40rpx;
text-align: center;
color: #999;
font-size: 26rpx;
letter-spacing: 4rpx;
}
.ai-actions {
position: relative;
z-index: 1;
padding: 20rpx 40rpx 40rpx;
display: flex;
justify-content: center;
}
.ai-action-btn {
padding: 20rpx 60rpx;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 40rpx;
font-size: 28rpx;
color: #FFFFFF;
letter-spacing: 4rpx;
transition: opacity 0.2s;
}
.ai-action-btn:active {
opacity: 0.8;
}