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

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

View File

@@ -2,25 +2,38 @@ App({
onLaunch() {
// 初始化音效上下文
this.globalData.audioContexts = {
flip: wx.createInnerAudioContext(),
success: wx.createInnerAudioContext()
flip: wx.createInnerAudioContext(), // 翻页声
success: wx.createInnerAudioContext(), // 收藏成功
inkDrop: wx.createInnerAudioContext(), // 水滴声
swipe: wx.createInnerAudioContext() // 滑动切换
};
// 预设音效资源 (用户可替换为本地或云端 URL)
// 示例使用了一些开源的基础音效,仅作占位参考
this.globalData.audioContexts.flip.src = 'https://assets.mixkit.co/active_storage/sfx/2571/2571-preview.mp3'; // 类似翻页声
this.globalData.audioContexts.success.src = 'https://assets.mixkit.co/active_storage/sfx/2000/2000-preview.mp3'; // 类似轻微铃声
// 预设音效资源
// 翻页声 - 纸张摩擦
this.globalData.audioContexts.flip.src = 'https://assets.mixkit.co/active_storage/sfx/2571/2571-preview.mp3';
// 收藏成功 - 清脆铃声
this.globalData.audioContexts.success.src = 'https://assets.mixkit.co/active_storage/sfx/2000/2000-preview.mp3';
// 水滴声 - 水墨滴落
this.globalData.audioContexts.inkDrop.src = 'https://assets.mixkit.co/active_storage/sfx/2578/2578-preview.mp3';
// 滑动声
this.globalData.audioContexts.swipe.src = 'https://assets.mixkit.co/active_storage/sfx/2571/2571-preview.mp3';
},
playAudio(type) {
const ctx = this.globalData.audioContexts[type];
if (ctx) {
ctx.stop();
ctx.play();
ctx.play().catch(err => {
console.log('音效播放失败:', err);
});
}
},
globalData: {
audioContexts: {}
audioContexts: {},
// API 基础地址 - 修改这里即可切换环境
// apiBaseUrl: 'http://localhost:8080'
apiBaseUrl: 'https://feast.yidaima.cn/jsu'
// 生产环境:'https://api.yourdomain.com'
}
});
});

View File

@@ -1,7 +1,8 @@
{
"pages": [
"pages/home/home",
"pages/index/index"
"pages/index/index",
"pages/profile/profile"
],
"window": {
"backgroundColor": "#FFFFFF",

66
miniprogram/app.wxss Normal file
View File

@@ -0,0 +1,66 @@
/* 全局变量定义 */
page {
--jiansu-bg: #ffffff;
--jiansu-text: #2c2c2c;
--jiansu-text-secondary: #4a4a4a;
--jiansu-text-tertiary: #a0a0a0;
--jiansu-border: #f0f0f0;
--jiansu-border-light: #e0e0e0;
--jiansu-accent: #8e8e8e;
--jiansu-red: #b22222;
}
/* 全局基础样式 */
page {
font-family: "Noto Serif SC", "Source Han Serif SC", "PingFang SC", "Microsoft YaHei", serif;
background-color: var(--jiansu-bg);
color: var(--jiansu-text);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* 隐藏滚动条 */
::-webkit-scrollbar {
display: none;
}
/* 通用按钮重置 */
button {
margin: 0;
padding: 0;
background: none;
border: none;
font-family: inherit;
}
button::after {
border: none;
}
/* 通用输入框重置 */
input {
font-family: inherit;
outline: none;
}
/* 通用文本样式 */
.text-primary {
color: var(--jiansu-text);
}
.text-secondary {
color: var(--jiansu-text-secondary);
}
.text-tertiary {
color: var(--jiansu-text-tertiary);
}
/* 通用边框样式 */
.border-light {
border-color: var(--jiansu-border);
}
.border-lighter {
border-color: var(--jiansu-border-light);
}

View File

@@ -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);
}
});
});

View File

@@ -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>

View File

@@ -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; }
}
}

View File

