bug修复

This commit is contained in:
2025-12-22 23:40:47 +08:00
parent 7d53a059d7
commit 1fefd98d74
19 changed files with 1339 additions and 534 deletions
+13 -7
View File
@@ -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>
)
}
/>
+16 -9
View File
@@ -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>
);
};
+27
View File
@@ -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% {
+9 -3
View File
@@ -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') {
+35
View File
@@ -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 {
+1 -1
View File
@@ -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',
+12 -5
View File
@@ -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;
+32 -16
View File
@@ -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>
))