Initial commit

This commit is contained in:
王鹏
2026-04-16 11:25:29 +08:00
commit c4db35b183
39 changed files with 1725 additions and 0 deletions

7
.claude/settings.json Normal file
View File

@@ -0,0 +1,7 @@
{
"permissions": {
"allow": [
"Read(//c/Users/南音/.m2/repository/com/squareup/okhttp3/okhttp/**)"
]
}
}

66
DEVELOPMENT.md Normal file
View File

@@ -0,0 +1,66 @@
# 「见素」开发文档 (JianSu Naming Dev Doc)
## 1. 项目概述与核心理念
「见素」是一款专注于“留白”与“故事化”体验的起名应用。其核心理念在于摒弃繁杂,回归名字本身的诗意与文化底蕴。应用旨在通过极简的视觉设计、带有物理反馈的微交互,以及基于 AI 的深度叙事解读,为用户提供一种“审美溢价”的起名体验。
## 2. 视觉与 UI 架构
为了对齐“潮汐风格”UI 架构需严格遵循 **0.618 黄金比例**的布局,强调极致的“留白”。
### 2.1 色彩系统
* **主背景**`#FFFFFF`(纯白),营造空灵感。
* **文字**`#2D2D2D`(带有温度的深灰,而非纯黑),减轻视觉疲劳,增加温润感。
* **辅助色**`#E0E0E0`(用于极细的分割线),保持界面的通透。
### 2.2 字体栈 (Typography)
* **标题/名字展示**:优先调用 `Noto Serif SC` (思源宋体),展现汉字的古典韵味与雕刻感。
* **说明文字/正文**:使用 `PingFang SC` (苹方),保证移动端的清晰易读性。
## 3. 交互设计 (Interaction)
摒弃传统的简单 Swiper 效果,为滑动增加“摩擦力”和“阻尼感”,打造“见素”时刻。
### 3.1 卡片设计
* **正面 (Front)**:极致极简,只放置一个巨大的、居中的名字,下方配以一行极小的出处诗句。
* **反面 (Back)**:点击卡片翻转,展示名字背后的“叙事性解读”(通感描述)。
### 3.2 交互与微反馈
* **左滑 (Dislike/Skip)**:无感交互,卡片像烟雾一样逐渐变淡消失。
* **右滑 (Like/Save)**:触发轻微的 Haptic Feedback触觉震动名字化作一个墨点落入底部的“收藏”图标中。
## 4. 技术核心
结合 Gemini API 与声韵学算法,打造有灵魂的起名引擎。
### 4.1 AI 故事化 Prompt 策略
**目标**:拒绝生硬的 JSON 数据堆砌,让 AI 扮演一位“隐居的诗人”。
**System Prompt 示例**
> “你是「见素」的灵魂导师。当用户输入期待时请从《诗经》、《楚辞》或宋词中提取意象生成3个名字。每个名字必须配有一段 50 字以内的‘通感’描述,包含气味、光线或声音的描写,拒绝说教。”
### 4.2 技术栈与核心逻辑 (Tech Stack)
* **后端**Java SpringBoot提供起名接口、声韵分析及 API 转发。
* **小程序端**:原生微信小程序 (Native Mini Program),利用 WXS 与动画库实现阻尼感滑动。
**声韵分析逻辑 (Algorithm)**
在后端 Service 层实现姓名声韵评估:
1. **平仄匹配**:检测姓氏(平/仄)与名字(平/仄)的组合。
2. **优选模式**:推荐“平仄平”、“仄平仄”等有起伏感、抑扬顿挫的组合。
3. **叠音/开口度检查**:避免连续的闭口音(如 iao, iu, in 等连续出现),确保名字喊出来时是响亮、大气的。
## 5. 开发路线图 (MVP Roadmap)
从基础骨架到最终的美化,分为四个阶段进行:
* **Phase 1 (骨架)**
* **后端**:搭建 SpringBoot 基础环境,配置跨域与 API 基础接口。
* **小程序**:使用原生小程序框架,实现带阻尼感的卡片滑动组件与翻转动效。
* **Phase 2 (大脑)**:后端集成 Gemini API调试“故事化起名”的 Prompt确保输出文本的 Vibe 符合“见素”的调性。
* **Phase 3 (细节)**:加入微音效(如类似翻书声或清脆的铃声)和后端的声韵评分逻辑算法。
* **Phase 4 (美化)**:优化字体渲染,增加 Canvas 名字海报生成功能。
## 6. 商业化预留 (暂不实现,仅做架构预留)
商业化路线拒绝弹窗广告,主打“审美溢价”。
### 6.1 名字壁纸生成 (高级感变现)
* 利用 Canvas 生成极简海报:居中的大字 + 用户的姓氏印章 + 独一无二的编号(如:见素第 8921 号灵感)。
* **变现点**:基础版免费,高清无水印版(可设置价格梯度如 ¥1.9 - ¥9.9)。
### 6.2 “见素”锦囊 (深度定制)
* 为难以刷到满意名字的用户提供“深度定制”入口。
* 调用更高级别的模型(如 Claude 3.5 Sonnet 或 Gemini Ultra进行 1v1 生成,按次收费。

65
README.md Normal file
View File

@@ -0,0 +1,65 @@
1. 视觉底座:极致的“留白”
为了对齐“潮汐风格”,你的 UI 架构需要遵循 0.618 黄金比例 的布局。
色彩系统: * 主背景:#FFFFFF(纯白)。
文字:#2D2D2D(带有温度的深灰,而非纯黑)。
辅助色:#E0E0E0(极细的分割线)。
字体栈: * 标题/名字展示:优先调用 Noto Serif SC (思源宋体),展现古典韵味。
说明文字:使用 PingFang SC保证移动端的易读性。
2. 交互灵魂:卡片滑动与“见素”时刻
不要简单的 Swiper要给滑动增加“摩擦力”和“阻尼感”。
卡片设计:
正面:只放一个巨大的、居中的名字,下方一行极小的出处诗句。
反面(点击翻转):展示**“叙事性解读”**。
反馈微交互:
左滑(无感): 卡片像烟雾一样变淡消失。
右滑(收藏): 触发轻微的 Haptic Feedback震动名字化作一个墨点落入底部的“收藏”图标中。
3. 技术核心AI 故事化与声韵算法
既然你有 Gemini API 的开发经验,这里可以做得很硬核:
AI 故事化 Prompt 策略
不要让 AI 只返回 JSON要让它扮演一个“隐居的诗人”。
System Prompt 示例:
“你是「见素」的灵魂导师。当用户输入期待时请从《诗经》、《楚辞》或宋词中提取意象生成3个名字。每个名字必须配有一段 50 字以内的‘通感’描述,包含气味、光线或声音的描写,拒绝说教。”
声韵分析逻辑 (Algorithm)
你可以写一个简单的 Python 或 Node.js 脚本来处理:
平仄匹配: 检测姓氏(平/仄)与名字(平/仄)的组合。
优选模式: 平仄平、仄平仄(有起伏感)。
叠音/开口度检查: 避免连续的闭口音,确保名字喊出来时是响亮的。
4. 商业化与衍生:高级感变现(先不实现)
拒绝弹窗广告,我们走**“审美溢价”**路线。
名字壁纸生成: * 利用 Canvas 生成一张极简海报:居中的大字 + 用户的姓氏印章 + 独一无二的编号(如:见素第 8921 号灵感)。
变现点: 基础版免费,高清无水印版 ¥1.9 - ¥9.9。
“见素”锦囊:
如果用户一直刷不到满意的,提供一个“深度定制”入口,调用更高级别的模型(如 Claude 3.5 Sonnet 或 Gemini Ultra进行 1v1 生成,按次收费。
5. 开发者视角的 MVP 路径 (Roadmap)
Phase 1 (骨架): 搭建基础的 Next.js 或 Uni-app 框架,实现卡片滑动组件。
Phase 2 (大脑): 接入 Gemini API调试“故事化起名”的提示词确保输出的 Vibe 符合“见素”。
Phase 3 (细节): 加入音效(类似翻书声或清脆的铃声)和声韵评分逻辑。
Phase 4 (美化): 优化字体渲染,增加 Canvas 海报生成功能。

8
backend/.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
# Editor-based HTTP Client requests
/httpRequests/

19
backend/.idea/compiler.xml generated Normal file
View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<annotationProcessing>
<profile default="true" name="Default" enabled="true" />
<profile name="Maven default annotation processors profile" enabled="true">
<sourceOutputDir name="target/generated-sources/annotations" />
<sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
<outputRelativeToContentRoot value="true" />
<module name="naming" />
</profile>
</annotationProcessing>
</component>
<component name="JavacSettings">
<option name="ADDITIONAL_OPTIONS_OVERRIDE">
<module name="naming" options="-parameters" />
</option>
</component>
</project>

6
backend/.idea/encodings.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding">
<file url="file://$PROJECT_DIR$/src/main/java" charset="UTF-8" />
</component>
</project>

20
backend/.idea/jarRepositories.xml generated Normal file
View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RemoteRepositoriesConfiguration">
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Maven Central repository" />
<option name="url" value="https://repo1.maven.org/maven2" />
</remote-repository>
<remote-repository>
<option name="id" value="jboss.community" />
<option name="name" value="JBoss Community repository" />
<option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
</remote-repository>
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Central Repository" />
<option name="url" value="https://maven.aliyun.com/repository/central" />
</remote-repository>
</component>
</project>

12
backend/.idea/misc.xml generated Normal file
View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="MavenProjectsManager">
<option name="originalFiles">
<list>
<option value="$PROJECT_DIR$/pom.xml" />
</list>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" default="true" project-jdk-name="1.8" project-jdk-type="JavaSDK" />
</project>

10
backend/.idea/runConfigurations.xml generated Normal file
View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RunConfigurationProducerService">
<option name="ignoredProducers">
<set>
<option value="com.android.tools.idea.compose.preview.runconfiguration.ComposePreviewRunConfigurationProducer" />
</set>
</option>
</component>
</project>

67
backend/pom.xml Normal file
View File

@@ -0,0 +1,67 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.18</version>
<relativePath/>
</parent>
<groupId>com.jiansu</groupId>
<artifactId>naming</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>jiansu-naming</name>
<description>JianSu Naming AI Backend</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.83</version>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.12.0</version>
</dependency>
<dependency>
<groupId>com.belerweb</groupId>
<artifactId>pinyin4j</artifactId>
<version>2.5.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,11 @@
package com.jiansu.naming;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class NamingApplication {
public static void main(String[] args) {
SpringApplication.run(NamingApplication.class, args);
}
}

View File

@@ -0,0 +1,22 @@
package com.jiansu.naming.controller;
import com.jiansu.naming.model.NameCard;
import com.jiansu.naming.service.MiniMaxService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/names")
@CrossOrigin(origins = "*")
public class NamingController {
@Autowired
private MiniMaxService miniMaxService;
@GetMapping("/generate")
public List<NameCard> generate(@RequestParam(defaultValue = "清冷") String keyword) {
return miniMaxService.generateNames(keyword);
}
}

View File

@@ -0,0 +1,18 @@
package com.jiansu.naming.model;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class NameCard {
private String name; // 名字
private String origin; // 出处/诗句
private String description; // “通感”叙事描述
private Double score; // 声韵评分(后端计算)
private String tone; // 平仄分析 (如:平仄)
}

View File

@@ -0,0 +1,184 @@
package com.jiansu.naming.service;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.jiansu.naming.model.NameCard;
import lombok.extern.slf4j.Slf4j;
import okhttp3.*;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
@Slf4j
@Service
public class MiniMaxService {
@Value("${minimax.api-key}")
private String apiKey;
@Value("${minimax.api-url}")
private String apiUrl;
private final ToneAnalysisService toneAnalysisService;
public MiniMaxService(ToneAnalysisService toneAnalysisService) {
this.toneAnalysisService = toneAnalysisService;
}
private final OkHttpClient client = new OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.readTimeout(60, TimeUnit.SECONDS)
.build();
private static final String SYSTEM_PROMPT =
"你是「见素」的灵魂导师。当用户输入期待或关键词时请从《诗经》、《楚辞》或宋词中提取意象生成3个极具美感的名字。" +
"每个名字必须配有一段 50 字以内的'通感'描述,包含气味、光线或声音的描写,拒绝说教。" +
"请直接以 JSON 数组格式返回JSON 对象包含三个字段name (名字), origin (诗词出处), description (通感描述)。不要包含任何 Markdown 格式。";
public List<NameCard> generateNames(String keyword) {
// 后门:当关键词为 "test" 时,返回固定的测试数据
if ("test".equalsIgnoreCase(keyword)) {
log.info("Keyword is 'test', returning hardcoded test data.");
return createTestData();
}
// 构建 MiniMax Token Plan (OpenAI 兼容) 请求体
JSONObject jsonBody = new JSONObject();
jsonBody.put("model", "MiniMax-M2.7");
JSONArray messages = new JSONArray();
JSONObject systemMsg = new JSONObject();
systemMsg.put("role", "system");
systemMsg.put("content", SYSTEM_PROMPT);
messages.add(systemMsg);
JSONObject userMsg = new JSONObject();
userMsg.put("role", "user");
userMsg.put("content", "用户期待/关键词是:" + keyword);
messages.add(userMsg);
jsonBody.put("messages", messages);
RequestBody body = RequestBody.create(
jsonBody.toJSONString(),
MediaType.parse("application/json; charset=utf-8")
);
Request request = new Request.Builder()
.url(apiUrl)
.post(body)
.addHeader("Authorization", "Bearer " + apiKey)
.addHeader("Content-Type", "application/json")
.build();
try (Response response = client.newCall(request).execute()) {
if (response.isSuccessful() && response.body() != null) {
String responseBody = response.body().string();
log.info("MiniMax raw response: {}", responseBody);
return parseMiniMaxResponse(responseBody);
} else {
String errorMsg = response.body() != null ? response.body().string() : response.message();
log.error("MiniMax API Error: {} - {}", response.code(), errorMsg);
throw new RuntimeException("MiniMax API 调用失败: " + errorMsg);
}
} catch (IOException e) {
log.error("API Error: ", e);
throw new RuntimeException("网络请求失败");
}
}
private List<NameCard> parseMiniMaxResponse(String responseBody) {
List<NameCard> nameCards = new ArrayList<>();
try {
JSONObject root = JSON.parseObject(responseBody);
// 检查 API 错误
JSONObject baseResp = root.getJSONObject("base_resp");
if (baseResp != null) {
int statusCode = baseResp.getIntValue("status_code");
if (statusCode != 0) {
String statusMsg = baseResp.getString("status_msg");
log.error("MiniMax API error: {} - {}", statusCode, statusMsg);
throw new RuntimeException("MiniMax API 错误: " + statusMsg);
}
}
JSONArray choices = root.getJSONArray("choices");
if (choices == null || choices.isEmpty()) {
throw new RuntimeException("MiniMax 返回内容为空");
}
JSONObject firstChoice = choices.getJSONObject(0);
JSONObject message = firstChoice.getJSONObject("message");
String aiText = message.getString("content");
if (aiText == null || aiText.isEmpty()) {
throw new RuntimeException("MiniMax 返回内容为空");
}
// 清理可能存在的 Markdown 代码块标记
aiText = aiText.replaceAll("```json", "").replaceAll("```", "").trim();
JSONArray jsonArray = JSON.parseArray(aiText);
for (int i = 0; i < jsonArray.size(); i++) {
JSONObject obj = jsonArray.getJSONObject(i);
String name = obj.getString("name");
// 进行声韵分析
ToneAnalysisService.ToneResult tr = toneAnalysisService.analyze(name);
NameCard card = NameCard.builder()
.name(name)
.origin(obj.getString("origin"))
.description(obj.getString("description"))
.score(tr.score)
.tone(tr.tonePattern)
.build();
nameCards.add(card);
}
} catch (RuntimeException e) {
throw e;
} catch (Exception e) {
log.error("Parse Error: ", e);
throw new RuntimeException("数据解析失败");
}
return nameCards;
}
private List<NameCard> createTestData() {
List<NameCard> testData = new ArrayList<>();
testData.add(NameCard.builder()
.name("月白")
.origin("《月出》:月出皎兮,佼人僚兮。")
.description("像月光洒在雪地,清冷又干净,带着一丝不易察觉的、风吹过竹林的微响。")
.score(9.8)
.tone("仄仄")
.build());
testData.add(NameCard.builder()
.name("南絮")
.origin("晏几道《御街行》:街南绿树春饶絮。")
.description("春天街巷里柔软的柳絮,随风飘浮,带着南方阳光的暖意和青草的气息。")
.score(9.5)
.tone("平仄")
.build());
testData.add(NameCard.builder()
.name("疏影")
.origin("林逋《山园小梅》:疏影横斜水清浅,暗香浮动月黄昏。")
.description("黄昏月下,梅枝稀疏的影子落在清浅的水面,空气中浮动着若有似无的冷香。")
.score(9.7)
.tone("平仄")
.build());
return testData;
}
}

View File

@@ -0,0 +1,102 @@
package com.jiansu.naming.service;
import net.sourceforge.pinyin4j.PinyinHelper;
import net.sourceforge.pinyin4j.format.HanyuPinyinOutputFormat;
import net.sourceforge.pinyin4j.format.HanyuPinyinToneType;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
/**
* 见素声韵分析逻辑
* 1. 平仄匹配1,2声为平3,4声为仄
* 2. 优选模式:平仄平、仄平仄、平平仄、仄仄平
* 3. 连读检查:避免连续闭口音
*/
@Service
public class ToneAnalysisService {
private final HanyuPinyinOutputFormat format;
public ToneAnalysisService() {
format = new HanyuPinyinOutputFormat();
format.setToneType(HanyuPinyinToneType.WITH_TONE_NUMBER);
}
public static class ToneResult {
public double score;
public String tonePattern;
public String msg;
}
public ToneResult analyze(String name) {
ToneResult result = new ToneResult();
if (name == null || name.isEmpty()) {
result.score = 0;
return result;
}
List<Integer> tones = new ArrayList<>();
List<String> pinyins = new ArrayList<>();
for (char c : name.toCharArray()) {
try {
String[] pinyinArray = PinyinHelper.toHanyuPinyinStringArray(c, format);
if (pinyinArray != null && pinyinArray.length > 0) {
String p = pinyinArray[0];
pinyins.add(p);
// 取最后一位数字作为声调
int tone = Character.getNumericValue(p.charAt(p.length() - 1));
tones.add(tone);
}
} catch (Exception e) {
// 忽略无法识别的字符
}
}
// 构建平仄模式
StringBuilder pattern = new StringBuilder();
for (int tone : tones) {
if (tone == 1 || tone == 2) pattern.append("");
else if (tone == 3 || tone == 4) pattern.append("");
}
result.tonePattern = pattern.toString();
// 基础分 8.0
double score = 8.0;
// 1. 平仄起伏检查
if (pattern.length() >= 2) {
boolean hasChange = false;
for (int i = 0; i < pattern.length() - 1; i++) {
if (pattern.charAt(i) != pattern.charAt(i + 1)) {
hasChange = true;
break;
}
}
if (hasChange) score += 1.0; // 有起伏感 +1
else score -= 0.5; // 全平或全仄 -0.5
}
// 2. 优选模式加分
String pStr = pattern.toString();
if (pStr.equals("平仄平") || pStr.equals("仄平仄") || pStr.equals("平平仄") || pStr.equals("仄仄平")) {
score += 0.5;
}
// 3. 开口度简单检查 (韵母 a, e, o 结尾通常更响亮)
int openCount = 0;
for (String p : pinyins) {
String yumu = p.substring(0, p.length() - 1);
if (yumu.endsWith("a") || yumu.endsWith("e") || yumu.endsWith("o") || yumu.endsWith("ang") || yumu.endsWith("ong")) {
openCount++;
}
}
if (openCount >= 1) score += 0.3;
// 限制最高分 9.9
result.score = Math.min(score, 9.9);
return result;
}
}

View File

@@ -0,0 +1,11 @@
server:
port: 8080
spring:
application:
name: jiansu-naming
# MiniMax API 配置
minimax:
api-key: sk-cp-n0eCZgH5s-NpduAVPo8rpWM9eUBsMOBnIroISIaH6y8eFIpT0VSrCMttzE4bVDbQ-loiMR1b8ZpIsgotQ_yqQRk8_fcUxKHsbhtLfN70oCVaV6-94ZC9Wjk
api-url: https://api.minimax.chat/v1/text/chatcompletion_v2

View File

@@ -0,0 +1,11 @@
server:
port: 8080
spring:
application:
name: jiansu-naming
# MiniMax API 配置
minimax:
api-key: sk-cp-n0eCZgH5s-NpduAVPo8rpWM9eUBsMOBnIroISIaH6y8eFIpT0VSrCMttzE4bVDbQ-loiMR1b8ZpIsgotQ_yqQRk8_fcUxKHsbhtLfN70oCVaV6-94ZC9Wjk
api-url: https://api.minimax.chat/v1/text/chatcompletion_v2

View File

@@ -0,0 +1,5 @@
com\jiansu\naming\model\NameCard$NameCardBuilder.class
com\jiansu\naming\model\NameCard.class
com\jiansu\naming\controller\NamingController.class
com\jiansu\naming\service\MiniMaxService.class
com\jiansu\naming\NamingApplication.class

View File

@@ -0,0 +1,4 @@
C:\Users\<5C><><EFBFBD><EFBFBD>\Desktop\JianSu-Naming\backend\src\main\java\com\jiansu\naming\controller\NamingController.java
C:\Users\<5C><><EFBFBD><EFBFBD>\Desktop\JianSu-Naming\backend\src\main\java\com\jiansu\naming\model\NameCard.java
C:\Users\<5C><><EFBFBD><EFBFBD>\Desktop\JianSu-Naming\backend\src\main\java\com\jiansu\naming\service\GeminiService.java
C:\Users\<5C><><EFBFBD><EFBFBD>\Desktop\JianSu-Naming\backend\src\main\java\com\jiansu\naming\NamingApplication.java

26
miniprogram/app.js Normal file
View File

@@ -0,0 +1,26 @@
App({
onLaunch() {
// 初始化音效上下文
this.globalData.audioContexts = {
flip: wx.createInnerAudioContext(),
success: 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'; // 类似轻微铃声
},
playAudio(type) {
const ctx = this.globalData.audioContexts[type];
if (ctx) {
ctx.stop();
ctx.play();
}
},
globalData: {
audioContexts: {}
}
});

15
miniprogram/app.json Normal file
View File

@@ -0,0 +1,15 @@
{
"pages": [
"pages/home/home",
"pages/index/index"
],
"window": {
"backgroundColor": "#FFFFFF",
"backgroundTextStyle": "light",
"navigationBarBackgroundColor": "#FFFFFF",
"navigationBarTitleText": "见素",
"navigationBarTextStyle": "black"
},
"sitemapLocation": "sitemap.json",
"lazyCodeLoading": "requiredComponents"
}

View File

@@ -0,0 +1,25 @@
Page({
data: {
keyword: ''
},
onInput(e) {
this.setData({
keyword: e.detail.value
});
},
startNaming() {
if (!this.data.keyword.trim()) {
wx.showToast({
title: '请输入一抹期待',
icon: 'none'
});
return;
}
wx.navigateTo({
url: `/pages/index/index?keyword=${encodeURIComponent(this.data.keyword)}`
});
}
});

View File

@@ -0,0 +1,3 @@
{
"usingComponents": {}
}

View File

@@ -0,0 +1,25 @@
<view class="container">
<view class="content">
<view class="title">见素</view>
<view class="subtitle">回归名字的诗意与留白</view>
<view class="input-section">
<input
class="keyword-input"
placeholder="输入关键词,如:自由、清冷"
placeholder-class="placeholder"
bindinput="onInput"
value="{{keyword}}"
/>
<view class="line"></view>
</view>
<view class="action-section">
<button class="generate-btn" bindtap="startNaming">感悟名字</button>
</view>
</view>
<view class="footer">
<text>© 见素 · 审美溢价</text>
</view>
</view>

View File

@@ -0,0 +1,123 @@
page {
background-color: #FFFFFF;
height: 100%;
}
.container {
height: 100%;
display: flex;
flex-direction: column;
padding: 0 80rpx;
overflow: hidden;
}
.content {
margin-top: 35vh;
display: flex;
flex-direction: column;
align-items: center;
animation: contentFadeIn 1.5s cubic-bezier(0.19, 1, 0.22, 1);
}
.title {
font-family: "Noto Serif SC", serif;
font-size: 80rpx;
color: #2D2D2D;
letter-spacing: 24rpx;
font-weight: 300;
margin-bottom: 20rpx;
padding-left: 24rpx;
}
.subtitle {
font-size: 22rpx;
color: #D0D0D0;
letter-spacing: 6rpx;
margin-bottom: 150rpx;
}
.input-section {
width: 100%;
margin-bottom: 80rpx;
}
.keyword-input {
width: 100%;
height: 80rpx;
text-align: center;
font-size: 30rpx;
color: #4A4A4A;
letter-spacing: 2rpx;
}
.placeholder {
color: #E0E0E0;
font-weight: 200;
}
.line {
width: 40rpx;
height: 1rpx;
background-color: #F0F0F0;
margin: 10rpx auto 0;
transition: width 0.6s cubic-bezier(0.165, 0.84, 0.44, 1);
}
.input-section:focus-within .line {
width: 80rpx;
background-color: #2D2D2D;
}
.action-section {
width: 100%;
display: flex;
justify-content: center;
margin-top: 40rpx;
}
.generate-btn {
background: none !important;
color: #A0A0A0 !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;
border: none !important;
}
.generate-btn::after {
border: none !important;
}
.generate-btn:active {
color: #2D2D2D !important;
transform: scale(0.98);
}
.footer {
position: absolute;
bottom: 80rpx;
left: 0;
right: 0;
text-align: center;
animation: footerFadeIn 3s ease-in;
}
.footer text {
font-size: 16rpx;
color: #F0F0F0;
letter-spacing: 4rpx;
}
@keyframes contentFadeIn {
from { opacity: 0; transform: translateY(20rpx); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes footerFadeIn {
0% { opacity: 0; }
70% { opacity: 0; }
100% { opacity: 1; }
}

View File

@@ -0,0 +1,242 @@
const EXIT_THRESHOLD = 80;
const FAST_SWIPE_VELOCITY = 0.5;
Page({
isDragging: false,
isExiting: false,
startTime: 0,
startX: 0,
startY: 0,
lastX: 0,
data: {
nameList: [],
currentIndex: 0,
isLoading: true,
isFlipped: false,
keyword: '清冷',
// 动画控制
translateX: 0,
rotate: 0,
opacity: 1,
transition: 'none',
cardKey: 0,
collectedNames: [],
showCollection: false
},
onLoad(options) {
const keyword = options.keyword || this.data.keyword;
this.setData({ keyword });
this.fetchNames(keyword);
},
fetchNames(keyword) {
this.setData({ isLoading: true });
wx.showLoading({ title: '见素正在感悟...', mask: true });
wx.request({
url: 'http://localhost:8080/api/names/generate',
data: { keyword },
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 });
} else {
wx.showToast({ title: '意境未达,请重试', icon: 'none' });
}
},
fail: () => wx.showToast({ title: '网络疏离,请检查后端', icon: 'none' }),
complete: () => { this.setData({ isLoading: false }); wx.hideLoading(); }
});
},
onFlip() {
// 守卫如果这是一次拖拽isDragging 会为 true则不执行翻转
if (this.isDragging || this.isExiting) return;
getApp().playAudio('flip');
this.setData({ isFlipped: !this.data.isFlipped });
},
onTouchStart(e) {
if (this.isExiting) return;
this.isDragging = false; // 每次开始触摸时,都假定为点击,而非拖拽
this.startX = e.touches[0].clientX;
this.lastX = this.startX;
this.startY = e.touches[0].clientY;
this.startTime = Date.now();
this.setData({ transition: 'none' });
},
onTouchMove(e) {
if (this.isExiting) return;
const deltaX = e.touches[0].clientX - this.startX;
// 只有移动超过5px才真正判定为“拖拽”
if (Math.abs(deltaX) > 5) {
this.isDragging = true;
}
// 如果不是拖拽,则不进行任何移动
if (!this.isDragging) return;
this.lastX = e.touches[0].clientX;
const rotate = deltaX * 0.05;
const opacity = 1 - Math.abs(deltaX) / 200;
this.setData({
translateX: deltaX,
rotate: rotate,
opacity: opacity
});
},
onTouchEnd() {
if (this.isExiting) return;
// 如果不是拖拽(即这是一次纯点击),则 onTouchEnd 不执行任何操作,交由 onFlip 处理
if (!this.isDragging) {
return;
}
// 以下是拖拽结束后的逻辑
const deltaX = this.lastX - this.startX;
const deltaTime = Date.now() - this.startTime;
const velocity = deltaX / deltaTime;
const shouldExit = Math.abs(deltaX) > EXIT_THRESHOLD || Math.abs(velocity) > FAST_SWIPE_VELOCITY;
if (shouldExit) {
this.isExiting = true;
const direction = deltaX > 0 ? 1 : -1;
const targetX = direction * 500;
this.setData({
translateX: targetX,
opacity: 0,
transition: 'all 0.3s cubic-bezier(0.6, -0.28, 0.735, 0.045)'
});
} else {
this.setData({
translateX: 0,
rotate: 0,
opacity: 1,
transition: 'all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275)'
});
}
// 延迟重置 isDragging 标志位。
// 这是为了确保在系统触发 tap 事件时isDragging 标志仍然为 true
// 从而让 onFlip 方法可以正确地忽略掉这次由拖拽产生的 tap。
setTimeout(() => {
this.isDragging = false;
}, 100);
},
onTransitionEnd() {
if (!this.isExiting) return;
const direction = this.data.translateX > 0 ? 'like' : 'dislike';
if (direction === 'like') {
this.handleLike();
} else {
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.handleCardExit('like');
},
handleCardExit() {
const nextIndex = this.data.currentIndex + 1;
wx.nextTick(() => {
this.setData({
transition: 'none',
translateX: 0,
rotate: 0,
opacity: 1,
isFlipped: false
}, () => {
if (nextIndex < this.data.nameList.length) {
this.setData({ currentIndex: nextIndex, cardKey: this.data.cardKey + 1 });
} else {
wx.showModal({
title: '见素时刻',
content: '这一波灵感已尽,是否再求几名?',
confirmText: '再求', cancelText: '返回',
success: (res) => { if (res.confirm) this.fetchNames(this.data.keyword); }
});
}
this.isExiting = false;
});
});
},
toggleCollectionView() {
this.setData({ showCollection: !this.data.showCollection });
},
onDeleteCollected(e) {
const nameToDelete = e.currentTarget.dataset.name;
this.setData({
collectedNames: this.data.collectedNames.filter(item => item.name !== nameToDelete)
});
wx.vibrateShort({ type: 'light' });
},
onSavePoster() {
const card = this.data.nameList[this.data.currentIndex];
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(() => {
wx.canvasToTempFilePath({
canvas: canvas,
success: (res) => {
wx.hideLoading();
wx.showShareImageMenu({ path: res.tempFilePath });
},
fail: () => {
wx.hideLoading();
wx.showToast({ title: '绘笔受阻', icon: 'none' });
}
});
}, 300);
});
}
});

