6.1 KiB
6.1 KiB
✅ 图片存储迁移完成
📝 迁移概述
已成功将所有图片存储从 IndexedDB 迁移到服务器文件系统。
🔄 修改的文件
1. 核心组件
components/ui/image-upload.tsx
- ✅ 修改为上传到服务器 API
- ✅ value 改为 URL 而不是 ID
- ✅ 移除 IndexedDB 相关代码
- ✅ 添加错误处理
components/ui/avatar-display.tsx
- ✅ 简化为直接使用 URL
- ✅ 移除 IndexedDB 加载逻辑
- ✅ 移除 useEffect 和状态管理
components/members/photo-gallery.tsx
- ✅ 照片上传改为服务器 API
- ✅ photoIds 改为存储 URL 数组
- ✅ 移除 IndexedDB 相关代码
2. 页面组件
app/members/[id]/page.tsx
- ✅ 头像上传使用服务器 API
- ✅ 头像加载优先使用 avatarUrl
- ✅ 保留对旧 avatarImageId 的兼容
app/members/page.tsx
- ✅ 成员列表使用 avatarUrl
- ✅ 移除 avatarImageId 参数
components/members/member-form.tsx
- ✅ 表单使用 avatarUrl 字段
- ✅ 移除 avatarImageId 字段
3. 家族树组件
components/tree/family-node.tsx
- ✅ 使用 avatarUrl 而不是 avatarImageId
components/tree/d3-org-chart-flow.tsx
- ✅ 接口定义改为 avatarUrl
- ✅ 简化头像缓存逻辑
- ✅ 移除 IndexedDB 加载代码
hooks/use-avatar-cache.ts
- ✅ 简化为直接返回 URL
- ✅ 移除 IndexedDB 相关逻辑
📊 数据字段变更
FamilyMember 类型
| 旧字段 | 新字段 | 说明 |
|---|---|---|
avatarImageId |
avatarUrl |
头像存储方式 |
photoIds (ID数组) |
photoIds (URL数组) |
照片墙存储方式 |
🎯 功能对比
之前(IndexedDB)
// 上传
const imageId = uuidv4()
await db.images.add({
id: imageId,
blob: file,
mimeType: file.type,
})
onChange(imageId)
// 显示
const image = await db.images.get(imageId)
const url = URL.createObjectURL(image.blob)
现在(服务器文件系统)
// 上传
const formData = new FormData()
formData.append('file', file)
const response = await fetch('/api/upload', {
method: 'POST',
body: formData,
})
const { url } = await response.json()
onChange(url)
// 显示
<img src={url} />
✨ 优势
1. 跨设备访问
- ✅ 所有用户都能看到图片
- ✅ 不同设备间同步
- ✅ 支持多用户协作
2. 数据持久化
- ✅ 不会因清除浏览器数据而丢失
- ✅ 服务器端统一备份
- ✅ 更可靠的存储
3. 性能优化
- ✅ 浏览器缓存
- ✅ 减少客户端存储压力
- ✅ 更快的加载速度
4. 易于管理
- ✅ 集中式存储
- ✅ 便于备份和迁移
- ✅ 可以添加 CDN 加速
🔒 安全性
已实现
- ✅ 用户身份验证
- ✅ 文件类型验证
- ✅ 文件大小限制(5MB)
- ✅ 唯一文件名生成
建议增强
- 图片水印
- 访问权限控制
- 定期清理未使用的图片
- 病毒扫描
📂 文件存储
目录结构
public/
└── uploads/
├── .gitkeep
├── 1732435200000-a3b5c7.jpg
├── 1732435201234-b4c6d8.jpg
└── ...
Git 配置
# Uploads
public/uploads/*
!public/uploads/.gitkeep
🔄 兼容性
向后兼容
代码保留了对旧数据的兼容:
// 优先使用新的 avatarUrl
if (member?.avatarUrl) {
setAvatarBlobUrl(member.avatarUrl)
}
// 兼容旧的 IndexedDB 存储
else if (member?.avatarImageId) {
db.images.get(member.avatarImageId).then((image) => {
if (image) {
const url = URL.createObjectURL(image.blob)
setAvatarBlobUrl(url)
}
})
}
数据迁移
如果有旧数据需要迁移,可以运行迁移脚本:
// 迁移脚本示例
async function migrateImages() {
const members = await prisma.familyMember.findMany({
where: {
avatarImageId: { not: null }
}
})
for (const member of members) {
try {
// 从 IndexedDB 获取图片
const image = await db.images.get(member.avatarImageId!)
if (!image) continue
// 上传到服务器
const formData = new FormData()
formData.append('file', image.blob)
const response = await fetch('/api/upload', {
method: 'POST',
body: formData,
})
const { url } = await response.json()
// 更新数据库
await prisma.familyMember.update({
where: { id: member.id },
data: { avatarUrl: url }
})
console.log(`Migrated avatar for ${member.fullName}`)
} catch (error) {
console.error(`Failed to migrate ${member.fullName}:`, error)
}
}
}
🚀 部署注意事项
1. 确保目录存在
mkdir -p public/uploads
chmod 755 public/uploads
2. 环境变量
无需额外配置,直接使用本地文件系统
3. 备份策略
# 定期备份
rsync -av public/uploads/ /backup/uploads/
# 或使用 cron 定时任务
0 2 * * * rsync -av /path/to/app/public/uploads/ /backup/uploads/
4. 磁盘监控
# 查看上传目录大小
du -sh public/uploads
# 监控磁盘使用
df -h
📈 性能优化建议
1. 图片压缩
已实现:
- 头像:最大 1MB,1024x1024
- 照片:最大 2MB,1920x1920
2. 浏览器缓存
Next.js 自动处理 public 目录的缓存
3. CDN 加速(可选)
location /uploads/ {
alias /path/to/app/public/uploads/;
expires 30d;
add_header Cache-Control "public, immutable";
}
4. 懒加载
<img
src={avatarUrl}
loading="lazy"
alt="头像"
/>
✅ 测试清单
- 头像上传功能
- 头像显示功能
- 照片墙上传
- 照片墙显示
- 成员列表头像
- 成员详情头像
- 家族树节点头像
- D3 组织图头像
- 成员表单头像上传
- 向后兼容性
🎉 迁移完成
所有图片存储已成功从 IndexedDB 迁移到服务器文件系统!
主要改进
- ✅ 跨设备访问
- ✅ 数据持久化
- ✅ 多用户协作
- ✅ 更好的性能
- ✅ 易于备份和管理
下一步
- 测试所有功能
- 迁移现有数据(如果有)
- 监控磁盘使用
- 考虑添加 CDN(可选)