@@ -1,6 +1,18 @@
const EXIT_THRESHOLD = 80;
const FAST_SWIPE_VELOCITY = 0.5;
// 加载文案库
const LOADING_QUOTES = [
"正在翻阅《古今集成》...",
"于诗书中寻觅意境...",
"聆听平仄的韵律...",
"在字里行间游走...",
"采撷一缕清风入名...",
"品味楚辞的芬芳...",
"捕捉诗经的灵光...",
"与古人隔空对话..."
];
Page({
isDragging: false,
isExiting: false,
@@ -8,6 +20,7 @@ Page({
startX: 0,
startY: 0,
lastX: 0,
loadingQuoteTimer: null,
data: {
nameList: [],
@@ -15,6 +28,10 @@ Page({
isLoading: true,
isFlipped: false,
keyword: '清冷',
mode: 'classic',
modeName: '拾遗',
surname: '',
loadingQuote: '正在翻阅《古今集成》...',
// 动画控制
translateX: 0,
@@ -24,30 +41,197 @@ Page({
cardKey: 0,
collectedNames: [],
showCollection: false
showCollection: false,
// 海报相关
showPoster: false,
posterNameChars: []
},
onLoad(options) {
const keyword = options.keyword || this.data.keyword;
this.setData({ keyword });
this.fetchNames(keyword);
// 解码 URL 参数
const keyword = options.keyword ? decodeURIComponent(options.keyword) : this.data.keyword;
const mode = options.mode || 'classic';
const surname = options.surname ? decodeURIComponent(options.surname) : '';
// 模式名称映射
const modeNameMap = {
'baby': '宝宝',
'persona': '人设',
'classic': '拾遗'
};
this.setData({
keyword,
mode,
surname,
modeName: modeNameMap[mode] || '拾遗',
isLoading: true
});
// 启动文案轮换
this.startLoadingQuoteRotation();
// 分批加载先请求2个再静默加载剩余
this.fetchNamesBatch(keyword, mode, surname);
},
fetchNames(keyword) {
this.setData({ isLoading: true });
wx.showLoading({ title: '见素正在感悟...', mask: true });
onUnload() {
// 清理定时器
if (this.loadingQuoteTimer) {
clearInterval(this.loadingQuoteTimer);
this.loadingQuoteTimer = null;
}
},
onShow() {
// 每次显示页面时加载收藏列表
this.loadCollectedNames();
},
// 从后端加载收藏列表
loadCollectedNames() {
const openid = 'test_openid'; // 实际应从登录获取
const apiBaseUrl = getApp().globalData.apiBaseUrl;
wx.request({
url: 'http://localhost:8080/api/names/generate',
data: { keyword },
url: `${apiBaseUrl}/api/favorites/list`,
data: { openid },
success: (res) => {
if (res.data && res.data.success) {
this.setData({ collectedNames: res.data.data || [] });
}
}
});
},
// 启动加载文案轮换
startLoadingQuoteRotation() {
let index = 0;
this.loadingQuoteTimer = setInterval(() => {
index = (index + 1) % LOADING_QUOTES.length;
this.setData({ loadingQuote: LOADING_QUOTES[index] });
}, 2000);
},
// 分批加载名字
fetchNamesBatch(keyword, mode, surname) {
// 第一波请求2个名字快速展示
this.fetchFirstBatch(keyword, mode, surname, 2);
},
// 第一波快速获取前2个名字
fetchFirstBatch(keyword, mode, surname, count) {
const apiBaseUrl = getApp().globalData.apiBaseUrl;
const requestData = {
keyword,
count: count,
batch: 'first'
};
if (mode) requestData.mode = mode;
if (surname) requestData.surname = surname;
wx.request({
url: `${apiBaseUrl}/api/names/generate`,
data: requestData,
success: (res) => {
if (res.statusCode === 200 && res.data && res.data.length > 0) {
this.setData({ nameList: res.data, currentIndex: 0, isLoading: false, cardKey: this.data.cardKey + 1 });
// 清除定时器
if (this.loadingQuoteTimer) {
clearInterval(this.loadingQuoteTimer);
this.loadingQuoteTimer = null;
}
// 立即展示第一波结果
this.setData({
nameList: res.data,
currentIndex: 0,
isLoading: false,
cardKey: this.data.cardKey + 1
});
// 静默加载剩余3个名字
this.fetchSecondBatch(keyword, mode, surname, 3);
} else {
wx.showToast({ title: '意境未达,请重试', icon: 'none' });
this.handleFetchError('意境未达,请重试');
}
},
fail: () => wx.showToast({ title: '网络疏离,请检查后端', icon: 'none' }),
complete: () => { this.setData({ isLoading: false }); wx.hideLoading(); }
fail: () => {
this.handleFetchError('网络疏离,请检查后端');
}
});
},
// 第二波:静默加载剩余名字
fetchSecondBatch(keyword, mode, surname, count) {
const apiBaseUrl = getApp().globalData.apiBaseUrl;
const requestData = {
keyword,
count: count,
batch: 'second'
};
if (mode) requestData.mode = mode;
if (surname) requestData.surname = surname;
wx.request({
url: `${apiBaseUrl}/api/names/generate`,
data: requestData,
success: (res) => {
if (res.statusCode === 200 && res.data && res.data.length > 0) {
// 平滑合并数据
const currentList = this.data.nameList;
const newList = [...currentList, ...res.data];
this.setData({ nameList: newList });
console.log('第二波数据已静默加载,当前共', newList.length, '个名字');
}
},
fail: (err) => {
console.log('第二波加载失败(不影响已展示内容):', err);
}
});
},
// 处理加载错误
handleFetchError(message) {
if (this.loadingQuoteTimer) {
clearInterval(this.loadingQuoteTimer);
this.loadingQuoteTimer = null;
}
this.setData({ isLoading: false });
wx.showToast({ title: message, icon: 'none' });
},
// 返回首页
onBack() {
wx.navigateBack({
success: () => {
// 返回成功后,首页的 onShow 会重置状态
}
});
},
// 点击不喜欢按钮
onDislike() {
if (this.isExiting) return;
this.isExiting = true;
getApp().playAudio('swipe');
this.setData({
translateX: -500,
opacity: 0,
transition: 'all 0.3s cubic-bezier(0.6, -0.28, 0.735, 0.045)'
});
},
// 点击喜欢按钮
onLike() {
if (this.isExiting) return;
this.handleLike();
this.isExiting = true;
this.setData({
translateX: 500,
opacity: 0,
transition: 'all 0.3s cubic-bezier(0.6, -0.28, 0.735, 0.045)'
});
},
@@ -74,7 +258,7 @@ Page({
if (this.isExiting) return;
const deltaX = e.touches[0].clientX - this.startX;
// 只有移动超过5px才真正判定为拖拽
// 只有移动超过5px才真正判定为"拖拽"
if (Math.abs(deltaX) > 5) {
this.isDragging = true;
}
@@ -110,6 +294,12 @@ Page({
this.isExiting = true;
const direction = deltaX > 0 ? 1 : -1;
const targetX = direction * 500;
// 播放滑动音效
getApp().playAudio('swipe');
// 触觉反馈
wx.vibrateShort({ type: 'light' });
this.setData({
translateX: targetX,
opacity: 0,
@@ -138,19 +328,53 @@ Page({
const direction = this.data.translateX > 0 ? 'like' : 'dislike';
if (direction === 'like') {
this.handleLike();
} else {
this.handleCardExit(direction);
}
this.handleCardExit(direction);
},
handleLike() {
// 触觉反馈 - 中等震动
wx.vibrateShort({ type: 'medium' });
// 音效反馈
getApp().playAudio('success');
const card = this.data.nameList[this.data.currentIndex];
if (!this.data.collectedNames.some(item => item.name === card.name)) {
this.setData({ collectedNames: [...this.data.collectedNames, card] });
// 同步到后端
this.syncFavoriteToBackend(card);
}
this.handleCardExit('like');
},
// 同步收藏到后端
syncFavoriteToBackend(card) {
const openid = 'test_openid'; // 实际应从登录获取
const apiBaseUrl = getApp().globalData.apiBaseUrl;
wx.request({
url: `${apiBaseUrl}/api/favorites/add?openid=${openid}&mode=${this.data.mode}&keyword=${encodeURIComponent(this.data.keyword)}`,
method: 'POST',
header: {
'Content-Type': 'application/json'
},
data: {
name: card.name,
origin: card.origin,
description: card.description,
tone: card.tone,
score: card.score
},
success: (res) => {
if (res.data && res.data.success) {
console.log('收藏同步成功');
} else {
console.log('收藏同步失败:', res.data);
}
},
fail: (err) => {
console.log('收藏同步失败:', err);
}
});
},
handleCardExit() {
@@ -170,7 +394,15 @@ Page({
title: '见素时刻',
content: '这一波灵感已尽,是否再求几名?',
confirmText: '再求', cancelText: '返回',
success: (res) => { if (res.confirm) this.fetchNames(this.data.keyword); }
success: (res) => {
if (res.confirm) {
this.setData({ isLoading: true });
this.startLoadingQuoteRotation();
this.fetchNamesBatch(this.data.keyword, this.data.mode, this.data.surname);
} else {
this.onBack();
}
}
});
}
this.isExiting = false;
@@ -179,7 +411,10 @@ Page({
},
toggleCollectionView() {
this.setData({ showCollection: !this.data.showCollection });
// 跳转到 profile 页面
wx.navigateTo({
url: '/pages/profile/profile'
});
},
onDeleteCollected(e) {
@@ -190,53 +425,76 @@ Page({
wx.vibrateShort({ type: 'light' });
},
// 显示海报弹窗
onSavePoster() {
const card = this.data.nameList[this.data.currentIndex];
const nameChars = card.name.split('');
this.setData({
showPoster: true,
posterNameChars: nameChars
});
// 触觉反馈
wx.vibrateShort({ type: 'light' });
},
// 关闭海报弹窗
closePoster() {
this.setData({ showPoster: false });
},
// 保存海报到相册
savePoster() {
wx.showLoading({ title: '绘笔收录中...', mask: true });
const query = wx.createSelectorQuery().in(this);
query.select('#posterCanvas')
.fields({ node: true, size: true })
.exec((res) => {
const canvas = res[0].node;
const ctx = canvas.getContext('2d');
const dpr = wx.getSystemInfoSync().pixelRatio;
canvas.width = 750 * dpr;
canvas.height = 1334 * dpr;
ctx.scale(dpr, dpr);
ctx.fillStyle = '#FFFFFF';
ctx.fillRect(0, 0, 750, 1334);
ctx.fillStyle = '#2D2D2D';
ctx.font = '300 120px serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
const name = card.name;
const charArray = name.split('');
const charHeight = 140;
const startY = 667 - (charArray.length * charHeight) / 2 + charHeight / 2;
charArray.forEach((char, index) => ctx.fillText(char, 375, startY + index * charHeight));
const sealChar = this.data.keyword.substring(0, 1) || '素';
ctx.fillStyle = '#B22222';
ctx.fillRect(100, 100, 80, 80);
ctx.fillStyle = '#FFFFFF';
ctx.font = '40px serif';
ctx.fillText(sealChar, 140, 140);
const randomNum = Math.floor(Math.random() * 9000) + 1000;
ctx.fillStyle = '#D0D0D0';
ctx.font = '24px sans-serif';
ctx.fillText(`见素第 ${randomNum} 号灵感`, 375, 1200);
setTimeout(() => {
// 使用 snapshot 组件截图
wx.nextTick(() => {
const query = wx.createSelectorQuery().in(this);
query.select('#posterArea')
.node()
.exec((res) => {
const node = res[0].node;
// 使用 snapshot 截图
wx.canvasToTempFilePath({
canvas: canvas,
x: 0,
y: 0,
width: node.width,
height: node.height,
destWidth: node.width * 2,
destHeight: node.height * 2,
canvasId: 'posterCanvas',
success: (res) => {
wx.hideLoading();
wx.showShareImageMenu({ path: res.tempFilePath });
wx.saveImageToPhotosAlbum({
filePath: res.tempFilePath,
success: () => {
wx.showToast({ title: '已保存到相册', icon: 'success' });
},
fail: () => {
wx.showToast({ title: '保存失败', icon: 'none' });
}
});
},
fail: () => {
wx.hideLoading();
wx.showToast({ title: '绘笔受阻', icon: 'none' });
}
});
}, 300);
});
});
});
},
// 分享海报
sharePoster() {
wx.showShareImageMenu({
path: '/images/share.png'
});
},
// 阻止事件冒泡
preventBubble() {
// 什么都不做,只是阻止事件冒泡
}
});

View File

@@ -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>

View File

@@ -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);
}

View 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();
}
});

View File

@@ -0,0 +1,6 @@
{
"navigationBarTitleText": "我的心动名单",
"navigationBarBackgroundColor": "#FFFFFF",
"navigationBarTextStyle": "black",
"backgroundColor": "#FAFAFA"
}

View 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>

View 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;
}

View File

@@ -1,5 +1,5 @@
{
"appid": "wxf634a69e89290cc7",
"appid": "wx4793dc8fe6f34d2d",
"compileType": "miniprogram",
"libVersion": "3.15.2",
"packOptions": {