feat: 优化管理后台页面UI、修复TS编译错误、新增人生事件模块

- 优化 AI 配置列表页面:重构统计卡片、搜索表单、表格列展示
- 修复 3 处 TypeScript TS6133 编译错误,恢复构建
- 新增管理员修改密码和重置密码功能
- 优化小程序多个页面样式和交互
- 人生事件模块完善

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-10 23:23:09 +08:00
parent 60c63850ee
commit 755059807a
62 changed files with 4661 additions and 3019 deletions
+258
View File
@@ -0,0 +1,258 @@
import { state } from './data.js';
import { RegistrationWizard, AppPages } from './components.js';
const DOM = {
onboarding: document.getElementById('onboarding'),
wizardContent: document.getElementById('wizard-content'),
mainContent: document.getElementById('main-content'),
pageRender: document.getElementById('page-render'),
bottomNav: document.getElementById('bottom-nav'),
musicToggle: document.getElementById('music-toggle'),
bgMusic: document.getElementById('bg-music'),
musicDisc: document.getElementById('music-disc'),
profileBtn: document.getElementById('profile-btn')
};
function initStars() {
const starContainer = document.getElementById('stars');
starContainer.innerHTML = '';
for (let i = 0; i < 60; i++) {
const star = document.createElement('div');
star.className = 'star';
const size = Math.random() * 3 + 1;
const xMove = (Math.random() - 0.5) * 100;
const yMove = (Math.random() - 0.5) * 100;
const duration = 15 + Math.random() * 20;
const delay = Math.random() * -20;
const opacity = 0.2 + Math.random() * 0.5;
star.style.width = `${size}px`;
star.style.height = `${size}px`;
star.style.left = `${Math.random() * 100}%`;
star.style.top = `${Math.random() * 100}%`;
star.style.setProperty('--x', `${xMove}px`);
star.style.setProperty('--y', `${yMove}px`);
star.style.setProperty('--duration', `${duration}s`);
star.style.setProperty('--opacity', opacity);
star.style.animationDelay = `${delay}s`;
starContainer.appendChild(star);
}
}
function updateWizard() {
DOM.wizardContent.innerHTML = `
<div class="flex-1 overflow-y-auto custom-scroll">
${RegistrationWizard.renderStep(state.onboardingStep, state.user)}
</div>
<div class="pt-6 flex justify-between items-center">
<div class="flex gap-1.5">
${[0, 1, 2, 3, 4].map(i => `<div class="w-6 h-1 rounded-full transition-all duration-700 ${i === state.onboardingStep ? 'bg-purple-400 w-10 shadow-[0_0_12px_rgba(168,85,247,0.6)]' : 'bg-white/10'}"></div>`).join('')}
</div>
<button id="next-step" class="px-10 py-3.5 bg-gradient-to-r from-purple-500 to-violet-400 rounded-2xl text-white text-sm font-bold shadow-xl shadow-purple-500/10 active:scale-95 transition-all">
${state.onboardingStep === 4 ? '同步星海' : '继续'}
</button>
</div>
`;
lucide.createIcons();
document.getElementById('next-step').addEventListener('click', () => {
saveCurrentStepData();
if (state.onboardingStep < 4) {
state.onboardingStep++;
gsap.fromTo('#wizard-content > div:first-child', { opacity: 0, y: 20 }, { opacity: 1, y: 0, duration: 0.5, ease: "power2.out" });
updateWizard();
} else {
completeOnboarding();
}
});
}
function saveCurrentStepData() {
const s = state.onboardingStep;
if (s === 0) {
state.user.nickname = document.getElementById('nickname').value;
state.user.gender = document.getElementById('gender').value;
state.user.zodiac = document.getElementById('zodiac').value;
state.user.mbti = document.getElementById('mbti').value;
state.user.hobbies = document.getElementById('hobbies').value.split(',').map(v => v.trim()).filter(v => v);
} else if (s === 1) {
state.user.childhood = document.getElementById('childhood').value;
state.user.childhoodDate = document.getElementById('childhood-date').value;
} else if (s === 2) {
state.user.happyMoment = document.getElementById('happy-moment').value;
state.user.happyDate = document.getElementById('happy-date').value;
} else if (s === 3) {
state.user.lowPoint = document.getElementById('low-point').value;
state.user.lowDate = document.getElementById('low-date').value;
} else if (s === 4) {
state.user.aspirations = document.getElementById('aspirations').value;
}
}
function completeOnboarding() {
gsap.to(DOM.onboarding, {
opacity: 0,
duration: 1.5,
ease: "power3.inOut",
onComplete: () => {
DOM.onboarding.style.display = 'none';
DOM.mainContent.style.display = 'flex';
DOM.bottomNav.style.display = 'flex';
switchTab('record');
}
});
}
function switchTab(tab) {
document.querySelectorAll('.nav-item').forEach(el => {
el.classList.toggle('active', el.dataset.tab === tab);
});
let content = "";
if (tab === 'record') content = AppPages.record(state.events);
if (tab === 'script') content = AppPages.script(state.scripts, state.isGenerating, state.scriptConfig, state.npcConfig, state.user, state.customPersonas);
if (tab === 'path') content = AppPages.path(state.currentPath);
if (tab === 'profile') content = AppPages.profile(state.user);
DOM.pageRender.innerHTML = content;
lucide.createIcons();
if (tab === 'record') {
document.getElementById('save-event').addEventListener('click', handleSaveEvent);
}
if (tab === 'script') {
document.getElementById('gen-script')?.addEventListener('click', handleGenerateScript);
document.getElementById('edit-name')?.addEventListener('input', (e) => state.user.nickname = e.target.value);
document.getElementById('edit-zodiac')?.addEventListener('change', (e) => state.user.zodiac = e.target.value);
document.getElementById('edit-mbti')?.addEventListener('change', (e) => state.user.mbti = e.target.value);
document.getElementById('edit-job')?.addEventListener('input', (e) => state.user.profession = e.target.value);
document.getElementById('add-persona-btn')?.addEventListener('click', () => {
const name = document.getElementById('npc-name').value;
const role = document.getElementById('npc-role').value;
if (name) {
state.customPersonas.push({ name, role, id: Date.now() });
switchTab('script');
}
});
document.querySelectorAll('.delete-persona-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
const idx = e.currentTarget.dataset.idx;
state.customPersonas.splice(idx, 1);
switchTab('script');
});
});
document.getElementById('npc-name')?.addEventListener('input', (e) => state.npcConfig.name = e.target.value);
document.getElementById('npc-role')?.addEventListener('change', (e) => state.npcConfig.role = e.target.value);
document.getElementById('npc-relation')?.addEventListener('change', (e) => state.npcConfig.relation = e.target.value);
document.getElementById('npc-desc')?.addEventListener('input', (e) => state.npcConfig.desc = e.target.value);
document.querySelectorAll('.style-btn').forEach(btn => {
btn.addEventListener('click', () => {
state.scriptConfig.style = btn.dataset.val;
switchTab('script');
});
});
document.querySelectorAll('.length-btn').forEach(btn => {
btn.addEventListener('click', () => {
state.scriptConfig.length = btn.dataset.val;
switchTab('script');
});
});
document.querySelectorAll('.select-script').forEach(btn => {
btn.addEventListener('click', (e) => {
const id = e.currentTarget.dataset.id;
state.currentPath = state.scripts.find(s => s.id == id);
switchTab('path');
});
});
}
}
function handleSaveEvent() {
const title = document.getElementById('event-title').value;
const time = document.getElementById('event-time').value;
const content = document.getElementById('event-content').value;
if (!title || !content) return;
const newEvent = {
id: Date.now(),
title,
time: time || new Date().toISOString().split('T')[0],
content,
aiReply: "星河守望者正在解读这段紫色波频...",
isNew: true
};
state.events.unshift(newEvent);
switchTab('record');
setTimeout(() => {
const fullReply = `这段记忆碎片在星海中漾起涟漪。你在${title}中展现的特质,正在重新定义你人生OS的底层代码。继续保持这份觉知。`;
state.events[0].aiReply = fullReply;
state.events[0].isNew = false;
switchTab('record');
}, 1500);
}
function handleGenerateScript() {
const theme = document.getElementById('script-theme').value;
if (!theme) return;
if (state.isGenerating) return;
state.isGenerating = true;
switchTab('script');
setTimeout(() => {
const { name, role, relation, desc } = state.npcConfig;
const npcFrag = name ? `,在 ${name}(你的${role},关系:${relation})的陪伴下,` : "";
const script = {
id: Date.now(),
title: `${theme}》· 星源篇`,
summary: `基于你(${state.user.nickname},一名${state.user.profession})过往的紫气东来,在${state.scriptConfig.style}的世界观中${npcFrag}你将重新定义自我。${desc ? '命运齿轮已然转动:' + desc : ''}`,
persona: state.user.mbti || "追光者",
steps: [
{ task: "紫微入命", desc: `在"${theme}"的意志中开启新的人生循环。`, done: true },
{ task: "能量共振", desc: name ? `${name} 达成灵魂契约。` : "在孤独中寻找真理,掌握世界的运行法则。", done: true },
{ task: "代码重写", desc: "在最为艰难的时刻,将过去的所有记录化作突围的力量。", done: false },
{ task: "最终归宿", desc: "达成人生OS的最高成就,实现与理想自我的合一。", done: false }
]
};
state.scripts.unshift(script);
state.isGenerating = false;
switchTab('script');
}, 2500);
}
DOM.musicToggle.addEventListener('click', () => {
if (state.isPlaying) {
DOM.bgMusic.pause();
DOM.musicDisc.classList.remove('animate-spin-slow');
DOM.musicToggle.classList.add('opacity-40');
} else {
DOM.bgMusic.play();
DOM.musicDisc.classList.add('animate-spin-slow');
DOM.musicToggle.classList.remove('opacity-40');
}
state.isPlaying = !state.isPlaying;
});
DOM.bottomNav.addEventListener('click', (e) => {
const btn = e.target.closest('button');
if (btn) switchTab(btn.dataset.tab);
});
DOM.profileBtn.addEventListener('click', () => switchTab('profile'));
initStars();
updateWizard();
lucide.createIcons();
+334
View File
@@ -0,0 +1,334 @@
import { zodiacs, mbtis, hintWords, scriptStyles, scriptLengths, npcRoles, npcRelations } from './data.js';
export const RegistrationWizard = {
renderStep: (step, user) => {
const steps = [
`<div class="space-y-6">
<div class="space-y-2">
<h2 class="text-3xl font-light tracking-tight text-white">系统初始化</h2>
<p class="text-purple-400/60 text-sm italic">定义你在人生OS中的唯一代码</p>
</div>
<div class="space-y-4">
<input id="nickname" type="text" placeholder="你的昵称" class="w-full bg-transparent border-b border-purple-500/30 py-3 focus:border-purple-400 outline-none transition-all" value="${user.nickname}">
<div class="grid grid-cols-2 gap-4">
<select id="gender" class="bg-white/5 p-3 rounded-xl border border-white/10 outline-none text-purple-100">
<option value="">性别</option><option value="male" ${user.gender==='male'?'selected':''}>男性</option><option value="female" ${user.gender==='female'?'selected':''}>女性</option>
</select>
<select id="zodiac" class="bg-white/5 p-3 rounded-xl border border-white/10 outline-none text-purple-100">
<option value="">星座</option>
${zodiacs.map(z => `<option value="${z}" ${user.zodiac===z?'selected':''}>${z}</option>`).join('')}
</select>
</div>
<select id="mbti" class="w-full bg-white/5 p-3 rounded-xl border border-white/10 outline-none text-purple-100">
<option value="">MBTI人格</option>
${mbtis.map(m => `<option value="${m}" ${user.mbti===m?'selected':''}>${m}</option>`).join('')}
</select>
<input id="hobbies" type="text" placeholder="兴趣爱好 (逗号分隔)" class="w-full bg-transparent border-b border-purple-500/30 py-3 focus:border-purple-400 outline-none" value="${user.hobbies.join(', ')}">
</div>
</div>`,
`<div class="space-y-6">
<div class="space-y-2">
<h2 class="text-3xl font-light text-white">回溯起源</h2>
<p class="text-purple-400/60 text-sm">一段让你感到温暖的早期时光</p>
</div>
<div class="space-y-4">
<input type="date" id="childhood-date" value="${user.childhoodDate}" class="w-full bg-white/5 p-3 rounded-xl border border-white/10 text-sm text-purple-200">
<textarea id="childhood" placeholder="回忆一段具体且温暖的午后..." class="w-full h-32 bg-white/5 p-4 rounded-xl border border-white/10 outline-none resize-none">${user.childhood}</textarea>
<div class="hint-container">
<p class="text-[10px] text-purple-400/50 mb-3 px-1 flex items-center gap-2 uppercase tracking-widest">
<i data-lucide="sparkles" class="w-3 h-3"></i> 灵感气泡
</p>
<div class="flex flex-wrap gap-2">
${hintWords.childhood.map((h, i) => `<button class="hint-chip bubble-anim" style="--delay: ${i}" onclick="document.getElementById('childhood').value += '${h} '"><span>${h}</span></button>`).join('')}
</div>
</div>
</div>
</div>`,
`<div class="space-y-6">
<div class="space-y-2">
<h2 class="text-3xl font-light text-white">悦然时刻</h2>
<p class="text-purple-400/60 text-sm">一段感受到生命跃动的经历</p>
</div>
<div class="space-y-4">
<input type="date" id="happy-date" value="${user.happyDate}" class="w-full bg-white/5 p-3 rounded-xl border border-white/10 text-sm text-purple-200">
<textarea id="happy-moment" placeholder="想一个令你会心一笑的瞬间..." class="w-full h-32 bg-white/5 p-4 rounded-xl border border-white/10 outline-none resize-none">${user.happyMoment}</textarea>
<div class="hint-container">
<p class="text-[10px] text-purple-400/50 mb-3 px-1 flex items-center gap-2 uppercase tracking-widest">
<i data-lucide="sparkles" class="w-3 h-3"></i> 灵感气泡
</p>
<div class="flex flex-wrap gap-2">
${hintWords.happy.map((h, i) => `<button class="hint-chip bubble-anim" style="--delay: ${i}" onclick="document.getElementById('happy-moment').value += '${h} '"><span>${h}</span></button>`).join('')}
</div>
</div>
</div>
</div>`,
`<div class="space-y-6">
<div class="space-y-2">
<h2 class="text-3xl font-light text-white">破茧成蝶</h2>
<p class="text-purple-400/60 text-sm">在宁静中默默积蓄力量的日子</p>
</div>
<div class="space-y-4">
<input type="date" id="low-date" value="${user.lowDate}" class="w-full bg-white/5 p-3 rounded-xl border border-white/10 text-sm text-purple-200">
<textarea id="low-point" placeholder="描述那段有些艰难但让你成长的日子..." class="w-full h-32 bg-white/5 p-4 rounded-xl border border-white/10 outline-none resize-none">${user.lowPoint}</textarea>
<div class="hint-container">
<p class="text-[10px] text-purple-400/50 mb-3 px-1 flex items-center gap-2 uppercase tracking-widest">
<i data-lucide="sparkles" class="w-3 h-3"></i> 灵感气泡
</p>
<div class="flex flex-wrap gap-2">
${hintWords.low.map((h, i) => `<button class="hint-chip bubble-anim" style="--delay: ${i}" onclick="document.getElementById('low-point').value += '${h} '"><span>${h}</span></button>`).join('')}
</div>
</div>
</div>
</div>`,
`<div class="space-y-6">
<div class="space-y-2">
<h2 class="text-3xl font-light text-white">未来憧憬</h2>
<p class="text-purple-400/60 text-sm">对未来理想生活状态的预见</p>
</div>
<div class="space-y-4">
<textarea id="aspirations" placeholder="你想成为怎样的人?尝试描述那幅画面..." class="w-full h-48 bg-white/5 p-4 rounded-xl border border-white/10 outline-none resize-none">${user.aspirations}</textarea>
<div class="flex items-center gap-2 p-4 glass-gold rounded-xl border-purple-500/30">
<i data-lucide="sparkles" class="w-5 h-5 text-purple-400"></i>
<span class="text-xs text-purple-200/80 italic">“照见未来,便是创造的开始。”</span>
</div>
</div>
</div>`
];
return steps[step];
}
};
export const AppPages = {
record: (events) => `
<div class="space-y-6 pb-4">
<div class="glass p-5 rounded-3xl space-y-4 border-white/10">
<div class="flex items-center gap-2 mb-2">
<div class="w-2 h-2 rounded-full bg-purple-400"></div>
<span class="text-[10px] font-bold text-purple-400 uppercase tracking-widest">记叙当下</span>
</div>
<input id="event-title" type="text" placeholder="为这段记忆命名" class="w-full bg-transparent border-b border-white/10 py-2 outline-none text-purple-50">
<input id="event-time" type="date" class="bg-transparent text-sm text-purple-400/60 outline-none">
<textarea id="event-content" placeholder="此刻的心境或发生的事..." class="w-full bg-transparent h-20 outline-none resize-none text-sm text-white/80"></textarea>
<button id="save-event" class="w-full py-3.5 bg-gradient-to-r from-purple-600/40 to-purple-400/40 border border-purple-500/30 rounded-2xl text-white text-sm font-medium hover:from-purple-600/60 transition-all">镌刻至星海</button>
</div>
<div class="space-y-6" id="event-list">
${events.map(ev => `
<div class="glass p-5 rounded-3xl space-y-3 relative overflow-hidden group border-white/5">
<div class="flex justify-between items-start">
<h3 class="font-medium text-purple-100">${ev.title}</h3>
<span class="text-[10px] text-purple-400/50">${ev.time}</span>
</div>
<p class="text-sm text-white/70 leading-relaxed">${ev.content}</p>
<div class="pt-3 border-t border-white/5 space-y-2 ai-card-glow-gold rounded-2xl p-4 bg-purple-500/5">
<div class="flex items-center gap-2 text-[10px] text-purple-400 font-bold uppercase tracking-widest">
<i data-lucide="sparkle" class="w-3 h-3 text-purple-400"></i> Life Harmony AI
</div>
<p class="text-xs text-purple-100/80 italic leading-snug ${ev.isNew ? 'typing-text' : ''}">${ev.aiReply}</p>
</div>
</div>
`).join('')}
</div>
</div>
`,
script: (scripts, isGenerating, config, npc, user, customPersonas) => `
<div class="space-y-6">
<h2 class="text-xl font-light text-purple-100">剧本生成器</h2>
<!-- User Base Info Section -->
<div class="glass p-5 rounded-3xl border-purple-500/20 space-y-4">
<div class="flex items-center justify-between">
<span class="text-[10px] text-purple-400 uppercase tracking-wider font-bold">我的基础人设</span>
<span class="text-[8px] text-white/30 italic">可自由修改</span>
</div>
<div class="grid grid-cols-2 gap-3">
<input id="edit-name" type="text" value="${user.nickname}" placeholder="姓名" class="bg-white/5 border border-white/10 rounded-xl p-3 text-[11px] outline-none text-white focus:border-purple-500/50">
<select id="edit-zodiac" class="bg-white/5 border border-white/10 rounded-xl p-3 text-[11px] outline-none text-white">
${zodiacs.map(z => `<option value="${z}" ${user.zodiac===z?'selected':''}>${z}</option>`).join('')}
</select>
<select id="edit-mbti" class="bg-white/5 border border-white/10 rounded-xl p-3 text-[11px] outline-none text-white">
${mbtis.map(m => `<option value="${m}" ${user.mbti===m?'selected':''}>${m}</option>`).join('')}
</select>
<input id="edit-job" type="text" value="${user.profession}" placeholder="职业" class="bg-white/5 border border-white/10 rounded-xl p-3 text-[11px] outline-none text-white focus:border-purple-500/50">
</div>
</div>
<div class="glass p-6 rounded-[2rem] border-white/10 space-y-6">
<div class="space-y-2">
<label class="text-[10px] text-purple-400 uppercase tracking-wider font-bold">1. 剧本主题</label>
<input id="script-theme" type="text" placeholder="如:巅峰重现、治愈之旅、赛博觉醒..." class="w-full bg-white/5 border border-white/10 rounded-2xl p-4 text-sm outline-none focus:border-purple-400/50 transition-all">
</div>
<!-- NPC / Other Persona Setup -->
<div class="space-y-4">
<div class="flex items-center justify-between">
<label class="text-[10px] text-purple-400 uppercase tracking-wider font-bold">2. 关键配角/新的人设</label>
<button id="add-persona-btn" class="flex items-center gap-1 text-[9px] text-purple-400 border border-purple-400/30 px-2 py-1 rounded-lg hover:bg-purple-500/10 transition-all">
<i data-lucide="plus" class="w-3 h-3"></i> 添加
</button>
</div>
<div class="bg-white/5 border border-white/10 rounded-2xl p-4 space-y-4">
<div class="grid grid-cols-3 gap-2">
<input id="npc-name" type="text" placeholder="姓名" value="${npc.name}" class="bg-white/5 border border-white/10 rounded-xl p-3 text-[10px] outline-none text-white focus:border-purple-500/50">
<select id="npc-role" class="bg-white/5 border border-white/10 rounded-xl p-3 text-[10px] outline-none text-white">
${npcRoles.map(r => `<option value="${r}" ${npc.role===r?'selected':''}>${r}</option>`).join('')}
</select>
<select id="npc-relation" class="bg-white/5 border border-white/10 rounded-xl p-3 text-[10px] outline-none text-white">
${npcRelations.map(r => `<option value="${r}" ${npc.relation===r?'selected':''}>${r}</option>`).join('')}
</select>
</div>
<textarea id="npc-desc" placeholder="自由描述TA的人设特点或关键剧情点..." class="w-full h-16 bg-white/5 border border-white/10 rounded-xl p-3 text-[10px] outline-none resize-none text-white focus:border-purple-500/50">${npc.desc}</textarea>
</div>
<!-- Custom Personas List -->
<div class="flex flex-wrap gap-2">
${customPersonas.map((p, idx) => `
<div class="group relative px-3 py-1.5 bg-purple-500/10 border border-purple-500/30 rounded-full flex items-center gap-2">
<span class="text-[10px] text-purple-200">${p.name} (${p.role})</span>
<button class="delete-persona-btn opacity-40 hover:opacity-100 transition-opacity" data-idx="${idx}">
<i data-lucide="x" class="w-3 h-3"></i>
</button>
</div>
`).join('')}
</div>
</div>
<div class="space-y-2">
<label class="text-[10px] text-purple-400 uppercase tracking-wider font-bold">3. 核心参数</label>
<div class="grid grid-cols-2 gap-3">
<div class="space-y-2">
<p class="text-[9px] text-white/40 ml-1">叙事风格</p>
<div class="flex flex-wrap gap-1.5">
${scriptStyles.map(s => `
<button data-val="${s}" class="style-btn px-3 py-1.5 rounded-xl text-[10px] border transition-all ${config.style === s ? 'bg-purple-400/20 border-purple-400 text-purple-300' : 'bg-white/5 border-white/5 text-white/40'}">${s}</button>
`).join('')}
</div>
</div>
<div class="space-y-2">
<p class="text-[9px] text-white/40 ml-1">故事篇幅</p>
<div class="flex flex-wrap gap-1.5">
${scriptLengths.map(l => `
<button data-val="${l}" class="length-btn px-3 py-1.5 rounded-xl text-[10px] border transition-all ${config.length === l ? 'bg-purple-400/20 border-purple-400 text-purple-300' : 'bg-white/5 border-white/5 text-white/40'}">${l}</button>
`).join('')}
</div>
</div>
</div>
</div>
<button id="gen-script" class="w-full py-4 mt-2 bg-gradient-to-r from-purple-500 to-violet-500 text-white rounded-2xl font-bold text-sm shadow-[0_8px_20px_rgba(168,85,247,0.3)] flex items-center justify-center gap-2 active:scale-95 transition-all">
${isGenerating ? '<div class="starlight-gathering-gold w-4 h-4"></div> 命运编织中...' : '生成平行人生剧本'}
</button>
</div>
${scripts.length === 0 && !isGenerating ? `
<div class="h-40 glass rounded-[2rem] flex flex-col items-center justify-center p-8 text-center space-y-4 opacity-40">
<i data-lucide="film" class="w-8 h-8 text-purple-500"></i>
<p class="text-[10px] text-white/60">尚未生成剧本,定义你的未来篇章</p>
</div>
` : ''}
${isGenerating ? `
<div class="glass p-10 rounded-[2rem] flex flex-col items-center gap-4 animate-pulse border-purple-500/30">
<div class="starlight-gathering-gold"></div>
<p class="text-xs text-purple-300/60 font-serif tracking-widest">正在采集星海中的深紫色碎屑...</p>
</div>
` : scripts.map(s => `
<div class="glass p-6 rounded-[2rem] space-y-4 border-l-4 border-purple-400 relative overflow-hidden group">
<div class="absolute top-0 right-0 p-3 opacity-10 group-hover:opacity-30 transition-opacity">
<i data-lucide="crown" class="w-12 h-12 text-purple-400"></i>
</div>
<h3 class="text-lg text-purple-100 font-medium">${s.title}</h3>
<p class="text-xs text-white/50 line-clamp-3 leading-relaxed">${s.summary}</p>
<div class="flex justify-between items-center pt-2">
<span class="px-3 py-1 bg-purple-400/10 text-purple-400 text-[10px] rounded-full border border-purple-400/20">${s.persona}</span>
<button class="text-xs text-purple-400 font-bold select-script flex items-center gap-1" data-id="${s.id}">
路径映射 <i data-lucide="arrow-right" class="w-3 h-3"></i>
</button>
</div>
</div>
`).join('')}
</div>
`,
path: (activePath) => `
<div class="space-y-6">
<h2 class="text-xl font-light text-purple-100">实现路径</h2>
${!activePath ? `
<div class="glass p-12 rounded-[2rem] text-center space-y-4 border-white/5">
<i data-lucide="milestone" class="mx-auto w-10 h-10 text-purple-500/20"></i>
<p class="text-xs text-purple-300/40 italic">“未来尚未写就,请先在剧本页选择一个目标。”</p>
</div>
` : `
<div class="space-y-6">
<div class="p-6 glass-gold rounded-[2rem] border-purple-400/30 mb-4">
<div class="text-[10px] uppercase tracking-widest text-purple-400 mb-2 font-bold">目标:${activePath.title}</div>
<div class="text-sm font-medium text-purple-100">${activePath.summary.slice(0, 80)}...</div>
</div>
<div class="relative pl-8 space-y-12">
<div class="absolute left-[15px] top-4 bottom-4 w-0.5 bg-gradient-to-b from-purple-400 via-purple-400/20 to-transparent opacity-30"></div>
${activePath.steps.map((step, i) => `
<div class="relative">
<div class="absolute -left-10 w-6 h-6 rounded-full border-2 border-purple-400 bg-[#080B12] flex items-center justify-center z-10 shadow-[0_0_15px_rgba(168,85,247,0.4)]">
<div class="w-2 h-2 rounded-full ${step.done ? 'bg-purple-400 animate-pulse' : 'bg-white/10'}"></div>
</div>
<div class="glass p-5 rounded-[1.5rem] ${step.done ? 'border-purple-400/30' : 'opacity-40'}">
<div class="text-[10px] text-purple-400 font-bold mb-1 uppercase tracking-tighter">节点 ${i+1}</div>
<h4 class="text-sm font-medium mb-1 text-white">${step.task}</h4>
<p class="text-[10px] text-white/50 leading-relaxed">${step.desc}</p>
</div>
</div>
`).join('')}
</div>
</div>
`}
</div>
`,
profile: (user) => `
<div class="space-y-8 pb-10">
<div class="flex flex-col items-center text-center space-y-4">
<div class="w-24 h-24 rounded-full border-2 border-purple-400/30 p-1 relative shadow-[0_0_40px_rgba(168,85,247,0.1)]">
<img src="https://api.dicebear.com/7.x/avataaars/svg?seed=${user.nickname || 'life'}&backgroundColor=A855F7" class="w-full h-full rounded-full bg-purple-900/10">
<div class="absolute bottom-1 right-1 w-6 h-6 bg-purple-500 rounded-full border-2 border-[#080B12] flex items-center justify-center">
<i data-lucide="shield-check" class="w-3 h-3 text-white"></i>
</div>
</div>
<div>
<h2 class="text-2xl font-light text-purple-50">${user.nickname || '未同步系统'}</h2>
<p class="text-[10px] text-purple-400 font-mono tracking-widest uppercase">${user.mbti || 'QUESTER'} · ${user.zodiac || 'STAR'} · ${user.profession || '星民'}</p>
</div>
</div>
<div class="grid grid-cols-2 gap-4">
<div class="glass p-5 rounded-[1.5rem] flex flex-col items-center gap-1">
<span class="text-[10px] text-white/30 uppercase tracking-widest">觉醒深度</span>
<span class="text-xl font-light text-purple-200">Lv.4</span>
</div>
<div class="glass p-5 rounded-[1.5rem] flex flex-col items-center gap-1">
<span class="text-[10px] text-white/30 uppercase tracking-widest">星历契合</span>
<span class="text-xl font-light text-purple-200">98%</span>
</div>
</div>
<div class="space-y-3">
<button class="w-full glass p-5 rounded-[1.5rem] flex justify-between items-center text-sm hover:bg-white/5 transition-all">
<div class="flex items-center gap-4 text-white/80"><i data-lucide="user" class="w-5 h-5 text-purple-400"></i><span>个人档案设置</span></div>
<i data-lucide="chevron-right" class="w-4 h-4 opacity-30"></i>
</button>
<button class="w-full glass p-5 rounded-[1.5rem] flex justify-between items-center text-sm hover:bg-white/5 transition-all">
<div class="flex items-center gap-4 text-white/80"><i data-lucide="layers" class="w-5 h-5 text-purple-400"></i><span>多账号切换</span></div>
<i data-lucide="chevron-right" class="w-4 h-4 opacity-30"></i>
</button>
<button class="w-full glass p-5 rounded-[1.5rem] flex justify-between items-center text-sm hover:bg-white/5 transition-all">
<div class="flex items-center gap-4 text-white/80"><i data-lucide="mail" class="w-5 h-5 text-purple-400"></i><span>与开发者对话</span></div>
<i data-lucide="external-link" class="w-4 h-4 opacity-30"></i>
</button>
</div>
<button class="w-full py-4 text-[10px] text-white/20 font-bold tracking-[0.3em] uppercase hover:text-purple-500/50 transition-colors">Terminate Life Harmony</button>
</div>
`
};
+55
View File
@@ -0,0 +1,55 @@
export const state = {
user: {
nickname: "",
gender: "",
zodiac: "",
mbti: "",
profession: "星际探索者", // Added profession
hobbies: [],
childhood: "",
childhoodDate: "",
happyMoment: "",
happyDate: "",
lowPoint: "",
lowDate: "",
aspirations: "",
},
events: [
{
id: 1,
time: "2025-06-12",
title: "开启人生OS",
content: "在繁星之下的那晚,我决定建立人生OS,系统地梳理我的过去与未来。",
aiReply: "这是一个极具觉知力的决定。紫色的星云预示着智慧的觉醒,从这一刻起,你开始重新编写人生的底层代码。",
isNew: false
}
],
scripts: [],
customPersonas: [], // New for user-defined personas
scriptConfig: {
style: "爽文",
length: "中篇"
},
npcConfig: {
name: "",
role: "伙伴",
relation: "信任",
desc: ""
},
currentPath: null,
onboardingStep: 0,
isPlaying: false,
isGenerating: false
};
export const zodiacs = ["白羊座", "金牛座", "双子座", "巨蟹座", "狮子座", "处女座", "天秤座", "天蝎座", "射手座", "摩羯座", "水瓶座", "双鱼座"];
export const mbtis = ["INTJ", "INTP", "ENTJ", "ENTP", "INFJ", "INFP", "ENFJ", "ENFP", "ISTJ", "ISFJ", "ESTJ", "ESFJ", "ISTP", "ISFP", "ESTP", "ESFP"];
export const scriptStyles = ["爽文", "治愈", "热血", "玄幻", "职场", "赛博"];
export const scriptLengths = ["短篇", "中篇", "长篇", "史诗"];
export const npcRoles = ["伙伴", "宿敌", "导师", "挚爱", "下属", "路人"];
export const npcRelations = ["信任", "对立", "暧昧", "敬畏", "背叛", "守护"];
export const hintWords = {
childhood: ["秘密花园", "老旧弄堂", "秋千", "夏蝉", "被保护的", "好奇心"],
happy: ["突破", "共鸣", "清晨阳光", "认可", "旅行终点", "深呼吸"],
low: ["迷茫", "无力感", "雨后街道", "重新出发", "裂痕中的光", "蜕变"]
};
+40
View File
@@ -0,0 +1,40 @@
<svg width="1024" height="1024" viewBox="0 0 1024 1024" fill="none" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="logoGradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#a78bfa;stop-opacity:1" />
<stop offset="50%" style="stop-color:#7c3aed;stop-opacity:1" />
<stop offset="100%" style="stop-color:#5b21b6;stop-opacity:1" />
</linearGradient>
<filter id="glow">
<feGaussianBlur stdDeviation="20" result="coloredBlur"/>
<feMerge>
<feMergeNode in="coloredBlur"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
</defs>
<!-- Background -->
<rect width="1024" height="1024" rx="200" fill="url(#logoGradient)" filter="url(#glow)"/>
<!-- Inner Design - Abstract Mind/Brain Symbol -->
<circle cx="512" cy="512" r="280" fill="none" stroke="rgba(255,255,255,0.3)" stroke-width="8"/>
<circle cx="512" cy="512" r="200" fill="none" stroke="rgba(255,255,255,0.4)" stroke-width="8"/>
<circle cx="512" cy="512" r="120" fill="none" stroke="rgba(255,255,255,0.5)" stroke-width="8"/>
<!-- Center Symbol - Spark/Star -->
<path d="M512 380 L540 480 L640 512 L540 544 L512 644 L484 544 L384 512 L484 480 Z"
fill="white" opacity="0.9" filter="url(#glow)"/>
<!-- Decorative Lines -->
<path d="M512 200 L512 280" stroke="rgba(255,255,255,0.6)" stroke-width="4" stroke-linecap="round"/>
<path d="M512 744 L512 824" stroke="rgba(255,255,255,0.6)" stroke-width="4" stroke-linecap="round"/>
<path d="M200 512 L280 512" stroke="rgba(255,255,255,0.6)" stroke-width="4" stroke-linecap="round"/>
<path d="M744 512 L824 512" stroke="rgba(255,255,255,0.6)" stroke-width="4" stroke-linecap="round"/>
<!-- Corner Accents -->
<path d="M280 280 L340 280 L280 340 Z" fill="rgba(255,255,255,0.4)"/>
<path d="M744 280 L684 280 L744 340 Z" fill="rgba(255,255,255,0.4)"/>
<path d="M280 744 L340 744 L280 684 Z" fill="rgba(255,255,255,0.4)"/>
<path d="M744 744 L684 744 L744 684 Z" fill="rgba(255,255,255,0.4)"/>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