View File

@@ -0,0 +1,74 @@
<view class="container">
<view class="card-stack" wx:if="{{!isLoading && nameList.length > 0}}">
<view
class="card-container"
style="transform: translateX({{translateX}}px) rotate({{rotate}}deg); transition: {{transition}};"
bindtouchstart="onTouchStart"
bindtouchmove="onTouchMove"
bindtouchend="onTouchEnd"
bindtransitionend="onTransitionEnd"
>
<view class="card {{isFlipped ? 'flipped' : ''}}">
<!-- 正面:名字与诗词 -->
<view class="card-face front" bindtap="onFlip">
<text class="name">{{nameList[currentIndex].name}}</text>
<view class="poem-container">
<text class="poem">{{nameList[currentIndex].origin}}</text>
</view>
</view>
<!-- 反面:故事化解读 -->
<view class="card-face back">
<view class="desc-container">
<text class="desc">{{nameList[currentIndex].description}}</text>
</view>
<view class="analysis-container">
<view class="tone-tag">声韵:{{nameList[currentIndex].tone}}</view>
<view class="score-tag">见素评分:{{nameList[currentIndex].score}}</view>
<view class="save-btn" catchtap="onSavePoster">
<text class="save-icon"></text>
<text>存为海报</text>
</view>
</view>
</view>
</view>
</view>
</view>
<!-- 海报生成画布 (隐藏在屏幕外) -->
<canvas type="2d" id="posterCanvas" style="width: 750px; height: 1334px; position: absolute; left: -9999px;"></canvas>
<!-- 底部操作提示 -->
<view class="footer" wx:if="{{!isLoading}}">
<view class="hint">左滑无感 · 右滑收藏</view>
</view>
<!-- 空状态 -->
<view class="empty-state" wx:if="{{!isLoading && nameList.length === 0}}">
<text>暂无灵感,请重试</text>
<button bindtap="onLoad" style="margin-top: 20px; font-weight: 300;">重新感悟</button>
</view>
<!-- 收藏锦囊 -->
<view class="collection-bag" bindtap="toggleCollectionView">
<text class="bag-icon">囊</text>
<view class="collection-count">{{collectedNames.length}}</view>
</view>
<!-- 收藏列表浮层 -->
<view class="collection-overlay {{showCollection ? 'visible' : ''}}" bindtap="toggleCollectionView">
<view class="collection-content" catchtap>
<view class="collection-title">见素锦囊</view>
<scroll-view scroll-y class="collection-scroll">
<view class="collection-item" wx:for="{{collectedNames}}" wx:key="name">
<view>
<view class="item-name">{{item.name}}</view>
<view class="item-origin">{{item.origin}}</view>
</view>
<view class="delete-btn" data-name="{{item.name}}" catchtap="onDeleteCollected">×</view>
</view>
<view wx:if="{{collectedNames.length === 0}}" class="collection-empty">
锦囊空空,静待灵感。
</view>
</scroll-view>
</view>
</view>
</view>

