- 实现 AI 起名功能(Kimi API 接入) - 添加用户收藏功能(MySQL 数据库) - 实现海报生成与分享 - 添加音效和触觉反馈 - 配置生产环境部署(WAR 包 + Nginx) - 支持多种起名模式(经典、诗词、自然、现代) - 实现分批加载优化体验
14 KiB
14 KiB
既然阶段 1:后端 API 增强(包括 NameCard 模型适配、MiniMaxService 的多模式 Prompt 引擎、以及 NamingController 的参数接收)已经开发完成,那么接下来的核心目标是完成前端的“大换装”。
下一阶段(阶段 2)我们将聚焦于前端重设计 - 输入页 (home 页面)。这是用户进入小程序的第一印象,目标是实现“极简”与“仪式感”。
下一阶段开发规划:前端极简输入页重构
🎯 阶段目标
重写 home 页面,实现多模式选择、水墨晕开动画效果,并对接已完成的后端多模式接口。
2.1 结构重构 (home.wxml)
需要打破原有简单的输入框布局,引入模式切换逻辑。
- 模式切换器:
- 使用
wx:for渲染“宝宝”、“人设”、“拾遗”三个标签。 - 绑定
bindtap="selectMode",通过data-id切换状态。
- 使用
- 输入增强:
- 保留
keyword输入框。 - 可选:根据
step2.md预留surname(姓氏)输入框(仅在“宝宝”模式下通过显隐逻辑控制展示)。
- 保留
- 动画占位:
- 新增
<canvas>或<view>容器用于承载“墨水滴入水中”的加载动效。
- 新增
2.2 视觉与动效设计 (home.wxss)
这是体现“见素”品牌美学的关键。
- 全局样式变量应用:
- 使用
var(--jiansu-text)等变量。 - 设置
font-family: "Noto Serif SC",确保宋体质感。
- 使用
- 交互动效:
- 选中态:选中的模式下方显示一个逐渐生成的微小黑点或下划线。
- 水墨加载动画:编写 CSS
animation。利用radial-gradient和opacity的变化,模拟墨滴在水中不规则扩散、变淡的效果。
- 按钮排版:
.generate-btn:背景全黑,文字留白,无圆角,增加letter-spacing。
2.3 交互逻辑实现 (home.js)
- 初始化管理:
data: { modes: [ { id: 'baby', name: '宝宝' }, { id: 'persona', name: '人设' }, { id: 'classic', name: '拾遗' } ], activeMode: 'baby', isGenerating: false, // 控制加载动画和按钮禁用 keyword: '', surname: '' } - 模式切换逻辑:点击切换
activeMode,同时清空或保留当前输入内容。 - 提交跳转逻辑:
- 校验
keyword是否为空。 - 触发
isGenerating: true,显示水墨动画。 - 延迟跳转:为了让用户看到动画(仪式感),建议设置 800ms-1200ms 的延迟,再执行
wx.navigateTo。 - 组装 URL:
/pages/index/index?mode=baby&keyword=平安&surname=张。
- 校验
✅ 验收标准
- 视觉:首页字体呈现思源宋体,布局高度留白,符合极简美学。
- 交互:点击不同模式有清晰的选中反馈。
- 流程:点击“求名”后,先出现水墨晕开动效,随后平滑跳转至卡片页。
- 接口:确认跳转时携带了正确的
mode和surname参数。
好的,我们现在开始 阶段 2 的具体代码实现。
这一部分的重点在于视觉层面的“仪式感”。我们将通过 CSS 动画实现“水墨滴入水中晕开”的效果,并构建极简的模式选择器。
1. 结构层:home.wxml
在 home.wxml 中,我们需要构建模式标签和动画容器。
<view class="container">
<view class="mode-selector">
<view
wx:for="{{modes}}"
wx:key="id"
class="mode-item {{activeMode === item.id ? 'active' : ''}}"
bindtap="selectMode"
data-id="{{item.id}}"
>
<text>{{item.name}}</text>
<view class="dot" wx:if="{{activeMode === item.id}}"></view>
</view>
</view>
<view class="main-content">
<view class="input-group">
<input
wx:if="{{activeMode === 'baby'}}"
class="surname-input"
placeholder="姓"
placeholder-class="p-class"
bindinput="onSurnameInput"
maxlength="2"
/>
<input
class="keyword-input"
placeholder="{{activeMode === 'persona' ? '输入你的职业、性格或期望' : '输入意向字或意境'}}"
placeholder-class="p-class"
bindinput="onKeywordInput"
/>
</view>
<view class="btn-container">
<button class="generate-btn" bindtap="startGenerate" disabled="{{isGenerating}}">
<text wx:if="{{!isGenerating}}">求名</text>
<view wx:else class="ink-loading"></view>
</button>
</view>
</view>
<view class="ink-overlay {{isGenerating ? 'show' : ''}}"></view>
</view>
2. 表现层:home.wxss
这里我们定义全局 CSS 变量,并利用 radial-gradient 实现水墨动效。
/* 模式选择器样式 */
.mode-selector {
display: flex;
justify-content: center;
margin-top: 100rpx;
gap: 60rpx;
}
.mode-item {
position: relative;
font-size: 28rpx;
color: var(--jiansu-accent);
transition: all 0.3s ease;
padding: 20rpx;
}
.mode-item.active {
color: var(--jiansu-text);
font-weight: 500;
}
.mode-item .dot {
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 6rpx;
height: 6rpx;
background: var(--jiansu-text);
border-radius: 50%;
animation: fadeIn 0.5s ease;
}
/* 输入框样式 */
.input-group {
margin-top: 150rpx;
display: flex;
flex-direction: column;
align-items: center;
gap: 40rpx;
}
.keyword-input, .surname-input {
border-bottom: 1rpx solid #eee;
padding: 20rpx;
text-align: center;
width: 80%;
font-family: var(--jiansu-font);
}
/* 求名按钮 - 极致方正 */
.generate-btn {
background: var(--jiansu-text) !important;
color: #fff !important;
border-radius: 0 !important;
width: 240rpx !important;
height: 90rpx;
line-height: 90rpx;
padding: 0;
margin-top: 100rpx;
font-weight: 300;
letter-spacing: 10rpx;
transition: all 0.3s;
}
/* 水墨晕开动画遮罩 */
.ink-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: #fff;
z-index: 100;
pointer-events: none;
opacity: 0;
}
.ink-overlay.show {
animation: inkSpread 1.2s ease-in-out forwards;
}
@keyframes inkSpread {
0% {
opacity: 0;
clip-path: circle(0% at 50% 50%);
background: #000;
}
50% {
opacity: 1;
clip-path: circle(100% at 50% 50%);
background: #000;
}
100% {
opacity: 1;
clip-path: circle(150% at 50% 50%);
background: #fff; /* 最后切回到白色,准备进入下一页 */
}
}
3. 逻辑层:home.js
处理参数组装和仪式感跳转。
Page({
data: {
modes: [
{ id: 'baby', name: '宝宝' },
{ id: 'persona', name: '人设' },
{ id: 'classic', name: '拾遗' }
],
activeMode: 'baby',
keyword: '',
surname: '',
isGenerating: false
},
selectMode(e) {
const { id } = e.currentTarget.dataset;
this.setData({ activeMode: id });
// 切换模式时可以播放一个轻微的震动反馈
wx.vibrateShort({ type: 'light' });
},
onKeywordInput(e) {
this.setData({ keyword: e.detail.value });
},
onSurnameInput(e) {
this.setData({ surname: e.detail.value });
},
startGenerate() {
if (!this.data.keyword) {
wx.showToast({ title: '请输入关键词', icon: 'none' });
return;
}
// 开启仪式感动画
this.setData({ isGenerating: true });
// 延迟 1s 跳转,等待水墨动效执行到视觉重心位置
setTimeout(() => {
const { keyword, activeMode, surname } = this.data;
const url = `/pages/index/index?keyword=${encodeURIComponent(keyword)}&mode=${activeMode}&surname=${encodeURIComponent(surname)}`;
wx.navigateTo({
url,
complete: () => {
// 页面跳转后重置加载状态,确保返回时按钮可用
setTimeout(() => {
this.setData({ isGenerating: false });
}, 500);
}
});
}, 1000);
}
});
🎨 接下来你可以做的优化:
- 字体引入:在
app.wxss中确保@import了思源宋体的网络字体。 - 视觉微调:
inkSpread动画中的clip-path效果非常吃性能,如果低端机型卡顿,可以改用opacity和scale的简单组合。
这部分代码完成后,前端的“多模式入口”就彻底打通了。下一步我们需要处理的是 index 卡片页的适配,包括接收这些参数并显示对应的头部信息。需要继续吗?
既然首页的“水墨仪式感”已经铺设好,我们进入阶段 3:前端适配 - 卡片页 (index)。
这一阶段的任务是让卡片页能够“听懂”首页传来的参数,并在视觉上呈现出「见素」品牌的统一感:散文诗排版、操作栏重构以及顶部状态展示。
1. 结构层:index.wxml
我们需要在顶部增加当前模式的展示,并重构底部的交互按钮。
<view class="container">
<view class="header-info">
<text class="brand">见素</text>
<text class="divider">·</text>
<text class="mode-tag">{{currentModeDisplay}}</text>
<text class="divider">·</text>
<text class="keyword-tag">{{currentKeyword}}</text>
</view>
<view class="card-stack">
<view
wx:for="{{nameList}}"
wx:key="id"
class="card {{index === currentIndex ? 'active' : ''}}"
style="z-index: {{nameList.length - index}}"
>
<view class="card-front">
<text class="name-text">{{item.name}}</text>
</view>
<view class="card-back">
<view class="origin">—— {{item.origin}}</view>
<view class="description">
<text>{{item.description}}</text>
</view>
<view class="score-badge">五行 {{item.tone}} · 意蕴 {{item.score}}</view>
</view>
</view>
</view>
<view class="action-bar">
<view class="action-btn back-home" bindtap="onBack">
<view class="icon-back"></view>
</view>
<view class="main-actions">
<view class="circle-btn dislike" bindtap="onDislike">
<text>×</text>
</view>
<view class="circle-btn like" bindtap="onLike">
<text>♥</text>
</view>
</view>
<view class="action-btn share" bindtap="onSavePoster">
<view class="icon-share"></view>
</view>
</view>
</view>
2. 表现层:index.wxss
这里的重点是散文诗般的行间距和底部的悬浮感按钮。
/* 顶部信息流 */
.header-info {
display: flex;
justify-content: center;
align-items: center;
padding: 40rpx 0;
font-size: 24rpx;
color: var(--jiansu-accent);
letter-spacing: 2rpx;
}
.header-info .brand {
color: var(--jiansu-text);
font-weight: bold;
}
.divider {
margin: 0 15rpx;
}
/* 卡片背面排版优化 */
.card-back {
padding: 60rpx;
display: flex;
flex-direction: column;
}
.card-back .description {
margin-top: 40rpx;
font-size: 32rpx;
line-height: 1.8; /* 散文诗般的行高 */
color: var(--jiansu-text);
text-align: justify;
letter-spacing: 1rpx;
white-space: pre-wrap; /* 保持后端返回的换行 */
}
/* 底部操作栏 */
.action-bar {
position: fixed;
bottom: 80rpx;
left: 0;
width: 100%;
display: flex;
justify-content: space-around;
align-items: center;
padding: 0 60rpx;
box-sizing: border-box;
}
.circle-btn {
width: 120rpx;
height: 120rpx;
border-radius: 50%;
border: 1rpx solid #eee;
display: flex;
justify-content: center;
align-items: center;
font-size: 40rpx;
background: #fff;
box-shadow: 0 10rpx 30rpx rgba(0,0,0,0.05);
transition: transform 0.2s;
}
.circle-btn:active {
transform: scale(0.9);
}
.circle-btn.like {
color: #e74c3c;
}
.circle-btn.dislike {
color: #95a5a6;
}
.main-actions {
display: flex;
gap: 80rpx;
}
/* 返回图标(简单线条实现) */
.icon-back {
width: 40rpx;
height: 40rpx;
border-left: 2rpx solid var(--jiansu-accent);
border-top: 2rpx solid var(--jiansu-accent);
transform: rotate(-45deg);
}
3. 逻辑层:index.js
这里需要处理参数接收和多模式的映射。
Page({
data: {
currentKeyword: '',
currentMode: '',
currentModeDisplay: '',
currentIndex: 0,
nameList: []
},
onLoad(options) {
const { keyword, mode, surname } = options;
// 建立模式 ID 到中文名称的映射
const modeMap = {
'baby': '宝宝起名',
'persona': '人设灵感',
'classic': '拾遗经典'
};
this.setData({
currentKeyword: keyword,
currentMode: mode,
currentModeDisplay: modeMap[mode] || '起名'
});
this.fetchNames(keyword, mode, surname);
},
fetchNames(keyword, mode, surname) {
wx.showLoading({ title: '灵感汇聚中...' });
wx.request({
url: 'https://your-api.com/api/naming/generate', // 替换为你的后端地址
method: 'GET',
data: { keyword, mode, surname },
success: (res) => {
this.setData({
nameList: res.data
});
},
fail: () => {
wx.showToast({ title: '灵感中断,请重试', icon: 'none' });
},
complete: () => {
wx.hideLoading();
}
});
},
onBack() {
wx.navigateBack();
},
onLike() {
// 逻辑:当前卡片右滑移出,并加入收藏
this.handleCardExit('right');
wx.vibrateShort({ type: 'medium' });
},
onDislike() {
// 逻辑:当前卡片左滑移出
this.handleCardExit('left');
},
handleCardExit(direction) {
// 这里触发卡片动画逻辑(如果你使用的是 CSS 动画控制 class)
// ... 原有的滑块逻辑适配 ...
this.setData({
currentIndex: this.data.currentIndex + 1
});
}
});
🛠 阶段性检查点
- 参数接收:确认
index.js能正确拿到mode和keyword。 - 视觉审美:检查卡片背面的
line-height是否足够舒展,是否像一段现代诗。 - 按钮交互:点击底部的
×和♥应该能触发卡片的切换效果(即使你不去手动滑动)。