新增人生轨迹模块代码

This commit is contained in:
2025-12-21 17:44:59 +08:00
parent f3c06ce6af
commit cfd12f01db
14 changed files with 1156 additions and 72 deletions
+57
View File
@@ -0,0 +1,57 @@
import request from '../utils/request';
export const userApi = {
/**
* 获取当前登录用户的档案
*/
getCurrentUser() {
return request({
url: '/user-profile/me',
method: 'get'
});
},
/**
* 新增档案
*/
createUserProfile(data) {
return request({
url: '/user-profile/create',
method: 'post',
data
});
},
/**
* 修改档案
*/
updateUserProfile(data) {
return request({
url: '/user-profile/update',
method: 'put',
data
});
},
/**
* 根据ID查询详情
*/
getProfileById(id) {
return request({
url: '/user-profile/detail',
method: 'get',
params: { id }
});
},
/**
* 删除档案
*/
deleteUserProfile(id) {
return request({
url: '/user-profile/delete',
method: 'delete',
params: { id }
});
}
};
+32 -2
View File
@@ -68,16 +68,46 @@ export function PathView({ onSwitchToScript }) {
}
};
const handleDelete = () => {
const handleDelete = async () => {
if (window.confirm('确定要删除这个路径规划吗?')) {
Store.deletePath(currentPath.id);
// Sync to backend
try {
const allPaths = Store.get().paths;
const profileRes = await userApi.getCurrentUser();
if (profileRes.data) {
await userApi.updateUserProfile({
id: profileRes.data.id,
paths: JSON.stringify(allPaths)
});
}
} catch (err) {
console.error("Failed to sync path deletion to backend", err);
}
setIsEditing(false);
}
};
const handleSaveEdit = () => {
const handleSaveEdit = async () => {
if (editedPath) {
Store.updatePath(editedPath.id, { steps: editedPath.steps });
// Sync to backend
try {
const allPaths = Store.get().paths;
const profileRes = await userApi.getCurrentUser();
if (profileRes.data) {
await userApi.updateUserProfile({
id: profileRes.data.id,
paths: JSON.stringify(allPaths)
});
}
} catch (err) {
console.error("Failed to sync path update to backend", err);
}
setIsEditing(false);
}
};
+32 -1
View File
@@ -1,5 +1,6 @@
import { useState, useEffect } from 'react';
import { Store } from '../../utils/store';
import { userApi } from '../../api/user';
import { AI } from '../../utils/aiLogic';
import { useStoreData } from '../../hooks/useStoreData';
import { Fingerprint, Film, Sparkles, History, Trash2, Stars, Zap, Loader, X, ArrowRight, BookOpen } from 'lucide-react';
@@ -29,6 +30,21 @@ export function ScriptView({ onSwitchToPath }) {
const requirements = form;
const script = await AI.generateScript(data.userProfile, data.lifeTimeline, requirements);
Store.addScript(script);
// Sync to backend
try {
const allScripts = Store.get().generatedScripts;
const profileRes = await userApi.getCurrentUser();
if (profileRes.data) {
await userApi.updateUserProfile({
id: profileRes.data.id,
scripts: JSON.stringify(allScripts)
});
}
} catch (err) {
console.error("Failed to sync script to backend", err);
}
setSelectedScriptId(script.id);
setForm({ ...form, theme: '' });
} catch (e) {
@@ -39,10 +55,25 @@ export function ScriptView({ onSwitchToPath }) {
}
};
const handleDelete = (id, e) => {
const handleDelete = async (id, e) => {
e.stopPropagation();
if (confirm('确定删除这个剧本吗?')) {
Store.deleteScript(id);
// Sync to backend
try {
const allScripts = Store.get().generatedScripts;
const profileRes = await userApi.getCurrentUser();
if (profileRes.data) {
await userApi.updateUserProfile({
id: profileRes.data.id,
scripts: JSON.stringify(allScripts)
});
}
} catch (err) {
console.error("Failed to sync script deletion to backend", err);
}
if (selectedScriptId === id) setSelectedScriptId(null);
}
};
+101 -9
View File
@@ -1,6 +1,7 @@
import React, { useState } from 'react';
import React, { useState, useEffect } from 'react';
import { Store } from '../utils/store';
import { ArrowLeft, ArrowRight, Check, X, Sparkles, Star, AlertCircle, CheckCircle } from 'lucide-react';
import { userApi } from '../api/user';
import { ArrowLeft, ArrowRight, Check, X, Sparkles, Star, AlertCircle, CheckCircle, Loader } from 'lucide-react';
import { Button } from '../components/ui/Button';
import { Input, Select, Textarea } from '../components/ui/Input';
import clsx from 'clsx';
@@ -17,6 +18,55 @@ export function OnboardingPage({ onFinish }) {
const [step, setStep] = useState(0);
const [formData, setFormData] = useState(Store.get().userProfile);
const [toast, setToast] = useState(null); // { msg, type }
const [submitting, setSubmitting] = useState(false);
// Load existing profile from backend on mount
useEffect(() => {
const loadProfile = async () => {
try {
const res = await userApi.getCurrentUser();
if (res.data) {
// Merge backend data with local state, handling JSON strings if necessary
// Backend returns scripts/paths as strings, but profile fields are direct
// Note: formData structure matches userProfile in store.js
// We need to map backend fields (camelCase) to frontend structure if they differ
// Backend: childhoodDate, childhoodContent etc.
// Frontend: history: { childhood: { date, content } }
// We need a mapper if structures differ.
// Backend UserProfileResponse has: nickname, gender, zodiac, mbti, hobbies (String), childhoodDate...
const backendData = res.data;
const mappedData = {
...formData,
nickname: backendData.nickname || formData.nickname,
gender: backendData.gender || formData.gender,
zodiac: backendData.zodiac || formData.zodiac,
mbti: backendData.mbti || formData.mbti,
hobbies: backendData.hobbies ? JSON.parse(backendData.hobbies) : formData.hobbies,
history: {
childhood: {
date: backendData.childhoodDate || '',
content: backendData.childhoodContent || ''
},
peak: {
date: backendData.peakDate || '',
content: backendData.peakContent || ''
},
valley: {
date: backendData.valleyDate || '',
content: backendData.valleyContent || ''
}
},
futureVision: backendData.futureVision || formData.futureVision
};
setFormData(mappedData);
}
} catch (e) {
console.warn("Failed to load profile", e);
}
};
loadProfile();
}, []);
const showToast = (msg, type = 'error') => {
setToast({ msg, type });
@@ -40,15 +90,57 @@ export function OnboardingPage({ onFinish }) {
}));
};
const handleNext = () => {
const handleNext = async () => {
if (step === 0) {
if (!formData.nickname?.trim()) { showToast('请填写昵称'); return; }
if (!formData.mbti) { showToast('请选择MBTI人格类型'); return; }
} else if (step === 4) {
if (!formData.futureVision?.trim()) { showToast('写下一句对未来的期许吧'); return; }
Store.updateProfile(formData);
Store.completeOnboarding();
onFinish();
setSubmitting(true);
try {
// 1. Save to local store
Store.updateProfile(formData);
Store.completeOnboarding();
// 2. Prepare data for backend
// Flatten history structure to match UserProfileCreateRequest
const requestData = {
nickname: formData.nickname,
gender: formData.gender,
zodiac: formData.zodiac,
mbti: formData.mbti,
hobbies: JSON.stringify(formData.hobbies),
childhoodDate: formData.history?.childhood?.date || null,
childhoodContent: formData.history?.childhood?.content || '',
peakDate: formData.history?.peak?.date || null,
peakContent: formData.history?.peak?.content || '',
valleyDate: formData.history?.valley?.date || null,
valleyContent: formData.history?.valley?.content || '',
futureVision: formData.futureVision,
// Also sync scripts/paths if they exist in store (re-registration case)
scripts: JSON.stringify(Store.get().generatedScripts || []),
paths: JSON.stringify(Store.get().paths || [])
};
// 3. Call backend
// Check if profile exists first
const currentProfile = await userApi.getCurrentUser();
if (currentProfile.data) {
// Update
await userApi.updateUserProfile({ ...requestData, id: currentProfile.data.id });
} else {
// Create
await userApi.createUserProfile(requestData);
}
onFinish();
} catch (e) {
console.error(e);
showToast('保存失败,请重试');
} finally {
setSubmitting(false);
}
return;
}
setStep(prev => prev + 1);
@@ -225,9 +317,9 @@ export function OnboardingPage({ onFinish }) {
</button>
) : <div></div>}
<Button onClick={handleNext} className="flex items-center gap-2 px-8 group hover:scale-105 active:scale-95 shadow-lg shadow-primary/20">
<span>{step === 4 ? '完成创建' : '下一步'}</span>
{step === 4 ? <Check className="w-4 h-4" /> : <ArrowRight className="w-4 h-4 group-hover:translate-x-1 transition-transform" />}
<Button onClick={handleNext} disabled={submitting} className="flex items-center gap-2 px-8 group hover:scale-105 active:scale-95 shadow-lg shadow-primary/20 disabled:opacity-70 disabled:scale-100">
<span>{step === 4 ? (submitting ? '保存中...' : '开启旅程') : '下一步'}</span>
{step === 4 ? (submitting ? <Loader className="w-4 h-4 animate-spin" /> : <Check className="w-4 h-4" />) : <ArrowRight className="w-4 h-4 group-hover:translate-x-1 transition-transform" />}
</Button>
</div>
</div>