+11
View File
@@ -0,0 +1,11 @@
@echo off
chcp 65001 >nul
:: 启动 Life OS 原型预览服务器
echo 🚀 启动 Life OS 原型预览服务器...
echo 📱 访问地址:http://localhost:8080
echo 💡 按 Ctrl+C 停止服务器
echo ""
cd /d "%~dp0"
python -m http.server 18181
+10
View File
@@ -0,0 +1,10 @@
#!/bin/bash
# 启动 Life OS 原型预览服务器
echo "🚀 启动 Life OS 原型预览服务器..."
echo "📱 访问地址:http://localhost:8080"
echo "💡 按 Ctrl+C 停止服务器"
echo ""
cd "$(dirname "$0")"
python3 -m http.server 18181
+150
View File
@@ -0,0 +1,150 @@
@import url('https://fonts.googleapis.com/css2?family=Cinzel:wght@400;700&family=Inter:wght@300;400;500;600&display=swap');
:root {
--primary: #A855F7; /* Purple 500 */
--primary-light: #C084FC; /* Purple 400 */
--accent: #E879F9; /* Fuchsia 400 */
--bg-dark: #0F071A; /* Dark Deep Purple */
}
body {
font-family: 'Inter', -apple-system, sans-serif;
background-color: var(--bg-dark);
}
h1, h2, .font-serif {
font-family: 'Cinzel', serif;
}
.glass {
background: rgba(168, 85, 247, 0.05);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid rgba(168, 85, 247, 0.15);
}
.glass-gold {
background: linear-gradient(135deg, rgba(168, 85, 247, 0.15), rgba(232, 121, 249, 0.1));
border: 1px solid rgba(168, 85, 247, 0.3);
}
.ai-card-glow-gold {
background: rgba(168, 85, 247, 0.08);
border: 1px solid rgba(168, 85, 247, 0.25);
box-shadow: 0 0 20px rgba(168, 85, 247, 0.1);
}
.nav-item {
color: rgba(255, 255, 255, 0.3);
transition: all 0.4s cubic-bezier(0.23, 1, 0.32, 1);
}
.nav-item.active {
color: var(--primary-light);
transform: translateY(-4px);
text-shadow: 0 0 15px rgba(168, 85, 247, 0.8);
}
.animate-spin-slow {
animation: spin 12s linear infinite;
}
.animate-pulse-slow {
animation: pulse 6s ease-in-out infinite;
}
@keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
@keyframes pulse { 0%, 100% { opacity: 0.3; } 50% { opacity: 0.7; } }
#stars {
position: absolute;
width: 100%;
height: 100%;
}
.star {
position: absolute;
background: white;
border-radius: 50%;
filter: blur(0.5px);
animation: float-star var(--duration) ease-in-out infinite;
opacity: var(--opacity);
}
@keyframes float-star {
0%, 100% { transform: translate(0, 0) scale(1); opacity: var(--opacity); }
50% { transform: translate(var(--x), var(--y)) scale(1.5); opacity: 1; }
}
.hint-container {
@apply p-6 rounded-[2.5rem] bg-purple-900/10 border border-purple-500/10 mt-6;
box-shadow: inset 0 0 30px rgba(168, 85, 247, 0.08);
}
.hint-chip {
@apply relative px-1 py-1 bg-white/5 rounded-full text-[11px] text-purple-200 border border-white/10 transition-all cursor-pointer inline-block whitespace-nowrap overflow-hidden;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
backdrop-filter: blur(8px);
}
.hint-chip span {
@apply relative z-10 px-3 py-1 rounded-full bg-purple-500/20 text-purple-100 font-medium;
display: inline-block;
}
.hint-chip:hover {
@apply border-purple-400/50 scale-105;
box-shadow: 0 8px 15px rgba(168, 85, 247, 0.3);
}
.hint-chip:hover span {
@apply bg-purple-400/40 text-white;
}
.hint-chip:active {
@apply scale-95 opacity-80;
}
.bubble-anim {
animation: bubble-float 5s ease-in-out infinite;
animation-delay: calc(var(--delay, 0) * 0.4s);
}
@keyframes bubble-float {
0%, 100% { transform: translateY(0) rotate(0deg); }
33% { transform: translateY(-4px) rotate(1deg); }
66% { transform: translateY(2px) rotate(-1deg); }
}
.typing-text {
overflow: hidden;
white-space: pre-wrap;
animation: reveal 2s steps(60, end);
}
@keyframes reveal { from { opacity: 0; transform: translateY(5px); } to { opacity: 1; transform: translateY(0); } }
.starlight-gathering-gold {
width: 44px;
height: 44px;
border-radius: 50%;
border: 2px solid var(--primary);
border-top-color: transparent;
animation: spin 0.8s infinite linear;
}
input[type="date"]::-webkit-calendar-picker-indicator {
filter: invert(1) sepia(100%) saturate(200%) hue-rotate(240deg);
opacity: 0.8;
}
select {
appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23A855F7' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 12px center;
background-size: 16px;
}
::-webkit-scrollbar { width: 4px; }
::-webkit-scrollbar-thumb { background: rgba(168, 85, 247, 0.3); border-radius: 10px; }
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB