feat: 完成见素起名小程序核心功能
- 实现 AI 起名功能(Kimi API 接入) - 添加用户收藏功能(MySQL 数据库) - 实现海报生成与分享 - 添加音效和触觉反馈 - 配置生产环境部署(WAR 包 + Nginx) - 支持多种起名模式(经典、诗词、自然、现代) - 实现分批加载优化体验
This commit is contained in:
@@ -1,25 +1,98 @@
|
||||
Page({
|
||||
data: {
|
||||
keyword: ''
|
||||
keyword: '',
|
||||
surname: '',
|
||||
activeMode: 'baby',
|
||||
isGenerating: false,
|
||||
modes: [
|
||||
{ key: 'baby', label: '宝宝' },
|
||||
{ key: 'persona', label: '人设' },
|
||||
{ key: 'classic', label: '拾遗' }
|
||||
]
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
// 页面加载时检查是否需要重置状态
|
||||
},
|
||||
|
||||
onShow() {
|
||||
// 每次显示页面时重置生成状态
|
||||
this.setData({
|
||||
isGenerating: false
|
||||
});
|
||||
},
|
||||
|
||||
// 选择模式
|
||||
selectMode(e) {
|
||||
const mode = e.currentTarget.dataset.mode;
|
||||
this.setData({
|
||||
activeMode: mode
|
||||
});
|
||||
},
|
||||
|
||||
// 姓氏输入
|
||||
onSurnameInput(e) {
|
||||
this.setData({
|
||||
surname: e.detail.value
|
||||
});
|
||||
},
|
||||
|
||||
// 关键词输入
|
||||
onInput(e) {
|
||||
this.setData({
|
||||
keyword: e.detail.value
|
||||
});
|
||||
},
|
||||
|
||||
startNaming() {
|
||||
if (!this.data.keyword.trim()) {
|
||||
// 开始生成(带水墨动画)
|
||||
startGenerate() {
|
||||
const { keyword, surname, activeMode } = this.data;
|
||||
|
||||
// 非空校验
|
||||
if (!keyword.trim()) {
|
||||
const placeholderMap = {
|
||||
'baby': '请输入期待',
|
||||
'persona': '请输入人设',
|
||||
'classic': '请输入关键词'
|
||||
};
|
||||
wx.showToast({
|
||||
title: '请输入一抹期待',
|
||||
title: placeholderMap[activeMode] || '请输入内容',
|
||||
icon: 'none'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 宝宝模式需要姓氏
|
||||
if (activeMode === 'baby' && !surname.trim()) {
|
||||
wx.showToast({
|
||||
title: '请输入姓氏',
|
||||
icon: 'none'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
wx.navigateTo({
|
||||
url: `/pages/index/index?keyword=${encodeURIComponent(this.data.keyword)}`
|
||||
// 触发水墨动画
|
||||
this.setData({
|
||||
isGenerating: true
|
||||
});
|
||||
|
||||
// 播放水滴声
|
||||
getApp().playAudio('inkDrop');
|
||||
|
||||
// 触觉反馈
|
||||
wx.vibrateShort({ type: 'light' });
|
||||
|
||||
// 延迟 1000ms 后跳转,确保仪式感完整
|
||||
setTimeout(() => {
|
||||
// 构建跳转 URL
|
||||
let url = `/pages/index/index?keyword=${encodeURIComponent(keyword)}&mode=${activeMode}`;
|
||||
if (activeMode === 'baby' && surname) {
|
||||
url += `&surname=${encodeURIComponent(surname)}`;
|
||||
}
|
||||
|
||||
wx.navigateTo({
|
||||
url: url
|
||||
});
|
||||
}, 1000);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,12 +1,45 @@
|
||||
<view class="container">
|
||||
<view class="content">
|
||||
<!-- 水墨动画遮罩 -->
|
||||
<view class="ink-overlay {{isGenerating ? 'active' : ''}}">
|
||||
<view class="ink-circle"></view>
|
||||
</view>
|
||||
|
||||
<view class="content {{isGenerating ? 'fade-out' : ''}}">
|
||||
<view class="title">见素</view>
|
||||
<view class="subtitle">回归名字的诗意与留白</view>
|
||||
|
||||
<!-- 模式选择 -->
|
||||
<view class="mode-selector">
|
||||
<view
|
||||
wx:for="{{modes}}"
|
||||
wx:key="key"
|
||||
class="mode-item {{activeMode === item.key ? 'active' : ''}}"
|
||||
bindtap="selectMode"
|
||||
data-mode="{{item.key}}"
|
||||
>
|
||||
{{item.label}}
|
||||
<view class="mode-dot" wx:if="{{activeMode === item.key}}"></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 姓氏输入(仅在宝宝模式显示) -->
|
||||
<view class="surname-section" wx:if="{{activeMode === 'baby'}}">
|
||||
<input
|
||||
class="surname-input"
|
||||
placeholder="姓氏"
|
||||
placeholder-class="placeholder"
|
||||
bindinput="onSurnameInput"
|
||||
value="{{surname}}"
|
||||
maxlength="2"
|
||||
/>
|
||||
<view class="line"></view>
|
||||
</view>
|
||||
|
||||
<!-- 关键词输入 -->
|
||||
<view class="input-section">
|
||||
<input
|
||||
class="keyword-input"
|
||||
placeholder="输入关键词,如:自由、清冷"
|
||||
placeholder="{{activeMode === 'baby' ? '期待,如:自由、清冷' : activeMode === 'persona' ? '人设,如:温柔、坚韧' : '关键词,如:自由、清冷'}}"
|
||||
placeholder-class="placeholder"
|
||||
bindinput="onInput"
|
||||
value="{{keyword}}"
|
||||
@@ -15,11 +48,11 @@
|
||||
</view>
|
||||
|
||||
<view class="action-section">
|
||||
<button class="generate-btn" bindtap="startNaming">感悟名字</button>
|
||||
<button class="generate-btn" bindtap="startGenerate">感悟名字</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="footer">
|
||||
<view class="footer {{isGenerating ? 'fade-out' : ''}}">
|
||||
<text>© 见素 · 审美溢价</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -9,14 +9,67 @@ page {
|
||||
flex-direction: column;
|
||||
padding: 0 80rpx;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 水墨动画遮罩 */
|
||||
.ink-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 100;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
overflow: hidden;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.ink-overlay.active {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.ink-circle {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 0;
|
||||
height: 0;
|
||||
background-color: #000000;
|
||||
border-radius: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.ink-overlay.active .ink-circle {
|
||||
animation: inkSpread 1.8s cubic-bezier(0.25, 0.1, 0.25, 1) forwards;
|
||||
}
|
||||
|
||||
@keyframes inkSpread {
|
||||
0% {
|
||||
width: 0;
|
||||
height: 0;
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
width: 350vmax;
|
||||
height: 350vmax;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
margin-top: 35vh;
|
||||
margin-top: 30vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
animation: contentFadeIn 1.5s cubic-bezier(0.19, 1, 0.22, 1);
|
||||
transition: opacity 0.5s ease;
|
||||
}
|
||||
|
||||
.content.fade-out {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.title {
|
||||
@@ -33,12 +86,61 @@ page {
|
||||
font-size: 22rpx;
|
||||
color: #D0D0D0;
|
||||
letter-spacing: 6rpx;
|
||||
margin-bottom: 150rpx;
|
||||
margin-bottom: 80rpx;
|
||||
}
|
||||
|
||||
/* 模式选择器 */
|
||||
.mode-selector {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
margin-bottom: 60rpx;
|
||||
gap: 60rpx;
|
||||
}
|
||||
|
||||
.mode-item {
|
||||
font-size: 24rpx;
|
||||
color: #B0B0B0;
|
||||
letter-spacing: 4rpx;
|
||||
padding: 10rpx 0;
|
||||
position: relative;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.mode-item.active {
|
||||
color: #2D2D2D;
|
||||
}
|
||||
|
||||
.mode-dot {
|
||||
position: absolute;
|
||||
bottom: -4rpx;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 6rpx;
|
||||
height: 6rpx;
|
||||
background-color: #2D2D2D;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
/* 姓氏输入 */
|
||||
.surname-section {
|
||||
width: 100%;
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.surname-input {
|
||||
width: 100%;
|
||||
height: 80rpx;
|
||||
text-align: center;
|
||||
font-size: 32rpx;
|
||||
color: #4A4A4A;
|
||||
letter-spacing: 4rpx;
|
||||
}
|
||||
|
||||
/* 关键词输入 */
|
||||
.input-section {
|
||||
width: 100%;
|
||||
margin-bottom: 80rpx;
|
||||
margin-bottom: 60rpx;
|
||||
}
|
||||
|
||||
.keyword-input {
|
||||
@@ -63,6 +165,7 @@ page {
|
||||
transition: width 0.6s cubic-bezier(0.165, 0.84, 0.44, 1);
|
||||
}
|
||||
|
||||
.surname-section:focus-within .line,
|
||||
.input-section:focus-within .line {
|
||||
width: 80rpx;
|
||||
background-color: #2D2D2D;
|
||||
@@ -75,16 +178,18 @@ page {
|
||||
margin-top: 40rpx;
|
||||
}
|
||||
|
||||
/* 全黑背景、无圆角、加宽字间距的按钮 */
|
||||
.generate-btn {
|
||||
background: none !important;
|
||||
color: #A0A0A0 !important;
|
||||
background-color: #1a1a1a !important;
|
||||
color: #ffffff !important;
|
||||
font-size: 24rpx !important;
|
||||
font-weight: 200 !important;
|
||||
padding: 20rpx 60rpx !important;
|
||||
letter-spacing: 12rpx !important;
|
||||
text-indent: 12rpx !important;
|
||||
transition: all 0.3s;
|
||||
font-weight: 300 !important;
|
||||
padding: 24rpx 80rpx !important;
|
||||
letter-spacing: 16rpx !important;
|
||||
text-indent: 16rpx !important;
|
||||
transition: all 0.3s ease;
|
||||
border: none !important;
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
|
||||
.generate-btn::after {
|
||||
@@ -92,7 +197,7 @@ page {
|
||||
}
|
||||
|
||||
.generate-btn:active {
|
||||
color: #2D2D2D !important;
|
||||
background-color: #333333 !important;
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
@@ -103,6 +208,11 @@ page {
|
||||
right: 0;
|
||||
text-align: center;
|
||||
animation: footerFadeIn 3s ease-in;
|
||||
transition: opacity 0.5s ease;
|
||||
}
|
||||
|
||||
.footer.fade-out {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.footer text {
|
||||
@@ -120,4 +230,4 @@ page {
|
||||
0% { opacity: 0; }
|
||||
70% { opacity: 0; }
|
||||
100% { opacity: 1; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,18 @@
|
||||
const EXIT_THRESHOLD = 80;
|
||||
const FAST_SWIPE_VELOCITY = 0.5;
|
||||
|
||||
// 加载文案库
|
||||
const LOADING_QUOTES = [
|
||||
"正在翻阅《古今集成》...",
|
||||
"于诗书中寻觅意境...",
|
||||
"聆听平仄的韵律...",
|
||||
"在字里行间游走...",
|
||||
"采撷一缕清风入名...",
|
||||
"品味楚辞的芬芳...",
|
||||
"捕捉诗经的灵光...",
|
||||
"与古人隔空对话..."
|
||||
];
|
||||
|
||||
Page({
|
||||
isDragging: false,
|
||||
isExiting: false,
|
||||
@@ -8,6 +20,7 @@ Page({
|
||||
startX: 0,
|
||||
startY: 0,
|
||||
lastX: 0,
|
||||
loadingQuoteTimer: null,
|
||||
|
||||
data: {
|
||||
nameList: [],
|
||||
@@ -15,6 +28,10 @@ Page({
|
||||
isLoading: true,
|
||||
isFlipped: false,
|
||||
keyword: '清冷',
|
||||
mode: 'classic',
|
||||
modeName: '拾遗',
|
||||
surname: '',
|
||||
loadingQuote: '正在翻阅《古今集成》...',
|
||||
|
||||
// 动画控制
|
||||
translateX: 0,
|
||||
@@ -24,30 +41,197 @@ Page({
|
||||
|
||||
cardKey: 0,
|
||||
collectedNames: [],
|
||||
showCollection: false
|
||||
showCollection: false,
|
||||
|
||||
// 海报相关
|
||||
showPoster: false,
|
||||
posterNameChars: []
|
||||
},
|
||||
|
||||
onLoad(options) {
|
||||
const keyword = options.keyword || this.data.keyword;
|
||||
this.setData({ keyword });
|
||||
this.fetchNames(keyword);
|
||||
// 解码 URL 参数
|
||||
const keyword = options.keyword ? decodeURIComponent(options.keyword) : this.data.keyword;
|
||||
const mode = options.mode || 'classic';
|
||||
const surname = options.surname ? decodeURIComponent(options.surname) : '';
|
||||
|
||||
// 模式名称映射
|
||||
const modeNameMap = {
|
||||
'baby': '宝宝',
|
||||
'persona': '人设',
|
||||
'classic': '拾遗'
|
||||
};
|
||||
|
||||
this.setData({
|
||||
keyword,
|
||||
mode,
|
||||
surname,
|
||||
modeName: modeNameMap[mode] || '拾遗',
|
||||
isLoading: true
|
||||
});
|
||||
|
||||
// 启动文案轮换
|
||||
this.startLoadingQuoteRotation();
|
||||
|
||||
// 分批加载:先请求2个,再静默加载剩余
|
||||
this.fetchNamesBatch(keyword, mode, surname);
|
||||
},
|
||||
|
||||
fetchNames(keyword) {
|
||||
this.setData({ isLoading: true });
|
||||
wx.showLoading({ title: '见素正在感悟...', mask: true });
|
||||
onUnload() {
|
||||
// 清理定时器
|
||||
if (this.loadingQuoteTimer) {
|
||||
clearInterval(this.loadingQuoteTimer);
|
||||
this.loadingQuoteTimer = null;
|
||||
}
|
||||
},
|
||||
|
||||
onShow() {
|
||||
// 每次显示页面时加载收藏列表
|
||||
this.loadCollectedNames();
|
||||
},
|
||||
|
||||
// 从后端加载收藏列表
|
||||
loadCollectedNames() {
|
||||
const openid = 'test_openid'; // 实际应从登录获取
|
||||
const apiBaseUrl = getApp().globalData.apiBaseUrl;
|
||||
wx.request({
|
||||
url: 'http://localhost:8080/api/names/generate',
|
||||
data: { keyword },
|
||||
url: `${apiBaseUrl}/api/favorites/list`,
|
||||
data: { openid },
|
||||
success: (res) => {
|
||||
if (res.data && res.data.success) {
|
||||
this.setData({ collectedNames: res.data.data || [] });
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// 启动加载文案轮换
|
||||
startLoadingQuoteRotation() {
|
||||
let index = 0;
|
||||
this.loadingQuoteTimer = setInterval(() => {
|
||||
index = (index + 1) % LOADING_QUOTES.length;
|
||||
this.setData({ loadingQuote: LOADING_QUOTES[index] });
|
||||
}, 2000);
|
||||
},
|
||||
|
||||
// 分批加载名字
|
||||
fetchNamesBatch(keyword, mode, surname) {
|
||||
// 第一波:请求2个名字,快速展示
|
||||
this.fetchFirstBatch(keyword, mode, surname, 2);
|
||||
},
|
||||
|
||||
// 第一波:快速获取前2个名字
|
||||
fetchFirstBatch(keyword, mode, surname, count) {
|
||||
const apiBaseUrl = getApp().globalData.apiBaseUrl;
|
||||
const requestData = {
|
||||
keyword,
|
||||
count: count,
|
||||
batch: 'first'
|
||||
};
|
||||
if (mode) requestData.mode = mode;
|
||||
if (surname) requestData.surname = surname;
|
||||
|
||||
wx.request({
|
||||
url: `${apiBaseUrl}/api/names/generate`,
|
||||
data: requestData,
|
||||
success: (res) => {
|
||||
if (res.statusCode === 200 && res.data && res.data.length > 0) {
|
||||
this.setData({ nameList: res.data, currentIndex: 0, isLoading: false, cardKey: this.data.cardKey + 1 });
|
||||
// 清除定时器
|
||||
if (this.loadingQuoteTimer) {
|
||||
clearInterval(this.loadingQuoteTimer);
|
||||
this.loadingQuoteTimer = null;
|
||||
}
|
||||
|
||||
// 立即展示第一波结果
|
||||
this.setData({
|
||||
nameList: res.data,
|
||||
currentIndex: 0,
|
||||
isLoading: false,
|
||||
cardKey: this.data.cardKey + 1
|
||||
});
|
||||
|
||||
// 静默加载剩余3个名字
|
||||
this.fetchSecondBatch(keyword, mode, surname, 3);
|
||||
} else {
|
||||
wx.showToast({ title: '意境未达,请重试', icon: 'none' });
|
||||
this.handleFetchError('意境未达,请重试');
|
||||
}
|
||||
},
|
||||
fail: () => wx.showToast({ title: '网络疏离,请检查后端', icon: 'none' }),
|
||||
complete: () => { this.setData({ isLoading: false }); wx.hideLoading(); }
|
||||
fail: () => {
|
||||
this.handleFetchError('网络疏离,请检查后端');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// 第二波:静默加载剩余名字
|
||||
fetchSecondBatch(keyword, mode, surname, count) {
|
||||
const apiBaseUrl = getApp().globalData.apiBaseUrl;
|
||||
const requestData = {
|
||||
keyword,
|
||||
count: count,
|
||||
batch: 'second'
|
||||
};
|
||||
if (mode) requestData.mode = mode;
|
||||
if (surname) requestData.surname = surname;
|
||||
|
||||
wx.request({
|
||||
url: `${apiBaseUrl}/api/names/generate`,
|
||||
data: requestData,
|
||||
success: (res) => {
|
||||
if (res.statusCode === 200 && res.data && res.data.length > 0) {
|
||||
// 平滑合并数据
|
||||
const currentList = this.data.nameList;
|
||||
const newList = [...currentList, ...res.data];
|
||||
this.setData({ nameList: newList });
|
||||
console.log('第二波数据已静默加载,当前共', newList.length, '个名字');
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
console.log('第二波加载失败(不影响已展示内容):', err);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// 处理加载错误
|
||||
handleFetchError(message) {
|
||||
if (this.loadingQuoteTimer) {
|
||||
clearInterval(this.loadingQuoteTimer);
|
||||
this.loadingQuoteTimer = null;
|
||||
}
|
||||
this.setData({ isLoading: false });
|
||||
wx.showToast({ title: message, icon: 'none' });
|
||||
},
|
||||
|
||||
// 返回首页
|
||||
onBack() {
|
||||
wx.navigateBack({
|
||||
success: () => {
|
||||
// 返回成功后,首页的 onShow 会重置状态
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// 点击不喜欢按钮
|
||||
onDislike() {
|
||||
if (this.isExiting) return;
|
||||
this.isExiting = true;
|
||||
|
||||
getApp().playAudio('swipe');
|
||||
this.setData({
|
||||
translateX: -500,
|
||||
opacity: 0,
|
||||
transition: 'all 0.3s cubic-bezier(0.6, -0.28, 0.735, 0.045)'
|
||||
});
|
||||
},
|
||||
|
||||
// 点击喜欢按钮
|
||||
onLike() {
|
||||
if (this.isExiting) return;
|
||||
this.handleLike();
|
||||
|
||||
this.isExiting = true;
|
||||
this.setData({
|
||||
translateX: 500,
|
||||
opacity: 0,
|
||||
transition: 'all 0.3s cubic-bezier(0.6, -0.28, 0.735, 0.045)'
|
||||
});
|
||||
},
|
||||
|
||||
@@ -74,7 +258,7 @@ Page({
|
||||
if (this.isExiting) return;
|
||||
|
||||
const deltaX = e.touches[0].clientX - this.startX;
|
||||
// 只有移动超过5px,才真正判定为“拖拽”
|
||||
// 只有移动超过5px,才真正判定为"拖拽"
|
||||
if (Math.abs(deltaX) > 5) {
|
||||
this.isDragging = true;
|
||||
}
|
||||
@@ -110,6 +294,12 @@ Page({
|
||||
this.isExiting = true;
|
||||
const direction = deltaX > 0 ? 1 : -1;
|
||||
const targetX = direction * 500;
|
||||
|
||||
// 播放滑动音效
|
||||
getApp().playAudio('swipe');
|
||||
// 触觉反馈
|
||||
wx.vibrateShort({ type: 'light' });
|
||||
|
||||
this.setData({
|
||||
translateX: targetX,
|
||||
opacity: 0,
|
||||
@@ -138,19 +328,53 @@ Page({
|
||||
const direction = this.data.translateX > 0 ? 'like' : 'dislike';
|
||||
if (direction === 'like') {
|
||||
this.handleLike();
|
||||
} else {
|
||||
this.handleCardExit(direction);
|
||||
}
|
||||
this.handleCardExit(direction);
|
||||
},
|
||||
|
||||
handleLike() {
|
||||
// 触觉反馈 - 中等震动
|
||||
wx.vibrateShort({ type: 'medium' });
|
||||
// 音效反馈
|
||||
getApp().playAudio('success');
|
||||
|
||||
const card = this.data.nameList[this.data.currentIndex];
|
||||
if (!this.data.collectedNames.some(item => item.name === card.name)) {
|
||||
this.setData({ collectedNames: [...this.data.collectedNames, card] });
|
||||
|
||||
// 同步到后端
|
||||
this.syncFavoriteToBackend(card);
|
||||
}
|
||||
this.handleCardExit('like');
|
||||
},
|
||||
|
||||
// 同步收藏到后端
|
||||
syncFavoriteToBackend(card) {
|
||||
const openid = 'test_openid'; // 实际应从登录获取
|
||||
const apiBaseUrl = getApp().globalData.apiBaseUrl;
|
||||
wx.request({
|
||||
url: `${apiBaseUrl}/api/favorites/add?openid=${openid}&mode=${this.data.mode}&keyword=${encodeURIComponent(this.data.keyword)}`,
|
||||
method: 'POST',
|
||||
header: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
data: {
|
||||
name: card.name,
|
||||
origin: card.origin,
|
||||
description: card.description,
|
||||
tone: card.tone,
|
||||
score: card.score
|
||||
},
|
||||
success: (res) => {
|
||||
if (res.data && res.data.success) {
|
||||
console.log('收藏同步成功');
|
||||
} else {
|
||||
console.log('收藏同步失败:', res.data);
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
console.log('收藏同步失败:', err);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
handleCardExit() {
|
||||
@@ -170,7 +394,15 @@ Page({
|
||||
title: '见素时刻',
|
||||
content: '这一波灵感已尽,是否再求几名?',
|
||||
confirmText: '再求', cancelText: '返回',
|
||||
success: (res) => { if (res.confirm) this.fetchNames(this.data.keyword); }
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
this.setData({ isLoading: true });
|
||||
this.startLoadingQuoteRotation();
|
||||
this.fetchNamesBatch(this.data.keyword, this.data.mode, this.data.surname);
|
||||
} else {
|
||||
this.onBack();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
this.isExiting = false;
|
||||
@@ -179,7 +411,10 @@ Page({
|
||||
},
|
||||
|
||||
toggleCollectionView() {
|
||||
this.setData({ showCollection: !this.data.showCollection });
|
||||
// 跳转到 profile 页面
|
||||
wx.navigateTo({
|
||||
url: '/pages/profile/profile'
|
||||
});
|
||||
},
|
||||
|
||||
onDeleteCollected(e) {
|
||||
@@ -190,53 +425,76 @@ Page({
|
||||
wx.vibrateShort({ type: 'light' });
|
||||
},
|
||||
|
||||
// 显示海报弹窗
|
||||
onSavePoster() {
|
||||
const card = this.data.nameList[this.data.currentIndex];
|
||||
const nameChars = card.name.split('');
|
||||
|
||||
this.setData({
|
||||
showPoster: true,
|
||||
posterNameChars: nameChars
|
||||
});
|
||||
|
||||
// 触觉反馈
|
||||
wx.vibrateShort({ type: 'light' });
|
||||
},
|
||||
|
||||
// 关闭海报弹窗
|
||||
closePoster() {
|
||||
this.setData({ showPoster: false });
|
||||
},
|
||||
|
||||
// 保存海报到相册
|
||||
savePoster() {
|
||||
wx.showLoading({ title: '绘笔收录中...', mask: true });
|
||||
const query = wx.createSelectorQuery().in(this);
|
||||
query.select('#posterCanvas')
|
||||
.fields({ node: true, size: true })
|
||||
.exec((res) => {
|
||||
const canvas = res[0].node;
|
||||
const ctx = canvas.getContext('2d');
|
||||
const dpr = wx.getSystemInfoSync().pixelRatio;
|
||||
canvas.width = 750 * dpr;
|
||||
canvas.height = 1334 * dpr;
|
||||
ctx.scale(dpr, dpr);
|
||||
ctx.fillStyle = '#FFFFFF';
|
||||
ctx.fillRect(0, 0, 750, 1334);
|
||||
ctx.fillStyle = '#2D2D2D';
|
||||
ctx.font = '300 120px serif';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
const name = card.name;
|
||||
const charArray = name.split('');
|
||||
const charHeight = 140;
|
||||
const startY = 667 - (charArray.length * charHeight) / 2 + charHeight / 2;
|
||||
charArray.forEach((char, index) => ctx.fillText(char, 375, startY + index * charHeight));
|
||||
const sealChar = this.data.keyword.substring(0, 1) || '素';
|
||||
ctx.fillStyle = '#B22222';
|
||||
ctx.fillRect(100, 100, 80, 80);
|
||||
ctx.fillStyle = '#FFFFFF';
|
||||
ctx.font = '40px serif';
|
||||
ctx.fillText(sealChar, 140, 140);
|
||||
const randomNum = Math.floor(Math.random() * 9000) + 1000;
|
||||
ctx.fillStyle = '#D0D0D0';
|
||||
ctx.font = '24px sans-serif';
|
||||
ctx.fillText(`见素第 ${randomNum} 号灵感`, 375, 1200);
|
||||
setTimeout(() => {
|
||||
|
||||
// 使用 snapshot 组件截图
|
||||
wx.nextTick(() => {
|
||||
const query = wx.createSelectorQuery().in(this);
|
||||
query.select('#posterArea')
|
||||
.node()
|
||||
.exec((res) => {
|
||||
const node = res[0].node;
|
||||
|
||||
// 使用 snapshot 截图
|
||||
wx.canvasToTempFilePath({
|
||||
canvas: canvas,
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: node.width,
|
||||
height: node.height,
|
||||
destWidth: node.width * 2,
|
||||
destHeight: node.height * 2,
|
||||
canvasId: 'posterCanvas',
|
||||
success: (res) => {
|
||||
wx.hideLoading();
|
||||
wx.showShareImageMenu({ path: res.tempFilePath });
|
||||
wx.saveImageToPhotosAlbum({
|
||||
filePath: res.tempFilePath,
|
||||
success: () => {
|
||||
wx.showToast({ title: '已保存到相册', icon: 'success' });
|
||||
},
|
||||
fail: () => {
|
||||
wx.showToast({ title: '保存失败', icon: 'none' });
|
||||
}
|
||||
});
|
||||
},
|
||||
fail: () => {
|
||||
wx.hideLoading();
|
||||
wx.showToast({ title: '绘笔受阻', icon: 'none' });
|
||||
}
|
||||
});
|
||||
}, 300);
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
// 分享海报
|
||||
sharePoster() {
|
||||
wx.showShareImageMenu({
|
||||
path: '/images/share.png'
|
||||
});
|
||||
},
|
||||
|
||||
// 阻止事件冒泡
|
||||
preventBubble() {
|
||||
// 什么都不做,只是阻止事件冒泡
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,4 +1,20 @@
|
||||
<view class="container">
|
||||
<!-- 意境感加载页 -->
|
||||
<view class="loading-container {{isLoading ? 'visible' : ''}}">
|
||||
<view class="loading-content">
|
||||
<view class="loading-circle"></view>
|
||||
<text class="loading-text">{{loadingQuote}}</text>
|
||||
<text class="loading-subtext">见素正在感悟...</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 顶部标题 -->
|
||||
<view class="header" wx:if="{{!isLoading}}">
|
||||
<view class="back-btn" bindtap="onBack">←</view>
|
||||
<view class="header-title">见素 · {{modeName}} · {{keyword}}</view>
|
||||
<view class="header-spacer"></view>
|
||||
</view>
|
||||
|
||||
<view class="card-stack" wx:if="{{!isLoading && nameList.length > 0}}">
|
||||
<view
|
||||
class="card-container"
|
||||
@@ -17,7 +33,7 @@
|
||||
</view>
|
||||
</view>
|
||||
<!-- 反面:故事化解读 -->
|
||||
<view class="card-face back">
|
||||
<view class="card-face back" bindtap="onFlip">
|
||||
<view class="desc-container">
|
||||
<text class="desc">{{nameList[currentIndex].description}}</text>
|
||||
</view>
|
||||
@@ -33,12 +49,55 @@
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 海报生成画布 (隐藏在屏幕外) -->
|
||||
<canvas type="2d" id="posterCanvas" style="width: 750px; height: 1334px; position: absolute; left: -9999px;"></canvas>
|
||||
<!-- 海报生成区域 (使用 snapshot 组件) -->
|
||||
<view class="poster-modal {{showPoster ? 'visible' : ''}}" bindtap="closePoster">
|
||||
<view class="poster-content" catchtap="preventBubble">
|
||||
<view class="poster-close" bindtap="closePoster">×</view>
|
||||
|
||||
<!-- snapshot 目标区域 -->
|
||||
<view class="poster-capture-area" id="posterArea">
|
||||
<view class="poster-bg">
|
||||
<!-- 大字竖排 -->
|
||||
<view class="poster-name-vertical">
|
||||
<text wx:for="{{posterNameChars}}" wx:key="index">{{item}}</text>
|
||||
</view>
|
||||
|
||||
<!-- 出处 -->
|
||||
<view class="poster-origin">{{nameList[currentIndex].origin}}</view>
|
||||
|
||||
<!-- 印章 -->
|
||||
<view class="poster-seal">见素</view>
|
||||
|
||||
<!-- 底部信息 -->
|
||||
<view class="poster-footer">
|
||||
<view class="poster-desc">{{nameList[currentIndex].description}}</view>
|
||||
<view class="poster-qrcode">
|
||||
<text class="qrcode-text">扫码起名</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<view class="poster-actions">
|
||||
<view class="poster-btn save-btn" bindtap="savePoster">
|
||||
<text>保存到相册</text>
|
||||
</view>
|
||||
<view class="poster-btn share-btn" bindtap="sharePoster">
|
||||
<text>分享给好友</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部操作提示 -->
|
||||
<view class="footer" wx:if="{{!isLoading}}">
|
||||
<view class="hint">左滑无感 · 右滑收藏</view>
|
||||
<!-- 底部操作栏 -->
|
||||
<view class="action-bar" wx:if="{{!isLoading && nameList.length > 0}}">
|
||||
<view class="action-btn dislike-btn" bindtap="onDislike">
|
||||
<text class="action-icon">×</text>
|
||||
</view>
|
||||
<view class="action-btn like-btn" bindtap="onLike">
|
||||
<text class="action-icon">♥</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
@@ -71,4 +130,4 @@
|
||||
</scroll-view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -1,195 +1,302 @@
|
||||
page {
|
||||
background-color: #FFFFFF;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* 容器 */
|
||||
.container {
|
||||
height: 100%;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #FAFAFA;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 意境感加载页 */
|
||||
.loading-container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(135deg, #FAFAFA 0%, #F5F5F5 100%);
|
||||
z-index: 100;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: all 0.4s ease;
|
||||
}
|
||||
|
||||
.loading-container.visible {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.loading-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 40rpx;
|
||||
}
|
||||
|
||||
.loading-circle {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
border: 4rpx solid #E8E8E8;
|
||||
border-top-color: #2D2D2D;
|
||||
border-radius: 50%;
|
||||
animation: rotate 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes rotate {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
font-family: "Noto Serif SC", serif;
|
||||
font-size: 32rpx;
|
||||
color: #4A4A4A;
|
||||
letter-spacing: 8rpx;
|
||||
animation: fadeInOut 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.loading-subtext {
|
||||
font-size: 24rpx;
|
||||
color: #A0A0A0;
|
||||
letter-spacing: 4rpx;
|
||||
}
|
||||
|
||||
@keyframes fadeInOut {
|
||||
0%, 100% { opacity: 0.6; }
|
||||
50% { opacity: 1; }
|
||||
}
|
||||
|
||||
/* 顶部标题 */
|
||||
.header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 40rpx;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
font-size: 36rpx;
|
||||
color: #A0A0A0;
|
||||
padding: 20rpx;
|
||||
margin-left: -20rpx;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
font-family: "Noto Serif SC", serif;
|
||||
font-size: 28rpx;
|
||||
color: #2D2D2D;
|
||||
letter-spacing: 4rpx;
|
||||
}
|
||||
|
||||
.header-spacer {
|
||||
width: 76rpx;
|
||||
}
|
||||
|
||||
/* 卡片堆叠区域 */
|
||||
.card-stack {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 40rpx;
|
||||
position: relative;
|
||||
width: 70vw;
|
||||
height: 100vw;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.card-container {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-width: 600rpx;
|
||||
aspect-ratio: 3/4;
|
||||
perspective: 1000px;
|
||||
}
|
||||
|
||||
.card {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
box-shadow: 0 20px 60px rgba(0,0,0,0.04);
|
||||
border: 1px solid #F0F0F0;
|
||||
border-radius: 8rpx;
|
||||
background: white;
|
||||
transform-style: preserve-3d;
|
||||
transition: transform 0.6s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.card.flipped {
|
||||
transform: rotateY(180deg);
|
||||
}
|
||||
|
||||
.card-face {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
backface-visibility: hidden;
|
||||
background: #FFFFFF;
|
||||
border-radius: 24rpx;
|
||||
box-shadow: 0 10rpx 40rpx rgba(0, 0, 0, 0.08);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 60rpx;
|
||||
box-sizing: border-box;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.front {
|
||||
transform: rotateY(0deg);
|
||||
.card-face.front {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.back {
|
||||
.card-face.back {
|
||||
transform: rotateY(180deg);
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.card.flipped .front {
|
||||
transform: rotateY(-180deg);
|
||||
}
|
||||
.card.flipped .back {
|
||||
transform: rotateY(0deg);
|
||||
}
|
||||
|
||||
.front .name {
|
||||
font-family: "Noto Serif SC", "Source Han Serif SC", "PingFang SC", serif;
|
||||
font-size: 140rpx;
|
||||
.name {
|
||||
font-family: "Noto Serif SC", serif;
|
||||
font-size: 96rpx;
|
||||
color: #2D2D2D;
|
||||
margin-bottom: 60rpx;
|
||||
font-weight: 300;
|
||||
letter-spacing: 10rpx;
|
||||
text-align: center;
|
||||
letter-spacing: 16rpx;
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.poem-container {
|
||||
height: 350rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.front .poem {
|
||||
font-size: 26rpx;
|
||||
color: #A0A0A0;
|
||||
letter-spacing: 12rpx;
|
||||
writing-mode: vertical-rl;
|
||||
line-height: 1.5;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.back {
|
||||
transform: rotateY(180deg);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between !important;
|
||||
padding: 80rpx 60rpx !important;
|
||||
.poem {
|
||||
font-family: "Noto Serif SC", serif;
|
||||
font-size: 28rpx;
|
||||
color: #666666;
|
||||
line-height: 2;
|
||||
letter-spacing: 4rpx;
|
||||
}
|
||||
|
||||
.desc-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.back .desc {
|
||||
.desc {
|
||||
font-size: 28rpx;
|
||||
color: #4A4A4A;
|
||||
line-height: 2.2;
|
||||
line-height: 2;
|
||||
text-align: justify;
|
||||
letter-spacing: 2rpx;
|
||||
}
|
||||
|
||||
.analysis-container {
|
||||
margin-top: 40rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
border-top: 1rpx solid #F0F0F0;
|
||||
padding-top: 30rpx;
|
||||
gap: 20rpx;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.tone-tag, .score-tag {
|
||||
font-size: 18rpx;
|
||||
color: #D0D0D0;
|
||||
font-size: 22rpx;
|
||||
color: #888888;
|
||||
letter-spacing: 2rpx;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.save-btn {
|
||||
margin-top: 20rpx;
|
||||
padding: 16rpx 40rpx;
|
||||
background: #F5F5F5;
|
||||
border-radius: 30rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 16rpx;
|
||||
color: #A0A0A0;
|
||||
border: 1rpx solid #F0F0F0;
|
||||
padding: 8rpx 20rpx;
|
||||
border-radius: 40rpx;
|
||||
letter-spacing: 2rpx;
|
||||
gap: 8rpx;
|
||||
font-size: 24rpx;
|
||||
color: #666666;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.save-btn:active {
|
||||
background: #FAFAFA;
|
||||
transform: scale(0.95);
|
||||
background: #E8E8E8;
|
||||
}
|
||||
|
||||
.save-icon {
|
||||
margin-right: 8rpx;
|
||||
font-size: 14rpx;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
/* 退出动画 */
|
||||
.card-exit-left {
|
||||
transform: translate3d(-150%, 0, 0) rotate(-20deg) !important;
|
||||
opacity: 0 !important;
|
||||
transition: all 0.5s ease-in !important;
|
||||
/* 底部操作栏 */
|
||||
.action-bar {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 80rpx;
|
||||
padding: 60rpx 40rpx;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.card-exit-right {
|
||||
transform: translate3d(150%, 0, 0) rotate(20deg) !important;
|
||||
opacity: 0 !important;
|
||||
transition: all 0.5s ease-in !important;
|
||||
.action-btn {
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 48rpx;
|
||||
transition: all 0.2s;
|
||||
box-shadow: 0 8rpx 30rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin-top: 100rpx;
|
||||
.action-btn.dislike-btn {
|
||||
background: #FFFFFF;
|
||||
color: #888888;
|
||||
}
|
||||
|
||||
.hint {
|
||||
font-size: 22rpx;
|
||||
color: #D0D0D0;
|
||||
.action-btn.like-btn {
|
||||
background: #2D2D2D;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
.action-btn:active {
|
||||
transform: scale(0.9);
|
||||
}
|
||||
|
||||
/* 空状态 */
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 60vh;
|
||||
}
|
||||
|
||||
.empty-state text {
|
||||
font-size: 28rpx;
|
||||
color: #A0A0A0;
|
||||
letter-spacing: 4rpx;
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.empty-state button {
|
||||
background: none !important;
|
||||
color: #2D2D2D !important;
|
||||
font-size: 24rpx !important;
|
||||
border: 1rpx solid #E0E0E0 !important;
|
||||
padding: 16rpx 40rpx !important;
|
||||
letter-spacing: 4rpx !important;
|
||||
}
|
||||
|
||||
.empty-state button::after {
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
/* 收藏锦囊 */
|
||||
.collection-bag {
|
||||
position: fixed;
|
||||
right: 60rpx;
|
||||
bottom: 120rpx;
|
||||
right: 40rpx;
|
||||
bottom: 200rpx;
|
||||
width: 100rpx;
|
||||
height: 100rpx;
|
||||
background-color: #FFFFFF;
|
||||
background: #FFFFFF;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 10rpx 40rpx rgba(0,0,0,0.08);
|
||||
box-shadow: 0 8rpx 30rpx rgba(0, 0, 0, 0.1);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
z-index: 10;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
@@ -241,12 +348,11 @@ page {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 60vh;
|
||||
background-color: rgba(255, 255, 255, 0.8);
|
||||
border-top-left-radius: 40rpx;
|
||||
border-top-right-radius: 40rpx;
|
||||
box-shadow: 0 -10rpx 60rpx rgba(0,0,0,0.05);
|
||||
right: 0;
|
||||
background: #FFFFFF;
|
||||
border-radius: 40rpx 40rpx 0 0;
|
||||
padding: 60rpx 40rpx;
|
||||
max-height: 70vh;
|
||||
transform: translateY(100%);
|
||||
transition: transform 0.4s cubic-bezier(0.165, 0.84, 0.44, 1);
|
||||
}
|
||||
@@ -256,37 +362,38 @@ page {
|
||||
}
|
||||
|
||||
.collection-title {
|
||||
font-family: "Noto Serif SC", serif;
|
||||
font-size: 32rpx;
|
||||
color: #2D2D2D;
|
||||
text-align: center;
|
||||
padding: 40rpx 0;
|
||||
font-size: 24rpx;
|
||||
color: #A0A0A0;
|
||||
letter-spacing: 4rpx;
|
||||
border-bottom: 1rpx solid #F0F0F0;
|
||||
margin-bottom: 40rpx;
|
||||
letter-spacing: 8rpx;
|
||||
}
|
||||
|
||||
.collection-scroll {
|
||||
height: calc(100% - 110rpx);
|
||||
max-height: 50vh;
|
||||
}
|
||||
|
||||
.collection-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 30rpx 60rpx;
|
||||
padding: 30rpx 0;
|
||||
border-bottom: 1rpx solid #F0F0F0;
|
||||
}
|
||||
|
||||
.item-name {
|
||||
font-family: "Noto Serif SC", serif;
|
||||
font-size: 32rpx;
|
||||
font-size: 36rpx;
|
||||
color: #2D2D2D;
|
||||
margin-bottom: 4rpx;
|
||||
margin-bottom: 8rpx;
|
||||
letter-spacing: 8rpx;
|
||||
}
|
||||
|
||||
.item-origin {
|
||||
font-size: 20rpx;
|
||||
color: #A0A0A0;
|
||||
font-weight: 200;
|
||||
font-size: 22rpx;
|
||||
color: #888888;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.delete-btn {
|
||||
@@ -299,13 +406,214 @@ page {
|
||||
}
|
||||
|
||||
.delete-btn:active {
|
||||
background-color: #F0F0F0;
|
||||
color: #B22222;
|
||||
background: #FAFAFA;
|
||||
}
|
||||
|
||||
.collection-empty {
|
||||
text-align: center;
|
||||
padding: 100rpx;
|
||||
padding: 100rpx 60rpx;
|
||||
font-size: 24rpx;
|
||||
color: #E0E0E0;
|
||||
}
|
||||
color: #D0D0D0;
|
||||
letter-spacing: 4rpx;
|
||||
}
|
||||
|
||||
/* 海报弹窗 */
|
||||
.poster-modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
z-index: 100;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.poster-modal.visible {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.poster-content {
|
||||
width: 80%;
|
||||
max-width: 600rpx;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.poster-close {
|
||||
position: absolute;
|
||||
top: -80rpx;
|
||||
right: 0;
|
||||
font-size: 48rpx;
|
||||
color: #FFFFFF;
|
||||
padding: 20rpx;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
/* 海报内容区域 */
|
||||
.poster-capture-area {
|
||||
background: #FFFFFF;
|
||||
border-radius: 16rpx;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 20rpx 60rpx rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.poster-bg {
|
||||
width: 100%;
|
||||
aspect-ratio: 3/4;
|
||||
background: linear-gradient(135deg, #FAFAFA 0%, #F0F0F0 100%);
|
||||
position: relative;
|
||||
padding: 60rpx;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* 大字竖排 */
|
||||
.poster-name-vertical {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.poster-name-vertical text {
|
||||
font-family: "Noto Serif SC", serif;
|
||||
font-size: 120rpx;
|
||||
color: #2D2D2D;
|
||||
writing-mode: vertical-rl;
|
||||
text-orientation: upright;
|
||||
letter-spacing: 20rpx;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
/* 出处 */
|
||||
.poster-origin {
|
||||
position: absolute;
|
||||
top: 60rpx;
|
||||
right: 60rpx;
|
||||
font-family: "Noto Serif SC", serif;
|
||||
font-size: 22rpx;
|
||||
color: #888888;
|
||||
writing-mode: vertical-rl;
|
||||
text-orientation: mixed;
|
||||
letter-spacing: 4rpx;
|
||||
line-height: 2;
|
||||
max-height: 300rpx;
|
||||
}
|
||||
|
||||
/* 印章 */
|
||||
.poster-seal {
|
||||
position: absolute;
|
||||
bottom: 180rpx;
|
||||
right: 60rpx;
|
||||
width: 100rpx;
|
||||
height: 100rpx;
|
||||
border: 4rpx solid #B22222;
|
||||
border-radius: 12rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-family: "Noto Serif SC", serif;
|
||||
font-size: 36rpx;
|
||||
color: #B22222;
|
||||
writing-mode: vertical-rl;
|
||||
text-orientation: upright;
|
||||
letter-spacing: 8rpx;
|
||||
transform: rotate(-5deg);
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
/* 底部信息 */
|
||||
.poster-footer {
|
||||
position: absolute;
|
||||
bottom: 60rpx;
|
||||
left: 60rpx;
|
||||
right: 60rpx;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.poster-desc {
|
||||
font-size: 20rpx;
|
||||
color: #A0A0A0;
|
||||
line-height: 1.8;
|
||||
max-width: 60%;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.poster-qrcode {
|
||||
width: 100rpx;
|
||||
height: 100rpx;
|
||||
background: #F5F5F5;
|
||||
border-radius: 8rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.qrcode-text {
|
||||
font-size: 16rpx;
|
||||
color: #C0C0C0;
|
||||
writing-mode: vertical-rl;
|
||||
text-orientation: upright;
|
||||
letter-spacing: 2rpx;
|
||||
}
|
||||
|
||||
/* 海报操作按钮 */
|
||||
.poster-actions {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 40rpx;
|
||||
margin-top: 40rpx;
|
||||
}
|
||||
|
||||
.poster-btn {
|
||||
width: 240rpx;
|
||||
height: 80rpx;
|
||||
border-radius: 40rpx;
|
||||
font-size: 26rpx;
|
||||
letter-spacing: 4rpx;
|
||||
transition: all 0.2s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.poster-btn.save-btn {
|
||||
background: #FFFFFF;
|
||||
color: #2D2D2D;
|
||||
}
|
||||
|
||||
.poster-btn.save-btn:active {
|
||||
background: #F0F0F0;
|
||||
}
|
||||
|
||||
.poster-btn.share-btn {
|
||||
background: transparent;
|
||||
color: #FFFFFF;
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.poster-btn.share-btn:active {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
106
miniprogram/pages/profile/profile.js
Normal file
106
miniprogram/pages/profile/profile.js
Normal file
@@ -0,0 +1,106 @@
|
||||
Page({
|
||||
data: {
|
||||
favorites: [],
|
||||
showDetail: false,
|
||||
selectedItem: {},
|
||||
selectedIndex: -1,
|
||||
openid: 'test_openid' // 实际应从登录获取
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.loadFavorites();
|
||||
},
|
||||
|
||||
onShow() {
|
||||
// 每次显示页面时刷新列表
|
||||
this.loadFavorites();
|
||||
},
|
||||
|
||||
// 加载收藏列表
|
||||
loadFavorites() {
|
||||
const apiBaseUrl = getApp().globalData.apiBaseUrl;
|
||||
wx.request({
|
||||
url: `${apiBaseUrl}/api/favorites/list`,
|
||||
data: { openid: this.data.openid },
|
||||
success: (res) => {
|
||||
if (res.data && res.data.success) {
|
||||
this.setData({ favorites: res.data.data || [] });
|
||||
}
|
||||
},
|
||||
fail: () => {
|
||||
wx.showToast({ title: '加载失败', icon: 'none' });
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// 返回上一页
|
||||
onBack() {
|
||||
wx.navigateBack();
|
||||
},
|
||||
|
||||
// 点击收藏项
|
||||
onItemTap(e) {
|
||||
const index = e.currentTarget.dataset.index;
|
||||
const item = this.data.favorites[index];
|
||||
this.setData({
|
||||
selectedItem: item,
|
||||
selectedIndex: index,
|
||||
showDetail: true
|
||||
});
|
||||
wx.vibrateShort({ type: 'light' });
|
||||
},
|
||||
|
||||
// 关闭详情弹窗
|
||||
closeDetail() {
|
||||
this.setData({ showDetail: false });
|
||||
},
|
||||
|
||||
// 阻止冒泡
|
||||
preventBubble() {
|
||||
// 什么都不做,只是阻止事件冒泡
|
||||
},
|
||||
|
||||
// 分享
|
||||
onShare() {
|
||||
const item = this.data.selectedItem;
|
||||
wx.showShareImageMenu({
|
||||
path: '/images/share.png' // 实际应生成海报图片
|
||||
});
|
||||
},
|
||||
|
||||
// 移除收藏
|
||||
onRemove() {
|
||||
const item = this.data.selectedItem;
|
||||
wx.showModal({
|
||||
title: '确认移除',
|
||||
content: `确定要从心动名单中移除「${item.name}」吗?`,
|
||||
confirmColor: '#B22222',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
const apiBaseUrl = getApp().globalData.apiBaseUrl;
|
||||
wx.request({
|
||||
url: `${apiBaseUrl}/api/favorites/remove`,
|
||||
method: 'POST',
|
||||
data: {
|
||||
openid: this.data.openid,
|
||||
name: item.name
|
||||
},
|
||||
success: (res) => {
|
||||
if (res.data && res.data.success) {
|
||||
wx.showToast({ title: '已移除', icon: 'success' });
|
||||
this.closeDetail();
|
||||
this.loadFavorites();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// 下拉刷新
|
||||
onPullDownRefresh() {
|
||||
this.loadFavorites();
|
||||
wx.stopPullDownRefresh();
|
||||
}
|
||||
});
|
||||
6
miniprogram/pages/profile/profile.json
Normal file
6
miniprogram/pages/profile/profile.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"navigationBarTitleText": "我的心动名单",
|
||||
"navigationBarBackgroundColor": "#FFFFFF",
|
||||
"navigationBarTextStyle": "black",
|
||||
"backgroundColor": "#FAFAFA"
|
||||
}
|
||||
49
miniprogram/pages/profile/profile.wxml
Normal file
49
miniprogram/pages/profile/profile.wxml
Normal file
@@ -0,0 +1,49 @@
|
||||
<view class="container">
|
||||
<!-- 顶部标题 -->
|
||||
<view class="header">
|
||||
<view class="back-btn" bindtap="onBack">←</view>
|
||||
<view class="header-title">心动名单</view>
|
||||
<view class="header-spacer"></view>
|
||||
</view>
|
||||
|
||||
<!-- 收藏列表 -->
|
||||
<scroll-view scroll-y class="favorites-scroll" wx:if="{{favorites.length > 0}}">
|
||||
<view class="favorites-grid">
|
||||
<view
|
||||
class="favorite-item"
|
||||
wx:for="{{favorites}}"
|
||||
wx:key="name"
|
||||
bindtap="onItemTap"
|
||||
data-index="{{index}}"
|
||||
>
|
||||
<view class="name">{{item.name}}</view>
|
||||
<view class="origin">{{item.origin}}</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view class="empty-state" wx:if="{{favorites.length === 0}}">
|
||||
<view class="empty-icon">素</view>
|
||||
<text class="empty-text">锦囊空空,静待灵感</text>
|
||||
<text class="empty-subtext">在见素中右滑收藏心动的名字</text>
|
||||
</view>
|
||||
|
||||
<!-- 半屏弹窗 - 详情展示 -->
|
||||
<view class="detail-modal {{showDetail ? 'visible' : ''}}" bindtap="closeDetail">
|
||||
<view class="detail-content" catchtap="preventBubble">
|
||||
<view class="detail-close" bindtap="closeDetail">×</view>
|
||||
<view class="detail-name">{{selectedItem.name}}</view>
|
||||
<view class="detail-origin">{{selectedItem.origin}}</view>
|
||||
<view class="detail-desc">{{selectedItem.description}}</view>
|
||||
<view class="detail-meta">
|
||||
<text class="detail-tone">声韵:{{selectedItem.tone}}</text>
|
||||
<text class="detail-score">见素评分:{{selectedItem.score}}</text>
|
||||
</view>
|
||||
<view class="detail-actions">
|
||||
<view class="detail-btn share-btn" bindtap="onShare">分享</view>
|
||||
<view class="detail-btn remove-btn" bindtap="onRemove">移除</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
217
miniprogram/pages/profile/profile.wxss
Normal file
217
miniprogram/pages/profile/profile.wxss
Normal file
@@ -0,0 +1,217 @@
|
||||
page {
|
||||
background-color: #FAFAFA;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.container {
|
||||
min-height: 100%;
|
||||
padding: 0 40rpx 40rpx;
|
||||
}
|
||||
|
||||
/* 顶部标题 */
|
||||
.header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 40rpx 0;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
font-size: 36rpx;
|
||||
color: #A0A0A0;
|
||||
padding: 20rpx;
|
||||
margin-left: -20rpx;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
font-family: "Noto Serif SC", serif;
|
||||
font-size: 32rpx;
|
||||
color: #2D2D2D;
|
||||
letter-spacing: 4rpx;
|
||||
}
|
||||
|
||||
.header-spacer {
|
||||
width: 76rpx;
|
||||
}
|
||||
|
||||
/* 收藏列表 */
|
||||
.favorites-scroll {
|
||||
height: calc(100vh - 200rpx);
|
||||
}
|
||||
|
||||
.favorites-grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 30rpx;
|
||||
}
|
||||
|
||||
.favorite-item {
|
||||
background: #FFFFFF;
|
||||
border-radius: 16rpx;
|
||||
padding: 40rpx;
|
||||
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.04);
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.favorite-item:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.favorite-item .name {
|
||||
font-family: "Noto Serif SC", serif;
|
||||
font-size: 48rpx;
|
||||
color: #2D2D2D;
|
||||
margin-bottom: 16rpx;
|
||||
letter-spacing: 8rpx;
|
||||
}
|
||||
|
||||
.favorite-item .origin {
|
||||
font-size: 24rpx;
|
||||
color: #888888;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* 空状态 */
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding-top: 200rpx;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-family: "Noto Serif SC", serif;
|
||||
font-size: 120rpx;
|
||||
color: #E0E0E0;
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-family: "Noto Serif SC", serif;
|
||||
font-size: 32rpx;
|
||||
color: #A0A0A0;
|
||||
letter-spacing: 4rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.empty-subtext {
|
||||
font-size: 24rpx;
|
||||
color: #C0C0C0;
|
||||
}
|
||||
|
||||
/* 半屏弹窗 */
|
||||
.detail-modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
z-index: 100;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.detail-modal.visible {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.detail-content {
|
||||
width: 100%;
|
||||
background: #FFFFFF;
|
||||
border-radius: 40rpx 40rpx 0 0;
|
||||
padding: 60rpx 50rpx 80rpx;
|
||||
transform: translateY(100%);
|
||||
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.detail-modal.visible .detail-content {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.detail-close {
|
||||
position: absolute;
|
||||
top: 30rpx;
|
||||
right: 40rpx;
|
||||
font-size: 48rpx;
|
||||
color: #C0C0C0;
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
.detail-name {
|
||||
font-family: "Noto Serif SC", serif;
|
||||
font-size: 72rpx;
|
||||
color: #2D2D2D;
|
||||
text-align: center;
|
||||
margin-bottom: 30rpx;
|
||||
letter-spacing: 16rpx;
|
||||
}
|
||||
|
||||
.detail-origin {
|
||||
font-size: 26rpx;
|
||||
color: #888888;
|
||||
text-align: center;
|
||||
margin-bottom: 40rpx;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.detail-desc {
|
||||
font-size: 28rpx;
|
||||
color: #4A4A4A;
|
||||
line-height: 2;
|
||||
text-align: justify;
|
||||
margin-bottom: 40rpx;
|
||||
padding: 0 20rpx;
|
||||
}
|
||||
|
||||
.detail-meta {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 40rpx;
|
||||
margin-bottom: 50rpx;
|
||||
}
|
||||
|
||||
.detail-tone, .detail-score {
|
||||
font-size: 22rpx;
|
||||
color: #A0A0A0;
|
||||
letter-spacing: 2rpx;
|
||||
}
|
||||
|
||||
.detail-actions {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 40rpx;
|
||||
}
|
||||
|
||||
.detail-btn {
|
||||
padding: 20rpx 60rpx;
|
||||
border-radius: 40rpx;
|
||||
font-size: 26rpx;
|
||||
letter-spacing: 4rpx;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.share-btn {
|
||||
background: #2D2D2D;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
.share-btn:active {
|
||||
background: #1A1A1A;
|
||||
}
|
||||
|
||||
.remove-btn {
|
||||
background: #F5F5F5;
|
||||
color: #888888;
|
||||
}
|
||||
|
||||
.remove-btn:active {
|
||||
background: #E8E8E8;
|
||||
}
|
||||
Reference in New Issue
Block a user