View File

@@ -0,0 +1,311 @@
page {
background-color: #FFFFFF;
height: 100%;
}
.container {
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
overflow: hidden;
}
.card-stack {
position: relative;
width: 70vw;
height: 100vw;
z-index: 1;
}
.card-container {
position: absolute;
width: 100%;
height: 100%;
}
.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-face {
position: absolute;
width: 100%;
height: 100%;
backface-visibility: hidden;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60rpx;
box-sizing: border-box;
border-radius: 8rpx;
}
.front {
transform: rotateY(0deg);
}
.back {
transform: rotateY(180deg);
}
.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;
color: #2D2D2D;
margin-bottom: 60rpx;
font-weight: 300;
letter-spacing: 10rpx;
text-align: center;
}
.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;
}
.desc-container {
flex: 1;
display: flex;
align-items: center;
}
.back .desc {
font-size: 28rpx;
color: #4A4A4A;
line-height: 2.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;
}
.tone-tag, .score-tag {
font-size: 18rpx;
color: #D0D0D0;
letter-spacing: 2rpx;
margin-bottom: 10rpx;
}
.save-btn {
margin-top: 20rpx;
display: flex;
align-items: center;
font-size: 16rpx;
color: #A0A0A0;
border: 1rpx solid #F0F0F0;
padding: 8rpx 20rpx;
border-radius: 40rpx;
letter-spacing: 2rpx;
transition: all 0.2s;
}
.save-btn:active {
background: #FAFAFA;
transform: scale(0.95);
}
.save-icon {
margin-right: 8rpx;
font-size: 14rpx;
}
/* 退出动画 */
.card-exit-left {
transform: translate3d(-150%, 0, 0) rotate(-20deg) !important;
opacity: 0 !important;
transition: all 0.5s ease-in !important;
}
.card-exit-right {
transform: translate3d(150%, 0, 0) rotate(20deg) !important;
opacity: 0 !important;
transition: all 0.5s ease-in !important;
}
.footer {
margin-top: 100rpx;
}
.hint {
font-size: 22rpx;
color: #D0D0D0;
letter-spacing: 4rpx;
}
/* 收藏锦囊 */
.collection-bag {
position: fixed;
right: 60rpx;
bottom: 120rpx;
width: 100rpx;
height: 100rpx;
background-color: #FFFFFF;
border-radius: 50%;
box-shadow: 0 10rpx 40rpx rgba(0,0,0,0.08);
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
z-index: 10;
transition: transform 0.2s;
}
.collection-bag:active {
transform: scale(0.9);
}
.bag-icon {
font-family: "Noto Serif SC", serif;
font-size: 40rpx;
color: #4A4A4A;
}
.collection-count {
position: absolute;
top: 0;
right: 0;
background-color: #B22222;
color: white;
font-size: 18rpx;
border-radius: 50%;
padding: 4rpx 10rpx;
min-width: 18rpx;
text-align: center;
}
/* 收藏列表 */
.collection-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(255, 255, 255, 0.5);
backdrop-filter: blur(20px);
z-index: 20;
opacity: 0;
visibility: hidden;
transition: all 0.4s cubic-bezier(0.165, 0.84, 0.44, 1);
}
.collection-overlay.visible {
opacity: 1;
visibility: visible;
}
.collection-content {
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);
transform: translateY(100%);
transition: transform 0.4s cubic-bezier(0.165, 0.84, 0.44, 1);
}
.collection-overlay.visible .collection-content {
transform: translateY(0);
}
.collection-title {
text-align: center;
padding: 40rpx 0;
font-size: 24rpx;
color: #A0A0A0;
letter-spacing: 4rpx;
border-bottom: 1rpx solid #F0F0F0;
}
.collection-scroll {
height: calc(100% - 110rpx);
}
.collection-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 30rpx 60rpx;
border-bottom: 1rpx solid #F0F0F0;
}
.item-name {
font-family: "Noto Serif SC", serif;
font-size: 32rpx;
color: #2D2D2D;
margin-bottom: 4rpx;
}
.item-origin {
font-size: 20rpx;
color: #A0A0A0;
font-weight: 200;
}
.delete-btn {
font-size: 40rpx;
color: #E0E0E0;
font-weight: 200;
padding: 10rpx 20rpx;
border-radius: 50%;
transition: all 0.2s;
}
.delete-btn:active {
background-color: #F0F0F0;
color: #B22222;
}
.collection-empty {
text-align: center;
padding: 100rpx;
font-size: 24rpx;
color: #E0E0E0;
}

