经过全面代码审查,发现以下关键问题需要解决:
症状: 用户报告保存图片功能无法使用(分享弹窗 + 月历首页)
根本原因:
-
month-client-page.tsx 仍在使用 Tailwind 梯度类:
const cardColors = [ "bg-gradient-to-br from-pink-100 to-pink-200 dark:from-pink-950 dark:to-pink-900", "bg-gradient-to-br from-yellow-100 to-yellow-200 dark:from-yellow-950 dark:to-yellow-900", // ... 更多颜色 ]
-
Tailwind CSS 会将这些类转换为
lab()颜色函数(特别是 dark mode 变体) -
html2canvas 不支持
lab()颜色函数,导致转换失败:Error: Attempting to parse an unsupported color function "lab"
影响范围:
- ❌ 月历页面保存图片功能完全失效
- ✅ 分享卡片已修复(使用 inline RGB 渐变)
文件位置:
app/month/[month]/month-client-page.tsx第 16-24 行app/month/[month]/month-client-page.tsx第 206 行(使用 colorClass)
症状: AI 生成建议后,左侧卡片被撑得很高,和右边的"生成日历"框不对齐,页面很丑
根本原因:
-
布局使用 Grid 两列等宽,但不等高:
<div className="grid md:grid-cols-2 gap-6"> {/* 左侧:目标输入 + AI建议 */} <Card className="p-8"> <Textarea className="min-h-[200px]" /> {aiSuggestions && ( <motion.div className="mt-6 p-4 ..."> <div className="whitespace-pre-wrap">{aiSuggestions}</div> </motion.div> )} </Card> {/* 右侧:生成日历按钮 */} <Card className="p-8 ...">...</Card> </div>
-
AI 建议内容长度不可控:
- 使用
whitespace-pre-wrap保留所有换行 - AI 可能返回很长的建议(3-5 段,每段多行)
- 左侧 Card 被撑到 800px+ 高度
- 使用
-
右侧生成按钮固定高度,导致不对齐:
- 左侧高,右侧矮
- Grid 布局不会自动平衡高度
- 视觉上很不协调
影响范围:
- Onboarding 第 3 步的 UI/UX
文件位置:
app/onboarding/page.tsx第 405-526 行
症状: 用户点击生成后,先看到默认日历,过了很久才出现"生成 30 天"的提示
根本原因:
业务流程问题:
用户体验流程(当前):
1. 点击"生成日历"按钮
2. onboarding 立即跳转到 /calendar 页面
3. calendar 页面加载,调用 getMonthTheme() 获取 12 个月的主题
4. 因为数据库还没有 AI 数据,fallback 到模板数据
5. 用户看到默认日历(模板生成)
6. 后台 AI 还在慢慢生成(30-60 秒)
7. 用户困惑:这是 AI 生成的吗?为什么这么快?
8. 过了很久,Toast 提示"成功生成 30 个行动"
9. 但页面没有刷新,用户看到的还是旧数据
10. 用户需要手动刷新才能看到真实 AI 数据
核心问题:
- 跳转时机错误: 在 AI 生成完成前就跳转
- 数据加载混乱: hybrid 模式的 fallback 机制导致用户看到错误数据
- 缺少状态同步: AI 生成完成后没有通知 calendar 页面刷新
- 用户体验差: 用户不知道发生了什么,看到的数据不对
代码证据:
app/onboarding/page.tsx 第 124-194 行:
if (user) {
// Phase 2: AI 生成中
setGenerationPhase("generating")
const response = await fetch("/api/generate-calendar-progressive", {
method: "POST",
body: JSON.stringify({ userId: user.id, profile: profileData, phase: "initial" })
})
const result = await response.json()
if (result.success) {
toast.success(`成功生成 ${result.actionsCount} 个搞钱行动!`)
await new Promise(resolve => setTimeout(resolve, 2000))
}
}
// ❌ 问题:立即跳转,没有等待数据真正可用
router.replace("/calendar")lib/calendar-hybrid.ts 第 18-50 行:
export async function getMonthTheme(userId, month, profile) {
if (!userId) {
return getPersonalizedMonthTheme(month, profile) // 模板数据
}
try {
const actions = await getUserCalendarActions(userId, year, month)
if (actions.length > 0) {
return { /* 数据库数据 */ }
}
// ❌ 问题:如果数据库还没有数据,立即 fallback 到模板
return getPersonalizedMonthTheme(month, profile)
} catch (error) {
return getPersonalizedMonthTheme(month, profile)
}
}影响范围:
- 整个 onboarding → calendar 流程
- 用户首次体验非常差
文件位置:
app/onboarding/page.tsx第 100-200 行(handleComplete)app/calendar/page.tsx第 22-109 行(状态检查和跳转逻辑)lib/calendar-hybrid.ts第 18-92 行(hybrid 数据加载)
目标: 修复核心 bug,最小化改动
文件: app/month/[month]/month-client-page.tsx
修改: 将 Tailwind 梯度类转换为 inline RGB 渐变
// BEFORE
const cardColors = [
"bg-gradient-to-br from-pink-100 to-pink-200 dark:from-pink-950 dark:to-pink-900",
"bg-gradient-to-br from-yellow-100 to-yellow-200 dark:from-yellow-950 dark:to-yellow-900",
"bg-gradient-to-br from-blue-100 to-blue-200 dark:from-blue-950 dark:to-blue-900",
"bg-gradient-to-br from-purple-100 to-purple-200 dark:from-purple-950 dark:to-purple-900",
"bg-gradient-to-br from-green-100 to-green-200 dark:from-green-950 dark:to-green-900",
"bg-gradient-to-br from-orange-100 to-orange-200 dark:from-orange-950 dark:to-orange-900",
"bg-gradient-to-br from-teal-100 to-teal-200 dark:from-teal-950 dark:to-teal-900",
]
// AFTER
const getCardStyle = (day: number) => {
const gradients = [
"linear-gradient(135deg, #fce7f3 0%, #fbcfe8 100%)", // pink
"linear-gradient(135deg, #fef3c7 0%, #fde68a 100%)", // yellow
"linear-gradient(135deg, #dbeafe 0%, #bfdbfe 100%)", // blue
"linear-gradient(135deg, #f3e8ff 0%, #e9d5ff 100%)", // purple
"linear-gradient(135deg, #d1fae5 0%, #a7f3d0 100%)", // green
"linear-gradient(135deg, #fed7aa 0%, #fdba74 100%)", // orange
"linear-gradient(135deg, #ccfbf1 0%, #99f6e4 100%)", // teal
]
return {
background: gradients[day % gradients.length]
}
}使用:
<Card
style={action ? getCardStyle(day) : undefined}
className={action ? "hover:shadow-lg hover:scale-[1.05] ..." : "opacity-50 bg-muted"}
>文件: app/onboarding/page.tsx
修改策略: 改用 Flexbox 布局 + 限制 AI 建议高度
// BEFORE
<div className="grid md:grid-cols-2 gap-6">
{/* 左侧 */}
<Card className="p-8">
<Textarea className="min-h-[200px]" />
{aiSuggestions && (
<motion.div className="mt-6 p-4 ...">
<div className="whitespace-pre-wrap">{aiSuggestions}</div>
</motion.div>
)}
</Card>
{/* 右侧 */}
<Card className="p-8 ...">...</Card>
</div>
// AFTER
<div className="flex flex-col md:flex-row gap-6 items-start">
{/* 左侧 - 固定宽度 */}
<Card className="p-8 flex-1 max-w-2xl">
<Textarea className="min-h-[200px]" />
{aiSuggestions && (
<motion.div
className="mt-6 p-4 bg-accent/10 rounded-lg border border-accent/20 max-h-[400px] overflow-y-auto"
>
<div className="flex items-center gap-2 mb-3">
<Sparkles className="h-5 w-5 text-accent flex-shrink-0" />
<h3 className="font-semibold text-lg">AI 为你推荐的行动</h3>
</div>
<div className="text-sm text-muted-foreground leading-relaxed">
{aiSuggestions}
</div>
<p className="text-xs text-muted-foreground mt-3 italic">
💡 这些建议会融入你的 365 天行动日历中
</p>
</motion.div>
)}
</Card>
{/* 右侧 - 固定宽度,自适应高度 */}
<div className="md:sticky md:top-6 w-full md:w-[400px] flex-shrink-0">
<Card className="p-8 bg-gradient-to-br from-orange-500/10 to-pink-500/10 border-accent/20">
{/* 生成日历按钮 */}
...
</Card>
</div>
</div>关键改进:
- ✅ 使用
flex而不是grid,允许不等高 - ✅ 右侧使用
md:sticky md:top-6,固定在视口中 - ✅ AI 建议添加
max-h-[400px] overflow-y-auto,限制高度 - ✅ 移除
whitespace-pre-wrap,改用正常文本渲染 - ✅ 左侧
max-w-2xl,防止过宽
核心策略: 在 onboarding 页面等待 AI 生成完成,再跳转
修改文件: app/onboarding/page.tsx
// BEFORE
if (result.success) {
toast.success(`成功生成 ${result.actionsCount} 个搞钱行动!`)
await new Promise(resolve => setTimeout(resolve, 2000))
}
// 立即跳转
router.replace("/calendar")
// AFTER
if (result.success) {
console.log("[Onboarding] ✅ AI 生成完成,等待数据可用...")
// Phase 3: 验证数据
setGenerationPhase("saving")
setGenerationProgress(80)
setCurrentAction("验证数据完整性...")
// 等待数据真正写入数据库并可查询
let retries = 0
const maxRetries = 10
let dataReady = false
while (retries < maxRetries && !dataReady) {
await new Promise(resolve => setTimeout(resolve, 500))
// 检查数据是否可用
const checkResponse = await fetch(`/api/check-calendar-data?userId=${user.id}`)
const checkResult = await checkResponse.json()
if (checkResult.hasData && checkResult.count >= 30) {
dataReady = true
console.log("[Onboarding] ✅ 数据已就绪,可以跳转")
} else {
retries++
console.log(`[Onboarding] 等待数据就绪... (${retries}/${maxRetries})`)
}
}
if (!dataReady) {
console.warn("[Onboarding] ⚠️ 数据验证超时,继续跳转")
}
setGenerationProgress(100)
setGenerationPhase("complete")
toast.success(`成功生成 ${result.actionsCount} 个搞钱行动!`)
await new Promise(resolve => setTimeout(resolve, 1500))
}
// 现在可以安全跳转了
sessionStorage.setItem("onboarding_just_completed", "true")
router.replace("/calendar")新增 API: app/api/check-calendar-data/route.ts
import { NextResponse } from "next/server"
import { createClient } from "@/lib/supabase/server"
export async function GET(request: Request) {
const { searchParams } = new URL(request.url)
const userId = searchParams.get("userId")
if (!userId) {
return NextResponse.json({ hasData: false, count: 0 })
}
try {
const supabase = await createClient()
const { data, error } = await supabase
.from("daily_actions")
.select("id", { count: "exact" })
.eq("user_id", userId)
if (error) {
console.error("[Check Calendar Data] Error:", error)
return NextResponse.json({ hasData: false, count: 0, error: error.message })
}
return NextResponse.json({
hasData: (data?.length || 0) > 0,
count: data?.length || 0
})
} catch (error) {
console.error("[Check Calendar Data] Exception:", error)
return NextResponse.json({ hasData: false, count: 0 })
}
}优化 calendar 页面的 hybrid 逻辑:
// lib/calendar-hybrid.ts
export async function getMonthTheme(
userId: string | null,
month: number,
profile: UserProfile
): Promise<MonthTheme> {
// 如果没有登录,使用模板
if (!userId) {
return getPersonalizedMonthTheme(month, profile)
}
try {
const year = new Date().getFullYear()
const actions = await getUserCalendarActions(userId, year, month)
if (actions.length > 0) {
// ✅ 有数据,使用数据库数据
return {
month,
name: `${month}月`,
theme: actions[0].theme || getPersonalizedMonthTheme(month, profile).theme,
description: getPersonalizedMonthTheme(month, profile).description,
emoji: actions[0].emoji || getPersonalizedMonthTheme(month, profile).emoji,
}
}
// ❌ BEFORE: 立即 fallback
// return getPersonalizedMonthTheme(month, profile)
// ✅ AFTER: 检查是否刚完成 onboarding
const justCompleted = typeof window !== 'undefined'
? sessionStorage.getItem("onboarding_just_completed")
: null
if (justCompleted) {
// 如果刚完成 onboarding,等待 AI 数据(最多 3 秒)
console.log(`[Calendar Hybrid] 检测到刚完成 onboarding,等待 AI 数据...`)
for (let i = 0; i < 6; i++) {
await new Promise(resolve => setTimeout(resolve, 500))
const retryActions = await getUserCalendarActions(userId, year, month)
if (retryActions.length > 0) {
console.log(`[Calendar Hybrid] ✅ AI 数据已就绪`)
return {
month,
name: `${month}月`,
theme: retryActions[0].theme || getPersonalizedMonthTheme(month, profile).theme,
description: getPersonalizedMonthTheme(month, profile).description,
emoji: retryActions[0].emoji || getPersonalizedMonthTheme(month, profile).emoji,
}
}
}
console.log(`[Calendar Hybrid] ⚠️ 等待超时,使用模板数据`)
}
// Fallback 到模板
return getPersonalizedMonthTheme(month, profile)
} catch (error) {
console.error("[Calendar Hybrid] 获取月度主题失败:", error)
return getPersonalizedMonthTheme(month, profile)
}
}目标: 全面优化架构和性能
为什么:
- Gemini 2.5 Pro:慢,贵,适合复杂推理
- Gemini 2.5 Flash:快 2-3 倍,便宜 80%,质量相近
修改:
lib/gemini.ts:
// BEFORE
const model = genAI.getGenerativeModel({ model: "gemini-2.5-pro" })
// AFTER
const model = genAI.getGenerativeModel({
model: "gemini-2.5-flash",
generationConfig: {
temperature: 0.9, // 增加创意性
topP: 0.95,
topK: 40,
maxOutputTokens: 4096,
}
})app/api/generate-calendar-progressive/route.ts:
// 第 136 行
const model = genAI.getGenerativeModel({ model: "gemini-2.5-flash" })预期效果:
- 生成速度:60 秒 → 20-25 秒
- 成本降低:80%
- 用户体验大幅提升
策略: 分批生成,实时显示进度
// 新文件:app/api/generate-calendar-batch/route.ts
export async function POST(request: Request) {
const { userId, profile, batchSize = 30 } = await request.json()
// 生成器函数,分批返回
const encoder = new TextEncoder()
const stream = new ReadableStream({
async start(controller) {
try {
const totalDays = 365
let generated = 0
while (generated < totalDays) {
const remainingDays = totalDays - generated
const currentBatch = Math.min(batchSize, remainingDays)
// 生成这一批
const actions = await generateBatch(userId, profile, generated, currentBatch)
// 保存到数据库
await saveBatch(userId, actions)
generated += currentBatch
// 发送进度
const progress = {
generated,
total: totalDays,
percentage: Math.round((generated / totalDays) * 100)
}
controller.enqueue(encoder.encode(`data: ${JSON.stringify(progress)}\n\n`))
}
controller.close()
} catch (error) {
controller.error(error)
}
}
})
return new Response(stream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
}
})
}添加组合索引:
-- supabase/migrations/009_performance_optimization.sql
-- 优化 daily_actions 查询
CREATE INDEX IF NOT EXISTS idx_daily_actions_user_date_composite
ON daily_actions(user_id, date DESC);
-- 优化 check_ins 查询
CREATE INDEX IF NOT EXISTS idx_check_ins_user_date_composite
ON check_ins(user_id, date DESC);
-- 添加部分索引(只索引未来的日期)
CREATE INDEX IF NOT EXISTS idx_daily_actions_future
ON daily_actions(user_id, date)
WHERE date >= CURRENT_DATE;优化统计触发器:
-- 使用物化视图代替实时计算
CREATE MATERIALIZED VIEW user_stats AS
SELECT
user_id,
COUNT(*) as total_check_ins,
COUNT(*) * 10 as total_coins,
MAX(date) as last_check_in
FROM check_ins
GROUP BY user_id;
-- 创建唯一索引以支持并发刷新
CREATE UNIQUE INDEX ON user_stats(user_id);
-- 每小时刷新一次(或在打卡时手动刷新)
REFRESH MATERIALIZED VIEW CONCURRENTLY user_stats;使用 React Query / SWR:
// lib/hooks/useCalendarData.ts
import useSWR from 'swr'
export function useMonthTheme(month: number) {
const { user, profile } = useAuth()
const { data, error, isLoading } = useSWR(
user && profile ? [`/api/month-theme/${month}`, user.id] : null,
() => getMonthTheme(user!.id, month, profile!),
{
revalidateOnFocus: false,
revalidateOnReconnect: false,
dedupingInterval: 60000, // 1 分钟内不重复请求
}
)
return {
theme: data,
isLoading,
error
}
}预计时间: 1-2 小时
-
✅ 修复图片保存(15 分钟)
- 修改
month-client-page.tsx - 测试保存功能
- 修改
-
✅ 修复 AI 建议布局(30 分钟)
- 修改
onboarding/page.tsx布局 - 限制 AI 建议高度
- 测试响应式
- 修改
-
✅ 修复生成日历逻辑(45 分钟)
- 添加
/api/check-calendar-dataAPI - 修改
onboarding/page.tsx等待逻辑 - 优化
calendar-hybrid.ts加载逻辑 - 完整测试生成流程
- 添加
部署: 立即部署到生产环境
预计时间: 4-8 小时
- 升级到 Gemini 2.5 Flash(30 分钟)
- 实现批量生成策略(1 小时)
- 数据库性能优化(30 分钟)
- 前端缓存机制(1 小时)
- 完整测试(1 小时)
部署: 充分测试后部署
- 月历页面点击"保存图片"
- 检查浏览器控制台,确保没有
lab()错误 - 验证生成的图片质量和颜色
- 测试 dark mode(如果支持)
- 输入目标并生成 AI 建议
- 检查左右两侧是否对齐
- 测试不同长度的 AI 建议
- 测试响应式布局(手机、平板、桌面)
- 验证滚动条是否正常工作
- 新用户注册 → onboarding
- 选择 MBTI 和职业
- 点击"生成日历"
- 观察进度提示是否正确
- 等待生成完成(应该停留在 onboarding)
- 跳转到 calendar 页面
- 验证显示的是 AI 数据,不是模板数据
- 检查控制台日志,确认数据流正确
步骤 1: 执行之前的快速修复脚本(如果还没执行)
-- supabase/migrations/008_quick_fix_existing_db.sql步骤 2: 执行性能优化脚本(可选)
-- supabase/migrations/009_performance_optimization.sql步骤 1: 创建新分支
git checkout -b refactor/fix-critical-issues步骤 2: 依次修改文件
app/month/[month]/month-client-page.tsxapp/onboarding/page.tsxapp/api/check-calendar-data/route.ts(新增)lib/calendar-hybrid.ts
步骤 3: 本地测试
npm run dev
# 完整测试所有功能步骤 4: 提交和部署
git add .
git commit -m "fix: 修复图片保存、AI建议布局和生成日历流程"
git push origin refactor/fix-critical-issues
# 部署到生产环境
vercel --prod-
图片保存:
- ✅ 点击保存,立即生成图片
- ✅ 没有错误提示
- ✅ 颜色渐变正常显示
-
AI 建议布局:
- ✅ 左右对齐,视觉协调
- ✅ 建议内容可滚动,不会撑破布局
- ✅ 响应式布局在所有设备上正常
-
生成日历流程:
- ✅ 点击生成后,显示清晰的进度提示
- ✅ 生成完成前不跳转
- ✅ 跳转后立即显示 AI 数据
- ✅ 没有"默认数据"的闪烁
-
性能提升(如果执行阶段 2):
- ✅ 生成速度:60 秒 → 20-25 秒
- ✅ 页面加载速度提升 30%
- ✅ 数据库查询减少 50%
- ✅ 图片保存修复(只改样式,不影响逻辑)
- ✅ AI 建议布局(纯 UI 改动)
⚠️ 生成日历流程(涉及核心业务逻辑)- 缓解措施: 充分测试,保留降级方案
- 🔴 数据库结构变更(如果执行阶段 2)
- 缓解措施: 先在开发环境测试,备份生产数据
完成重构后,建议:
- 监控错误: 使用 Sentry 或类似工具
- 性能监控: 使用 Vercel Analytics
- 用户反馈: 收集真实用户体验
- 持续优化: 根据数据调整策略
文档创建时间: 2025-10-26 版本: 1.0 作者: Claude Code