import React, { useState, useRef } from 'react'; import Head from 'next/head'; import { snapdom } from '@zumer/snapdom'; // TODO: 从这里引入怪怪的,但是先这样吧! import { type AIGeneratedMagicalGirl } from './api/generate-magical-girl'; import { MainColor } from '../lib/main-color'; import Link from 'next/link'; import { useCooldown } from '../lib/cooldown'; import { quickCheck } from '@/lib/sensitive-word-filter'; import { useRouter } from 'next/router'; interface MagicalGirl { realName: string; name: string; flowerDescription: string; appearance: { height: string; weight: string; hairColor: string; hairStyle: string; eyeColor: string; skinTone: string; wearing: string; specialFeature: string; mainColor: string; // 写法有点诡异,但是能用就行.jpg firstPageColor: string; secondPageColor: string; }; spell: string; level: string; levelEmoji: string; } // 保留原有的 levels 数组和相关函数 const levels = [ { name: '种', emoji: '🌱' }, { name: '芽', emoji: '🍃' }, { name: '叶', emoji: '🌿' }, { name: '蕾', emoji: '🌸' }, { name: '花', emoji: '🌺' }, { name: '宝石权杖', emoji: '💎' } ]; // 定义8套渐变配色方案,与 MainColor 枚举顺序对应 const gradientColors: Record = { [MainColor.Red]: { first: '#ff6b6b', second: '#ee5a6f' }, [MainColor.Orange]: { first: '#ff922b', second: '#ffa94d' }, [MainColor.Cyan]: { first: '#22b8cf', second: '#66d9e8' }, [MainColor.Blue]: { first: '#5c7cfa', second: '#748ffc' }, [MainColor.Purple]: { first: '#9775fa', second: '#b197fc' }, [MainColor.Pink]: { first: '#ff9a9e', second: '#fecfef' }, [MainColor.Yellow]: { first: '#f59f00', second: '#fcc419' }, [MainColor.Green]: { first: '#51cf66', second: '#8ce99a' } }; function seedRandom(str: string): number { let hash = 0; for (let i = 0; i < str.length; i++) { const char = str.charCodeAt(i); hash = ((hash << 5) - hash) + char; hash = hash & hash; } return Math.abs(hash); } function getWeightedRandomFromSeed(array: T[], weights: number[], seed: number, offset: number = 0): T { // 使用种子生成 0-1 之间的伪随机数 const pseudoRandom = ((seed + offset) * 9301 + 49297) % 233280 / 233280.0; // 累计权重 let cumulativeWeight = 0; const cumulativeWeights = weights.map(weight => cumulativeWeight += weight); const totalWeight = cumulativeWeights[cumulativeWeights.length - 1]; // 找到对应的索引 const randomValue = pseudoRandom * totalWeight; const index = cumulativeWeights.findIndex(weight => randomValue <= weight); return array[index >= 0 ? index : 0]; } function checkNameLength(name: string): boolean { return name.length <= 300; } // 使用 API 路由生成魔法少女 async function generateMagicalGirl(inputName: string): Promise { try { const response = await fetch('/api/generate-magical-girl', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ name: inputName }), }); if (!response.ok) { const error = await response.json(); // 处理不同的 HTTP 状态码 if (response.status === 429) { const retryAfter = error.retryAfter || 60; throw new Error(`请求过于频繁!请等待 ${retryAfter} 秒后再试。`); } else if (response.status >= 500) { throw new Error('服务器内部错误,请稍后重试'); } else { throw new Error(error.message || error.error || '生成失败'); } } const aiGenerated: AIGeneratedMagicalGirl = await response.json(); // 等级概率配置: [种, 芽, 叶, 蕾, 花, 宝石权杖] const levelProbabilities = [0.1, 0.2, 0.3, 0.3, 0.07, 0.03]; // 使用加权随机选择生成 level const seed = seedRandom(aiGenerated.flowerName + inputName); const level = getWeightedRandomFromSeed(levels, levelProbabilities, seed, 6); return { realName: inputName, name: aiGenerated.flowerName, flowerDescription: aiGenerated.flowerDescription, appearance: aiGenerated.appearance, spell: aiGenerated.spell, level: level.name, levelEmoji: level.emoji }; } catch (error) { // 处理网络错误和其他异常 if (error instanceof Error) { // 如果已经是我们抛出的错误,直接重新抛出 if (error.message.includes('请求过于频繁') || error.message.includes('服务器内部错误') || error.message.includes('生成失败')) { throw error; } } // 处理网络连接错误 if (error instanceof TypeError && error.message.includes('fetch')) { throw new Error('网络连接失败,请检查网络后重试'); } // 其他未知错误 throw new Error('生成魔法少女时发生未知错误,请重试'); } } export default function Home() { const [inputName, setInputName] = useState(''); const [magicalGirl, setMagicalGirl] = useState(null); const [isGenerating, setIsGenerating] = useState(false); const [showImageModal, setShowImageModal] = useState(false); const [savedImageUrl, setSavedImageUrl] = useState(null); const [error, setError] = useState(null); const resultRef = useRef(null); const { isCooldown, startCooldown, remainingTime } = useCooldown('generateMagicalGirlCooldown', 60000); const router = useRouter(); const handleGenerate = async () => { if (isCooldown) { setError(`请等待 ${remainingTime} 秒后再生成`); return; } if (!inputName.trim()) return; if (!checkNameLength(inputName)) { setError('名字太长啦,你怎么回事!'); return; } // 检查敏感词 const result = await quickCheck(inputName.trim()); if (result.hasSensitiveWords) { router.push('/arrested'); return; } setIsGenerating(true); setError(null); // 清除之前的错误 try { const result = await generateMagicalGirl(inputName.trim()); setMagicalGirl(result); setError(null); // 成功时清除错误 } catch (error) { // 处理不同类型的错误 if (error instanceof Error) { const errorMessage = error.message; // 检查是否是 rate limit 错误 if (errorMessage.includes('请求过于频繁')) { setError('🚫 请求太频繁了!每2分钟只能生成一次魔法少女哦~请稍后再试吧!'); } else if (errorMessage.includes('网络')) { setError('🌐 网络连接有问题!请检查网络后重试~'); } else { setError(`✨ 魔法失效了!${errorMessage}`); } } else { setError('✨ 魔法失效了!可能是用的人太多狸!请再生成一次试试吧~'); } } finally { setIsGenerating(false); startCooldown(); } }; const handleSaveImage = async () => { if (!resultRef.current) return; try { // 临时隐藏保存按钮和说明文字 const saveButton = resultRef.current.querySelector('.save-button') as HTMLElement; const logoPlaceholder = resultRef.current.querySelector('.logo-placeholder') as HTMLElement; if (saveButton) saveButton.style.display = 'none'; if (logoPlaceholder) logoPlaceholder.style.display = 'flex'; const result = await snapdom(resultRef.current, { scale: 1, }); // 恢复按钮显示 if (saveButton) saveButton.style.display = 'block'; if (logoPlaceholder) logoPlaceholder.style.display = 'none'; // 获取 result.toPng() 生成的 HTMLImageElement 的图片 URL // toPng() 返回 Promise,可通过 .src 获取图片的 base64 url const imgElement = await result.toPng(); const imageUrl = imgElement.src; setSavedImageUrl(imageUrl); setShowImageModal(true); } catch { alert('生成图片失败,请重试'); // 确保在失败时也恢复按钮显示 const saveButton = resultRef.current?.querySelector('.save-button') as HTMLElement; const logoPlaceholder = resultRef.current?.querySelector('.logo-placeholder') as HTMLElement; if (saveButton) saveButton.style.display = 'block'; if (logoPlaceholder) logoPlaceholder.style.display = 'none'; } }; return ( <>
Logo

