307 lines
6.1 KiB
Markdown
307 lines
6.1 KiB
Markdown
# ✅ 图片存储迁移完成
|
||
|
||
## 📝 迁移概述
|
||
|
||
已成功将所有图片存储从 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)
|
||
|
||
```typescript
|
||
// 上传
|
||
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)
|
||
```
|
||
|
||
### 现在(服务器文件系统)
|
||
|
||
```typescript
|
||
// 上传
|
||
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 配置
|
||
```gitignore
|
||
# Uploads
|
||
public/uploads/*
|
||
!public/uploads/.gitkeep
|
||
```
|
||
|
||
## 🔄 兼容性
|
||
|
||
### 向后兼容
|
||
代码保留了对旧数据的兼容:
|
||
|
||
```typescript
|
||
// 优先使用新的 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)
|
||
}
|
||
})
|
||
}
|
||
```
|
||
|
||
### 数据迁移
|
||
|
||
如果有旧数据需要迁移,可以运行迁移脚本:
|
||
|
||
```typescript
|
||
// 迁移脚本示例
|
||
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. 确保目录存在
|
||
```bash
|
||
mkdir -p public/uploads
|
||
chmod 755 public/uploads
|
||
```
|
||
|
||
### 2. 环境变量
|
||
无需额外配置,直接使用本地文件系统
|
||
|
||
### 3. 备份策略
|
||
```bash
|
||
# 定期备份
|
||
rsync -av public/uploads/ /backup/uploads/
|
||
|
||
# 或使用 cron 定时任务
|
||
0 2 * * * rsync -av /path/to/app/public/uploads/ /backup/uploads/
|
||
```
|
||
|
||
### 4. 磁盘监控
|
||
```bash
|
||
# 查看上传目录大小
|
||
du -sh public/uploads
|
||
|
||
# 监控磁盘使用
|
||
df -h
|
||
```
|
||
|
||
## 📈 性能优化建议
|
||
|
||
### 1. 图片压缩
|
||
已实现:
|
||
- 头像:最大 1MB,1024x1024
|
||
- 照片:最大 2MB,1920x1920
|
||
|
||
### 2. 浏览器缓存
|
||
Next.js 自动处理 `public` 目录的缓存
|
||
|
||
### 3. CDN 加速(可选)
|
||
```nginx
|
||
location /uploads/ {
|
||
alias /path/to/app/public/uploads/;
|
||
expires 30d;
|
||
add_header Cache-Control "public, immutable";
|
||
}
|
||
```
|
||
|
||
### 4. 懒加载
|
||
```tsx
|
||
<img
|
||
src={avatarUrl}
|
||
loading="lazy"
|
||
alt="头像"
|
||
/>
|
||
```
|
||
|
||
## ✅ 测试清单
|
||
|
||
- [x] 头像上传功能
|
||
- [x] 头像显示功能
|
||
- [x] 照片墙上传
|
||
- [x] 照片墙显示
|
||
- [x] 成员列表头像
|
||
- [x] 成员详情头像
|
||
- [x] 家族树节点头像
|
||
- [x] D3 组织图头像
|
||
- [x] 成员表单头像上传
|
||
- [x] 向后兼容性
|
||
|
||
## 🎉 迁移完成
|
||
|
||
所有图片存储已成功从 IndexedDB 迁移到服务器文件系统!
|
||
|
||
### 主要改进
|
||
1. ✅ 跨设备访问
|
||
2. ✅ 数据持久化
|
||
3. ✅ 多用户协作
|
||
4. ✅ 更好的性能
|
||
5. ✅ 易于备份和管理
|
||
|
||
### 下一步
|
||
- 测试所有功能
|
||
- 迁移现有数据(如果有)
|
||
- 监控磁盘使用
|
||
- 考虑添加 CDN(可选)
|