Files
happy-life-star/PncyssD/dashboard.js
T
2025-12-21 16:57:54 +08:00

397 lines
22 KiB
JavaScript

import { state } from './state.js';
import { UI } from './components.js';
import { AIService } from './api.js';
export const Dashboard = {
render() {
const container = document.getElementById('view-container');
const nav = document.getElementById('top-nav');
if (nav) nav.classList.remove('hidden');
container.innerHTML = `
<div class="grid grid-cols-1 md:grid-cols-12 h-full">
<!-- Sidebar Nav -->
<aside class="md:col-span-3 border-r border-white/5 p-6 flex flex-col gap-6 bg-black/20">
<div class="space-y-2">
<div class="px-3 py-2 text-[10px] text-white/30 uppercase tracking-[0.2em] font-bold">回溯过去</div>
<button class="nav-item active w-full flex items-center gap-3 p-4 rounded-2xl glass-btn text-white/50" data-view="timeline">
<i data-lucide="history" class="w-5 h-5"></i> <span>生命长河</span>
</button>
</div>
<div class="space-y-2">
<div class="px-3 py-2 text-[10px] text-white/30 uppercase tracking-[0.2em] font-bold">创造未来</div>
<button class="nav-item w-full flex items-center gap-3 p-4 rounded-2xl glass-btn text-white/50" data-view="script">
<i data-lucide="sparkles" class="w-5 h-5"></i> <span>爽文剧本</span>
</button>
<button class="nav-item w-full flex items-center gap-3 p-4 rounded-2xl glass-btn text-white/50" data-view="path">
<i data-lucide="map" class="w-5 h-5"></i> <span>实现路径</span>
</button>
</div>
<div class="mt-auto p-4 bg-white/[0.02] rounded-2xl border border-white/5">
<p class="text-[10px] text-white/20 italic leading-relaxed">
“回溯过去、记录当下、创造未来。”
</p>
</div>
</aside>
<!-- Content Area -->
<section id="dash-content" class="md:col-span-9 p-8 overflow-y-auto custom-scrollbar relative">
</section>
</div>
`;
this.initEventListeners();
this.loadTimeline();
lucide.createIcons();
},
initEventListeners() {
document.querySelectorAll('.nav-item').forEach(btn => {
btn.onclick = () => {
if (btn.classList.contains('active')) return;
document.querySelectorAll('.nav-item').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
const view = btn.dataset.view;
this.animateTransition(() => {
if(view === 'timeline') this.loadTimeline();
if(view === 'script') this.loadScriptGenerator();
if(view === 'path') this.loadPathGenerator();
});
};
});
const profileBtn = document.getElementById('user-profile-btn');
if (profileBtn) profileBtn.onclick = () => this.showProfile();
},
animateTransition(callback) {
const content = document.getElementById('dash-content');
gsap.to(content, { opacity: 0, y: 10, duration: 0.3, onComplete: () => {
callback();
gsap.to(content, { opacity: 1, y: 0, duration: 0.6, ease: "power2.out" });
lucide.createIcons();
}});
},
loadTimeline() {
const content = document.getElementById('dash-content');
const hasEvents = state.lifeEvents.length > 0;
content.innerHTML = `
<div class="flex justify-between items-end mb-12">
<div>
<h3 class="text-4xl font-serif text-white/90">生命长河</h3>
<p class="text-sm text-white/30 mt-2">塑造你的每一刻,都被星辰见证。</p>
</div>
<button id="add-event-btn" class="glass-btn px-6 py-3 rounded-full text-sm font-bold flex items-center gap-2 bg-orange-200/5 text-orange-200 border-orange-200/20 shadow-lg">
<i data-lucide="plus" class="w-4 h-4"></i> 记录足迹
</button>
</div>
<div id="timeline-container" class="relative pl-8">
${hasEvents ? '<div class="timeline-line"></div>' : ''}
<div class="space-y-10">
${hasEvents ? state.lifeEvents.sort((a,b) => new Date(b.time) - new Date(a.time)).map(ev => this.renderEventCard(ev)).join('') : this.renderEmpty()}
</div>
</div>
`;
document.getElementById('add-event-btn').onclick = () => this.showEventModal();
},
renderEventCard(ev) {
return `
<div class="relative group">
<div class="timeline-dot absolute left-[-39px] top-6 z-10"></div>
<div class="glass-card p-6 border-white/5 hover:border-orange-200/20 transition-all duration-700">
<div class="flex justify-between items-start mb-4">
<h4 class="text-xl font-medium text-white/80">${ev.title}</h4>
<span class="text-[10px] font-mono tracking-widest text-white/30 uppercase">${ev.time}</span>
</div>
<p class="text-sm text-white/60 leading-relaxed mb-6">${ev.content}</p>
<div class="ai-glow-card p-5 rounded-2xl bg-orange-200/[0.02] border border-orange-200/5">
<div class="flex items-center gap-2 mb-2">
<i data-lucide="sparkles" class="w-3 h-3 text-orange-200"></i>
<span class="text-[9px] uppercase tracking-[0.2em] text-orange-200/60 font-bold">引路人洞察</span>
</div>
<p class="text-xs italic text-white/50 leading-loose">${ev.aiFeedback}</p>
</div>
</div>
</div>
`;
},
showEventModal() {
const modal = document.getElementById('modal-overlay');
const body = document.getElementById('modal-body');
modal.classList.remove('hidden');
body.innerHTML = `
<div class="space-y-6">
<div class="mb-4"><h3 class="text-2xl font-serif">记录足迹</h3></div>
${UI.renderInput('事件标题', 'ev-title', 'text', '给这段经历起个名字')}
${UI.renderInput('发生时间', 'ev-time', 'date')}
${UI.renderTextArea('经历详情', 'ev-content', '当时发生了什么?你的感受如何?')}
<button id="save-event" class="w-full glass-btn py-4 rounded-2xl bg-orange-200/10 text-orange-200 font-bold tracking-[0.2em]">开启 AI 疗愈</button>
</div>
`;
document.getElementById('save-event').onclick = async () => {
const btn = document.getElementById('save-event');
const event = { title: document.getElementById('ev-title').value, time: document.getElementById('ev-time').value, content: document.getElementById('ev-content').value, aiFeedback: "分析中..." };
if(!event.title || !event.time || !event.content) return alert("请完整填写记录。");
btn.disabled = true;
btn.innerHTML = `<span class="animate-pulse">正在共鸣生命轨迹...</span>`;
event.aiFeedback = await AIService.analyzeLifeEvent(event);
state.addLifeEvent(event);
modal.classList.add('hidden');
this.loadTimeline();
};
lucide.createIcons();
},
loadScriptGenerator() {
const content = document.getElementById('dash-content');
const userData = state.registrationData;
const styles = [
{value:'都市', label:'都市沉浮'}, {value:'古风', label:'快意恩仇'},
{value:'爱情', label:'唯美浪漫'}, {value:'科幻', label:'星际远征'},
{value:'喜剧', label:'荒诞不经'}, {value:'悬疑', label:'迷雾重重'}, {value:'恐怖', label:'午夜回响'}
];
const lengths = [{value:'短', label:'极简'}, {value:'中', label:'连载'}, {value:'长', label:'史诗'}];
content.innerHTML = `
<div class="grid grid-cols-1 lg:grid-cols-12 gap-8">
<div class="lg:col-span-4 space-y-6">
<div class="glass-card p-6 border-white/10 space-y-4">
<div class="flex items-center gap-2 pb-2 border-b border-white/5">
<i data-lucide="user-cog" class="w-4 h-4 text-orange-200"></i>
<h4 class="text-sm font-bold tracking-widest text-white/80 uppercase">角色设定</h4>
</div>
<div class="grid grid-cols-2 gap-x-2 gap-y-4 text-[11px]">
<div><label class="text-white/20 block">昵称</label><span class="text-white/70">${userData.nickname}</span></div>
<div><label class="text-white/20 block">星座</label><span class="text-white/70">${userData.zodiac}</span></div>
<div><label class="text-white/20 block">MBTI</label><span class="text-white/70">${userData.mbti}</span></div>
<div class="col-span-2"><label class="text-white/20 block">兴趣爱好</label><span class="text-white/70">${userData.hobbies.join(', ')}</span></div>
</div>
<button onclick="document.getElementById('user-profile-btn').click()" class="w-full py-2 text-[10px] text-orange-200/50 hover:text-orange-200 border border-white/5 rounded-xl transition-all">修改人设</button>
</div>
<div class="glass-card p-6 border-white/10 space-y-6">
<div class="flex items-center gap-2 pb-2 border-b border-white/5">
<i data-lucide="pen-tool" class="w-4 h-4 text-orange-200"></i>
<h4 class="text-sm font-bold tracking-widest text-white/80 uppercase">创作需求</h4>
</div>
${UI.renderInput('剧本主题', 'sc-theme', 'text', '例如:我在职场逆袭了')}
${UI.renderSelect('叙事风格', 'sc-style', styles)}
${UI.renderSelect('剧本篇幅', 'sc-length', lengths)}
<button id="gen-script-btn" class="w-full glass-btn py-4 bg-orange-200/5 text-orange-200 font-bold text-sm tracking-widest border-orange-200/20">
开启天命编撰
</button>
</div>
<div class="space-y-4">
<h5 class="text-[10px] text-white/20 uppercase tracking-widest font-bold px-2">历史卷轴</h5>
<div class="space-y-2 max-h-[25vh] overflow-y-auto custom-scrollbar">
${state.scripts.length > 0 ? state.scripts.map(s => `
<div class="script-item p-3 glass-card text-left cursor-pointer hover:bg-white/5 border-white/5 transition-all ${s.id === state.selectedScriptId ? 'border-orange-200/30 bg-orange-200/5' : ''}" data-id="${s.id}">
<div class="text-[11px] text-white/80 truncate">${s.theme}</div>
<div class="text-[9px] text-white/30 flex justify-between mt-1"><span>${s.style} | ${s.length}</span><span>${s.date}</span></div>
</div>
`).join('') : '<p class="text-center text-xs text-white/10 py-4 italic">暂无卷轴</p>'}
</div>
</div>
</div>
<div class="lg:col-span-8">
<div id="script-target" class="h-full">
${state.selectedScriptId ? this.renderScript(state.getSelectedScript()) : this.renderEmptyScript()}
</div>
</div>
</div>
`;
document.getElementById('gen-script-btn').onclick = async () => {
const theme = document.getElementById('sc-theme').value;
if(!theme) return alert('请输入主题');
const btn = document.getElementById('gen-script-btn');
btn.disabled = true;
btn.innerHTML = `<i data-lucide="loader" class="animate-spin w-4 h-4 mr-2"></i> 编撰中...`;
lucide.createIcons();
const params = {
theme,
style: document.getElementById('sc-style').value,
length: document.getElementById('sc-length').value,
character: state.registrationData
};
const content = await AIService.generateEpicScript(params, state.lifeEvents);
state.addScript({ ...params, content });
this.animateTransition(() => this.loadScriptGenerator());
};
document.querySelectorAll('.script-item').forEach(item => {
item.onclick = () => {
state.selectedScriptId = parseInt(item.dataset.id);
state.save();
this.animateTransition(() => this.loadScriptGenerator());
};
});
lucide.createIcons();
},
renderScript(script) {
return `
<div class="glass-card p-10 h-full overflow-y-auto custom-scrollbar border-orange-200/20 shadow-2xl relative animate-fade-in">
<div class="prose prose-invert max-w-none">
<div class="flex justify-between items-center mb-8 pb-4 border-b border-white/5">
<div>
<h4 class="text-2xl font-serif text-orange-200">${script.theme}</h4>
<p class="text-[10px] text-white/30 mt-1 uppercase tracking-widest">${script.style}篇 · ${script.length}卷</p>
</div>
<i data-lucide="book-open" class="text-white/20"></i>
</div>
<div class="text-white/70 leading-loose whitespace-pre-wrap space-y-6 text-sm">
${script.content.replace(/【/g, '<div class="mt-8 mb-4 text-orange-100 font-bold text-lg border-l-2 border-orange-400 pl-4">【').replace(/】/g, '】</div>')}
</div>
</div>
</div>
`;
},
renderEmptyScript() {
return `
<div class="flex flex-col items-center justify-center h-full text-center opacity-20 py-32">
<i data-lucide="sparkles" class="w-20 h-20 mb-6"></i>
<p class="text-xl font-serif">请在左侧设定需求,开启你的天命爽文</p>
</div>
`;
},
loadPathGenerator() {
const content = document.getElementById('dash-content');
const script = state.getSelectedScript();
if (!script) {
content.innerHTML = `
<div class="flex flex-col items-center justify-center py-32 opacity-30 text-center">
<i data-lucide="map" class="w-16 h-16 mb-4"></i>
<p class="font-serif italic text-xl">先生成剧本,方能洞察路径。</p>
<button onclick="document.querySelector('[data-view=script]').click()" class="mt-6 glass-btn px-6 py-2 rounded-full text-xs">去生成剧本</button>
</div>
`;
return;
}
content.innerHTML = `
<div class="max-w-3xl mx-auto space-y-12 pb-20">
<div class="flex justify-between items-end">
<div>
<h3 class="text-4xl font-serif">实现路径</h3>
<p class="text-sm text-white/30 mt-2">基于《${script.theme}》,拆解达成目标的每一步。</p>
</div>
<button id="gen-path-btn" class="glass-btn px-8 py-3 rounded-full text-sm font-bold bg-blue-400/5 text-blue-300 border-blue-400/20">
${state.selectedPath ? '重新推演' : '开启人生导航'}
</button>
</div>
<div id="path-target" class="space-y-6">
${state.selectedPath ? this.renderPath(state.selectedPath) : this.renderEmptyPath()}
</div>
</div>
`;
document.getElementById('gen-path-btn').onclick = async () => {
const btn = document.getElementById('gen-path-btn');
btn.disabled = true;
btn.innerHTML = `<i data-lucide="loader" class="animate-spin w-4 h-4 mr-2"></i> 规划中...`;
const path = await AIService.generatePath(script.content);
state.setPath(path);
this.animateTransition(() => this.loadPathGenerator());
};
},
renderPath(path) {
return path.split(/【/).filter(s => s.trim()).map((s, i) => {
const parts = s.split(/】/);
return `
<div class="glass-card p-8 border-l-4 border-l-blue-400/40 bg-blue-400/[0.01] animate-fade-in" style="animation-delay: ${i * 0.1}s">
<h5 class="text-blue-200 font-bold mb-4 flex items-center gap-3">
<span class="w-6 h-6 rounded-full bg-blue-400/20 text-[10px] flex items-center justify-center">${i+1}</span>
${parts[0]}
</h5>
<div class="text-white/60 text-sm leading-relaxed whitespace-pre-wrap">${parts[1] || ''}</div>
</div>
`;
}).join('');
},
renderEmptyPath() {
return `<div class="py-20 text-center text-white/20 italic font-serif">等待开启人生导航...</div>`;
},
renderEmpty() {
return `
<div class="flex flex-col items-center justify-center py-32 text-center opacity-30">
<i data-lucide="wind" class="w-12 h-12 mb-4"></i>
<p class="font-serif italic text-lg">此间尚无回响,等待你执笔...</p>
</div>
`;
},
showProfile() {
const modal = document.getElementById('modal-overlay');
const body = document.getElementById('modal-body');
modal.classList.remove('hidden');
this.renderProfileMain(body);
lucide.createIcons();
},
renderProfileMain(container) {
const data = state.registrationData;
container.innerHTML = `
<div class="animate-fade-in space-y-8">
<div class="flex items-center gap-6">
<div class="w-20 h-20 rounded-3xl bg-gradient-to-br from-orange-400/20 to-orange-600/20 flex items-center justify-center text-3xl border border-white/10">
${(data.nickname || '人').charAt(0)}
</div>
<div>
<h4 class="text-2xl font-serif text-white/90">${data.nickname || '旅行者'}</h4>
<p class="text-[10px] text-white/30 uppercase tracking-[0.2em] mt-1">${data.mbti || '-'} | ${data.zodiac || '-'}</p>
</div>
</div>
<div class="grid grid-cols-2 gap-4">
<div class="p-4 bg-white/[0.02] rounded-2xl border border-white/5 text-center">
<div class="text-lg font-serif text-orange-200">${state.lifeEvents.length}</div>
<div class="text-[9px] text-white/30 uppercase tracking-widest mt-1">生命足迹</div>
</div>
<div class="p-4 bg-white/[0.02] rounded-2xl border border-white/5 text-center">
<div class="text-lg font-serif text-blue-200">${state.scripts.length}</div>
<div class="text-[9px] text-white/30 uppercase tracking-widest mt-1">天命卷轴</div>
</div>
</div>
<div class="space-y-4 pt-4 border-t border-white/5">
<button id="edit-profile" class="w-full glass-btn py-4 text-sm font-bold flex gap-3 items-center justify-center"><i data-lucide="settings" class="w-4 h-4"></i> 编辑资料</button>
<button id="logout-btn" class="w-full py-4 text-[10px] text-red-400/40 hover:text-red-400 uppercase tracking-widest transition-colors">清除数据并退出</button>
</div>
</div>
`;
document.getElementById('edit-profile').onclick = () => {
container.innerHTML = UI.renderAccountSettings(state.registrationData);
lucide.createIcons();
document.getElementById('cancel-edit-btn').onclick = () => this.renderProfileMain(container);
document.getElementById('save-profile-btn').onclick = () => {
state.updateRegistration({
nickname: document.getElementById('edit-nickname').value,
profession: document.getElementById('edit-profession').value,
mbti: document.getElementById('edit-mbti').value,
zodiac: document.getElementById('edit-zodiac').value,
hobbies: document.getElementById('edit-hobbies').value.split(',').map(s => s.trim())
});
this.renderProfileMain(container);
};
};
document.getElementById('logout-btn').onclick = () => {
if(confirm("确定要删除所有记录吗?此操作不可逆。")) state.clear();
};
}
};
const closeBtn = document.getElementById('close-modal');
if (closeBtn) closeBtn.onclick = () => document.getElementById('modal-overlay').classList.add('hidden');