你是什么魔法少女呢!

或者要不要来试试 奇妙妖精大调查或 二次元魔法少女生成器?

本测试设定来源于小说《下班,然后变成魔法少女》

以及广告位募集中

如有意向请联系魔法国度研究院院长 @祖母绿:1********

setInputName(e.target.value)} className="input-field" placeholder="例如:鹿目圆香" onKeyDown={(e) => e.key === 'Enter' && handleGenerate()} />
{error && (
{error}
)} {magicalGirl && (
{ const colors = gradientColors[magicalGirl.appearance.mainColor] || gradientColors[MainColor.Pink]; return `linear-gradient(135deg, ${colors.first} 0%, ${colors.second} 100%)`; })() }} >
Logo
✨ 真名解放
{magicalGirl.realName}
💝 魔法少女名
{magicalGirl.name}
「{magicalGirl.flowerDescription}」
👗 外貌
身高:{magicalGirl.appearance.height}
体重:{magicalGirl.appearance.weight}
发色:{magicalGirl.appearance.hairColor}
发型:{magicalGirl.appearance.hairStyle}
瞳色:{magicalGirl.appearance.eyeColor}
肤色:{magicalGirl.appearance.skinTone}
穿着:{magicalGirl.appearance.wearing}
特征:{magicalGirl.appearance.specialFeature}
✨ 变身咒语
{magicalGirl.spell}
⭐ 魔法等级
{magicalGirl.levelEmoji} {magicalGirl.level}
{/* Logo placeholder for saved images */}
Logo
)}
立绘生成功能开发中(大概)...
{/* Image Modal */} {showImageModal && savedImageUrl && (

💫 长按图片保存到相册

魔法少女登记表
)}
); }