feat: 添加AI智能取名功能

- 实用工具页面添加取名申请入口
- 创建取名申请页面tool-naming.vue
- 后端添加NamingController.java
- 添加AI取名提示模板
This commit is contained in:
H1d3rOne committed 2026-03-11 13:44:11 +08:00
1 parent a65d95c5f7
commit d7e144b877
6 files changed
+644 -3

No files matched your search

@@ -0,0 +1,136 @@
package xuan.controller;
import org.springframework.web.bind.annotation.*;
import xuan.service.HunYuanAiService;
import xuan.service.PromptTemplates;
import java.util.*;
@RestController
@RequestMapping("/api/naming")
public class NamingController {
private final HunYuanAiService aiService = new HunYuanAiService();
@PostMapping("/generate")
public Map<String, Object> generate(@RequestBody NamingRequest request) {
Map<String, Object> result = new HashMap<>();
try {
String surname = request.getSurname() != null ? request.getSurname() : "张";
String gender = request.getGender() != null ? request.getGender() : "男";
String requirements = request.getRequirements() != null ? request.getRequirements() : "寓意美好、朗朗上口";
String aiResponse = aiService.chat(
PromptTemplates.getNamingSystemPrompt(),
PromptTemplates.getNamingUserPrompt(surname, gender, requirements)
);
List<Map<String, String>> names = parseNamesFromAI(aiResponse, surname);
Map<String, Object> data = new HashMap<>();
data.put("surname", surname);
data.put("gender", gender);
data.put("requirements", requirements);
data.put("names", names);
data.put("aiAnalysis", aiResponse);
result.put("success", true);
result.put("data", data);
} catch (Exception e) {
result.put("success", false);
result.put("error", e.getMessage());
}
return result;
}
private List<Map<String, String>> parseNamesFromAI(String aiResponse, String surname) {
List<Map<String, String>> names = new ArrayList<>();
try {
String[] lines = aiResponse.split("\n");
for (String line : lines) {
line = line.trim();
if (line.isEmpty()) continue;
if (line.matches("^[1-3][.、.].*") || line.matches("^[①②③].*")) {
Map<String, String> nameInfo = new HashMap<>();
String name = "";
String meaning = "";
if (line.contains(":") || line.contains(":")) {
String[] parts = line.split("[::]", 2);
if (parts.length >= 2) {
String namePart = parts[0].replaceAll("^[1-3][.、.\\s]+", "")
.replaceAll("^[①②③\\s]+", "")
.trim();
name = surname + namePart;
meaning = parts[1].trim();
}
} else if (line.contains("(") || line.contains("(")) {
int bracketIndex = line.indexOf("(");
if (bracketIndex == -1) bracketIndex = line.indexOf("(");
String namePart = line.substring(0, bracketIndex)
.replaceAll("^[1-3][.、.\\s]+", "")
.replaceAll("^[①②③\\s]+", "")
.trim();
name = surname + namePart;
meaning = line.substring(bracketIndex).trim();
} else {
String namePart = line.replaceAll("^[1-3][.、.\\s]+", "")
.replaceAll("^[①②③\\s]+", "")
.trim();
if (namePart.length() >= 1 && namePart.length() <= 3) {
name = surname + namePart;
meaning = "寓意美好";
}
}
if (!name.isEmpty() && name.length() >= 2) {
nameInfo.put("name", name);
nameInfo.put("meaning", meaning.isEmpty() ? "寓意美好" : meaning);
names.add(nameInfo);
}
if (names.size() >= 3) break;
}
}
if (names.isEmpty()) {
String[] defaultNames = {"伟", "明", "华"};
String[] defaultMeanings = {
"寓意伟大、卓越,前途光明",
"寓意聪明、睿智,前程似锦",
"寓意才华横溢,光耀门楣"
};
for (int i = 0; i < 3; i++) {
Map<String, String> nameInfo = new HashMap<>();
nameInfo.put("name", surname + defaultNames[i]);
nameInfo.put("meaning", defaultMeanings[i]);
names.add(nameInfo);
}
}
} catch (Exception e) {
Map<String, String> nameInfo = new HashMap<>();
nameInfo.put("name", surname + "明");
nameInfo.put("meaning", "寓意聪明、睿智,前程似锦");
names.add(nameInfo);
}
return names;
}
public static class NamingRequest {
private String surname;
private String gender;
private String requirements;
public String getSurname() { return surname; }
public void setSurname(String surname) { this.surname = surname; }
public String getGender() { return gender; }
public void setGender(String gender) { this.gender = gender; }
public String getRequirements() { return requirements; }
public void setRequirements(String requirements) { this.requirements = requirements; }
}
}
@@ -404,4 +404,39 @@ public class PromptTemplates {
return sb.toString();
}
public static String getNamingSystemPrompt() {
return "你是一位精通中华传统文化和姓名学的取名大师。" +
"你擅长根据姓氏、性别和取名要求,结合五行八字、诗词典故、音律美感等因素," +
"为人们取一个寓意美好、朗朗上口、符合传统文化的名字。" +
"你会给出3个最匹配的名字,并详细解释每个名字的含义和寓意。" +
"请用温暖、专业的语气进行取名建议。";
}
public static String getNamingUserPrompt(String surname, String gender, String requirements) {
StringBuilder sb = new StringBuilder();
sb.append("请为以下情况取名:\n\n");
sb.append("姓氏:").append(surname).append("\n");
sb.append("性别:").append(gender).append("\n");
sb.append("取名要求:").append(requirements).append("\n\n");
sb.append("请给出3个最匹配的名字,每个名字需要包含:\n");
sb.append("1. 完整姓名(姓氏+名字)\n");
sb.append("2. 名字的含义和寓意\n");
sb.append("3. 名字的文化出处或典故(如有)\n");
sb.append("4. 名字的音律特点\n\n");
sb.append("请按以下格式输出:\n");
sb.append("1. 【姓名】含义:xxx\n");
sb.append("2. 【姓名】含义:xxx\n");
sb.append("3. 【姓名】含义:xxx\n\n");
sb.append("注意:\n");
sb.append("- 名字要符合性别特点\n");
sb.append("- 名字要有美好的寓意\n");
sb.append("- 名字要朗朗上口,避免生僻字\n");
sb.append("- 名字要有文化底蕴\n");
return sb.toString();
}
}
+9 -1
View File
@@ -253,7 +253,15 @@
"path": "pages/tool-xingzuo/tool-xingzuo",
"style": {
"navigationBarTitleText": "星座查询",
"navigationBarBackgroundColor": "#8B5CF6",
"navigationBarBackgroundColor": "#F59E0B",
"navigationBarTextStyle": "white"
}
},
{
"path": "pages/tool-naming/tool-naming",
"style": {
"navigationBarTitleText": "AI智能取名",
"navigationBarBackgroundColor": "#A855F7",
"navigationBarTextStyle": "white"
}
},
@@ -0,0 +1,432 @@
<template>
<view class="container">
<view class="header-card">
<view class="header-icon">
<text class="icon-text">✨</text>
</view>
<text class="header-title">AI智能取名</text>
<text class="header-subtitle">根据姓氏和要求,为您推荐最佳名字</text>
</view>
<view class="form-card">
<view class="form-section">
<view class="section-header">
<view class="section-dot"></view>
<text class="section-title">取名信息</text>
</view>
<view class="form-item">
<view class="form-label-wrapper">
<text class="form-label">姓氏</text>
<text class="form-required">*</text>
</view>
<input class="form-input" v-model="surname" placeholder="请输入姓氏(如:张、李、王)" maxlength="2" />
</view>
<view class="form-item">
<view class="form-label-wrapper">
<text class="form-label">性别</text>
<text class="form-required">*</text>
</view>
<view class="gender-options">
<view class="gender-option" :class="{ active: gender === '男' }" @click="gender = '男'">
<text class="gender-icon">👦</text>
<text class="gender-text">男孩</text>
</view>
<view class="gender-option" :class="{ active: gender === '女' }" @click="gender = '女'">
<text class="gender-icon">👧</text>
<text class="gender-text">女孩</text>
</view>
</view>
</view>
<view class="form-item">
<view class="form-label-wrapper">
<text class="form-label">取名要求</text>
</view>
<textarea class="form-textarea" v-model="requirements" placeholder="请输入取名要求(如:寓意美好、朗朗上口、有文化底蕴等)" maxlength="200" />
</view>
</view>
</view>
<view class="submit-card">
<button class="submit-btn" :disabled="!canSubmit || loading" @click="generateNames">
<text class="submit-btn-text">{{ loading ? '取名中...' : '✨ 开始取名' }}</text>
</button>
</view>
<view class="result-card" v-if="names.length > 0">
<view class="card-header">
<view class="card-dot gold"></view>
<text class="card-title">🎯 推荐名字</text>
</view>
<view class="name-list">
<view class="name-item" v-for="(item, index) in names" :key="index">
<view class="name-header">
<text class="name-index">NO.{{ index + 1 }}</text>
<text class="name-text">{{ item.name }}</text>
</view>
<view class="name-meaning">
<text class="meaning-label">寓意:</text>
<text class="meaning-text">{{ item.meaning }}</text>
</view>
</view>
</view>
</view>
</view>
</template>
<script>
import api from '@/utils/api.js'
export default {
data() {
return {
surname: '',
gender: '男',
requirements: '',
names: [],
loading: false
}
},
computed: {
canSubmit() {
return this.surname.trim() && this.gender
}
},
methods: {
async generateNames() {
if (!this.canSubmit) {
uni.showToast({
title: '请填写姓氏',
icon: 'none'
})
return
}
this.loading = true
uni.showLoading({
title: 'AI取名中...',
mask: true
})
try {
const res = await api.namingApi.generate({
surname: this.surname,
gender: this.gender,
requirements: this.requirements || '寓意美好、朗朗上口'
})
uni.hideLoading()
if (res.success && res.data && res.data.names) {
this.names = res.data.names
} else {
uni.showToast({
title: res.error || '取名失败,请重试',
icon: 'none'
})
}
} catch (e) {
uni.hideLoading()
uni.showToast({
title: '取名失败:' + (e.message || '未知错误'),
icon: 'none'
})
} finally {
this.loading = false
}
}
}
}
</script>
<style>
page {
background: linear-gradient(180deg, #F3E8FF 0%, #E9D5FF 50%, #DDD6FE 100%);
min-height: 100vh;
}
.container {
padding: 20rpx;
padding-bottom: 160rpx;
}
.header-card {
background: linear-gradient(135deg, #A855F7 0%, #7C3AED 100%);
border-radius: 32rpx;
padding: 48rpx 32rpx;
text-align: center;
margin-bottom: 24rpx;
box-shadow: 0 16rpx 48rpx rgba(168, 85, 247, 0.25);
}
.header-icon {
width: 96rpx;
height: 96rpx;
background: rgba(255, 255, 255, 0.2);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 24rpx;
}
.icon-text {
font-size: 48rpx;
}
.header-title {
font-size: 40rpx;
color: #ffffff;
font-weight: bold;
display: block;
margin-bottom: 12rpx;
}
.header-subtitle {
font-size: 26rpx;
color: rgba(255, 255, 255, 0.8);
}
.form-card {
background: rgba(255, 255, 255, 0.95);
border-radius: 24rpx;
padding: 32rpx;
margin-bottom: 24rpx;
box-shadow: 0 8rpx 32rpx rgba(168, 85, 247, 0.08);
}
.form-section {
margin-bottom: 24rpx;
}
.section-header {
display: flex;
align-items: center;
margin-bottom: 24rpx;
}
.section-dot {
width: 12rpx;
height: 12rpx;
background: linear-gradient(135deg, #A855F7 0%, #7C3AED 100%);
border-radius: 50%;
margin-right: 16rpx;
}
.section-title {
font-size: 30rpx;
font-weight: 600;
color: #333;
}
.form-item {
margin-bottom: 28rpx;
}
.form-label-wrapper {
display: flex;
align-items: center;
margin-bottom: 16rpx;
}
.form-label {
font-size: 28rpx;
color: #333;
font-weight: 500;
}
.form-required {
color: #EF4444;
margin-left: 8rpx;
}
.form-input {
width: 100%;
height: 88rpx;
background: #F9FAFB;
border: 2rpx solid #E5E7EB;
border-radius: 16rpx;
padding: 0 24rpx;
font-size: 28rpx;
color: #333;
box-sizing: border-box;
}
.form-input:focus {
border-color: #A855F7;
background: #fff;
}
.gender-options {
display: flex;
gap: 24rpx;
}
.gender-option {
flex: 1;
height: 100rpx;
background: #F9FAFB;
border: 2rpx solid #E5E7EB;
border-radius: 16rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
transition: all 0.3s;
}
.gender-option.active {
background: linear-gradient(135deg, #A855F7 0%, #7C3AED 100%);
border-color: #7C3AED;
}
.gender-icon {
font-size: 36rpx;
margin-bottom: 4rpx;
}
.gender-text {
font-size: 24rpx;
color: #666;
}
.gender-option.active .gender-text {
color: #fff;
}
.form-textarea {
width: 100%;
height: 160rpx;
background: #F9FAFB;
border: 2rpx solid #E5E7EB;
border-radius: 16rpx;
padding: 20rpx 24rpx;
font-size: 28rpx;
color: #333;
box-sizing: border-box;
}
.form-textarea:focus {
border-color: #A855F7;
background: #fff;
}
.submit-card {
margin-top: 32rpx;
}
.submit-btn {
width: 100%;
height: 96rpx;
background: linear-gradient(135deg, #A855F7 0%, #7C3AED 100%);
border-radius: 48rpx;
border: none;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 12rpx 32rpx rgba(168, 85, 247, 0.3);
}
.submit-btn[disabled] {
opacity: 0.5;
}
.submit-btn-text {
font-size: 32rpx;
color: #ffffff;
font-weight: 600;
}
.result-card {
background: rgba(255, 255, 255, 0.95);
border-radius: 24rpx;
padding: 32rpx;
margin-top: 24rpx;
box-shadow: 0 8rpx 32rpx rgba(168, 85, 247, 0.08);
}
.card-header {
display: flex;
align-items: center;
margin-bottom: 24rpx;
padding-bottom: 20rpx;
border-bottom: 2rpx solid #F3E8FF;
}
.card-dot {
width: 16rpx;
height: 16rpx;
background: linear-gradient(135deg, #A855F7 0%, #7C3AED 100%);
border-radius: 50%;
margin-right: 16rpx;
}
.card-dot.gold {
background: linear-gradient(135deg, #FFD700 0%, #FFA500 100%);
box-shadow: 0 0 12rpx rgba(255, 215, 0, 0.5);
}
.card-title {
font-size: 30rpx;
font-weight: 600;
color: #333;
}
.name-list {
display: flex;
flex-direction: column;
gap: 20rpx;
}
.name-item {
background: linear-gradient(135deg, #F9FAFB 0%, #F3E8FF 100%);
border-radius: 16rpx;
padding: 24rpx;
border: 2rpx solid #E9D5FF;
}
.name-header {
display: flex;
align-items: center;
margin-bottom: 16rpx;
}
.name-index {
font-size: 24rpx;
color: #7C3AED;
font-weight: 600;
margin-right: 16rpx;
background: #E9D5FF;
padding: 4rpx 16rpx;
border-radius: 8rpx;
}
.name-text {
font-size: 40rpx;
color: #333;
font-weight: bold;
}
.name-meaning {
display: flex;
align-items: flex-start;
}
.meaning-label {
font-size: 26rpx;
color: #7C3AED;
font-weight: 500;
margin-right: 8rpx;
flex-shrink: 0;
}
.meaning-text {
font-size: 26rpx;
color: #666;
line-height: 1.6;
}
</style>
+21 -2
View File
@@ -40,8 +40,27 @@ export default {
tools: [
{ name: '农历查询', desc: '公历农历互转', icon: '📅', color: 'linear-gradient(135deg, #F97316 0%, #EA580C 100%)', path: '/pages/tool-lunar/tool-lunar' },
{ name: '干支查询', desc: '年月日时干支', icon: '🕐', color: 'linear-gradient(135deg, #FB923C 0%, #F97316 100%)', path: '/pages/tool-ganzhi/tool-ganzhi' },
{ name: '生肖查询', desc: '十二生肖属性', icon: '🐉', color: 'linear-gradient(135deg, #FDBA74 0%, #FB923C 100%)', path: '/pages/tool-shengxiao/tool-shengxiao' },
{ name: '星座查询', desc: '十二星座运势', icon: '⭐', color: 'linear-gradient(135deg, #F59E0B 0%, #D97706 100%)', path: '/pages/tool-xingzuo/tool-xingzuo' },
{
name: '生肖查询',
desc: '十二生肖属性',
icon: '🐉',
color: 'linear-gradient(135deg, #FDBA74 0%, #FB923C 100%)',
path: '/pages/tool-shengxiao/tool-shengxiao'
},
{
name: '取名申请',
desc: 'AI智能取名',
icon: '✨',
color: 'linear-gradient(135deg, #A855F7 0%, #7C3AED 100%)',
path: '/pages/tool-naming/tool-naming'
},
{
name: '星座查询',
desc: '十二星座运势',
icon: '⭐',
color: 'linear-gradient(135deg, #F59E0B 0%, #D97706 100%)',
path: '/pages/tool-xingzuo/tool-xingzuo'
},
{ name: '五行查询', desc: '五行属性分析', icon: '🔥', color: 'linear-gradient(135deg, #EA580C 0%, #C2410C 100%)', path: '/pages/tool-wuxing/tool-wuxing' },
{ name: '吉日查询', desc: '黄道吉日查询', icon: '📆', color: 'linear-gradient(135deg, #FBBF24 0%, #F59E0B 100%)', path: '/pages/tool-jiri/tool-jiri' }
]
+11
View File
@@ -300,6 +300,16 @@ const toolApi = {
}
}
const namingApi = {
generate: (data) => {
return request({
url: '/naming/generate',
method: 'POST',
data: data
})
}
}
const coinConsumeApi = {
save: (data) => {
return request({
@@ -434,6 +444,7 @@ export default {
recordApi,
shareApi,
toolApi,
namingApi,
coinConsumeApi,
userApi,
getBaseUrl: () => BASE_URL