- 实现 AI 起名功能(Kimi API 接入) - 添加用户收藏功能(MySQL 数据库) - 实现海报生成与分享 - 添加音效和触觉反馈 - 配置生产环境部署(WAR 包 + Nginx) - 支持多种起名模式(经典、诗词、自然、现代) - 实现分批加载优化体验
541 lines
14 KiB
Markdown
541 lines
14 KiB
Markdown
既然**阶段 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`)
|
||
* **初始化管理**:
|
||
```javascript
|
||
data: {
|
||
modes: [
|
||
{ id: 'baby', name: '宝宝' },
|
||
{ id: 'persona', name: '人设' },
|
||
{ id: 'classic', name: '拾遗' }
|
||
],
|
||
activeMode: 'baby',
|
||
isGenerating: false, // 控制加载动画和按钮禁用
|
||
keyword: '',
|
||
surname: ''
|
||
}
|
||
```
|
||
* **模式切换逻辑**:点击切换 `activeMode`,同时清空或保留当前输入内容。
|
||
* **提交跳转逻辑**:
|
||
1. 校验 `keyword` 是否为空。
|
||
2. 触发 `isGenerating: true`,显示水墨动画。
|
||
3. **延迟跳转**:为了让用户看到动画(仪式感),建议设置 800ms-1200ms 的延迟,再执行 `wx.navigateTo`。
|
||
4. 组装 URL:`/pages/index/index?mode=baby&keyword=平安&surname=张`。
|
||
|
||
---
|
||
|
||
## ✅ 验收标准
|
||
1. **视觉**:首页字体呈现思源宋体,布局高度留白,符合极简美学。
|
||
2. **交互**:点击不同模式有清晰的选中反馈。
|
||
3. **流程**:点击“求名”后,先出现水墨晕开动效,随后平滑跳转至卡片页。
|
||
4. **接口**:确认跳转时携带了正确的 `mode` 和 `surname` 参数。
|
||
|
||
---
|
||
|
||
好的,我们现在开始 **阶段 2** 的具体代码实现。
|
||
|
||
这一部分的重点在于**视觉层面的“仪式感”**。我们将通过 CSS 动画实现“水墨滴入水中晕开”的效果,并构建极简的模式选择器。
|
||
|
||
---
|
||
|
||
### 1. 结构层:`home.wxml`
|
||
在 `home.wxml` 中,我们需要构建模式标签和动画容器。
|
||
|
||
```html
|
||
<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` 实现水墨动效。
|
||
|
||
```css
|
||
/* 模式选择器样式 */
|
||
.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`
|
||
处理参数组装和仪式感跳转。
|
||
|
||
```javascript
|
||
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);
|
||
}
|
||
});
|
||
```
|
||
|
||
---
|
||
|
||
### 🎨 接下来你可以做的优化:
|
||
1. **字体引入**:在 `app.wxss` 中确保 `@import` 了思源宋体的网络字体。
|
||
2. **视觉微调**:`inkSpread` 动画中的 `clip-path` 效果非常吃性能,如果低端机型卡顿,可以改用 `opacity` 和 `scale` 的简单组合。
|
||
|
||
**这部分代码完成后,前端的“多模式入口”就彻底打通了。下一步我们需要处理的是 `index` 卡片页的适配,包括接收这些参数并显示对应的头部信息。需要继续吗?**
|
||
|
||
既然首页的“水墨仪式感”已经铺设好,我们进入**阶段 3:前端适配 - 卡片页 (`index`)**。
|
||
|
||
这一阶段的任务是让卡片页能够“听懂”首页传来的参数,并在视觉上呈现出「见素」品牌的统一感:**散文诗排版**、**操作栏重构**以及**顶部状态展示**。
|
||
|
||
---
|
||
|
||
### 1. 结构层:`index.wxml`
|
||
我们需要在顶部增加当前模式的展示,并重构底部的交互按钮。
|
||
|
||
```html
|
||
<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`
|
||
这里的重点是**散文诗般的行间距**和底部的**悬浮感按钮**。
|
||
|
||
```css
|
||
/* 顶部信息流 */
|
||
.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`
|
||
这里需要处理参数接收和多模式的映射。
|
||
|
||
```javascript
|
||
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
|
||
});
|
||
}
|
||
});
|
||
```
|
||
|
||
---
|
||
|
||
### 🛠 阶段性检查点
|
||
1. **参数接收**:确认 `index.js` 能正确拿到 `mode` 和 `keyword`。
|
||
2. **视觉审美**:检查卡片背面的 `line-height` 是否足够舒展,是否像一段现代诗。
|
||
3. **按钮交互**:点击底部的 `×` 和 `♥` 应该能触发卡片的切换效果(即使你不去手动滑动)。
|