Files
JianSu-Naming/step3.md
王鹏 be1f5722ab feat: 完成见素起名小程序核心功能
- 实现 AI 起名功能(Kimi API 接入)
- 添加用户收藏功能(MySQL 数据库)
- 实现海报生成与分享
- 添加音效和触觉反馈
- 配置生产环境部署(WAR 包 + Nginx)
- 支持多种起名模式(经典、诗词、自然、现代)
- 实现分批加载优化体验
2026-04-17 15:34:51 +08:00

14 KiB
Raw Permalink Blame 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-gradientopacity 的变化,模拟墨滴在水中不规则扩散、变淡的效果。
  • 按钮排版
    • .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,同时清空或保留当前输入内容。
  • 提交跳转逻辑
    1. 校验 keyword 是否为空。
    2. 触发 isGenerating: true,显示水墨动画。
    3. 延迟跳转:为了让用户看到动画(仪式感),建议设置 800ms-1200ms 的延迟,再执行 wx.navigateTo
    4. 组装 URL/pages/index/index?mode=baby&keyword=平安&surname=张

验收标准

  1. 视觉:首页字体呈现思源宋体,布局高度留白,符合极简美学。
  2. 交互:点击不同模式有清晰的选中反馈。
  3. 流程:点击“求名”后,先出现水墨晕开动效,随后平滑跳转至卡片页。
  4. 接口:确认跳转时携带了正确的 modesurname 参数。

好的,我们现在开始 阶段 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);
  }
});

🎨 接下来你可以做的优化:

  1. 字体引入:在 app.wxss 中确保 @import 了思源宋体的网络字体。
  2. 视觉微调inkSpread 动画中的 clip-path 效果非常吃性能,如果低端机型卡顿,可以改用 opacityscale 的简单组合。

这部分代码完成后,前端的“多模式入口”就彻底打通了。下一步我们需要处理的是 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
    });
  }
});

🛠 阶段性检查点

  1. 参数接收:确认 index.js 能正确拿到 modekeyword
  2. 视觉审美:检查卡片背面的 line-height 是否足够舒展,是否像一段现代诗。
  3. 按钮交互:点击底部的 × 应该能触发卡片的切换效果(即使你不去手动滑动)。