Files
JianSu-Naming/step3.md

541 lines
14 KiB
Markdown
Raw Permalink Normal View History

既然**阶段 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. **按钮交互**:点击底部的 `×``♥` 应该能触发卡片的切换效果(即使你不去手动滑动)。