feat: 完善见素起名小程序功能
- 添加收藏锦囊功能,支持查看和删除收藏 - 实现积分系统,每日赠送5次灵感次数 - 添加静心阅读功能,阅读15秒可获得额外次数 - 实现灵感广场,展示用户分享的名字 - 添加字源溯源组件,长按汉字查看详情 - 优化空状态和结语卡片样式统一 - 添加音频控制(静音/风铃/雨落/古琴/白噪音/森林/溪流) - 优化名字生成逻辑,确保每次返回5个不重复名字 - 修复卡片翻转样式问题 - 移除首页动态提醒气泡
This commit is contained in:
@@ -8,32 +8,72 @@ App({
|
||||
swipe: wx.createInnerAudioContext() // 滑动切换
|
||||
};
|
||||
|
||||
// 预设音效资源
|
||||
// 预设音效资源(本地文件)
|
||||
// 翻页声 - 纸张摩擦
|
||||
this.globalData.audioContexts.flip.src = 'https://assets.mixkit.co/active_storage/sfx/2571/2571-preview.mp3';
|
||||
this.globalData.audioContexts.flip.src = '/assets/audio/flip.mp3';
|
||||
// 收藏成功 - 清脆铃声
|
||||
this.globalData.audioContexts.success.src = 'https://assets.mixkit.co/active_storage/sfx/2000/2000-preview.mp3';
|
||||
this.globalData.audioContexts.success.src = '/assets/audio/success.mp3';
|
||||
// 水滴声 - 水墨滴落
|
||||
this.globalData.audioContexts.inkDrop.src = 'https://assets.mixkit.co/active_storage/sfx/2578/2578-preview.mp3';
|
||||
this.globalData.audioContexts.inkDrop.src = '/assets/audio/inkDrop.mp3';
|
||||
// 滑动声
|
||||
this.globalData.audioContexts.swipe.src = 'https://assets.mixkit.co/active_storage/sfx/2571/2571-preview.mp3';
|
||||
this.globalData.audioContexts.swipe.src = '/assets/audio/swipe.mp3';
|
||||
|
||||
// 微信登录获取 openid
|
||||
this.wxLogin();
|
||||
},
|
||||
|
||||
// 微信登录
|
||||
wxLogin() {
|
||||
wx.login({
|
||||
success: (res) => {
|
||||
if (res.code) {
|
||||
// 发送 code 到后端换取 openid
|
||||
wx.request({
|
||||
url: `${this.globalData.apiBaseUrl}/api/auth/login`,
|
||||
method: 'POST',
|
||||
data: { code: res.code },
|
||||
success: (response) => {
|
||||
if (response.data && response.data.success) {
|
||||
this.globalData.openid = response.data.openid;
|
||||
console.log('登录成功,openid:', response.data.openid);
|
||||
} else {
|
||||
console.error('登录失败:', response.data);
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('登录请求失败:', err);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.error('登录失败:', res.errMsg);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// 获取 openid(如果还未获取到则返回 null)
|
||||
getOpenid() {
|
||||
return this.globalData.openid;
|
||||
},
|
||||
|
||||
playAudio(type) {
|
||||
const ctx = this.globalData.audioContexts[type];
|
||||
if (ctx) {
|
||||
ctx.stop();
|
||||
ctx.play().catch(err => {
|
||||
try {
|
||||
ctx.play();
|
||||
} catch (err) {
|
||||
console.log('音效播放失败:', err);
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
globalData: {
|
||||
audioContexts: {},
|
||||
openid: null,
|
||||
// API 基础地址 - 修改这里即可切换环境
|
||||
// apiBaseUrl: 'http://localhost:8080'
|
||||
apiBaseUrl: 'https://feast.yidaima.cn/jsu'
|
||||
apiBaseUrl: 'http://localhost:8080'
|
||||
// apiBaseUrl: 'https://feast.yidaima.cn/jsu'
|
||||
// 生产环境:'https://api.yourdomain.com'
|
||||
}
|
||||
});
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
"pages": [
|
||||
"pages/home/home",
|
||||
"pages/index/index",
|
||||
"pages/profile/profile"
|
||||
"pages/profile/profile",
|
||||
"pages/square/square"
|
||||
],
|
||||
"window": {
|
||||
"backgroundColor": "#FFFFFF",
|
||||
@@ -11,6 +12,7 @@
|
||||
"navigationBarTitleText": "见素",
|
||||
"navigationBarTextStyle": "black"
|
||||
},
|
||||
"requiredBackgroundModes": ["audio"],
|
||||
"sitemapLocation": "sitemap.json",
|
||||
"lazyCodeLoading": "requiredComponents"
|
||||
}
|
||||
108
miniprogram/components/charDetail/charDetail.js
Normal file
108
miniprogram/components/charDetail/charDetail.js
Normal file
@@ -0,0 +1,108 @@
|
||||
// 字源溯源弹窗组件
|
||||
Component({
|
||||
properties: {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
value: false
|
||||
},
|
||||
char: {
|
||||
type: String,
|
||||
value: ''
|
||||
}
|
||||
},
|
||||
|
||||
data: {
|
||||
pinyin: '',
|
||||
radical: '',
|
||||
strokes: '',
|
||||
wuxing: '',
|
||||
meaning: '',
|
||||
imagery: '',
|
||||
poetry: '',
|
||||
loading: false
|
||||
},
|
||||
|
||||
observers: {
|
||||
'visible, char': function(visible, char) {
|
||||
if (visible && char) {
|
||||
this.loadCharDetail(char);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
// 加载汉字详情
|
||||
loadCharDetail(char) {
|
||||
this.setData({ loading: true });
|
||||
|
||||
// 先从本地缓存查找
|
||||
const cache = wx.getStorageSync(`char_${char}`);
|
||||
if (cache) {
|
||||
this.setData({ ...cache, loading: false });
|
||||
return;
|
||||
}
|
||||
|
||||
// 调用后端接口获取
|
||||
const apiBaseUrl = getApp().globalData.apiBaseUrl;
|
||||
wx.request({
|
||||
url: `${apiBaseUrl}/api/char/detail`,
|
||||
data: { char },
|
||||
success: (res) => {
|
||||
if (res.data && res.data.success) {
|
||||
const data = res.data.data;
|
||||
this.setData({ ...data, loading: false });
|
||||
// 缓存结果
|
||||
wx.setStorageSync(`char_${char}`, data);
|
||||
} else {
|
||||
// 使用默认数据
|
||||
this.setDefaultData(char);
|
||||
}
|
||||
},
|
||||
fail: () => {
|
||||
this.setDefaultData(char);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// 设置默认数据
|
||||
setDefaultData(char) {
|
||||
// 简单的拼音映射(实际应该从后端获取)
|
||||
const pinyinMap = {
|
||||
'兰': 'lán', '泽': 'zé', '芳': 'fāng', '风': 'fēng',
|
||||
'墨': 'mò', '染': 'rǎn', '清': 'qīng', '欢': 'huān',
|
||||
'素': 'sù', '简': 'jiǎn', '雅': 'yǎ', '致': 'zhì'
|
||||
};
|
||||
|
||||
this.setData({
|
||||
pinyin: pinyinMap[char] || '',
|
||||
radical: '',
|
||||
strokes: '',
|
||||
wuxing: '',
|
||||
meaning: '暂无详细解析,点击"问问 AI"获取深度分析',
|
||||
imagery: '',
|
||||
poetry: '',
|
||||
loading: false
|
||||
});
|
||||
},
|
||||
|
||||
// 点击遮罩关闭
|
||||
onMaskTap() {
|
||||
this.triggerEvent('close');
|
||||
},
|
||||
|
||||
// 阻止冒泡
|
||||
onContainerTap() {
|
||||
// 什么都不做,只是阻止冒泡
|
||||
},
|
||||
|
||||
// 点击关闭按钮
|
||||
onClose() {
|
||||
this.triggerEvent('close');
|
||||
},
|
||||
|
||||
// 问问 AI
|
||||
onAskAI() {
|
||||
this.triggerEvent('askAI', { char: this.data.char });
|
||||
}
|
||||
}
|
||||
});
|
||||
4
miniprogram/components/charDetail/charDetail.json
Normal file
4
miniprogram/components/charDetail/charDetail.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {}
|
||||
}
|
||||
59
miniprogram/components/charDetail/charDetail.wxml
Normal file
59
miniprogram/components/charDetail/charDetail.wxml
Normal file
@@ -0,0 +1,59 @@
|
||||
<!-- 字源溯源弹窗组件 -->
|
||||
<view class="char-detail-mask {{visible ? 'show' : ''}}" bindtap="onMaskTap">
|
||||
<view class="char-detail-container {{visible ? 'show' : ''}}" catchtap="onContainerTap">
|
||||
<!-- 磨砂玻璃背景 -->
|
||||
<view class="glass-bg"></view>
|
||||
|
||||
<!-- 关闭按钮 -->
|
||||
<view class="close-btn" bindtap="onClose">×</view>
|
||||
|
||||
<!-- 大字展示 -->
|
||||
<view class="char-display">
|
||||
<text class="char-text">{{char}}</text>
|
||||
<view class="char-pinyin">{{pinyin}}</view>
|
||||
</view>
|
||||
|
||||
<!-- 字源解析 -->
|
||||
<scroll-view class="char-content" scroll-y>
|
||||
<!-- 基础信息 -->
|
||||
<view class="info-section">
|
||||
<view class="info-item">
|
||||
<text class="label">部首</text>
|
||||
<text class="value">{{radical || '未知'}}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="label">笔画</text>
|
||||
<text class="value">{{strokes || '未知'}}画</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="label">五行</text>
|
||||
<text class="value">{{wuxing || '未知'}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 字义解析 -->
|
||||
<view class="meaning-section">
|
||||
<view class="section-title">本义</view>
|
||||
<text class="meaning-text">{{meaning || '暂无解析'}}</text>
|
||||
</view>
|
||||
|
||||
<!-- 意象分析 -->
|
||||
<view class="imagery-section">
|
||||
<view class="section-title">起名意象</view>
|
||||
<text class="imagery-text">{{imagery || '暂无解析'}}</text>
|
||||
</view>
|
||||
|
||||
<!-- 诗词典故 -->
|
||||
<view class="poetry-section" wx:if="{{poetry}}">
|
||||
<view class="section-title">诗词典故</view>
|
||||
<text class="poetry-text">{{poetry}}</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- AI 解析按钮 -->
|
||||
<view class="ai-btn" bindtap="onAskAI">
|
||||
<text class="ai-icon">✦</text>
|
||||
<text class="ai-text">问问 AI</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
178
miniprogram/components/charDetail/charDetail.wxss
Normal file
178
miniprogram/components/charDetail/charDetail.wxss
Normal file
@@ -0,0 +1,178 @@
|
||||
/* 字源溯源弹窗组件 - 磨砂玻璃风格 */
|
||||
|
||||
.char-detail-mask {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: all 0.3s ease;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.char-detail-mask.show {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.char-detail-container {
|
||||
position: relative;
|
||||
width: 600rpx;
|
||||
max-height: 800rpx;
|
||||
border-radius: 24rpx;
|
||||
overflow: hidden;
|
||||
transform: scale(0.9) translateY(20rpx);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.char-detail-container.show {
|
||||
transform: scale(1) translateY(0);
|
||||
}
|
||||
|
||||
/* 磨砂玻璃背景 */
|
||||
.glass-bg {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(255, 255, 255, 0.85);
|
||||
backdrop-filter: blur(20rpx);
|
||||
-webkit-backdrop-filter: blur(20rpx);
|
||||
}
|
||||
|
||||
/* 关闭按钮 */
|
||||
.close-btn {
|
||||
position: absolute;
|
||||
top: 20rpx;
|
||||
right: 20rpx;
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 36rpx;
|
||||
color: #666;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
/* 大字展示 */
|
||||
.char-display {
|
||||
position: relative;
|
||||
padding: 60rpx 0 40rpx;
|
||||
text-align: center;
|
||||
border-bottom: 1rpx solid rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.char-text {
|
||||
font-size: 120rpx;
|
||||
font-family: 'KaiTi', 'STKaiti', serif;
|
||||
color: #2D2D2D;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.char-pinyin {
|
||||
margin-top: 16rpx;
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
letter-spacing: 4rpx;
|
||||
}
|
||||
|
||||
/* 内容区域 */
|
||||
.char-content {
|
||||
position: relative;
|
||||
max-height: 400rpx;
|
||||
padding: 30rpx 40rpx;
|
||||
}
|
||||
|
||||
/* 基础信息 */
|
||||
.info-section {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
padding: 20rpx 0;
|
||||
margin-bottom: 30rpx;
|
||||
border-bottom: 1rpx solid rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.info-item {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.info-item .label {
|
||||
display: block;
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.info-item .value {
|
||||
display: block;
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 章节标题 */
|
||||
.section-title {
|
||||
font-size: 26rpx;
|
||||
color: #2D2D2D;
|
||||
font-weight: 600;
|
||||
margin-bottom: 16rpx;
|
||||
padding-left: 16rpx;
|
||||
border-left: 4rpx solid #B22222;
|
||||
}
|
||||
|
||||
/* 内容文本 */
|
||||
.meaning-section,
|
||||
.imagery-section,
|
||||
.poetry-section {
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.meaning-text,
|
||||
.imagery-text,
|
||||
.poetry-text {
|
||||
font-size: 28rpx;
|
||||
color: #555;
|
||||
line-height: 1.8;
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
.poetry-text {
|
||||
font-style: italic;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* AI 按钮 */
|
||||
.ai-btn {
|
||||
position: relative;
|
||||
margin: 0 40rpx 40rpx;
|
||||
padding: 24rpx 0;
|
||||
background: linear-gradient(135deg, #2D2D2D 0%, #444 100%);
|
||||
border-radius: 40rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.ai-btn:active {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.ai-icon {
|
||||
font-size: 28rpx;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.ai-text {
|
||||
font-size: 28rpx;
|
||||
color: #fff;
|
||||
letter-spacing: 4rpx;
|
||||
}
|
||||
@@ -11,8 +11,19 @@ Page({
|
||||
]
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
onLoad(options) {
|
||||
// 页面加载时检查是否需要重置状态
|
||||
// 处理从其他页面传入的参数
|
||||
if (options.keyword) {
|
||||
this.setData({
|
||||
keyword: decodeURIComponent(options.keyword)
|
||||
});
|
||||
}
|
||||
if (options.mode) {
|
||||
this.setData({
|
||||
activeMode: options.mode
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
onShow() {
|
||||
@@ -22,6 +33,13 @@ Page({
|
||||
});
|
||||
},
|
||||
|
||||
// 跳转到灵感广场
|
||||
goToSquare() {
|
||||
wx.navigateTo({
|
||||
url: '/pages/square/square'
|
||||
});
|
||||
},
|
||||
|
||||
// 选择模式
|
||||
selectMode(e) {
|
||||
const mode = e.currentTarget.dataset.mode;
|
||||
@@ -89,7 +107,7 @@ Page({
|
||||
if (activeMode === 'baby' && surname) {
|
||||
url += `&surname=${encodeURIComponent(surname)}`;
|
||||
}
|
||||
|
||||
|
||||
wx.navigateTo({
|
||||
url: url
|
||||
});
|
||||
|
||||
@@ -52,6 +52,12 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 灵感广场入口 -->
|
||||
<view class="square-entry {{isGenerating ? 'fade-out' : ''}}" bindtap="goToSquare">
|
||||
<text class="entry-text">入林搜寻灵感</text>
|
||||
<view class="entry-arrow"></view>
|
||||
</view>
|
||||
|
||||
<view class="footer {{isGenerating ? 'fade-out' : ''}}">
|
||||
<text>© 见素 · 审美溢价</text>
|
||||
</view>
|
||||
|
||||
@@ -231,3 +231,42 @@ page {
|
||||
70% { opacity: 0; }
|
||||
100% { opacity: 1; }
|
||||
}
|
||||
|
||||
/* 灵感广场入口 */
|
||||
.square-entry {
|
||||
position: fixed;
|
||||
bottom: 140rpx;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20rpx 40rpx;
|
||||
opacity: 0.5;
|
||||
transition: opacity 0.3s ease;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.square-entry:active {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.square-entry.fade-out {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.entry-text {
|
||||
font-size: 24rpx;
|
||||
color: #888888;
|
||||
letter-spacing: 4rpx;
|
||||
font-family: "Noto Serif SC", serif;
|
||||
}
|
||||
|
||||
.entry-arrow {
|
||||
width: 10rpx;
|
||||
height: 10rpx;
|
||||
border-right: 1rpx solid #888888;
|
||||
border-top: 1rpx solid #888888;
|
||||
transform: rotate(45deg);
|
||||
margin-left: 10rpx;
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
3
miniprogram/pages/index/index.json
Normal file
3
miniprogram/pages/index/index.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"usingComponents": {}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -4,20 +4,47 @@ Page({
|
||||
showDetail: false,
|
||||
selectedItem: {},
|
||||
selectedIndex: -1,
|
||||
openid: 'test_openid' // 实际应从登录获取
|
||||
openid: null,
|
||||
creditsInfo: {
|
||||
dailyCredits: 5,
|
||||
totalCredits: 0,
|
||||
watchedAdCount: 0
|
||||
},
|
||||
// 静心阅读
|
||||
showMeditation: false,
|
||||
meditationPoem: null,
|
||||
meditationProgress: 0
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.loadFavorites();
|
||||
this.setOpenid();
|
||||
},
|
||||
|
||||
onShow() {
|
||||
// 每次显示页面时刷新列表
|
||||
this.loadFavorites();
|
||||
// 加载积分信息
|
||||
this.loadCreditsInfo();
|
||||
},
|
||||
|
||||
// 设置 openid
|
||||
setOpenid() {
|
||||
const openid = getApp().getOpenid();
|
||||
if (openid) {
|
||||
this.setData({ openid });
|
||||
this.loadFavorites();
|
||||
} else {
|
||||
// openid 还未获取到,延迟重试
|
||||
setTimeout(() => this.setOpenid(), 500);
|
||||
}
|
||||
},
|
||||
|
||||
// 加载收藏列表
|
||||
loadFavorites() {
|
||||
if (!this.data.openid) {
|
||||
console.log('openid 未获取到,跳过加载收藏');
|
||||
return;
|
||||
}
|
||||
const apiBaseUrl = getApp().globalData.apiBaseUrl;
|
||||
wx.request({
|
||||
url: `${apiBaseUrl}/api/favorites/list`,
|
||||
@@ -38,6 +65,140 @@ Page({
|
||||
wx.navigateBack();
|
||||
},
|
||||
|
||||
// 加载积分信息
|
||||
loadCreditsInfo() {
|
||||
const openid = getApp().getOpenid();
|
||||
if (!openid) {
|
||||
console.log('openid 未获取到,跳过加载积分');
|
||||
return;
|
||||
}
|
||||
const apiBaseUrl = getApp().globalData.apiBaseUrl;
|
||||
wx.request({
|
||||
url: `${apiBaseUrl}/api/credits/info`,
|
||||
data: { openid },
|
||||
success: (res) => {
|
||||
if (res.data && res.data.success) {
|
||||
this.setData({ creditsInfo: res.data.data });
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// 点击积分区域
|
||||
onCreditsTap() {
|
||||
const { creditsInfo } = this.data;
|
||||
|
||||
// 如果还有剩余次数,显示提示
|
||||
if (creditsInfo.dailyCredits > 0) {
|
||||
wx.showToast({
|
||||
title: `今日还有 ${creditsInfo.dailyCredits} 次灵感`,
|
||||
icon: 'none'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果次数用完且还可以看广告
|
||||
if (creditsInfo.watchedAdCount < 5) {
|
||||
wx.showModal({
|
||||
title: '静心阅读',
|
||||
content: '观看 15 秒静心画报,即可获得 3 次灵感',
|
||||
confirmText: '开始阅读',
|
||||
cancelText: '取消',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
this.watchAd();
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
wx.showToast({
|
||||
title: '今日次数已达上限,明日再来',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// 静心阅读 - 模拟 15 秒阅读体验
|
||||
watchAd() {
|
||||
const openid = getApp().getOpenid();
|
||||
if (!openid) {
|
||||
wx.showToast({ title: '请先登录', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
|
||||
// 随机选择一首诗词
|
||||
const poems = [
|
||||
{ text: '「采菊东篱下,悠然见南山」', author: '陶渊明《饮酒》' },
|
||||
{ text: '「行到水穷处,坐看云起时」', author: '王维《终南别业》' },
|
||||
{ text: '「人闲桂花落,夜静春山空」', author: '王维《鸟鸣涧》' },
|
||||
{ text: '「松风吹解带,山月照弹琴」', author: '王维《酬张少府》' },
|
||||
{ text: '「曲径通幽处,禅房花木深」', author: '常建《题破山寺后禅院》' },
|
||||
{ text: '「明月松间照,清泉石上流」', author: '王维《山居秋暝》' }
|
||||
];
|
||||
const poem = poems[Math.floor(Math.random() * poems.length)];
|
||||
|
||||
// 显示静心阅读页面
|
||||
this.setData({
|
||||
showMeditation: true,
|
||||
meditationPoem: poem,
|
||||
meditationProgress: 0
|
||||
});
|
||||
|
||||
// 15 秒倒计时 - 每 150ms 增加 1%,总共 150 * 100 = 15000ms = 15秒
|
||||
let progress = 0;
|
||||
const timer = setInterval(() => {
|
||||
progress += 1;
|
||||
this.setData({ meditationProgress: progress });
|
||||
|
||||
if (progress >= 100) {
|
||||
clearInterval(timer);
|
||||
this.meditationTimer = null;
|
||||
// 延迟一下再关闭,让用户看到完成状态
|
||||
setTimeout(() => {
|
||||
this.setData({ showMeditation: false });
|
||||
// 领取奖励
|
||||
this.rewardAd(openid);
|
||||
}, 500);
|
||||
}
|
||||
}, 150);
|
||||
|
||||
// 保存 timer 以便可以提前关闭
|
||||
this.meditationTimer = timer;
|
||||
},
|
||||
|
||||
// 提前关闭静心阅读
|
||||
closeMeditation() {
|
||||
if (this.meditationTimer) {
|
||||
clearInterval(this.meditationTimer);
|
||||
this.meditationTimer = null;
|
||||
}
|
||||
this.setData({ showMeditation: false });
|
||||
},
|
||||
|
||||
// 领取广告奖励
|
||||
rewardAd(openid) {
|
||||
const apiBaseUrl = getApp().globalData.apiBaseUrl;
|
||||
wx.request({
|
||||
url: `${apiBaseUrl}/api/credits/reward`,
|
||||
method: 'POST',
|
||||
header: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
},
|
||||
data: `openid=${encodeURIComponent(openid)}`,
|
||||
success: (res) => {
|
||||
if (res.data && res.data.success) {
|
||||
wx.showToast({ title: '获得 3 次灵感', icon: 'success' });
|
||||
this.loadCreditsInfo();
|
||||
} else {
|
||||
wx.showToast({ title: res.data.message || '领取失败', icon: 'none' });
|
||||
}
|
||||
},
|
||||
fail: () => {
|
||||
wx.showToast({ title: '领取失败', icon: 'none' });
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// 点击收藏项
|
||||
onItemTap(e) {
|
||||
const index = e.currentTarget.dataset.index;
|
||||
@@ -79,12 +240,8 @@ Page({
|
||||
if (res.confirm) {
|
||||
const apiBaseUrl = getApp().globalData.apiBaseUrl;
|
||||
wx.request({
|
||||
url: `${apiBaseUrl}/api/favorites/remove`,
|
||||
url: `${apiBaseUrl}/api/favorites/remove?openid=${this.data.openid}&name=${encodeURIComponent(item.name)}`,
|
||||
method: 'POST',
|
||||
data: {
|
||||
openid: this.data.openid,
|
||||
name: item.name
|
||||
},
|
||||
success: (res) => {
|
||||
if (res.data && res.data.success) {
|
||||
wx.showToast({ title: '已移除', icon: 'success' });
|
||||
|
||||
@@ -6,6 +6,18 @@
|
||||
<view class="header-spacer"></view>
|
||||
</view>
|
||||
|
||||
<!-- 积分信息 - 克制地放在角落 -->
|
||||
<view class="credits-bar" bindtap="onCreditsTap">
|
||||
<view class="credits-item">
|
||||
<text class="credits-icon">✦</text>
|
||||
<text class="credits-text">今日灵感 {{creditsInfo.dailyCredits || 0}}/5</text>
|
||||
</view>
|
||||
<view class="credits-item watch-ad" wx:if="{{(creditsInfo.dailyCredits || 0) === 0 && (creditsInfo.watchedAdCount || 0) < 5}}">
|
||||
<text class="credits-icon">📖</text>
|
||||
<text class="credits-text">静心阅读 +3</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 收藏列表 -->
|
||||
<scroll-view scroll-y class="favorites-scroll" wx:if="{{favorites.length > 0}}">
|
||||
<view class="favorites-grid">
|
||||
@@ -46,4 +58,23 @@
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 静心阅读全屏页 -->
|
||||
<view class="meditation-page {{showMeditation ? 'visible' : ''}}">
|
||||
<view class="meditation-close" bindtap="closeMeditation">×</view>
|
||||
<view class="meditation-content">
|
||||
<view class="meditation-title">静心阅读</view>
|
||||
<view class="meditation-poem" wx:if="{{meditationPoem}}">
|
||||
<text class="poem-text">{{meditationPoem.text}}</text>
|
||||
<text class="poem-author">——{{meditationPoem.author}}</text>
|
||||
</view>
|
||||
<view class="meditation-hint">静观 15 秒,心随诗远</view>
|
||||
</view>
|
||||
<view class="meditation-progress">
|
||||
<view class="progress-bar">
|
||||
<view class="progress-fill" style="width: {{meditationProgress}}%"></view>
|
||||
</view>
|
||||
<text class="progress-text">{{meditationProgress}}%</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -36,6 +36,47 @@ page {
|
||||
width: 76rpx;
|
||||
}
|
||||
|
||||
/* 积分信息 - 克制地放在角落 */
|
||||
.credits-bar {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
gap: 20rpx;
|
||||
margin-bottom: 30rpx;
|
||||
padding: 0 10rpx;
|
||||
}
|
||||
|
||||
.credits-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
padding: 12rpx 20rpx;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
border-radius: 30rpx;
|
||||
opacity: 0.7;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.credits-item:active {
|
||||
opacity: 1;
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.credits-item.watch-ad {
|
||||
background: #F5F5F0;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.credits-icon {
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.credits-text {
|
||||
font-size: 22rpx;
|
||||
color: #666;
|
||||
letter-spacing: 2rpx;
|
||||
}
|
||||
|
||||
/* 收藏列表 */
|
||||
.favorites-scroll {
|
||||
height: calc(100vh - 200rpx);
|
||||
@@ -215,3 +256,113 @@ page {
|
||||
.remove-btn:active {
|
||||
background: #E8E8E8;
|
||||
}
|
||||
|
||||
/* 静心阅读全屏页 */
|
||||
.meditation-page {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(180deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: all 0.5s ease;
|
||||
}
|
||||
|
||||
.meditation-page.visible {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.meditation-close {
|
||||
position: absolute;
|
||||
top: 60rpx;
|
||||
right: 40rpx;
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 48rpx;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.meditation-content {
|
||||
text-align: center;
|
||||
padding: 60rpx;
|
||||
}
|
||||
|
||||
.meditation-title {
|
||||
font-size: 32rpx;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
letter-spacing: 8rpx;
|
||||
margin-bottom: 80rpx;
|
||||
}
|
||||
|
||||
.meditation-poem {
|
||||
margin-bottom: 60rpx;
|
||||
}
|
||||
|
||||
.poem-text {
|
||||
display: block;
|
||||
font-family: "KaiTi", "STKaiti", serif;
|
||||
font-size: 56rpx;
|
||||
color: #ffffff;
|
||||
line-height: 1.8;
|
||||
letter-spacing: 8rpx;
|
||||
margin-bottom: 40rpx;
|
||||
text-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.poem-author {
|
||||
display: block;
|
||||
font-size: 28rpx;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
letter-spacing: 4rpx;
|
||||
}
|
||||
|
||||
.meditation-hint {
|
||||
font-size: 24rpx;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
letter-spacing: 4rpx;
|
||||
margin-top: 60rpx;
|
||||
}
|
||||
|
||||
.meditation-progress {
|
||||
position: absolute;
|
||||
bottom: 100rpx;
|
||||
left: 80rpx;
|
||||
right: 80rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
flex: 1;
|
||||
height: 4rpx;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 2rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
|
||||
border-radius: 2rpx;
|
||||
transition: width 0.15s linear;
|
||||
}
|
||||
|
||||
.progress-text {
|
||||
font-size: 24rpx;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
min-width: 60rpx;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
184
miniprogram/pages/square/square.js
Normal file
184
miniprogram/pages/square/square.js
Normal file
@@ -0,0 +1,184 @@
|
||||
// 灵感广场页面
|
||||
Page({
|
||||
data: {
|
||||
posts: [],
|
||||
leftPosts: [],
|
||||
rightPosts: [],
|
||||
page: 0,
|
||||
size: 20,
|
||||
loading: false,
|
||||
hasMore: true,
|
||||
currentTag: '',
|
||||
sortType: 'latest',
|
||||
showDetail: false,
|
||||
selectedPost: {}
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.loadPosts();
|
||||
},
|
||||
|
||||
onShow() {
|
||||
// 刷新列表
|
||||
if (this.data.posts.length === 0) {
|
||||
this.loadPosts();
|
||||
}
|
||||
},
|
||||
|
||||
// 加载帖子列表
|
||||
loadPosts(reset = false) {
|
||||
if (this.data.loading) return;
|
||||
|
||||
if (reset) {
|
||||
this.setData({
|
||||
page: 0,
|
||||
posts: [],
|
||||
leftPosts: [],
|
||||
rightPosts: [],
|
||||
hasMore: true
|
||||
});
|
||||
}
|
||||
|
||||
this.setData({ loading: true });
|
||||
|
||||
const apiBaseUrl = getApp().globalData.apiBaseUrl;
|
||||
const { page, size, currentTag, sortType } = this.data;
|
||||
|
||||
let url = `${apiBaseUrl}/api/square/posts?page=${page}&size=${size}&sort=${sortType}`;
|
||||
if (currentTag) {
|
||||
url += `&tag=${encodeURIComponent(currentTag)}`;
|
||||
}
|
||||
|
||||
wx.request({
|
||||
url,
|
||||
success: (res) => {
|
||||
if (res.data && res.data.success) {
|
||||
const newPosts = res.data.data || [];
|
||||
const allPosts = reset ? newPosts : [...this.data.posts, ...newPosts];
|
||||
|
||||
// 分配到左右两列
|
||||
const leftPosts = [];
|
||||
const rightPosts = [];
|
||||
allPosts.forEach((post, index) => {
|
||||
if (index % 2 === 0) {
|
||||
leftPosts.push(post);
|
||||
} else {
|
||||
rightPosts.push(post);
|
||||
}
|
||||
});
|
||||
|
||||
this.setData({
|
||||
posts: allPosts,
|
||||
leftPosts,
|
||||
rightPosts,
|
||||
hasMore: res.data.hasMore,
|
||||
page: page + 1,
|
||||
loading: false
|
||||
});
|
||||
} else {
|
||||
this.setData({ loading: false });
|
||||
}
|
||||
},
|
||||
fail: () => {
|
||||
this.setData({ loading: false });
|
||||
wx.showToast({ title: '加载失败', icon: 'none' });
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// 点击标签
|
||||
onTagTap(e) {
|
||||
const tag = e.currentTarget.dataset.tag;
|
||||
this.setData({ currentTag: tag });
|
||||
this.loadPosts(true);
|
||||
},
|
||||
|
||||
// 切换排序
|
||||
onSortTap(e) {
|
||||
const type = e.currentTarget.dataset.type;
|
||||
this.setData({ sortType: type });
|
||||
this.loadPosts(true);
|
||||
},
|
||||
|
||||
// 点击帖子
|
||||
onPostTap(e) {
|
||||
const post = e.currentTarget.dataset.post;
|
||||
this.setData({
|
||||
selectedPost: post,
|
||||
showDetail: true
|
||||
});
|
||||
},
|
||||
|
||||
// 关闭详情
|
||||
onCloseDetail() {
|
||||
this.setData({ showDetail: false });
|
||||
},
|
||||
|
||||
// 阻止冒泡
|
||||
onDetailTap() {
|
||||
// 什么都不做
|
||||
},
|
||||
|
||||
// 点赞
|
||||
onLikeTap() {
|
||||
const { selectedPost } = this.data;
|
||||
const apiBaseUrl = getApp().globalData.apiBaseUrl;
|
||||
|
||||
wx.request({
|
||||
url: `${apiBaseUrl}/api/square/posts/${selectedPost.id}/like`,
|
||||
method: 'POST',
|
||||
success: (res) => {
|
||||
if (res.data && res.data.success) {
|
||||
// 更新本地数据
|
||||
const updatedPost = { ...selectedPost, likeCount: (selectedPost.likeCount || 0) + 1 };
|
||||
this.setData({ selectedPost: updatedPost });
|
||||
|
||||
// 更新列表中的数据
|
||||
const posts = this.data.posts.map(p =>
|
||||
p.id === selectedPost.id ? updatedPost : p
|
||||
);
|
||||
this.setData({ posts });
|
||||
|
||||
wx.showToast({ title: '已共鸣', icon: 'none' });
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// 同款跳转(卡片上)
|
||||
onCopyTap(e) {
|
||||
const { keyword, mode } = e.currentTarget.dataset;
|
||||
this.navigateToHome(keyword, mode);
|
||||
},
|
||||
|
||||
// 同款跳转(详情中)
|
||||
onDetailCopyTap() {
|
||||
const { keyword, mode } = this.data.selectedPost;
|
||||
this.navigateToHome(keyword, mode);
|
||||
},
|
||||
|
||||
// 跳转到首页
|
||||
navigateToHome(keyword, mode) {
|
||||
if (!keyword) {
|
||||
wx.showToast({ title: '无法获取关键词', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
|
||||
wx.reLaunch({
|
||||
url: `/pages/home/home?keyword=${encodeURIComponent(keyword)}&mode=${mode || 'poetic'}`
|
||||
});
|
||||
},
|
||||
|
||||
// 下拉刷新
|
||||
onPullDownRefresh() {
|
||||
this.loadPosts(true);
|
||||
wx.stopPullDownRefresh();
|
||||
},
|
||||
|
||||
// 上拉加载更多
|
||||
onReachBottom() {
|
||||
if (this.data.hasMore && !this.data.loading) {
|
||||
this.loadPosts();
|
||||
}
|
||||
}
|
||||
});
|
||||
8
miniprogram/pages/square/square.json
Normal file
8
miniprogram/pages/square/square.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"navigationBarTitleText": "灵感广场",
|
||||
"navigationBarBackgroundColor": "#F5F5F0",
|
||||
"navigationBarTextStyle": "black",
|
||||
"backgroundColor": "#F5F5F0",
|
||||
"enablePullDownRefresh": true,
|
||||
"onReachBottomDistance": 100
|
||||
}
|
||||
141
miniprogram/pages/square/square.wxml
Normal file
141
miniprogram/pages/square/square.wxml
Normal file
@@ -0,0 +1,141 @@
|
||||
<!-- 灵感广场页面 - 瀑布流展示 -->
|
||||
<view class="square-container">
|
||||
<!-- 顶部导航 -->
|
||||
<view class="header">
|
||||
<view class="title">灵感广场</view>
|
||||
<view class="subtitle">发现诗意,共鸣美好</view>
|
||||
</view>
|
||||
|
||||
<!-- 筛选标签 -->
|
||||
<scroll-view class="tag-bar" scroll-x>
|
||||
<view class="tag-list">
|
||||
<view class="tag {{currentTag === '' ? 'active' : ''}}" bindtap="onTagTap" data-tag="">全部</view>
|
||||
<view class="tag {{currentTag === '清冷' ? 'active' : ''}}" bindtap="onTagTap" data-tag="清冷">清冷</view>
|
||||
<view class="tag {{currentTag === '温柔' ? 'active' : ''}}" bindtap="onTagTap" data-tag="温柔">温柔</view>
|
||||
<view class="tag {{currentTag === '古雅' ? 'active' : ''}}" bindtap="onTagTap" data-tag="古雅">古雅</view>
|
||||
<view class="tag {{currentTag === '诗意' ? 'active' : ''}}" bindtap="onTagTap" data-tag="诗意">诗意</view>
|
||||
<view class="tag {{currentTag === '自然' ? 'active' : ''}}" bindtap="onTagTap" data-tag="自然">自然</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 排序切换 -->
|
||||
<view class="sort-bar">
|
||||
<view class="sort-item {{sortType === 'latest' ? 'active' : ''}}" bindtap="onSortTap" data-type="latest">最新</view>
|
||||
<view class="sort-item {{sortType === 'hot' ? 'active' : ''}}" bindtap="onSortTap" data-type="hot">热门</view>
|
||||
</view>
|
||||
|
||||
<!-- 瀑布流列表 -->
|
||||
<view class="waterfall">
|
||||
<view class="column left">
|
||||
<view class="post-card"
|
||||
wx:for="{{leftPosts}}"
|
||||
wx:key="id"
|
||||
bindtap="onPostTap"
|
||||
data-post="{{item}}">
|
||||
<image class="post-image"
|
||||
src="{{item.imageUrl}}"
|
||||
mode="widthFix"
|
||||
lazy-load
|
||||
show-menu-by-longpress
|
||||
wx:if="{{item.imageUrl}}"/>
|
||||
<view class="post-image-placeholder" wx:if="{{!item.imageUrl}}">
|
||||
<text class="placeholder-text">{{item.name}}</text>
|
||||
</view>
|
||||
<view class="post-info">
|
||||
<view class="post-name">{{item.name}}</view>
|
||||
<view class="post-origin" wx:if="{{item.origin}}">{{item.origin}}</view>
|
||||
<view class="post-footer">
|
||||
<view class="post-keyword" wx:if="{{item.keyword}}">#{{item.keyword}}</view>
|
||||
<view class="post-likes">
|
||||
<text class="like-icon">♡</text>
|
||||
<text class="like-count">{{item.likeCount || 0}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 同款跳转按钮 -->
|
||||
<view class="copy-btn"
|
||||
catchtap="onCopyTap"
|
||||
data-keyword="{{item.keyword}}"
|
||||
data-mode="{{item.mode}}">
|
||||
<text>同款意境</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="column right">
|
||||
<view class="post-card"
|
||||
wx:for="{{rightPosts}}"
|
||||
wx:key="id"
|
||||
bindtap="onPostTap"
|
||||
data-post="{{item}}">
|
||||
<image class="post-image"
|
||||
src="{{item.imageUrl}}"
|
||||
mode="widthFix"
|
||||
lazy-load
|
||||
show-menu-by-longpress
|
||||
wx:if="{{item.imageUrl}}"/>
|
||||
<view class="post-image-placeholder" wx:if="{{!item.imageUrl}}">
|
||||
<text class="placeholder-text">{{item.name}}</text>
|
||||
</view>
|
||||
<view class="post-info">
|
||||
<view class="post-name">{{item.name}}</view>
|
||||
<view class="post-origin" wx:if="{{item.origin}}">{{item.origin}}</view>
|
||||
<view class="post-footer">
|
||||
<view class="post-keyword" wx:if="{{item.keyword}}">#{{item.keyword}}</view>
|
||||
<view class="post-likes">
|
||||
<text class="like-icon">♡</text>
|
||||
<text class="like-count">{{item.likeCount || 0}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 同款跳转按钮 -->
|
||||
<view class="copy-btn"
|
||||
catchtap="onCopyTap"
|
||||
data-keyword="{{item.keyword}}"
|
||||
data-mode="{{item.mode}}">
|
||||
<text>同款意境</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载更多 -->
|
||||
<view class="load-more" wx:if="{{loading}}">
|
||||
<view class="loading-dots">
|
||||
<view class="dot"></view>
|
||||
<view class="dot"></view>
|
||||
<view class="dot"></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="no-more" wx:if="{{!hasMore && posts.length > 0}}">
|
||||
<text>没有更多了</text>
|
||||
</view>
|
||||
|
||||
<view class="empty" wx:if="{{!loading && posts.length === 0}}">
|
||||
<text class="empty-icon">✿</text>
|
||||
<text class="empty-text">暂无灵感,快去生成并分享吧</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 帖子详情弹窗 -->
|
||||
<view class="detail-mask {{showDetail ? 'show' : ''}}" bindtap="onCloseDetail">
|
||||
<view class="detail-container {{showDetail ? 'show' : ''}}" catchtap="onDetailTap">
|
||||
<image class="detail-image" src="{{selectedPost.imageUrl}}" mode="widthFix"/>
|
||||
<view class="detail-info">
|
||||
<view class="detail-name">{{selectedPost.name}}</view>
|
||||
<view class="detail-origin" wx:if="{{selectedPost.origin}}">{{selectedPost.origin}}</view>
|
||||
<view class="detail-desc" wx:if="{{selectedPost.description}}">{{selectedPost.description}}</view>
|
||||
<view class="detail-actions">
|
||||
<view class="action-btn like" bindtap="onLikeTap">
|
||||
<text class="action-icon">♡</text>
|
||||
<text class="action-text">共鸣 {{selectedPost.likeCount || 0}}</text>
|
||||
</view>
|
||||
<view class="action-btn copy" bindtap="onDetailCopyTap">
|
||||
<text class="action-text">同款意境</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="detail-close" bindtap="onCloseDetail">×</view>
|
||||
</view>
|
||||
</view>
|
||||
376
miniprogram/pages/square/square.wxss
Normal file
376
miniprogram/pages/square/square.wxss
Normal file
@@ -0,0 +1,376 @@
|
||||
/* 灵感广场页面 - 瀑布流风格 */
|
||||
|
||||
.square-container {
|
||||
min-height: 100vh;
|
||||
background: #F5F5F0;
|
||||
padding-bottom: 40rpx;
|
||||
}
|
||||
|
||||
/* 顶部导航 */
|
||||
.header {
|
||||
padding: 40rpx 30rpx 20rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 40rpx;
|
||||
font-weight: 600;
|
||||
color: #2D2D2D;
|
||||
letter-spacing: 8rpx;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
margin-top: 8rpx;
|
||||
letter-spacing: 4rpx;
|
||||
}
|
||||
|
||||
/* 标签栏 */
|
||||
.tag-bar {
|
||||
padding: 20rpx 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.tag-list {
|
||||
padding: 0 20rpx;
|
||||
}
|
||||
|
||||
.tag {
|
||||
display: inline-block;
|
||||
padding: 12rpx 28rpx;
|
||||
margin: 0 10rpx;
|
||||
background: #fff;
|
||||
border-radius: 30rpx;
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.tag.active {
|
||||
background: #2D2D2D;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* 排序栏 */
|
||||
.sort-bar {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 10rpx 0 20rpx;
|
||||
gap: 40rpx;
|
||||
}
|
||||
|
||||
.sort-item {
|
||||
font-size: 26rpx;
|
||||
color: #999;
|
||||
padding: 8rpx 16rpx;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.sort-item.active {
|
||||
color: #2D2D2D;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.sort-item.active::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 20rpx;
|
||||
height: 4rpx;
|
||||
background: #B22222;
|
||||
border-radius: 2rpx;
|
||||
}
|
||||
|
||||
/* 瀑布流 */
|
||||
.waterfall {
|
||||
display: flex;
|
||||
padding: 0 20rpx;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.column {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
/* 帖子卡片 */
|
||||
.post-card {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.06);
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
|
||||
.post-card:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.post-image {
|
||||
width: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.post-image-placeholder {
|
||||
width: 100%;
|
||||
height: 300rpx;
|
||||
background: linear-gradient(135deg, #F5F5F0 0%, #E8E8E0 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.placeholder-text {
|
||||
font-size: 48rpx;
|
||||
font-family: 'KaiTi', 'STKaiti', serif;
|
||||
color: #999;
|
||||
letter-spacing: 8rpx;
|
||||
}
|
||||
|
||||
.post-info {
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
.post-name {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #2D2D2D;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.post-origin {
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
line-height: 1.5;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.post-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 16rpx;
|
||||
padding-top: 16rpx;
|
||||
border-top: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.post-keyword {
|
||||
font-size: 22rpx;
|
||||
color: #B22222;
|
||||
}
|
||||
|
||||
.post-likes {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6rpx;
|
||||
}
|
||||
|
||||
.like-icon {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.like-count {
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
/* 同款按钮 */
|
||||
.copy-btn {
|
||||
margin: 0 20rpx 20rpx;
|
||||
padding: 16rpx 0;
|
||||
background: #F5F5F0;
|
||||
border-radius: 8rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.copy-btn text {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* 加载更多 */
|
||||
.load-more {
|
||||
padding: 40rpx 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.loading-dots {
|
||||
display: flex;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.dot {
|
||||
width: 12rpx;
|
||||
height: 12rpx;
|
||||
background: #ccc;
|
||||
border-radius: 50%;
|
||||
animation: bounce 1.4s infinite ease-in-out both;
|
||||
}
|
||||
|
||||
.dot:nth-child(1) { animation-delay: -0.32s; }
|
||||
.dot:nth-child(2) { animation-delay: -0.16s; }
|
||||
|
||||
@keyframes bounce {
|
||||
0%, 80%, 100% { transform: scale(0); }
|
||||
40% { transform: scale(1); }
|
||||
}
|
||||
|
||||
.no-more {
|
||||
text-align: center;
|
||||
padding: 40rpx 0;
|
||||
color: #999;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
/* 空状态 */
|
||||
.empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 100rpx 0;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 80rpx;
|
||||
color: #ccc;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 26rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
/* 详情弹窗 */
|
||||
.detail-mask {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: all 0.3s;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.detail-mask.show {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.detail-container {
|
||||
width: 640rpx;
|
||||
max-height: 80vh;
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
overflow: hidden;
|
||||
transform: scale(0.9);
|
||||
transition: all 0.3s;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.detail-container.show {
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
.detail-image {
|
||||
width: 100%;
|
||||
max-height: 400rpx;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.detail-info {
|
||||
padding: 30rpx;
|
||||
}
|
||||
|
||||
.detail-name {
|
||||
font-size: 40rpx;
|
||||
font-weight: 600;
|
||||
color: #2D2D2D;
|
||||
text-align: center;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.detail-origin {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
line-height: 1.6;
|
||||
text-align: center;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.detail-desc {
|
||||
font-size: 26rpx;
|
||||
color: #555;
|
||||
line-height: 1.8;
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
.detail-actions {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 40rpx;
|
||||
margin-top: 30rpx;
|
||||
padding-top: 30rpx;
|
||||
border-top: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
padding: 16rpx 40rpx;
|
||||
border-radius: 40rpx;
|
||||
}
|
||||
|
||||
.action-btn.like {
|
||||
background: #F5F5F0;
|
||||
}
|
||||
|
||||
.action-btn.copy {
|
||||
background: #2D2D2D;
|
||||
}
|
||||
|
||||
.action-icon {
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.action-btn.copy .action-text {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.action-text {
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.detail-close {
|
||||
position: absolute;
|
||||
top: 20rpx;
|
||||
right: 20rpx;
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 36rpx;
|
||||
color: #fff;
|
||||
}
|
||||
260
miniprogram/utils/audio.js
Normal file
260
miniprogram/utils/audio.js
Normal file
@@ -0,0 +1,260 @@
|
||||
// 音频管理工具 - 环境白噪音与音效管理
|
||||
|
||||
// 背景音频管理器(用于环境白噪音)
|
||||
let bgmManager = null;
|
||||
|
||||
// 音效上下文缓存
|
||||
const soundEffects = {};
|
||||
|
||||
// 当前环境音类型
|
||||
let currentAmbienceType = 'silent';
|
||||
|
||||
// 音量渐变定时器
|
||||
let fadeTimer = null;
|
||||
|
||||
/**
|
||||
* 初始化背景音频管理器
|
||||
*/
|
||||
export const initBGM = () => {
|
||||
if (!bgmManager) {
|
||||
bgmManager = wx.getBackgroundAudioManager();
|
||||
bgmManager.title = '见素-环境音';
|
||||
bgmManager.epname = '环境白噪音';
|
||||
bgmManager.singer = '见素';
|
||||
}
|
||||
return bgmManager;
|
||||
};
|
||||
|
||||
/**
|
||||
* 播放环境白噪音
|
||||
* @param {string} type - 环境音类型: 'silent' | 'wind' | 'rain' | 'guqin' | 'white' | 'forest' | 'stream'
|
||||
* @param {boolean} fade - 是否淡入
|
||||
*/
|
||||
export const playAmbience = (type = 'wind', fade = true) => {
|
||||
const bgm = initBGM();
|
||||
|
||||
// 如果相同类型且正在播放,不重复操作
|
||||
if (currentAmbienceType === type && bgm.currentTime > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
currentAmbienceType = type;
|
||||
|
||||
// 静音模式
|
||||
if (type === 'silent') {
|
||||
fadeOutAndStop();
|
||||
return;
|
||||
}
|
||||
|
||||
// 音频资源映射(使用本地音频)
|
||||
const audioMap = {
|
||||
wind: '/assets/audios/wind.mp3', // 风铃
|
||||
rain: '/assets/audios/rain.mp3', // 雨声
|
||||
guqin: '/assets/audios/guqin.mp3', // 古琴
|
||||
white: '/assets/audios/white.mp3', // 白噪音
|
||||
forest: '/assets/audios/forest.mp3', // 森林
|
||||
stream: '/assets/audios/stream.mp3' // 溪流
|
||||
};
|
||||
|
||||
// 音频类型名称映射
|
||||
const typeNameMap = {
|
||||
wind: '风铃',
|
||||
rain: '雨落',
|
||||
guqin: '古琴',
|
||||
white: '白噪音',
|
||||
forest: '森林',
|
||||
stream: '溪流'
|
||||
};
|
||||
|
||||
const src = audioMap[type];
|
||||
if (!src) return;
|
||||
|
||||
// 设置音频属性
|
||||
bgm.title = `见素 - ${typeNameMap[type] || '环境音'}`;
|
||||
bgm.singer = '环境音';
|
||||
bgm.coverImgUrl = '';
|
||||
|
||||
// 监听错误事件
|
||||
bgm.onError((err) => {
|
||||
console.error('音频播放错误:', err);
|
||||
wx.showToast({
|
||||
title: '音频加载失败',
|
||||
icon: 'none'
|
||||
});
|
||||
});
|
||||
|
||||
// 淡入效果
|
||||
if (fade) {
|
||||
bgm.volume = 0;
|
||||
bgm.src = src;
|
||||
bgm.play();
|
||||
fadeIn(0.3, 1000); // 1秒内淡入到30%音量
|
||||
} else {
|
||||
bgm.volume = 0.3;
|
||||
bgm.src = src;
|
||||
bgm.play();
|
||||
}
|
||||
|
||||
console.log(`播放环境音: ${type}`);
|
||||
};
|
||||
|
||||
/**
|
||||
* 淡入音量
|
||||
* @param {number} targetVolume - 目标音量 0-1
|
||||
* @param {number} duration - 淡入时长 ms
|
||||
*/
|
||||
const fadeIn = (targetVolume = 0.3, duration = 1000) => {
|
||||
if (!bgmManager) return;
|
||||
|
||||
clearInterval(fadeTimer);
|
||||
const step = targetVolume / (duration / 50); // 每50ms调整一次
|
||||
|
||||
fadeTimer = setInterval(() => {
|
||||
if (bgmManager.volume < targetVolume) {
|
||||
bgmManager.volume = Math.min(bgmManager.volume + step, targetVolume);
|
||||
} else {
|
||||
clearInterval(fadeTimer);
|
||||
}
|
||||
}, 50);
|
||||
};
|
||||
|
||||
/**
|
||||
* 淡出并停止
|
||||
* @param {number} duration - 淡出时长 ms
|
||||
*/
|
||||
export const fadeOutAndStop = (duration = 500) => {
|
||||
if (!bgmManager) return;
|
||||
|
||||
clearInterval(fadeTimer);
|
||||
const startVolume = bgmManager.volume;
|
||||
const step = startVolume / (duration / 50);
|
||||
|
||||
fadeTimer = setInterval(() => {
|
||||
if (bgmManager.volume > 0.01) {
|
||||
bgmManager.volume = Math.max(bgmManager.volume - step, 0);
|
||||
} else {
|
||||
clearInterval(fadeTimer);
|
||||
bgmManager.stop();
|
||||
currentAmbienceType = 'silent';
|
||||
}
|
||||
}, 50);
|
||||
};
|
||||
|
||||
/**
|
||||
* 播放音效
|
||||
* @param {string} type - 音效类型
|
||||
* @param {object} options - 配置选项
|
||||
*/
|
||||
export const playSoundEffect = (type, options = {}) => {
|
||||
const { volume = 1, loop = false } = options;
|
||||
|
||||
// 音效资源映射
|
||||
const effectMap = {
|
||||
flip: '/assets/audios/paper.mp3', // 翻页/纸张摩擦声
|
||||
success: '/assets/audios/success.mp3', // 成功
|
||||
inkDrop: '/assets/audios/inkdrop.mp3', // 水滴/入墨声
|
||||
swipe: '/assets/audios/swipe.mp3', // 滑动
|
||||
tap: '/assets/audios/tap.mp3', // 点击
|
||||
breathe: '/assets/audios/breathe.mp3', // 呼吸
|
||||
char: '/assets/audios/char.mp3' // 字显现
|
||||
};
|
||||
|
||||
const src = effectMap[type];
|
||||
if (!src) return;
|
||||
|
||||
// 复用或创建音频上下文
|
||||
if (!soundEffects[type]) {
|
||||
soundEffects[type] = wx.createInnerAudioContext();
|
||||
}
|
||||
|
||||
const ctx = soundEffects[type];
|
||||
ctx.src = src;
|
||||
ctx.volume = volume;
|
||||
ctx.loop = loop;
|
||||
|
||||
ctx.stop();
|
||||
try {
|
||||
ctx.play();
|
||||
} catch (err) {
|
||||
console.log('音效播放失败:', err);
|
||||
}
|
||||
|
||||
return ctx;
|
||||
};
|
||||
|
||||
/**
|
||||
* 停止指定音效
|
||||
* @param {string} type - 音效类型
|
||||
*/
|
||||
export const stopSoundEffect = (type) => {
|
||||
if (soundEffects[type]) {
|
||||
soundEffects[type].stop();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 停止所有音效
|
||||
*/
|
||||
export const stopAllSoundEffects = () => {
|
||||
Object.keys(soundEffects).forEach(type => {
|
||||
soundEffects[type].stop();
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 停止环境音
|
||||
*/
|
||||
export const stopAmbience = () => {
|
||||
fadeOutAndStop();
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取当前环境音类型
|
||||
*/
|
||||
export const getCurrentAmbience = () => currentAmbienceType;
|
||||
|
||||
/**
|
||||
* 切换环境音
|
||||
* @param {string} type - 环境音类型
|
||||
*/
|
||||
export const toggleAmbience = (type) => {
|
||||
if (currentAmbienceType === type) {
|
||||
playAmbience('silent');
|
||||
} else {
|
||||
playAmbience(type);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 呼吸震动效果 - 配合名字逐字显现
|
||||
* @param {number} duration - 总时长 ms
|
||||
* @param {number} intensity - 震动强度 light/medium/heavy
|
||||
*/
|
||||
export const breatheVibration = (duration = 2000, intensity = 'light') => {
|
||||
const interval = duration / 4; // 分4次震动
|
||||
let count = 0;
|
||||
|
||||
const vibrate = () => {
|
||||
if (count < 4) {
|
||||
wx.vibrateShort({ type: intensity });
|
||||
count++;
|
||||
setTimeout(vibrate, interval);
|
||||
}
|
||||
};
|
||||
|
||||
vibrate();
|
||||
};
|
||||
|
||||
/**
|
||||
* 清理音频资源
|
||||
*/
|
||||
export const cleanup = () => {
|
||||
clearInterval(fadeTimer);
|
||||
if (bgmManager) {
|
||||
bgmManager.stop();
|
||||
}
|
||||
Object.keys(soundEffects).forEach(type => {
|
||||
soundEffects[type].destroy();
|
||||
delete soundEffects[type];
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user