commit c4db35b183a54e28732ba45bb6b01ce26c025158 Author: 王鹏 Date: Thu Apr 16 11:25:29 2026 +0800 Initial commit diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..d394e3e --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,7 @@ +{ + "permissions": { + "allow": [ + "Read(//c/Users/南音/.m2/repository/com/squareup/okhttp3/okhttp/**)" + ] + } +} diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md new file mode 100644 index 0000000..eb39b8c --- /dev/null +++ b/DEVELOPMENT.md @@ -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 生成,按次收费。 \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..cbd78b7 --- /dev/null +++ b/README.md @@ -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 海报生成功能。 \ No newline at end of file diff --git a/backend/.idea/.gitignore b/backend/.idea/.gitignore new file mode 100644 index 0000000..73f69e0 --- /dev/null +++ b/backend/.idea/.gitignore @@ -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/ diff --git a/backend/.idea/compiler.xml b/backend/.idea/compiler.xml new file mode 100644 index 0000000..dc6e96d --- /dev/null +++ b/backend/.idea/compiler.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/backend/.idea/encodings.xml b/backend/.idea/encodings.xml new file mode 100644 index 0000000..63e9001 --- /dev/null +++ b/backend/.idea/encodings.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/backend/.idea/jarRepositories.xml b/backend/.idea/jarRepositories.xml new file mode 100644 index 0000000..b074d70 --- /dev/null +++ b/backend/.idea/jarRepositories.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/backend/.idea/misc.xml b/backend/.idea/misc.xml new file mode 100644 index 0000000..d5cd614 --- /dev/null +++ b/backend/.idea/misc.xml @@ -0,0 +1,12 @@ + + + + + + + + \ No newline at end of file diff --git a/backend/.idea/runConfigurations.xml b/backend/.idea/runConfigurations.xml new file mode 100644 index 0000000..797acea --- /dev/null +++ b/backend/.idea/runConfigurations.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/backend/pom.xml b/backend/pom.xml new file mode 100644 index 0000000..86f810e --- /dev/null +++ b/backend/pom.xml @@ -0,0 +1,67 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 2.7.18 + + + com.jiansu + naming + 0.0.1-SNAPSHOT + jiansu-naming + JianSu Naming AI Backend + + 1.8 + + + + org.springframework.boot + spring-boot-starter-web + + + org.projectlombok + lombok + true + + + com.alibaba + fastjson + 1.2.83 + + + com.squareup.okhttp3 + okhttp + 4.12.0 + + + com.belerweb + pinyin4j + 2.5.1 + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + + \ No newline at end of file diff --git a/backend/src/main/java/com/jiansu/naming/NamingApplication.java b/backend/src/main/java/com/jiansu/naming/NamingApplication.java new file mode 100644 index 0000000..854b74c --- /dev/null +++ b/backend/src/main/java/com/jiansu/naming/NamingApplication.java @@ -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); + } +} \ No newline at end of file diff --git a/backend/src/main/java/com/jiansu/naming/controller/NamingController.java b/backend/src/main/java/com/jiansu/naming/controller/NamingController.java new file mode 100644 index 0000000..a898f8b --- /dev/null +++ b/backend/src/main/java/com/jiansu/naming/controller/NamingController.java @@ -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 generate(@RequestParam(defaultValue = "清冷") String keyword) { + return miniMaxService.generateNames(keyword); + } +} \ No newline at end of file diff --git a/backend/src/main/java/com/jiansu/naming/model/NameCard.java b/backend/src/main/java/com/jiansu/naming/model/NameCard.java new file mode 100644 index 0000000..a267ad8 --- /dev/null +++ b/backend/src/main/java/com/jiansu/naming/model/NameCard.java @@ -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; // 平仄分析 (如:平仄) +} \ No newline at end of file diff --git a/backend/src/main/java/com/jiansu/naming/service/MiniMaxService.java b/backend/src/main/java/com/jiansu/naming/service/MiniMaxService.java new file mode 100644 index 0000000..a36f634 --- /dev/null +++ b/backend/src/main/java/com/jiansu/naming/service/MiniMaxService.java @@ -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 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 parseMiniMaxResponse(String responseBody) { + List 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 createTestData() { + List 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; + } +} \ No newline at end of file diff --git a/backend/src/main/java/com/jiansu/naming/service/ToneAnalysisService.java b/backend/src/main/java/com/jiansu/naming/service/ToneAnalysisService.java new file mode 100644 index 0000000..aa499c8 --- /dev/null +++ b/backend/src/main/java/com/jiansu/naming/service/ToneAnalysisService.java @@ -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 tones = new ArrayList<>(); + List 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; + } +} \ No newline at end of file diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml new file mode 100644 index 0000000..1e201b2 --- /dev/null +++ b/backend/src/main/resources/application.yml @@ -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 diff --git a/backend/target/classes/application.yml b/backend/target/classes/application.yml new file mode 100644 index 0000000..1e201b2 --- /dev/null +++ b/backend/target/classes/application.yml @@ -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 diff --git a/backend/target/classes/com/jiansu/naming/NamingApplication.class b/backend/target/classes/com/jiansu/naming/NamingApplication.class new file mode 100644 index 0000000..c062b62 Binary files /dev/null and b/backend/target/classes/com/jiansu/naming/NamingApplication.class differ diff --git a/backend/target/classes/com/jiansu/naming/controller/NamingController.class b/backend/target/classes/com/jiansu/naming/controller/NamingController.class new file mode 100644 index 0000000..7a9e6ab Binary files /dev/null and b/backend/target/classes/com/jiansu/naming/controller/NamingController.class differ diff --git a/backend/target/classes/com/jiansu/naming/model/NameCard$NameCardBuilder.class b/backend/target/classes/com/jiansu/naming/model/NameCard$NameCardBuilder.class new file mode 100644 index 0000000..5ce0f94 Binary files /dev/null and b/backend/target/classes/com/jiansu/naming/model/NameCard$NameCardBuilder.class differ diff --git a/backend/target/classes/com/jiansu/naming/model/NameCard.class b/backend/target/classes/com/jiansu/naming/model/NameCard.class new file mode 100644 index 0000000..ae998b0 Binary files /dev/null and b/backend/target/classes/com/jiansu/naming/model/NameCard.class differ diff --git a/backend/target/classes/com/jiansu/naming/service/MiniMaxService.class b/backend/target/classes/com/jiansu/naming/service/MiniMaxService.class new file mode 100644 index 0000000..04d4c86 Binary files /dev/null and b/backend/target/classes/com/jiansu/naming/service/MiniMaxService.class differ diff --git a/backend/target/classes/com/jiansu/naming/service/ToneAnalysisService$ToneResult.class b/backend/target/classes/com/jiansu/naming/service/ToneAnalysisService$ToneResult.class new file mode 100644 index 0000000..d4d62e8 Binary files /dev/null and b/backend/target/classes/com/jiansu/naming/service/ToneAnalysisService$ToneResult.class differ diff --git a/backend/target/classes/com/jiansu/naming/service/ToneAnalysisService.class b/backend/target/classes/com/jiansu/naming/service/ToneAnalysisService.class new file mode 100644 index 0000000..ea61f1b Binary files /dev/null and b/backend/target/classes/com/jiansu/naming/service/ToneAnalysisService.class differ diff --git a/backend/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst b/backend/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst new file mode 100644 index 0000000..4cab702 --- /dev/null +++ b/backend/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst @@ -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 diff --git a/backend/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst b/backend/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst new file mode 100644 index 0000000..0b533ee --- /dev/null +++ b/backend/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst @@ -0,0 +1,4 @@ +C:\Users\\Desktop\JianSu-Naming\backend\src\main\java\com\jiansu\naming\controller\NamingController.java +C:\Users\\Desktop\JianSu-Naming\backend\src\main\java\com\jiansu\naming\model\NameCard.java +C:\Users\\Desktop\JianSu-Naming\backend\src\main\java\com\jiansu\naming\service\GeminiService.java +C:\Users\\Desktop\JianSu-Naming\backend\src\main\java\com\jiansu\naming\NamingApplication.java diff --git a/miniprogram/app.js b/miniprogram/app.js new file mode 100644 index 0000000..d78d8a6 --- /dev/null +++ b/miniprogram/app.js @@ -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: {} + } +}); \ No newline at end of file diff --git a/miniprogram/app.json b/miniprogram/app.json new file mode 100644 index 0000000..f6c9d96 --- /dev/null +++ b/miniprogram/app.json @@ -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" +} \ No newline at end of file diff --git a/miniprogram/pages/home/home.js b/miniprogram/pages/home/home.js new file mode 100644 index 0000000..d2e359a --- /dev/null +++ b/miniprogram/pages/home/home.js @@ -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)}` + }); + } +}); \ No newline at end of file diff --git a/miniprogram/pages/home/home.json b/miniprogram/pages/home/home.json new file mode 100644 index 0000000..8835af0 --- /dev/null +++ b/miniprogram/pages/home/home.json @@ -0,0 +1,3 @@ +{ + "usingComponents": {} +} \ No newline at end of file diff --git a/miniprogram/pages/home/home.wxml b/miniprogram/pages/home/home.wxml new file mode 100644 index 0000000..62a6e63 --- /dev/null +++ b/miniprogram/pages/home/home.wxml @@ -0,0 +1,25 @@ + + + 见素 + 回归名字的诗意与留白 + + + + + + + + + + + + + © 见素 · 审美溢价 + + \ No newline at end of file diff --git a/miniprogram/pages/home/home.wxss b/miniprogram/pages/home/home.wxss new file mode 100644 index 0000000..cc9cd13 --- /dev/null +++ b/miniprogram/pages/home/home.wxss @@ -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; } +} \ No newline at end of file diff --git a/miniprogram/pages/index/index.js b/miniprogram/pages/index/index.js new file mode 100644 index 0000000..38597e6 --- /dev/null +++ b/miniprogram/pages/index/index.js @@ -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); + }); + } +}); diff --git a/miniprogram/pages/index/index.wxml b/miniprogram/pages/index/index.wxml new file mode 100644 index 0000000..03131cf --- /dev/null +++ b/miniprogram/pages/index/index.wxml @@ -0,0 +1,74 @@ + + + + + + + {{nameList[currentIndex].name}} + + {{nameList[currentIndex].origin}} + + + + + + {{nameList[currentIndex].description}} + + + 声韵:{{nameList[currentIndex].tone}} + 见素评分:{{nameList[currentIndex].score}} + + + 存为海报 + + + + + + + + + + + + 左滑无感 · 右滑收藏 + + + + + 暂无灵感,请重试 + + + + + + + {{collectedNames.length}} + + + + + + 见素锦囊 + + + + {{item.name}} + {{item.origin}} + + × + + + 锦囊空空,静待灵感。 + + + + + \ No newline at end of file diff --git a/miniprogram/pages/index/index.wxss b/miniprogram/pages/index/index.wxss new file mode 100644 index 0000000..9a2cfce --- /dev/null +++ b/miniprogram/pages/index/index.wxss @@ -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; +} \ No newline at end of file diff --git a/miniprogram/project.config.json b/miniprogram/project.config.json new file mode 100644 index 0000000..96c62da --- /dev/null +++ b/miniprogram/project.config.json @@ -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 + } +} \ No newline at end of file diff --git a/miniprogram/project.private.config.json b/miniprogram/project.private.config.json new file mode 100644 index 0000000..00c8eb9 --- /dev/null +++ b/miniprogram/project.private.config.json @@ -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 + } +} \ No newline at end of file diff --git a/muti-mode-feature.md b/muti-mode-feature.md new file mode 100644 index 0000000..e791095 --- /dev/null +++ b/muti-mode-feature.md @@ -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 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`) +* **新增模式选择**:``,通过 `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 以保持项目整洁。 + +--- diff --git a/step2.md b/step2.md new file mode 100644 index 0000000..bfc492d --- /dev/null +++ b/step2.md @@ -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 + + + + + {{item.name}} + + + + + + + + + + + + 左滑无感 · 右滑收录 + + +四、 关键 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 出来的结果会有随机性,但好的意象是有限的。 + +姓氏平仄: 如果是“宝宝起名”模式,一定要让用户填姓氏。然后在后端加入逻辑:“如果姓氏是平声,第一个名优先选仄声”。 + +分享海报: 名字一定要足够大,背景一定要留/白。这是「见素」的核心竞争力。 \ No newline at end of file