小程序初始化
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
# 人生轨迹 (Life Trajectory)
|
||||
|
||||
一款结合数字疗愈美学与人工智能的人生管理工具。
|
||||
|
||||
## 核心功能
|
||||
1. **深度入站**:分步式采集人设与重要回忆。
|
||||
2. **人生回溯**:记录大事件,AI辅助分析与疗愈。
|
||||
3. **剧本生成**:将过去经历转化为高能爽文人生。
|
||||
4. **路径规划**:基于剧本反推现实可行的执行方案。
|
||||
|
||||
## 技术栈
|
||||
- 架构:原生 ES Modules 模块化开发
|
||||
- UI:Tailwind CSS + Glassmorphism 拟态
|
||||
- 动画:GSAP
|
||||
- AI:OpenRouter (DeepSeek)
|
||||
- 存储:LocalStorage
|
||||
|
||||
## 本地启动
|
||||
本地启动访问命令: python3 -m http.server 8081
|
||||
@@ -0,0 +1,47 @@
|
||||
const API_KEY = "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";
|
||||
const BASE_URL = "https://openrouter.ai/api/v1/chat/completions";
|
||||
|
||||
export const AIService = {
|
||||
async fetchAI(prompt, systemMsg) {
|
||||
try {
|
||||
const response = await fetch(BASE_URL, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${API_KEY}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: "deepseek/deepseek-chat-v3-0324:free",
|
||||
messages: [
|
||||
{ role: "system", content: systemMsg },
|
||||
{ role: "user", content: prompt }
|
||||
]
|
||||
})
|
||||
});
|
||||
const data = await response.json();
|
||||
return data.choices[0].message.content;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return "(AI 暂时陷入了沉思,请稍后再试)";
|
||||
}
|
||||
},
|
||||
|
||||
async analyzeLifeEvent(event) {
|
||||
const system = "你是一位温柔的生命引路人,擅长从平凡事件中发掘成长的力量。请分析用户记录的事件,提供情感价值、成长总结和疗愈鼓励。保持字数在150字左右。";
|
||||
const prompt = `事件标题:${event.title}\n时间:${event.time}\n内容:${event.content}`;
|
||||
return this.fetchAI(prompt, system);
|
||||
},
|
||||
|
||||
async generateEpicScript(params, events) {
|
||||
const system = `你是一位金牌爽文编剧。根据用户的角色设定和过往经历,生成一段符合用户设定、充满爽感的未来人生剧本。剧本必须包含起承转合,使用【标题】标记段落。`;
|
||||
const charInfo = `姓名:${params.character.nickname}, 性格:${params.character.mbti}, 兴趣:${params.character.hobbies.join(',')}, 星座:${params.character.zodiac}`;
|
||||
const eventSummary = events.map(e => e.title).join(', ');
|
||||
const prompt = `角色信息:${charInfo}\n过往经历关键词:${eventSummary}\n用户指定主题:${params.theme}\n指定风格:${params.style}\n篇幅要求:${params.length}\n\n请以此创作一段热血、精彩的人生剧本。`;
|
||||
return this.fetchAI(prompt, system);
|
||||
},
|
||||
|
||||
async generatePath(script) {
|
||||
const system = "你是一位人生规划导师。请将用户生成的剧本拆解为现实中可操作的路径。使用【阶段名称】加上具体建议。务必客观、可执行。";
|
||||
return this.fetchAI(script, system);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,74 @@
|
||||
export const UI = {
|
||||
inspirationClusters: {
|
||||
childhood: ['秋千', '晚霞', '糖果', '奔跑', '蝉鸣', '雨后泥土', '旧书包', '风筝'],
|
||||
joy: ['海浪', '拥抱', '掌声', '晨曦', '破土而出', '默契', '星空', '释放'],
|
||||
low: ['落叶', '雨伞', '长廊', '深呼吸', '自愈', '沉潜', '坚韧', '等待', '破茧']
|
||||
},
|
||||
|
||||
renderInspiration(type, targetId) {
|
||||
const words = this.inspirationClusters[type];
|
||||
return words.map(word => `
|
||||
<span class="prompt-tag px-3 py-1 bg-white/5 border border-white/5 rounded-full text-[10px] text-white/40 cursor-pointer hover:bg-orange-200/10 hover:text-orange-200 transition-all"
|
||||
onclick="document.getElementById('${targetId}').value += '${word}'">${word}</span>
|
||||
`).join('');
|
||||
},
|
||||
|
||||
renderInput(label, id, type = 'text', placeholder = '', value = '') {
|
||||
return `
|
||||
<div class="flex flex-col gap-2 mb-4 w-full">
|
||||
<label class="text-[10px] text-white/30 uppercase tracking-widest font-bold">${label}</label>
|
||||
<input type="${type}" id="${id}" value="${value}" placeholder="${placeholder}"
|
||||
class="glass-input w-full focus:ring-2 focus:ring-orange-200/50">
|
||||
</div>
|
||||
`;
|
||||
},
|
||||
|
||||
renderTextArea(label, id, placeholder = '', value = '') {
|
||||
return `
|
||||
<div class="flex flex-col gap-2 mb-4 w-full">
|
||||
<label class="text-[10px] text-white/30 uppercase tracking-widest font-bold">${label}</label>
|
||||
<textarea id="${id}" rows="4" placeholder="${placeholder}"
|
||||
class="glass-input w-full resize-none focus:ring-2 focus:ring-orange-200/50">${value}</textarea>
|
||||
</div>
|
||||
`;
|
||||
},
|
||||
|
||||
renderSelect(label, id, options, selectedValue = '') {
|
||||
return `
|
||||
<div class="flex flex-col gap-2 mb-4 w-full">
|
||||
<label class="text-[10px] text-white/30 uppercase tracking-widest font-bold">${label}</label>
|
||||
<select id="${id}" class="glass-input w-full appearance-none">
|
||||
${options.map(opt => `<option value="${opt.value}" ${opt.value === selectedValue ? 'selected' : ''}>${opt.label}</option>`).join('')}
|
||||
</select>
|
||||
</div>
|
||||
`;
|
||||
},
|
||||
|
||||
renderAccountSettings(data) {
|
||||
return `
|
||||
<div class="space-y-6 animate-fade-in">
|
||||
<div class="flex items-center gap-4 mb-8">
|
||||
<div class="w-12 h-12 rounded-full bg-orange-200/10 flex items-center justify-center">
|
||||
<i data-lucide="settings-2" class="text-orange-200 w-5 h-5"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="text-xl font-serif">个人设定</h4>
|
||||
<p class="text-xs text-white/40">在这里调整你的人生航向基础信息</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-6">
|
||||
${this.renderInput('昵称', 'edit-nickname', 'text', '你想被如何称呼?', data.nickname)}
|
||||
${this.renderInput('职业', 'edit-profession', 'text', '你当下的社会锚点', data.profession || '')}
|
||||
${this.renderInput('MBTI', 'edit-mbti', 'text', '性格色彩', data.mbti)}
|
||||
${this.renderInput('星座', 'edit-zodiac', 'text', '星辰指引', data.zodiac)}
|
||||
</div>
|
||||
${this.renderInput('兴趣爱好', 'edit-hobbies', 'text', '让灵魂起舞的事物', data.hobbies.join(', '))}
|
||||
|
||||
<div class="flex gap-4 mt-8 pt-6 border-t border-white/5">
|
||||
<button id="save-profile-btn" class="flex-1 glass-btn py-3 bg-orange-200/10 text-orange-100 font-bold tracking-widest">保存修改</button>
|
||||
<button id="cancel-edit-btn" class="px-6 glass-btn py-3 text-white/40">返回</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,396 @@
|
||||
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');
|
||||
@@ -0,0 +1,61 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>人生轨迹 | Life Trajectory</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>
|
||||
<script src="https://unpkg.com/lucide@latest"></script>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
<body class="overflow-hidden bg-[#0a0c10] text-slate-100 font-sans selection:bg-orange-200/30">
|
||||
<!-- Dynamic Fluid Background -->
|
||||
<div id="app-bg" class="fixed inset-0 z-[-1]">
|
||||
<div class="absolute inset-0 bg-gradient-to-br from-[#1a1c2c] via-[#0a0c10] to-[#2d1b10] opacity-80"></div>
|
||||
<div class="absolute top-[-10%] left-[-10%] w-[60%] h-[60%] bg-blue-900/20 blur-[120px] rounded-full animate-float"></div>
|
||||
<div class="absolute bottom-[-10%] right-[-10%] w-[50%] h-[50%] bg-orange-900/10 blur-[120px] rounded-full animate-float-delayed"></div>
|
||||
<img src="https://r2-bucket.flowith.net/f/845b300ff0a2b36e/digital_healing_background_design_index_1%401024x1024.jpeg"
|
||||
alt="texture" class="w-full h-full object-cover mix-blend-overlay opacity-30">
|
||||
</div>
|
||||
|
||||
<!-- Header -->
|
||||
<header class="fixed top-0 left-0 p-6 z-50 w-full flex justify-between items-center pointer-events-none">
|
||||
<div class="flex items-center gap-3 pointer-events-auto cursor-pointer" onclick="location.reload()">
|
||||
<img src="https://r2-bucket.flowith.net/f/cf8c6e7c020409c9/lifeline_app_logo_design_index_0%401024x1024.jpeg"
|
||||
alt="logo" class="w-10 h-10 rounded-full shadow-2xl border border-white/10">
|
||||
<h1 class="text-xl font-serif tracking-[0.2em] bg-clip-text text-transparent bg-gradient-to-r from-white to-white/50">人生轨迹</h1>
|
||||
</div>
|
||||
<nav id="top-nav" class="hidden pointer-events-auto">
|
||||
<button id="user-profile-btn" class="glass-btn p-2.5 rounded-full hover:shadow-orange-200/10 shadow-lg">
|
||||
<i data-lucide="user" class="w-5 h-5"></i>
|
||||
</button>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<!-- Main Container -->
|
||||
<main id="app-root" class="w-full h-screen flex items-center justify-center p-4">
|
||||
<div id="view-container" class="w-full max-w-5xl h-[85vh] relative rounded-[32px] overflow-hidden">
|
||||
<!-- Content Injected Here -->
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Loader -->
|
||||
<div id="loader" class="fixed inset-0 flex flex-col items-center justify-center z-[200] bg-[#0a0c10] hidden">
|
||||
<div class="w-16 h-16 border-2 border-orange-200/20 border-t-orange-200 rounded-full animate-spin mb-4"></div>
|
||||
<p class="text-xs tracking-[0.3em] text-white/40 uppercase">载入生命序列...</p>
|
||||
</div>
|
||||
|
||||
<!-- Global Modals -->
|
||||
<div id="modal-overlay" class="fixed inset-0 bg-black/60 backdrop-blur-xl z-[100] hidden flex items-center justify-center p-4">
|
||||
<div id="modal-content" class="glass-card max-w-lg w-full p-8 relative border border-white/10 shadow-2xl">
|
||||
<button id="close-modal" class="absolute top-6 right-6 text-white/40 hover:text-white transition-colors">
|
||||
<i data-lucide="x" class="w-5 h-5"></i>
|
||||
</button>
|
||||
<div id="modal-body" class="max-h-[70vh] overflow-y-auto pr-2 custom-scrollbar"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="module" src="index.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,44 @@
|
||||
import { state } from './state.js';
|
||||
import { Login } from './login.js';
|
||||
import { Onboarding } from './onboarding.js';
|
||||
import { Dashboard } from './dashboard.js';
|
||||
|
||||
const App = {
|
||||
async init() {
|
||||
state.load();
|
||||
this.render();
|
||||
},
|
||||
|
||||
async transition(to) {
|
||||
const container = document.getElementById('view-container');
|
||||
const loader = document.getElementById('loader');
|
||||
|
||||
await gsap.to(container, { opacity: 0, y: -20, duration: 0.5, ease: "power2.inOut" });
|
||||
|
||||
loader.classList.remove('hidden');
|
||||
gsap.fromTo(loader, { opacity: 0 }, { opacity: 1, duration: 0.4 });
|
||||
|
||||
state.view = to;
|
||||
state.save();
|
||||
|
||||
setTimeout(() => {
|
||||
this.render();
|
||||
gsap.to(loader, { opacity: 0, duration: 0.4, onComplete: () => loader.classList.add('hidden') });
|
||||
gsap.fromTo(container, { opacity: 0, y: 20 }, { opacity: 1, y: 0, duration: 0.8, ease: "power3.out" });
|
||||
}, 1000);
|
||||
},
|
||||
|
||||
render() {
|
||||
if (!state.isLoggedIn) {
|
||||
Login.render(() => this.transition('onboarding'));
|
||||
} else if (state.view === 'onboarding') {
|
||||
Onboarding.render(() => this.transition('dashboard'));
|
||||
} else {
|
||||
Dashboard.render();
|
||||
}
|
||||
lucide.createIcons();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('DOMContentLoaded', () => App.init());
|
||||
export default App;
|
||||
@@ -0,0 +1,78 @@
|
||||
import { state } from './state.js';
|
||||
|
||||
export const Login = {
|
||||
render(onLoginSuccess) {
|
||||
const container = document.getElementById('view-container');
|
||||
container.innerHTML = `
|
||||
<div class="w-full h-full flex items-center justify-center p-6 animate-fade-in">
|
||||
<div class="glass-card max-w-md w-full p-10 space-y-8 border-white/5 shadow-2xl">
|
||||
<div class="text-center space-y-2">
|
||||
<h2 class="text-3xl font-serif tracking-wider text-white/90">欢迎回来</h2>
|
||||
<p class="text-sm text-white/40 italic">开启你的数字生命档案</p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="space-y-2">
|
||||
<label class="text-[10px] text-white/30 uppercase tracking-[0.2em] font-bold">手机号码</label>
|
||||
<input type="tel" id="login-phone" placeholder="输入手机号"
|
||||
class="glass-input w-full text-center tracking-[0.1em]" maxlength="11">
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-3 gap-3">
|
||||
<div class="col-span-2 space-y-2">
|
||||
<label class="text-[10px] text-white/30 uppercase tracking-[0.2em] font-bold">验证码</label>
|
||||
<input type="text" id="login-code" placeholder="六位验证码"
|
||||
class="glass-input w-full text-center" maxlength="6">
|
||||
</div>
|
||||
<div class="flex items-end">
|
||||
<button id="get-code-btn" class="w-full h-[46px] rounded-2xl border border-white/5 bg-white/5 text-[10px] uppercase tracking-tighter hover:bg-white/10 transition-all">
|
||||
获取
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button id="login-submit" class="w-full glass-btn py-4 rounded-2xl bg-orange-200/5 text-orange-200 font-bold tracking-[0.3em] hover:bg-orange-200/10 transition-all border-orange-200/20 shadow-lg shadow-orange-900/10">
|
||||
开启旅程
|
||||
</button>
|
||||
|
||||
<p class="text-[10px] text-center text-white/20 px-4 leading-relaxed">
|
||||
登录即代表同意《用户协议》与《隐私政策》,我们将妥善保管您的生命数据。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const phoneInput = document.getElementById('login-phone');
|
||||
const codeInput = document.getElementById('login-code');
|
||||
const codeBtn = document.getElementById('get-code-btn');
|
||||
const loginBtn = document.getElementById('login-submit');
|
||||
|
||||
codeBtn.onclick = () => {
|
||||
if (phoneInput.value.length !== 11) return alert('请输入正确的手机号');
|
||||
codeBtn.disabled = true;
|
||||
let count = 60;
|
||||
const timer = setInterval(() => {
|
||||
codeBtn.innerText = `${count}S`;
|
||||
count--;
|
||||
if (count < 0) {
|
||||
clearInterval(timer);
|
||||
codeBtn.disabled = false;
|
||||
codeBtn.innerText = '获取';
|
||||
}
|
||||
}, 1000);
|
||||
alert('验证码已发送 (模拟验证码: 888888)');
|
||||
};
|
||||
|
||||
loginBtn.onclick = () => {
|
||||
if (phoneInput.value.length === 11 && codeInput.value === '888888') {
|
||||
state.isLoggedIn = true;
|
||||
state.phone = phoneInput.value;
|
||||
state.save();
|
||||
onLoginSuccess();
|
||||
} else {
|
||||
alert('验证失败,请检查手机号或验证码');
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,151 @@
|
||||
import { state } from './state.js';
|
||||
import { UI } from './components.js';
|
||||
|
||||
export const Onboarding = {
|
||||
onComplete: null,
|
||||
|
||||
render(onCompleteCallback) {
|
||||
if (onCompleteCallback) this.onComplete = onCompleteCallback;
|
||||
|
||||
const container = document.getElementById('view-container');
|
||||
container.innerHTML = `
|
||||
<div id="onboarding-root" class="w-full h-full glass-card p-10 flex flex-col justify-between overflow-hidden relative">
|
||||
<div id="step-content" class="flex-1 flex flex-col justify-center max-w-2xl mx-auto w-full"></div>
|
||||
|
||||
<div class="flex items-center justify-between mt-10 max-w-2xl mx-auto w-full border-t border-white/5 pt-8">
|
||||
<div id="step-indicator" class="flex gap-2">
|
||||
${[1,2,3,4,5].map(i => `<div class="step-dot-${i} w-3 h-1 rounded-full bg-white/10 transition-all duration-500"></div>`).join('')}
|
||||
</div>
|
||||
<div class="flex gap-4">
|
||||
<button id="prev-step" class="hidden text-white/40 px-6 py-2 text-sm hover:text-white transition-colors">返回</button>
|
||||
<button id="next-step" class="glass-btn px-8 py-3 rounded-full text-orange-200 font-bold tracking-widest text-sm shadow-xl shadow-orange-900/10">
|
||||
下一章 <i data-lucide="arrow-right" class="w-4 h-4 ml-2"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
this.renderStep(state.currentStep || 1);
|
||||
this.initEvents();
|
||||
},
|
||||
|
||||
renderStep(step) {
|
||||
const content = document.getElementById('step-content');
|
||||
const nextBtn = document.getElementById('next-step');
|
||||
const prevBtn = document.getElementById('prev-step');
|
||||
|
||||
|
||||
for(let i=1; i<=5; i++){
|
||||
const dot = document.querySelector(`.step-dot-${i}`);
|
||||
if(dot) dot.className = `step-dot-${i} h-1 rounded-full transition-all duration-500 ${i === step ? 'w-8 bg-orange-200' : 'w-3 bg-white/10'}`;
|
||||
}
|
||||
|
||||
prevBtn.classList.toggle('hidden', step === 1);
|
||||
nextBtn.innerHTML = step === 5 ? '开启人生 <i data-lucide="check" class="w-4 h-4 ml-2"></i>' : '继续 <i data-lucide="arrow-right" class="w-4 h-4 ml-2"></i>';
|
||||
|
||||
let html = '';
|
||||
if (step === 1) {
|
||||
html = `
|
||||
<div class="animate-fade-in space-y-8">
|
||||
<div class="mb-6">
|
||||
<h2 class="text-4xl font-serif mb-3">你是谁?</h2>
|
||||
<p class="text-white/40 italic text-sm">定义你生命坐标的初始属性。</p>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-6 gap-y-2">
|
||||
${UI.renderInput('称呼', 'reg-nickname', 'text', '例如:林中鹿', state.registrationData.nickname)}
|
||||
${UI.renderInput('性别', 'reg-gender', 'text', '自由填写', state.registrationData.gender)}
|
||||
${UI.renderInput('MBTI', 'reg-mbti', 'text', '如:INFJ', state.registrationData.mbti)}
|
||||
${UI.renderInput('星座', 'reg-zodiac', 'text', '星辰指引', state.registrationData.zodiac)}
|
||||
</div>
|
||||
${UI.renderInput('兴趣爱好', 'reg-hobbies', 'text', '用逗号分隔你的热爱', (state.registrationData.hobbies || []).join(','))}
|
||||
</div>
|
||||
`;
|
||||
} else if (step === 2) {
|
||||
html = this.renderMemoryStep('那段纯真的时光', '童年记忆', 'reg-child-text', 'reg-child-date', 'childhood', state.registrationData.childhood);
|
||||
} else if (step === 3) {
|
||||
html = this.renderMemoryStep('光芒闪耀的时刻', '开心的经历', 'reg-joy-text', 'reg-joy-date', 'joy', state.registrationData.joy);
|
||||
} else if (step === 4) {
|
||||
html = this.renderMemoryStep('在暗夜中潜行', '沮丧与低谷', 'reg-low-text', 'reg-low-date', 'low', state.registrationData.low);
|
||||
} else if (step === 5) {
|
||||
html = `
|
||||
<div class="animate-fade-in space-y-8">
|
||||
<div class="mb-6">
|
||||
<h2 class="text-4xl font-serif mb-3">未来想成为谁?</h2>
|
||||
<p class="text-white/40 italic text-sm">勾勒你对理想生活的全部向往。</p>
|
||||
</div>
|
||||
${UI.renderTextArea('对未来的憧憬', 'reg-future-vision', '你想成为一个什么样的人?', state.registrationData.future.vision || '')}
|
||||
${UI.renderTextArea('理想生活状态', 'reg-future-ideal', '你的理想清晨与傍晚是怎样的?', state.registrationData.future.ideal || '')}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
content.innerHTML = html;
|
||||
lucide.createIcons();
|
||||
},
|
||||
|
||||
renderMemoryStep(title, label, textId, dateId, type, data) {
|
||||
return `
|
||||
<div class="animate-fade-in space-y-6">
|
||||
<div>
|
||||
<h2 class="text-4xl font-serif mb-3">${title}</h2>
|
||||
<p class="text-white/40 italic text-sm">回望足迹,这些瞬间如何塑造了此时的你。</p>
|
||||
</div>
|
||||
<div class="space-y-4">
|
||||
<div class="flex flex-col gap-2">
|
||||
<label class="text-[10px] text-white/30 uppercase tracking-widest font-bold">${label}的日期</label>
|
||||
<input type="date" id="${dateId}" value="${data.date || ''}" class="glass-input max-w-xs">
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<label class="text-[10px] text-white/30 uppercase tracking-widest font-bold">详细描述</label>
|
||||
<textarea id="${textId}" rows="5" class="glass-input w-full text-sm" placeholder="描述那段时光发生的点滴...">${data.text || ''}</textarea>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2 pt-2">
|
||||
${UI.renderInspiration(type, textId)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
},
|
||||
|
||||
saveStepData() {
|
||||
const step = state.currentStep;
|
||||
if (step === 1) {
|
||||
state.updateRegistration({
|
||||
nickname: document.getElementById('reg-nickname').value,
|
||||
gender: document.getElementById('reg-gender').value,
|
||||
mbti: document.getElementById('reg-mbti').value,
|
||||
zodiac: document.getElementById('reg-zodiac').value,
|
||||
hobbies: document.getElementById('reg-hobbies').value.split(',').map(s => s.trim()).filter(s => s)
|
||||
});
|
||||
} else if (step === 2) {
|
||||
state.updateRegistration({ childhood: { date: document.getElementById('reg-child-date').value, text: document.getElementById('reg-child-text').value } });
|
||||
} else if (step === 3) {
|
||||
state.updateRegistration({ joy: { date: document.getElementById('reg-joy-date').value, text: document.getElementById('reg-joy-text').value } });
|
||||
} else if (step === 4) {
|
||||
state.updateRegistration({ low: { date: document.getElementById('reg-low-date').value, text: document.getElementById('reg-low-text').value } });
|
||||
} else if (step === 5) {
|
||||
state.updateRegistration({
|
||||
future: { vision: document.getElementById('reg-future-vision').value, ideal: document.getElementById('reg-future-ideal').value }
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
initEvents() {
|
||||
document.getElementById('next-step').onclick = () => {
|
||||
this.saveStepData();
|
||||
if (state.currentStep < 5) {
|
||||
state.currentStep++;
|
||||
this.renderStep(state.currentStep);
|
||||
} else {
|
||||
state.view = 'dashboard';
|
||||
state.save();
|
||||
if (this.onComplete) this.onComplete();
|
||||
}
|
||||
};
|
||||
document.getElementById('prev-step').onclick = () => {
|
||||
if (state.currentStep > 1) {
|
||||
state.currentStep--;
|
||||
this.renderStep(state.currentStep);
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,187 @@
|
||||
import { state } from './state.js';
|
||||
import { UI } from './components.js';
|
||||
|
||||
export const Registration = {
|
||||
render() {
|
||||
const container = document.getElementById('view-container');
|
||||
container.innerHTML = `
|
||||
<div id="step-container" class="w-full h-full flex flex-col items-center justify-center">
|
||||
<div id="step-content" class="glass-card w-full max-w-2xl p-8 min-h-[500px] flex flex-col justify-between overflow-y-auto">
|
||||
<!-- Step content will be injected here -->
|
||||
</div>
|
||||
<div class="mt-8 flex gap-3" id="step-indicators">
|
||||
<div class="step-indicator" data-step="1"></div>
|
||||
<div class="step-indicator" data-step="2"></div>
|
||||
<div class="step-indicator" data-step="3"></div>
|
||||
<div class="step-indicator" data-step="4"></div>
|
||||
<div class="step-indicator" data-step="5"></div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
this.goToStep(1);
|
||||
},
|
||||
|
||||
goToStep(step) {
|
||||
state.currentStep = step;
|
||||
const content = document.getElementById('step-content');
|
||||
|
||||
gsap.to(content, { opacity: 0, y: 10, duration: 0.3, onComplete: () => {
|
||||
this.renderStep(step, content);
|
||||
gsap.to(content, { opacity: 1, y: 0, duration: 0.5 });
|
||||
this.updateIndicators(step);
|
||||
lucide.createIcons();
|
||||
}});
|
||||
},
|
||||
|
||||
renderStep(step, el) {
|
||||
switch(step) {
|
||||
case 1:
|
||||
el.innerHTML = `
|
||||
<div class="text-center mb-6">
|
||||
<h2 class="text-3xl font-serif mb-2">你是谁?</h2>
|
||||
<p class="text-white/40">让我们从最基础的认知开始...</p>
|
||||
</div>
|
||||
<div class="space-y-4">
|
||||
${UI.renderInput('昵称', 'reg-nick', 'text', '你想如何称呼自己?')}
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
${UI.renderInput('星座', 'reg-zodiac', 'text', '如:天蝎座')}
|
||||
${UI.renderInput('MBTI', 'reg-mbti', 'text', '如:INFJ')}
|
||||
</div>
|
||||
${UI.renderInput('兴趣爱好', 'reg-hobbies', 'text', '用逗号分隔你的热爱')}
|
||||
</div>
|
||||
<div class="flex justify-end mt-8">
|
||||
<button id="btn-next" class="glass-btn px-8 py-3 rounded-full flex items-center gap-2">
|
||||
下一步 <i data-lucide="arrow-right" class="w-4 h-4"></i>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
document.getElementById('btn-next').onclick = () => {
|
||||
state.updateRegistration({
|
||||
nickname: document.getElementById('reg-nick').value,
|
||||
zodiac: document.getElementById('reg-zodiac').value,
|
||||
mbti: document.getElementById('reg-mbti').value,
|
||||
hobbies: document.getElementById('reg-hobbies').value.split(',')
|
||||
});
|
||||
this.goToStep(2);
|
||||
};
|
||||
break;
|
||||
|
||||
case 2: // Childhood
|
||||
el.innerHTML = `
|
||||
<div class="flex flex-col md:flex-row gap-8 h-full">
|
||||
<div class="flex-1">
|
||||
<h2 class="text-2xl font-serif mb-2">童年往事</h2>
|
||||
<p class="text-white/40 mb-6 text-sm">那个阶段,你的世界是什么颜色的?</p>
|
||||
${UI.renderInput('大约时间', 'reg-child-date', 'date')}
|
||||
${UI.renderTextArea('描述你的感受', 'reg-child-text', '那个午后,我在...')}
|
||||
<div class="flex flex-wrap gap-2 mt-2">${UI.renderInspiration('childhood', 'reg-child-text')}</div>
|
||||
</div>
|
||||
<div class="w-full md:w-1/3 flex items-center justify-center">
|
||||
<img src="https://r2-bucket.flowith.net/f/182f104dccfd0838/illustration_past_dimension_index_2%401024x1024.jpeg" class="rounded-2xl shadow-xl w-full aspect-square object-cover opacity-80">
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-between mt-8">
|
||||
<button onclick="window.reg_instance.goToStep(1)" class="text-white/40 hover:text-white">返回</button>
|
||||
<button id="btn-next" class="glass-btn px-8 py-3 rounded-full">继续探索</button>
|
||||
</div>
|
||||
`;
|
||||
document.getElementById('btn-next').onclick = () => {
|
||||
state.updateRegistration({ childhood: { date: document.getElementById('reg-child-date').value, text: document.getElementById('reg-child-text').value }});
|
||||
this.goToStep(3);
|
||||
};
|
||||
break;
|
||||
|
||||
case 3: // Joy
|
||||
el.innerHTML = `
|
||||
<div class="flex flex-col md:flex-row gap-8 h-full">
|
||||
<div class="flex-1">
|
||||
<h2 class="text-2xl font-serif mb-2">感到满足的瞬间</h2>
|
||||
<p class="text-white/40 mb-6 text-sm">那些让你嘴角上扬,感到充盈的经历。</p>
|
||||
${UI.renderInput('发生日期', 'reg-joy-date', 'date')}
|
||||
${UI.renderTextArea('那一刻发生了什么?', 'reg-joy-text', '微风拂过,我发现...')}
|
||||
<div class="flex flex-wrap gap-2 mt-2">${UI.renderInspiration('joy', 'reg-joy-text')}</div>
|
||||
</div>
|
||||
<div class="w-full md:w-1/3 flex items-center justify-center">
|
||||
<img src="https://r2-bucket.flowith.net/f/5ec7d5ffbfed2024/illustration_recording_present_moment_index_3%401024x1024.jpeg" class="rounded-2xl shadow-xl w-full aspect-square object-cover opacity-80">
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-between mt-8">
|
||||
<button onclick="window.reg_instance.goToStep(2)" class="text-white/40 hover:text-white">返回</button>
|
||||
<button id="btn-next" class="glass-btn px-8 py-3 rounded-full">继续探索</button>
|
||||
</div>
|
||||
`;
|
||||
document.getElementById('btn-next').onclick = () => {
|
||||
state.updateRegistration({ joy: { date: document.getElementById('reg-joy-date').value, text: document.getElementById('reg-joy-text').value }});
|
||||
this.goToStep(4);
|
||||
};
|
||||
break;
|
||||
|
||||
case 4: // Low points
|
||||
el.innerHTML = `
|
||||
<div class="flex flex-col md:flex-row gap-8 h-full">
|
||||
<div class="flex-1">
|
||||
<h2 class="text-2xl font-serif mb-2">沉潜的时光</h2>
|
||||
<p class="text-white/40 mb-6 text-sm">那些让你慢下来,向内生长的日子。</p>
|
||||
${UI.renderInput('大约时间', 'reg-low-date', 'date')}
|
||||
${UI.renderTextArea('你是如何度过的?', 'reg-low-text', '在寂静中,我开始思考...')}
|
||||
<div class="flex flex-wrap gap-2 mt-2">${UI.renderInspiration('low', 'reg-low-text')}</div>
|
||||
</div>
|
||||
<div class="w-full md:w-1/3 flex items-center justify-center">
|
||||
<div class="glass-card w-full aspect-square flex items-center justify-center bg-slate-800/50">
|
||||
<i data-lucide="cloud-rain" class="w-16 h-16 text-white/20"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-between mt-8">
|
||||
<button onclick="window.reg_instance.goToStep(3)" class="text-white/40 hover:text-white">返回</button>
|
||||
<button id="btn-next" class="glass-btn px-8 py-3 rounded-full">接近未来</button>
|
||||
</div>
|
||||
`;
|
||||
document.getElementById('btn-next').onclick = () => {
|
||||
state.updateRegistration({ low: { date: document.getElementById('reg-low-date').value, text: document.getElementById('reg-low-text').value }});
|
||||
this.goToStep(5);
|
||||
};
|
||||
break;
|
||||
|
||||
case 5: // Future
|
||||
el.innerHTML = `
|
||||
<div class="flex flex-col md:flex-row gap-8 h-full">
|
||||
<div class="flex-1">
|
||||
<h2 class="text-2xl font-serif mb-2">展望未来</h2>
|
||||
<p class="text-white/40 mb-6 text-sm">你渴望成为一个怎样的人?</p>
|
||||
${UI.renderTextArea('未来的愿景', 'reg-future-vision', '我希望在三年后...')}
|
||||
${UI.renderTextArea('理想生活状态', 'reg-future-ideal', '每天清晨,我能够...')}
|
||||
</div>
|
||||
<div class="w-full md:w-1/3 flex items-center justify-center">
|
||||
<img src="https://r2-bucket.flowith.net/f/5039a9a6936372c4/create_future_illustration_index_4%401024x1024.jpeg" class="rounded-2xl shadow-xl w-full aspect-square object-cover opacity-80">
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-between mt-8">
|
||||
<button onclick="window.reg_instance.goToStep(4)" class="text-white/40 hover:text-white">返回</button>
|
||||
<button id="btn-finish" class="glass-btn px-8 py-3 bg-orange-200/20 text-orange-200 border-orange-200/30 rounded-full">开启人生新篇章</button>
|
||||
</div>
|
||||
`;
|
||||
document.getElementById('btn-finish').onclick = () => {
|
||||
state.updateRegistration({ future: { vision: document.getElementById('reg-future-vision').value, ideal: document.getElementById('reg-future-ideal').value }});
|
||||
this.completeRegistration();
|
||||
};
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
updateIndicators(step) {
|
||||
document.querySelectorAll('.step-indicator').forEach(el => {
|
||||
el.classList.toggle('active', parseInt(el.dataset.step) === step);
|
||||
});
|
||||
},
|
||||
|
||||
completeRegistration() {
|
||||
const loader = document.getElementById('loader');
|
||||
loader.classList.remove('hidden');
|
||||
setTimeout(() => {
|
||||
state.view = 'dashboard';
|
||||
window.location.reload(); // Simple way to re-init app view
|
||||
}, 1500);
|
||||
}
|
||||
};
|
||||
window.reg_instance = Registration;
|
||||
@@ -0,0 +1,78 @@
|
||||
export const state = {
|
||||
isLoggedIn: false,
|
||||
phone: '',
|
||||
view: 'login', // 'login' | 'onboarding' | 'dashboard'
|
||||
currentStep: 1,
|
||||
registrationData: {
|
||||
nickname: '',
|
||||
gender: '',
|
||||
zodiac: '',
|
||||
mbti: '',
|
||||
profession: '',
|
||||
hobbies: [],
|
||||
childhood: { date: '', text: '' },
|
||||
joy: { date: '', text: '' },
|
||||
low: { date: '', text: '' },
|
||||
future: { vision: '', ideal: '' }
|
||||
},
|
||||
lifeEvents: [],
|
||||
scripts: [], // Array of { id, theme, style, length, content, date, character }
|
||||
selectedScriptId: null,
|
||||
selectedPath: null,
|
||||
|
||||
save() {
|
||||
const dataToSave = {
|
||||
isLoggedIn: this.isLoggedIn,
|
||||
phone: this.phone,
|
||||
registrationData: this.registrationData,
|
||||
lifeEvents: this.lifeEvents,
|
||||
scripts: this.scripts,
|
||||
selectedScriptId: this.selectedScriptId,
|
||||
selectedPath: this.selectedPath,
|
||||
view: this.view
|
||||
};
|
||||
localStorage.setItem('life_trajectory_v3', JSON.stringify(dataToSave));
|
||||
},
|
||||
|
||||
load() {
|
||||
const saved = localStorage.getItem('life_trajectory_v3');
|
||||
if (saved) {
|
||||
try {
|
||||
const parsed = JSON.parse(saved);
|
||||
Object.assign(this, parsed);
|
||||
} catch (e) {
|
||||
console.error("Failed to load state:", e);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
updateRegistration(data) {
|
||||
this.registrationData = { ...this.registrationData, ...data };
|
||||
this.save();
|
||||
},
|
||||
|
||||
addLifeEvent(event) {
|
||||
this.lifeEvents.push({ ...event, id: Date.now() });
|
||||
this.save();
|
||||
},
|
||||
|
||||
addScript(script) {
|
||||
this.scripts.unshift({ ...script, id: Date.now(), date: new Date().toLocaleDateString() });
|
||||
this.selectedScriptId = this.scripts[0].id;
|
||||
this.save();
|
||||
},
|
||||
|
||||
getSelectedScript() {
|
||||
return this.scripts.find(s => s.id === this.selectedScriptId);
|
||||
},
|
||||
|
||||
setPath(path) {
|
||||
this.selectedPath = path;
|
||||
this.save();
|
||||
},
|
||||
|
||||
clear() {
|
||||
localStorage.removeItem('life_trajectory_v3');
|
||||
window.location.reload();
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,144 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Noto+Serif+SC:wght@300;600&family=Noto+Sans+SC:wght@300;400;500&display=swap');
|
||||
|
||||
:root {
|
||||
--glass-bg: rgba(15, 17, 26, 0.4);
|
||||
--glass-border: rgba(255, 255, 255, 0.08);
|
||||
--accent-orange: #FFAB91;
|
||||
--accent-blue: #81D4FA;
|
||||
--card-shadow: 0 20px 50px -12px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
font-family: 'Noto Sans SC', sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
.font-serif { font-family: 'Noto Serif SC', serif; }
|
||||
|
||||
/* Advanced Glassmorphism */
|
||||
.glass-card {
|
||||
background: var(--glass-bg);
|
||||
backdrop-filter: blur(25px) saturate(180%);
|
||||
-webkit-backdrop-filter: blur(25px) saturate(180%);
|
||||
border: 1px solid var(--glass-border);
|
||||
border-radius: 32px;
|
||||
box-shadow: var(--card-shadow);
|
||||
}
|
||||
|
||||
.glass-btn {
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||
transition: all 0.5s cubic-bezier(0.23, 1, 0.32, 1);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.glass-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border-color: rgba(255, 255, 255, 0.2);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.glass-btn:active { transform: scale(0.98); }
|
||||
|
||||
.glass-input {
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||
border-radius: 16px;
|
||||
padding: 14px 20px;
|
||||
color: white;
|
||||
outline: none;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.glass-input:focus {
|
||||
border-color: var(--accent-orange);
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
box-shadow: 0 0 20px rgba(255, 171, 145, 0.1);
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
@keyframes float {
|
||||
0%, 100% { transform: translate(0, 0) scale(1); }
|
||||
50% { transform: translate(5%, 5%) scale(1.1); }
|
||||
}
|
||||
|
||||
.animate-float { animation: float 15s infinite ease-in-out; }
|
||||
.animate-float-delayed { animation: float 20s infinite ease-in-out reverse; }
|
||||
|
||||
.page-transition-enter { opacity: 0; transform: translateY(20px) scale(0.98); }
|
||||
.page-transition-active { opacity: 1; transform: translateY(0) scale(1); transition: all 0.8s cubic-bezier(0.16, 1, 0.3, 1); }
|
||||
|
||||
/* Timeline UI */
|
||||
.timeline-dot {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border: 2px solid var(--accent-orange);
|
||||
background: #0a0c10;
|
||||
box-shadow: 0 0 10px var(--accent-orange);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.timeline-line {
|
||||
position: absolute;
|
||||
left: 21px;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 1px;
|
||||
background: linear-gradient(180deg, transparent, rgba(255, 255, 255, 0.1) 15%, rgba(255, 255, 255, 0.1) 85%, transparent);
|
||||
}
|
||||
|
||||
/* Scrollbar Refinement */
|
||||
.custom-scrollbar::-webkit-scrollbar { width: 4px; }
|
||||
.custom-scrollbar::-webkit-scrollbar-track { background: transparent; }
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border-radius: 10px;
|
||||
}
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb:hover { background: rgba(255, 255, 255, 0.15); }
|
||||
|
||||
/* Responsive Form Adjustments */
|
||||
@media (max-width: 768px) {
|
||||
#view-container { height: 95vh; border-radius: 0; }
|
||||
.glass-card { border-radius: 20px; }
|
||||
.nav-item span { display: none; }
|
||||
.nav-item i { margin-right: 0 !important; }
|
||||
}
|
||||
|
||||
.prompt-tag {
|
||||
display: inline-block;
|
||||
padding: 6px 16px;
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: 99px;
|
||||
font-size: 0.75rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
user-select: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.prompt-tag:hover {
|
||||
background: var(--accent-orange);
|
||||
color: #000;
|
||||
border-color: var(--accent-orange);
|
||||
}
|
||||
|
||||
/* Sidebar Active State */
|
||||
.nav-item.active {
|
||||
background: rgba(255, 171, 145, 0.08);
|
||||
border-color: rgba(255, 171, 145, 0.2);
|
||||
color: var(--accent-orange) !important;
|
||||
}
|
||||
|
||||
/* AI Glow Effect */
|
||||
.ai-glow-card {
|
||||
background: linear-gradient(145deg, rgba(255,255,255,0.02) 0%, rgba(255,255,255,0.05) 100%);
|
||||
border-left: 2px solid var(--accent-orange);
|
||||
box-shadow: inset 0 0 20px rgba(255, 171, 145, 0.05);
|
||||
}
|
||||
Reference in New Issue
Block a user