前端重构实现

This commit is contained in:
2025-12-22 16:38:06 +08:00
parent cd6d995d5a
commit 26574e3db7
54 changed files with 8976 additions and 0 deletions
+219
View File
@@ -0,0 +1,219 @@
import { useState, useEffect } from 'react';
import { UserCog, PenTool, Sparkles, BookOpen, Loader2 } from 'lucide-react';
import { GlassCard, GlassButton, GlassInput, GlassSelect } from '../components/ui';
import useStore from '../store/useStore';
import { generateEpicScript } from '../services/ai';
import { scriptStyles, scriptLengths } from '../utils/constants';
/**
* ScriptView 组件
* 爽文剧本视图,包含角色设定、创作需求和剧本展示
* @param {Object} props
* @param {Function} props.onOpenProfile - 打开用户资料模态框回调
*/
const ScriptView = ({ onOpenProfile }) => {
const {
registrationData,
lifeEvents,
scripts,
selectedScriptId,
addScript,
setSelectedScriptId,
getSelectedScript,
loadScripts
} = useStore();
// 加载剧本列表
useEffect(() => {
loadScripts().catch(() => {
// 后端不可用时忽略错误
});
}, [loadScripts]);
// 表单状态
const [theme, setTheme] = useState('');
const [style, setStyle] = useState(scriptStyles[0].value);
const [length, setLength] = useState(scriptLengths[0].value);
const [isLoading, setIsLoading] = useState(false);
/**
* 处理剧本生成
*/
const handleGenerate = async () => {
if (!theme) {
alert('请输入主题');
return;
}
setIsLoading(true);
try {
const content = await generateEpicScript(
{ theme, style, length, character: registrationData },
lifeEvents
);
addScript({ theme, style, length, content });
setTheme('');
} catch (error) {
console.error('Failed to generate script:', error);
} finally {
setIsLoading(false);
}
};
/**
* 格式化剧本内容,高亮【标题】
*/
const formatScriptContent = (content) => {
if (!content) return '';
return content.replace(
/【([^】]+)】/g,
'<div class="mt-8 mb-4 text-orange-100 font-bold text-lg border-l-2 border-orange-400 pl-4">【$1】</div>'
);
};
const selectedScript = getSelectedScript();
return (
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8">
{/* 左侧面板 */}
<div className="lg:col-span-4 space-y-6">
{/* 角色设定卡片 */}
<GlassCard className="border-white/10 space-y-4">
<div className="flex items-center gap-2 pb-2 border-b border-white/5">
<UserCog className="w-4 h-4 text-orange-200" />
<h4 className="text-sm font-bold tracking-widest text-white/80 uppercase">角色设定</h4>
</div>
<div className="grid grid-cols-2 gap-x-2 gap-y-4 text-[11px]">
<div>
<label className="text-white/20 block">昵称</label>
<span className="text-white/70">{registrationData.nickname || '-'}</span>
</div>
<div>
<label className="text-white/20 block">星座</label>
<span className="text-white/70">{registrationData.zodiac || '-'}</span>
</div>
<div>
<label className="text-white/20 block">MBTI</label>
<span className="text-white/70">{registrationData.mbti || '-'}</span>
</div>
<div className="col-span-2">
<label className="text-white/20 block">兴趣爱好</label>
<span className="text-white/70">
{registrationData.hobbies?.join(', ') || '-'}
</span>
</div>
</div>
<button
onClick={onOpenProfile}
className="w-full py-2 text-[10px] text-orange-200/50 hover:text-orange-200 border border-white/5 rounded-xl transition-all"
>
修改人设
</button>
</GlassCard>
{/* 创作需求表单 */}
<GlassCard className="border-white/10 space-y-6">
<div className="flex items-center gap-2 pb-2 border-b border-white/5">
<PenTool className="w-4 h-4 text-orange-200" />
<h4 className="text-sm font-bold tracking-widest text-white/80 uppercase">创作需求</h4>
</div>
<GlassInput
label="剧本主题"
placeholder="例如:我在职场逆袭了"
value={theme}
onChange={setTheme}
/>
<GlassSelect
label="叙事风格"
options={scriptStyles}
value={style}
onChange={setStyle}
/>
<GlassSelect
label="剧本篇幅"
options={scriptLengths}
value={length}
onChange={setLength}
/>
<GlassButton
onClick={handleGenerate}
disabled={isLoading}
className="w-full py-4 bg-orange-200/5 text-orange-200 font-bold text-sm tracking-widest border-orange-200/20"
>
{isLoading ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
编撰中...
</>
) : (
'开启天命编撰'
)}
</GlassButton>
</GlassCard>
{/* 历史卷轴列表 */}
<div className="space-y-4">
<h5 className="text-[10px] text-white/20 uppercase tracking-widest font-bold px-2">
历史卷轴
</h5>
<div className="space-y-2 max-h-[25vh] overflow-y-auto custom-scrollbar">
{scripts.length > 0 ? (
scripts.map((script) => (
<div
key={script.id}
onClick={() => setSelectedScriptId(script.id)}
className={`
p-3 glass-card text-left cursor-pointer hover:bg-white/5 border-white/5 transition-all
${script.id === selectedScriptId ? 'border-orange-200/30 bg-orange-200/5' : ''}
`}
>
<div className="text-[11px] text-white/80 truncate">{script.theme}</div>
<div className="text-[9px] text-white/30 flex justify-between mt-1">
<span>{script.style} | {script.length}</span>
<span>{script.date}</span>
</div>
</div>
))
) : (
<p className="text-center text-xs text-white/10 py-4 italic">暂无卷轴</p>
)}
</div>
</div>
</div>
{/* 右侧剧本展示区 */}
<div className="lg:col-span-8">
<div className="h-full">
{selectedScript ? (
<GlassCard className="h-full overflow-y-auto custom-scrollbar border-orange-200/20 shadow-2xl animate-fade-in" padding="lg">
<div className="prose prose-invert max-w-none">
<div className="flex justify-between items-center mb-8 pb-4 border-b border-white/5">
<div>
<h4 className="text-2xl font-serif text-orange-200">{selectedScript.theme}</h4>
<p className="text-[10px] text-white/30 mt-1 uppercase tracking-widest">
{selectedScript.style} · {selectedScript.length}
</p>
</div>
<BookOpen className="text-white/20" />
</div>
<div
className="text-white/70 leading-loose whitespace-pre-wrap space-y-6 text-sm"
dangerouslySetInnerHTML={{ __html: formatScriptContent(selectedScript.content) }}
/>
</div>
</GlassCard>
) : (
<div className="flex flex-col items-center justify-center h-full text-center opacity-20 py-32">
<Sparkles className="w-20 h-20 mb-6" />
<p className="text-xl font-serif">请在左侧设定需求开启你的天命爽文</p>
</div>
)}
</div>
</div>
</div>
);
};
export default ScriptView;