新增人生轨迹模块代码
This commit is contained in:
@@ -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 }
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user