From c4db35b183a54e28732ba45bb6b01ce26c025158 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E9=B9=8F?= Date: Thu, 16 Apr 2026 11:25:29 +0800 Subject: [PATCH] Initial commit --- .claude/settings.json | 7 + DEVELOPMENT.md | 66 ++++ README.md | 65 ++++ backend/.idea/.gitignore | 8 + backend/.idea/compiler.xml | 19 ++ backend/.idea/encodings.xml | 6 + backend/.idea/jarRepositories.xml | 20 ++ backend/.idea/misc.xml | 12 + backend/.idea/runConfigurations.xml | 10 + backend/pom.xml | 67 ++++ .../com/jiansu/naming/NamingApplication.java | 11 + .../naming/controller/NamingController.java | 22 ++ .../com/jiansu/naming/model/NameCard.java | 18 + .../jiansu/naming/service/MiniMaxService.java | 184 +++++++++++ .../naming/service/ToneAnalysisService.java | 102 ++++++ backend/src/main/resources/application.yml | 11 + backend/target/classes/application.yml | 11 + .../com/jiansu/naming/NamingApplication.class | Bin 0 -> 741 bytes .../naming/controller/NamingController.class | Bin 0 -> 1331 bytes .../model/NameCard$NameCardBuilder.class | Bin 0 -> 1855 bytes .../com/jiansu/naming/model/NameCard.class | Bin 0 -> 3773 bytes .../naming/service/MiniMaxService.class | Bin 0 -> 9623 bytes .../ToneAnalysisService$ToneResult.class | Bin 0 -> 528 bytes .../naming/service/ToneAnalysisService.class | Bin 0 -> 3756 bytes .../compile/default-compile/createdFiles.lst | 5 + .../compile/default-compile/inputFiles.lst | 4 + miniprogram/app.js | 26 ++ miniprogram/app.json | 15 + miniprogram/pages/home/home.js | 25 ++ miniprogram/pages/home/home.json | 3 + miniprogram/pages/home/home.wxml | 25 ++ miniprogram/pages/home/home.wxss | 123 +++++++ miniprogram/pages/index/index.js | 242 ++++++++++++++ miniprogram/pages/index/index.wxml | 74 +++++ miniprogram/pages/index/index.wxss | 311 ++++++++++++++++++ miniprogram/project.config.json | 28 ++ miniprogram/project.private.config.json | 8 + muti-mode-feature.md | 76 +++++ step2.md | 121 +++++++ 39 files changed, 1725 insertions(+) create mode 100644 .claude/settings.json create mode 100644 DEVELOPMENT.md create mode 100644 README.md create mode 100644 backend/.idea/.gitignore create mode 100644 backend/.idea/compiler.xml create mode 100644 backend/.idea/encodings.xml create mode 100644 backend/.idea/jarRepositories.xml create mode 100644 backend/.idea/misc.xml create mode 100644 backend/.idea/runConfigurations.xml create mode 100644 backend/pom.xml create mode 100644 backend/src/main/java/com/jiansu/naming/NamingApplication.java create mode 100644 backend/src/main/java/com/jiansu/naming/controller/NamingController.java create mode 100644 backend/src/main/java/com/jiansu/naming/model/NameCard.java create mode 100644 backend/src/main/java/com/jiansu/naming/service/MiniMaxService.java create mode 100644 backend/src/main/java/com/jiansu/naming/service/ToneAnalysisService.java create mode 100644 backend/src/main/resources/application.yml create mode 100644 backend/target/classes/application.yml create mode 100644 backend/target/classes/com/jiansu/naming/NamingApplication.class create mode 100644 backend/target/classes/com/jiansu/naming/controller/NamingController.class create mode 100644 backend/target/classes/com/jiansu/naming/model/NameCard$NameCardBuilder.class create mode 100644 backend/target/classes/com/jiansu/naming/model/NameCard.class create mode 100644 backend/target/classes/com/jiansu/naming/service/MiniMaxService.class create mode 100644 backend/target/classes/com/jiansu/naming/service/ToneAnalysisService$ToneResult.class create mode 100644 backend/target/classes/com/jiansu/naming/service/ToneAnalysisService.class create mode 100644 backend/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst create mode 100644 backend/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst create mode 100644 miniprogram/app.js create mode 100644 miniprogram/app.json create mode 100644 miniprogram/pages/home/home.js create mode 100644 miniprogram/pages/home/home.json create mode 100644 miniprogram/pages/home/home.wxml create mode 100644 miniprogram/pages/home/home.wxss create mode 100644 miniprogram/pages/index/index.js create mode 100644 miniprogram/pages/index/index.wxml create mode 100644 miniprogram/pages/index/index.wxss create mode 100644 miniprogram/project.config.json create mode 100644 miniprogram/project.private.config.json create mode 100644 muti-mode-feature.md create mode 100644 step2.md 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 0000000000000000000000000000000000000000..c062b6290b5b7ce7af57ee778609e933740e8bfe GIT binary patch literal 741 zcma)4O;6iE5PcgG9Nh2~AZ=PWAXF*jz+MoSR7f-uCzn=Dl^%Lpn@!l%-i_94+P}pK z)I)zje-!GhiIhkUQZ0E#GxPSnd29c9{Pi2aDGowZu@|5f;w4@MXfv$7Q(C!mhLz6o zkfGW)*OFl~Qd(XWSuE`r5vSB?MkW#IP*~;bS+nYHRL<}=N=(KlO6a`cT4YL(`Bj-N zrqfgYY-KOOJS?D;RcAVF`d|E00rOTV22^p%jB>4e4|Z3M z>lFPM1X!ckcmg2A3#!(rUMCWN_^c!Y>di{`0V`kU=R=x!Y(S^x9Cd79lloiq))*Cj Po9!|-u|wI*?gD=Sk9EKY literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..7a9e6ab55a3e7001aa602120021b3c79d7fb559e GIT binary patch literal 1331 zcma)6-A)rh6#k~rme%qk0*XJNh+2v;H)u>i64FGyXu(j^8?UC@p$u$y>g+7Y1L*T; zV#Iji1Na{L7{)VOC{|X(#m?EYbIy0ZZ_b(h{_E>E04v~mr12n!K@{^?Kq-sGEFLn9 zv{j(0;%!Zu1LaAEQpMA4zNbVGMm!L--Qr<)lUMs+6t?5 z5HdWeXhW$TLkK%YwB?~T`@A6unC}SBYIDRzFM)4FRv#)On*^I#qe6C_VW7BBXBaH& zCNUeWs6cK;?S?e31XW`guV_#BbzzkAPd*OXT}9=2f(}my)@a|ChHu8o%DoR%b*Lo+ zX@n)<`C?^H90=|UQeU%9!sU^q^jwdoQr1|t4h18n=ge^Z>1qt zxteMP!bXOQytqJ-wFDJyIt||mkY!<-tLsE$CGo5eWrH_V(ERh5pG#X6osJX4kUkK8 zL`-u0O!{1ZOMB$|di(go^|s}kNQO3sp^Am3+!2v)>)pbcpI_encz?{0uj$Bm@|kjN zo&38w%kFUhHBm__x2a@bZp&ca_C8JRvq_3&W6*f)J@8XdX7!+Vk?$yd20fZY;K#pMW;sI z;j(3$o2^F0biWuB&-Xf2qrPpp7C(p1sJCY|DSDGiD;jQXy=B#FrYoR*ux-;V){SP< zq=3b8)oJMamSHzrI`u(S>kYc7uD3I1+C~UL1(veit>&%+l>BIVdrs}M;Zm8N=~6-G zu)~pD3tO&m%K|gmtJA3`)pG*U1V$*M2bGB}r{z}7lEp(GY)@sLBQ@N{w1zQ^YnWg$ z$zlq(G^8;ukhyBe3{Vspzj$qjw&~7&Z6kb@wP|##8aD7wAYRA>`?e}DRLK0Za^z48 z8Gq~i;F5q+$nc`k8jMAS&hQ!p8B{93cCW3uU9Q&GRCauz;O@P21 zvUJjdw46VI_)dm|49S@9j1xSd1p&B+2lOQ9nuip{$;+fA(n@~z2c-NrGP~2qf{-ws zg8e!L9#OQ+Xi37H=#3fa8Oeb~%wKs7A@97Tj^!0_9 zhcpAmET1C2bAm)_;20WVNu*MPfgNFcD6pe!4+pkB5{nHm(@@XYmhKU Sw@R4D4Cw^bFhbqkNBl3kV{+pF literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..ae998b0caabece472ea0248b83d8ae9824284a79 GIT binary patch literal 3773 zcmb7HZF3V<6n-|{&2ExSnpO%#Km%0lOG;7kg_QRKT5StbL;+E@>Ds2GNl8+Ccg7jV zarBFRa>gGZUvNf8TV}*@&<{A{AK(Y*&k*rBdw0_$U8Q!~bI-l!Ip?0Yd++x5zkmE0 zz+QY{;!+$MoER)j3n>Vh5#kDY!^CWyHjH3&Vg)1^v zEz~U3EiB3Gs)aW#yd~`0f_6;2qoH%sS#U?4>deulVrj;$YOu%3Ww$z7a%wfVra>Q{6vBH^AD^#j&I>#zY((fIH}WyUQH2hQ~djPIq`86|@Lj~C1CjKxXftEwO?jElcoYGmRS`-CeORvur8SReyy27nv+~K4jd5Zk8=HM+Z zOsV)=_itoC96Y0vPzzk`>09e*|5**`3Aa91nK|WD8ByJ>(#05+8oQHk-JJ{m?ky($ z&Bdfih}u5tURiQVG}!_@IX&+d>LdNy3d^yg$rSUcF`M)R2#s$1; zBa7`eb_m%iq!*9a*oEyHviCD+<0KB+cpOjII3V00ayIq|c}&P&A*b-7hE45fvvCEb zf++eV`v+{ii}%?0xvVO}0S&2Kwy6wUuyR?i4L$Jt?DAYz3PXw3Z{s|2JWH$lsYYh0 zoW(`AJfmSKq)2NokJa0!1s|N4HT4Pb*v>0@8}EAq5lJn%h-B9rnP6{Zj=hm-=GcW8 z9z`EthJMbn#LJI$6WX~Oh6Nq2#w<^JqeA!@lf+9QH9SqUp>dMtnv_m$nC$gT?hp6UT(3!K`i99}o=ILF%1iAQ$0XByXK>(a82a_W zUtoO&qvQJEEm#w|ffdBODVg|WjwG)S{f0zFzm2YIu!l1G3K9Xr&Krc-`Dn%vO!PLo zuOXQ+GSMbbGH();%v%{#K$Z_OGgcEQm5&jW%EvP?0mXe#EE8`6b>{6%LO`|; zA{T)4>kO@<>4d6&`$&b{=(jsKgu~2H4}QfFp4td6jK1K?bL3+9l5~uJajdWg&y!2wCv3qBx0gO-%CF((YojVq^DbgF|wNh1KPK@Y`=6xAVFPGP*CZq)C)0nKwKU z`@>VRGDaXu<)fY`+7Pj8lvZcn^h9hJU(57FTf@tHTDj@R)qziN8dD6yz{fa4F2aI* zh?f{^2cO_MoF%6-aT9o%oPl$EVP7E^MIDDFrmM({cw<|zN9 K@-qt$A@&d0N0X)i literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..04d4c869b1c9635d24b3f1633941baaa76b6ff7c GIT binary patch literal 9623 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 UZ1}UJ8}UxXpXJQqUr_4*0qo2bhX4Qo literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..d4d62e8e5d0af20ec0817471d356fa308775d31e GIT binary patch literal 528 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 literal 0 HcmV?d00001 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