View File

@@ -0,0 +1,28 @@
{
"appid": "wxf634a69e89290cc7",
"compileType": "miniprogram",
"libVersion": "3.15.2",
"packOptions": {
"ignore": [],
"include": []
},
"setting": {
"coverView": true,
"es6": true,
"postcss": true,
"minified": true,
"enhance": true,
"showShadowRootInWxmlPanel": true,
"packNpmRelationList": [],
"babelSetting": {
"ignore": [],
"disablePlugins": [],
"outputPath": ""
}
},
"condition": {},
"editorSetting": {
"tabIndent": "insertSpaces",
"tabSize": 2
}
}

View File

@@ -0,0 +1,8 @@
{
"description": "项目私有配置文件。此文件中的内容将覆盖 project.config.json 中的相同字段。项目的改动优先同步到此文件中。详见文档https://developers.weixin.qq.com/miniprogram/dev/devtools/projectconfig.html",
"projectname": "miniprogram",
"setting": {
"compileHotReLoad": true,
"urlCheck": false
}
}

76
muti-mode-feature.md Normal file
View File

@@ -0,0 +1,76 @@
# 「见素」多模式起名功能开发计划
## 📝 总体目标
实现多模式起名系统,涵盖后端 Prompt 引擎增强、前端极简输入页重构、以及卡片页的交互适配。
---
## 🏗️ 第一阶段:后端 API 增强 (Prompt 引擎)
### 1.1 模型层检查 (`NameCard.java`)
* **目标**:确认字段兼容性。
* **状态**:无需修改。确保 `name`, `origin`, `description`, `score`, `tone` 字段能承载 AI 返回的结构化数据。
### 1.2 逻辑层增强 (`MiniMaxService.java`)
* **动作**
1. **方法重载**:修改 `generateNames` 签名,新增 `mode``surname` 参数。
2. **模板管理**:维护 `Map<String, String> promptTemplates`,预置 `baby` (宝宝), `persona` (人设), `classic` (拾遗) 的 Prompt。
3. **动态构建**:根据 `mode` 选取模板,注入 `keyword``surname` 生成最终 Prompt。
4. **后门保留**:维持 `test` 关键字逻辑,便于快速联调前端 UI。
### 1.3 接口层更新 (`NamingController.java`)
* **动作**:更新 `@GetMapping` 接口,通过 `@RequestParam` 接收 `mode``surname`(均为 `required = false`),并透传至 Service 层。
---
## 🎨 第二阶段:前端重设计 - 输入页 (`home`)
### 2.1 结构重构 (`home.wxml`)
* **新增模式选择**`<view class="mode-selector">`,通过 `wx:for` 渲染模式标签。
* **动画容器**:预留 `loading-ink` 容器,用于存放“墨水晕开”动画。
* **表单保持**:保留 `keyword` 输入框与 `generate-btn`
### 2.2 视觉优化 (`home.wxss`)
* **极简风格**
* `.mode-item`:默认灰色,选中态 (`.active`) 加黑并显示下划点。
* `.generate-btn`:极致方正,无圆角,高字间距。
* **动效实现**:定义 `@keyframes ink-spread`,模拟水墨滴入的径向扩张效果。
### 2.3 交互逻辑 (`home.js`)
* **状态管理**:定义 `activeMode` (默认 `baby`)`isGenerating` (控制动画)。
* **路由跳转**`startGenerate` 组装 URL 参数,携带 `mode` 跳转至 `index` 页面。
---
## 🃏 第三阶段:前端适配 - 卡片页 (`index`)
### 3.1 界面调整 (`index.wxml`)
* **顶部标题**:显示 `见素 · {{modeName}} · {{keyword}}`
* **操作栏**:底部新增 `action-bar`,包含 `×` (Dislike), `♥` (Like) 两个圆形按钮及一个“返回”图标。
* **移除冗余**:删除旧版的滑动提示文字。
### 3.2 样式适配 (`index.wxss`)
* **散文诗排版**:增加 `.desc``line-height`,设置 `white-space: pre-wrap` 保持 AI 返回的换行格式。
* **圆形按钮**:设计极简线条风格的圆形交互元素。
### 3.3 数据联调 (`index.js`)
* **参数接收**:在 `onLoad` 中解析 URL 参数,存储当前模式并调用后端接口。
* **导航控制**:实现 `onBack` 返回首页,确保返回后重置首页的加载状态。
---
## 🧩 第四阶段:全局规范与清理
### 4.1 全局变量 (`app.wxss`)
* **变量定义**
```css
--jiansu-bg: #ffffff;
--jiansu-text: #2c2c2c;
--jiansu-accent: #8e8e8e;
```
* **字体引入**:全局应用 `Noto Serif SC` (思源宋体),强化品牌质感。
### 4.2 冗余清理
* **动作**:物理删除 `miniprogram/pages/index/index.wxs`。由于逻辑已迁移至 JS 动画处理,废弃 WXS 以保持项目整洁。
---

