bug修复
This commit is contained in:
+13
-7
@@ -11,13 +11,15 @@ import useStore from './store/useStore';
|
||||
/**
|
||||
* 路由守卫组件
|
||||
* 根据登录状态和注册完成状态进行路由重定向
|
||||
* - requireAuth: 需要登录才能访问
|
||||
* - requireOnboarding: 需要完成入站流程才能访问
|
||||
*/
|
||||
const ProtectedRoute = ({ children, requireAuth = false, requireOnboarding = false }) => {
|
||||
const { isLoggedIn, registrationData } = useStore();
|
||||
const navigate = useNavigate();
|
||||
|
||||
// 检查是否完成入站流程
|
||||
const hasCompletedOnboarding = registrationData.nickname && registrationData.future?.vision;
|
||||
// 检查是否完成入站流程(有昵称和未来愿景即视为已完成)
|
||||
const hasCompletedOnboarding = !!(registrationData.nickname && registrationData.future?.vision);
|
||||
|
||||
useEffect(() => {
|
||||
if (requireAuth && !isLoggedIn) {
|
||||
@@ -65,8 +67,8 @@ const AnimatedRoutes = () => {
|
||||
const location = useLocation();
|
||||
const { isLoggedIn, registrationData } = useStore();
|
||||
|
||||
// 检查是否完成入站流程
|
||||
const hasCompletedOnboarding = registrationData.nickname && registrationData.future?.vision;
|
||||
// 检查是否完成入站流程(有昵称和未来愿景即视为已完成)
|
||||
const hasCompletedOnboarding = !!(registrationData.nickname && registrationData.future?.vision);
|
||||
|
||||
return (
|
||||
<AnimatePresence mode="wait">
|
||||
@@ -89,15 +91,19 @@ const AnimatedRoutes = () => {
|
||||
}
|
||||
/>
|
||||
|
||||
{/* 入站流程页 */}
|
||||
{/* 入站流程页 - 已完成入站的用户直接跳转到首页 */}
|
||||
<Route
|
||||
path="/onboarding"
|
||||
element={
|
||||
<ProtectedRoute requireAuth>
|
||||
!isLoggedIn ? (
|
||||
<Navigate to="/" replace />
|
||||
) : hasCompletedOnboarding ? (
|
||||
<Navigate to="/dashboard" replace />
|
||||
) : (
|
||||
<PageTransition>
|
||||
<OnboardingPage />
|
||||
</PageTransition>
|
||||
</ProtectedRoute>
|
||||
)
|
||||
}
|
||||
/>
|
||||
|
||||
|
||||
@@ -21,6 +21,11 @@ const GlassInput = ({
|
||||
className = '',
|
||||
id
|
||||
}) => {
|
||||
// 日期类型输入框的特殊样式
|
||||
const dateInputClass = type === 'date'
|
||||
? 'cursor-pointer [&::-webkit-calendar-picker-indicator]:cursor-pointer [&::-webkit-calendar-picker-indicator]:opacity-100 [&::-webkit-calendar-picker-indicator]:w-full [&::-webkit-calendar-picker-indicator]:h-full [&::-webkit-calendar-picker-indicator]:absolute [&::-webkit-calendar-picker-indicator]:top-0 [&::-webkit-calendar-picker-indicator]:left-0 [&::-webkit-calendar-picker-indicator]:bg-transparent'
|
||||
: '';
|
||||
|
||||
return (
|
||||
<div className={`flex flex-col gap-2 ${className}`}>
|
||||
{label && (
|
||||
@@ -31,15 +36,17 @@ const GlassInput = ({
|
||||
{label}
|
||||
</label>
|
||||
)}
|
||||
<input
|
||||
id={id}
|
||||
type={type}
|
||||
placeholder={placeholder}
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
maxLength={maxLength}
|
||||
className="glass-input w-full focus:ring-2 focus:ring-orange-200/50"
|
||||
/>
|
||||
<div className={type === 'date' ? 'relative' : ''}>
|
||||
<input
|
||||
id={id}
|
||||
type={type}
|
||||
placeholder={placeholder}
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
maxLength={maxLength}
|
||||
className={`glass-input w-full focus:ring-2 focus:ring-orange-200/50 ${dateInputClass}`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -86,6 +86,33 @@ body {
|
||||
color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
/* 日期输入框样式优化 */
|
||||
input[type="date"].glass-input {
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
input[type="date"].glass-input::-webkit-calendar-picker-indicator {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
input[type="date"].glass-input::-webkit-datetime-edit {
|
||||
color: white;
|
||||
}
|
||||
|
||||
input[type="date"].glass-input::-webkit-datetime-edit-fields-wrapper {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
@keyframes float {
|
||||
0%, 100% {
|
||||
|
||||
@@ -42,6 +42,7 @@ const LoginPage = () => {
|
||||
|
||||
/**
|
||||
* 处理登录提交
|
||||
* 登录成功后根据用户档案状态决定跳转目标
|
||||
*/
|
||||
const handleSubmit = async () => {
|
||||
if (phone.length !== 11) {
|
||||
@@ -56,9 +57,14 @@ const LoginPage = () => {
|
||||
setIsSubmitting(true);
|
||||
|
||||
try {
|
||||
// 尝试调用后端登录
|
||||
await login(phone, code);
|
||||
navigate('/onboarding');
|
||||
// 尝试调用后端登录,返回值包含 hasProfile 标识
|
||||
const result = await login(phone, code);
|
||||
// 根据用户档案状态决定跳转:已有档案直接进入首页,否则进入入站流程
|
||||
if (result.hasProfile) {
|
||||
navigate('/dashboard');
|
||||
} else {
|
||||
navigate('/onboarding');
|
||||
}
|
||||
} catch (error) {
|
||||
// 后端不可用时,使用本地验证
|
||||
if (code === '888888') {
|
||||
|
||||
@@ -6,6 +6,7 @@ import { PromptTagGroup } from '../components/PromptTag';
|
||||
import useStore from '../store/useStore';
|
||||
import { inspirationClusters } from '../utils/constants';
|
||||
import * as dictionaryService from '../services/dictionary';
|
||||
import * as lifeEventService from '../services/lifeEvent';
|
||||
|
||||
/**
|
||||
* OnboardingPage 组件
|
||||
@@ -83,6 +84,28 @@ const OnboardingPage = () => {
|
||||
updateRegistration(dataToSave);
|
||||
};
|
||||
|
||||
/**
|
||||
* 保存生命事件到后端
|
||||
* @param {Object} eventData - 事件数据 { date, text }
|
||||
* @param {string} eventType - 事件类型标识
|
||||
* @param {string} title - 事件标题
|
||||
*/
|
||||
const saveLifeEvent = async (eventData, eventType, title) => {
|
||||
if (!eventData?.date || !eventData?.text) return;
|
||||
|
||||
try {
|
||||
await lifeEventService.createEvent({
|
||||
title,
|
||||
time: eventData.date,
|
||||
content: eventData.text,
|
||||
eventType: 'milestone',
|
||||
tags: [eventType]
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(`保存${title}失败:`, error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleNext = async () => {
|
||||
saveStepData();
|
||||
if (currentStep < 5) {
|
||||
@@ -90,7 +113,19 @@ const OnboardingPage = () => {
|
||||
} else {
|
||||
setIsSaving(true);
|
||||
try {
|
||||
// 保存用户档案
|
||||
await saveUserProfile();
|
||||
|
||||
// 保存生命事件(童年记忆、开心经历、低谷时期)
|
||||
const eventsToSave = [
|
||||
{ data: formData.childhood, type: 'childhood', title: '童年记忆' },
|
||||
{ data: formData.joy, type: 'joy', title: '光芒闪耀的时刻' },
|
||||
{ data: formData.low, type: 'low', title: '在暗夜中潜行' }
|
||||
];
|
||||
|
||||
await Promise.all(
|
||||
eventsToSave.map(({ data, type, title }) => saveLifeEvent(data, type, title))
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('保存档案失败:', error);
|
||||
} finally {
|
||||
|
||||
@@ -131,7 +131,7 @@ export const transformToFrontendFormat = (backendData) => {
|
||||
id,
|
||||
userId,
|
||||
title: title || '',
|
||||
time: eventDate || '',
|
||||
time: eventDate ? eventDate.split('T')[0] : '',
|
||||
content: content || '',
|
||||
aiFeedback: aiReply || '',
|
||||
eventType: eventType || 'daily_log',
|
||||
|
||||
@@ -86,6 +86,7 @@ const useStore = create(
|
||||
* 登录
|
||||
* @param {string} phone - 手机号
|
||||
* @param {string} smsCode - 验证码
|
||||
* @returns {Promise<Object>} 包含 hasProfile 标识,用于判断是否需要跳转到 onboarding
|
||||
*/
|
||||
login: async (phone, smsCode) => {
|
||||
set({ loading: true, error: null });
|
||||
@@ -97,18 +98,24 @@ const useStore = create(
|
||||
isLoggedIn: true,
|
||||
phone,
|
||||
userId,
|
||||
view: 'onboarding',
|
||||
loading: false
|
||||
});
|
||||
|
||||
// 尝试加载用户档案
|
||||
// 尝试加载用户档案,判断用户是否已完成注册
|
||||
let hasProfile = false;
|
||||
try {
|
||||
await get().loadUserProfile();
|
||||
const profileData = await get().loadUserProfile();
|
||||
// 检查档案是否完整(有昵称和未来愿景)
|
||||
hasProfile = !!(profileData && profileData.nickname && profileData.future?.vision);
|
||||
} catch {
|
||||
// 档案不存在,继续入站流程
|
||||
// 档案不存在,需要进入入站流程
|
||||
hasProfile = false;
|
||||
}
|
||||
|
||||
return response;
|
||||
// 根据档案状态设置视图
|
||||
set({ view: hasProfile ? 'dashboard' : 'onboarding' });
|
||||
|
||||
return { ...response, hasProfile };
|
||||
} catch (error) {
|
||||
set({ loading: false, error: error.message });
|
||||
throw error;
|
||||
|
||||
@@ -62,11 +62,18 @@ const TimelineView = () => {
|
||||
};
|
||||
|
||||
/**
|
||||
* 按时间倒序排列事件
|
||||
* 按事件时间倒序排列(最新的在最上面)
|
||||
* 空日期的事件排在最后
|
||||
*/
|
||||
const sortedEvents = [...lifeEvents].sort(
|
||||
(a, b) => new Date(b.time) - new Date(a.time)
|
||||
);
|
||||
const sortedEvents = [...lifeEvents].sort((a, b) => {
|
||||
// 如果两个都没有时间,保持原顺序
|
||||
if (!a.time && !b.time) return 0;
|
||||
// 没有时间的排在后面
|
||||
if (!a.time) return 1;
|
||||
if (!b.time) return -1;
|
||||
// 按时间倒序(最新的在前)
|
||||
return new Date(b.time) - new Date(a.time);
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -98,7 +105,14 @@ const TimelineView = () => {
|
||||
{/* 事件卡片 */}
|
||||
<GlassCard className="border-white/5 hover:border-orange-200/20 transition-all duration-700">
|
||||
<div className="flex justify-between items-start mb-4">
|
||||
<h4 className="text-xl font-medium text-white/80">{event.title}</h4>
|
||||
<div className="flex items-center gap-3">
|
||||
<h4 className="text-xl font-medium text-white/80">{event.title}</h4>
|
||||
{event.tags && event.tags.length > 0 && (
|
||||
<span className="text-[9px] px-2 py-1 rounded-full bg-orange-200/10 text-orange-200/60 uppercase tracking-wider">
|
||||
{event.tags[0] === 'childhood' ? '童年' : event.tags[0] === 'joy' ? '高光' : event.tags[0] === 'low' ? '低谷' : event.tags[0]}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-[10px] font-mono tracking-widest text-white/30 uppercase">
|
||||
{event.time}
|
||||
</span>
|
||||
@@ -107,18 +121,20 @@ const TimelineView = () => {
|
||||
{event.content}
|
||||
</p>
|
||||
|
||||
{/* AI 反馈区域 */}
|
||||
<div className="ai-glow-card p-5 rounded-2xl bg-orange-200/[0.02] border border-orange-200/5">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Sparkles className="w-3 h-3 text-orange-200" />
|
||||
<span className="text-[9px] uppercase tracking-[0.2em] text-orange-200/60 font-bold">
|
||||
引路人洞察
|
||||
</span>
|
||||
{/* AI 反馈区域 - 仅在有反馈时显示 */}
|
||||
{event.aiFeedback && (
|
||||
<div className="ai-glow-card p-5 rounded-2xl bg-orange-200/[0.02] border border-orange-200/5">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Sparkles className="w-3 h-3 text-orange-200" />
|
||||
<span className="text-[9px] uppercase tracking-[0.2em] text-orange-200/60 font-bold">
|
||||
引路人洞察
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs italic text-white/50 leading-loose">
|
||||
{event.aiFeedback}
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-xs italic text-white/50 leading-loose">
|
||||
{event.aiFeedback}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</GlassCard>
|
||||
</div>
|
||||
))
|
||||
|
||||
Reference in New Issue
Block a user