121
step2.md Normal file
View File

@@ -0,0 +1,121 @@
一、 后端设计:多模式分流架构
核心逻辑是根据前端传来的 mode动态拼接不同的 System Prompt。
1. API 接口定义
Endpoint: /api/names/generate
Method: POST
Params:
keyword: 用户输入的意象(如:清冷、自由)。
mode: 模式枚举 (baby | persona | classic)。
surname: 可选,用户姓氏(用于声韵分析)。
2. 核心 Prompt 引擎逻辑
在后端代码中通过一个 switch 或 Map 来管理 Prompt 模版:
JavaScript
const prompts = {
baby: "你现在是【见素·婴儿起名】导师。风格:端庄、温润。避开所有网红叠字。要求名字具备经得起时间考验的厚重感。",
persona: "你现在是【见素·人设起名】导师。风格:冷冽、张力、赛博。意象可以偏向孤傲、破败或极致的浪漫。",
classic: "你现在是【见素·诗词拾遗】导师。风格:考古学美感。专门寻找《全唐诗》中极少被现代人使用的冷门生僻字词。"
};
// 拼接最终发送给 minimax 的消息
const finalPrompt = `${prompts[mode]} \n
当前关键词:${keyword}。${surname ? '姓氏:' + surname : ''} \n
输出格式:严格 JSON 数组...`;
二、 小程序页面设计:从“输入”到“感悟”
我们要把传统的“表单填写”变成一种**“交互仪式”**。
1. 首页 (Input Page):极简选择
视觉: 全白背景,中心是一个极细的输入框。
模式切换: 底部放置三个极简的标签Tag选中的标签下方有一个小黑点。
[ 宝宝 ] [ 人设 ] [ 拾遗 ]
交互: * 用户输入关键词后,点击下方的“求名”按钮。
按钮点击后,文字变为“见素感悟中...”并触发一个类似墨水滴入水中慢慢晕开的动画CSS 实现)。
2. 卡片页 (Swipe Page):氛围渲染
头部: 展示当前的模式和关键词(如:见素 · 宝宝 · 清冷)。
卡片: * 正面: 巨大的宋体名字,垂直居中。右下方有一个淡淡的灰色印章。
反面: 解读文字不再是段落,而是散文诗般的排版(增加行间距)。
操作栏: 底部只有两个圆圈按钮(× 和 ♥),以及一个“返回”小图标。
三、 前端页面完整代码建议 (index.wxml 增强版)
在你的基础上,增加了模式选择和视觉氛围感。
XML
<wxs module="handler" src="./handler.wxs"></wxs>
<view class="container">
<view class="mode-selector" wx:if="{{!isSwiping}}">
<view
wx:for="{{modes}}"
wx:key="id"
class="mode-item {{activeMode === item.id ? 'active' : ''}}"
bindtap="selectMode"
data-id="{{item.id}}"
>{{item.name}}</view>
</view>
<view class="input-section" wx:if="{{!isSwiping}}">
<input
class="main-input"
placeholder="输入意象,如:孤鸿、冷月"
bindinput="onInput"
placeholder-style="color: #ccc;"
/>
<button class="submit-btn" bindtap="startGenerate">求名</button>
</view>
<view class="card-stack" wx:if="{{isSwiping && nameList.length > 0}}">
<view
class="card {{draggingClass}} {{isFlipped ? 'flipped' : ''}}"
wx:key="{{cardKey}}"
bindtap="onFlip"
bindtouchstart="{{handler.touchstart}}"
bindtouchmove="{{handler.touchmove}}"
bindtouchend="{{handler.touchend}}"
>
</view>
<view class="hint">左滑无感 · 右滑收录</view>
</view>
</view>
四、 关键 CSS 变量 (app.wxss)
为了保证“见素”的 Vibe建议在全局定义这些变量
CSS
page {
--jiansu-bg: #ffffff;
--jiansu-text: #2c2c2c;
--jiansu-accent: #8e8e8e;
--jiansu-font: "Noto Serif SC", serif; /* 记得引入思源宋体 */
background-color: var(--jiansu-bg);
color: var(--jiansu-text);
}
.submit-btn {
background: var(--jiansu-text);
color: #fff;
border-radius: 0; /* 极致方正 */
font-weight: 300;
letter-spacing: 4rpx;
margin-top: 60rpx;
}
五、 后期优化的建议
后端缓存: 同一个模式+同一能个关键词,缓存 24 小时。因为 Gemini API 出来的结果会有随机性,但好的意象是有限的。
姓氏平仄: 如果是“宝宝起名”模式,一定要让用户填姓氏。然后在后端加入逻辑:“如果姓氏是平声,第一个名优先选仄声”。
分享海报: 名字一定要足够大,背景一定要留/白。这是「见素」的核心竞争力。