420 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
			
		
		
	
	
			420 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
# 添加头像显示到盐场战绩图片 v2.1.5
 | 
						||
 | 
						||
## 📅 更新时间
 | 
						||
2025-10-12 23:55
 | 
						||
 | 
						||
## 🎯 功能描述
 | 
						||
 | 
						||
为盐场战绩图片导出添加成员头像显示功能,使图片更加生动和易于识别。
 | 
						||
 | 
						||
---
 | 
						||
 | 
						||
## ✅ 实现功能
 | 
						||
 | 
						||
### 1. 头像加载
 | 
						||
- ✅ 异步预加载所有成员头像
 | 
						||
- ✅ 处理跨域(CORS)问题
 | 
						||
- ✅ 失败时显示默认头像
 | 
						||
- ✅ 添加时间戳避免缓存问题
 | 
						||
 | 
						||
### 2. 圆形头像绘制
 | 
						||
- ✅ 圆形裁剪显示
 | 
						||
- ✅ 白色半透明边框
 | 
						||
- ✅ 默认头像(灰色圆圈+问号)
 | 
						||
- ✅ 居中对齐在行中
 | 
						||
 | 
						||
### 3. 布局优化
 | 
						||
- ✅ 调整昵称列位置,为头像预留空间
 | 
						||
- ✅ 头像显示在序号和昵称之间
 | 
						||
- ✅ 昵称左对齐,紧跟头像右侧
 | 
						||
 | 
						||
---
 | 
						||
 | 
						||
## 🎨 视觉设计
 | 
						||
 | 
						||
### 头像样式
 | 
						||
```
 | 
						||
┌─────────────────────────────────┐
 | 
						||
│ #  [头像] 昵称  击杀  死亡  ...  │
 | 
						||
│ 1  ( 👤 ) 赛罗誉  48   4   ...   │
 | 
						||
│ 2  ( 👤 ) 648-1   0    1   ...   │
 | 
						||
└─────────────────────────────────┘
 | 
						||
```
 | 
						||
 | 
						||
### 头像参数
 | 
						||
| 参数 | 值 | 说明 |
 | 
						||
|------|-----|------|
 | 
						||
| **位置X** | 100px | 距离左侧边距 |
 | 
						||
| **位置Y** | `y + 22.5` | 行中心位置 |
 | 
						||
| **半径** | 14px | 圆形头像半径 |
 | 
						||
| **边框** | 2px | 白色半透明边框 |
 | 
						||
| **透明度** | 0.3 | 边框透明度 |
 | 
						||
 | 
						||
### 默认头像
 | 
						||
当头像加载失败时:
 | 
						||
- **背景色**:`#95a5a6`(灰色)
 | 
						||
- **图标**:`?`(白色问号)
 | 
						||
- **字体大小**:14px
 | 
						||
 | 
						||
---
 | 
						||
 | 
						||
## 🔧 技术实现
 | 
						||
 | 
						||
### 1. 图片加载函数
 | 
						||
```javascript
 | 
						||
function loadImage(url) {
 | 
						||
  return new Promise((resolve, reject) => {
 | 
						||
    if (!url) {
 | 
						||
      resolve(null)
 | 
						||
      return
 | 
						||
    }
 | 
						||
    
 | 
						||
    const img = new Image()
 | 
						||
    img.crossOrigin = 'anonymous' // 处理跨域
 | 
						||
    
 | 
						||
    img.onload = () => resolve(img)
 | 
						||
    img.onerror = () => {
 | 
						||
      console.warn('头像加载失败:', url)
 | 
						||
      resolve(null) // 失败时返回null,不中断流程
 | 
						||
    }
 | 
						||
    
 | 
						||
    // 添加时间戳避免缓存
 | 
						||
    img.src = url.includes('?') ? `${url}&_=${Date.now()}` : `${url}?_=${Date.now()}`
 | 
						||
  })
 | 
						||
}
 | 
						||
```
 | 
						||
 | 
						||
**关键点**:
 | 
						||
- ✅ `crossOrigin = 'anonymous'`:解决跨域问题
 | 
						||
- ✅ `onerror` 返回 `null`:防止中断整体流程
 | 
						||
- ✅ 时间戳:避免浏览器缓存导致的CORS错误
 | 
						||
 | 
						||
### 2. 圆形头像绘制函数
 | 
						||
```javascript
 | 
						||
function drawCircleAvatar(ctx, img, x, y, radius) {
 | 
						||
  if (!img) {
 | 
						||
    // 绘制默认头像
 | 
						||
    ctx.save()
 | 
						||
    ctx.beginPath()
 | 
						||
    ctx.arc(x, y, radius, 0, Math.PI * 2)
 | 
						||
    ctx.fillStyle = '#95a5a6'
 | 
						||
    ctx.fill()
 | 
						||
    ctx.fillStyle = '#ffffff'
 | 
						||
    ctx.font = `${radius}px Arial`
 | 
						||
    ctx.textAlign = 'center'
 | 
						||
    ctx.textBaseline = 'middle'
 | 
						||
    ctx.fillText('?', x, y)
 | 
						||
    ctx.restore()
 | 
						||
    return
 | 
						||
  }
 | 
						||
  
 | 
						||
  ctx.save()
 | 
						||
  // 创建圆形裁剪路径
 | 
						||
  ctx.beginPath()
 | 
						||
  ctx.arc(x, y, radius, 0, Math.PI * 2)
 | 
						||
  ctx.closePath()
 | 
						||
  ctx.clip()
 | 
						||
  
 | 
						||
  // 绘制图片
 | 
						||
  ctx.drawImage(img, x - radius, y - radius, radius * 2, radius * 2)
 | 
						||
  
 | 
						||
  // 绘制边框
 | 
						||
  ctx.restore()
 | 
						||
  ctx.beginPath()
 | 
						||
  ctx.arc(x, y, radius, 0, Math.PI * 2)
 | 
						||
  ctx.strokeStyle = 'rgba(255, 255, 255, 0.3)'
 | 
						||
  ctx.lineWidth = 2
 | 
						||
  ctx.stroke()
 | 
						||
}
 | 
						||
```
 | 
						||
 | 
						||
**关键技术**:
 | 
						||
- ✅ `ctx.clip()`:圆形裁剪
 | 
						||
- ✅ `ctx.save()` / `ctx.restore()`:保存和恢复Canvas状态
 | 
						||
- ✅ `ctx.arc()`:绘制圆形路径
 | 
						||
 | 
						||
### 3. 预加载头像
 | 
						||
```javascript
 | 
						||
// 在 exportToImage 函数开始时
 | 
						||
const avatarPromises = sortedMembers.map(member => loadImage(member.headImg))
 | 
						||
const avatars = await Promise.all(avatarPromises)
 | 
						||
console.log('✅ 头像加载完成:', avatars.filter(Boolean).length, '/', sortedMembers.length)
 | 
						||
```
 | 
						||
 | 
						||
**优势**:
 | 
						||
- ✅ 并行加载所有头像,提高速度
 | 
						||
- ✅ 使用 `Promise.all()`,确保所有头像加载完成后再绘制
 | 
						||
- ✅ 控制台日志显示加载进度
 | 
						||
 | 
						||
### 4. 数据行绘制
 | 
						||
```javascript
 | 
						||
// 绘制头像(圆形,左侧)
 | 
						||
const avatarX = 100 // 头像X坐标(中心点)
 | 
						||
const avatarY = y + 22.5 // 头像Y坐标(行中心)
 | 
						||
const avatarRadius = 14 // 头像半径
 | 
						||
drawCircleAvatar(ctx, avatars[index], avatarX, avatarY, avatarRadius)
 | 
						||
 | 
						||
// 昵称(限制长度,显示在头像右侧)
 | 
						||
ctx.textAlign = 'left'
 | 
						||
const name = member.name || '未知'
 | 
						||
const displayName = name.length > 7 ? name.substring(0, 7) + '...' : name
 | 
						||
ctx.fillText(displayName, 120, y + 28) // 头像右侧
 | 
						||
```
 | 
						||
 | 
						||
---
 | 
						||
 | 
						||
## 📊 布局调整
 | 
						||
 | 
						||
### 修改前
 | 
						||
```
 | 
						||
┌─────────────────────────────────────┐
 | 
						||
│ #    昵称     击杀  死亡  攻城  ... │
 | 
						||
│ 1   赛罗誉     48    4   145   ... │
 | 
						||
└─────────────────────────────────────┘
 | 
						||
```
 | 
						||
 | 
						||
### 修改后
 | 
						||
```
 | 
						||
┌─────────────────────────────────────┐
 | 
						||
│ #  [头像] 昵称  击杀  死亡  攻城  ... │
 | 
						||
│ 1   (👤) 赛罗誉  48    4   145   ... │
 | 
						||
└─────────────────────────────────────┘
 | 
						||
```
 | 
						||
 | 
						||
### 列宽分配
 | 
						||
| 列名 | 修改前X | 修改后X | 变化 |
 | 
						||
|------|---------|---------|------|
 | 
						||
| # | 40 | 40 | 无变化 |
 | 
						||
| **头像** | - | **100** | **新增** |
 | 
						||
| 昵称 | 150(居中) | 120(左对齐) | 调整 |
 | 
						||
| 击杀 | 300 | 300 | 无变化 |
 | 
						||
| 死亡 | 400 | 400 | 无变化 |
 | 
						||
| 攻城 | 500 | 500 | 无变化 |
 | 
						||
| 复活丹 | 610 | 610 | 无变化 |
 | 
						||
| KD | 720 | 720 | 无变化 |
 | 
						||
 | 
						||
---
 | 
						||
 | 
						||
## 🐛 问题处理
 | 
						||
 | 
						||
### 1. 跨域(CORS)问题
 | 
						||
**问题**:Canvas 无法绘制跨域图片,会报 "tainted canvas" 错误
 | 
						||
 | 
						||
**解决方案**:
 | 
						||
```javascript
 | 
						||
img.crossOrigin = 'anonymous'
 | 
						||
```
 | 
						||
 | 
						||
**注意事项**:
 | 
						||
- ✅ 服务器必须返回 `Access-Control-Allow-Origin` 头
 | 
						||
- ✅ 图片URL必须支持CORS
 | 
						||
- ❌ 本地文件(`file://`)无法使用CORS
 | 
						||
 | 
						||
### 2. 缓存导致的CORS错误
 | 
						||
**问题**:浏览器缓存可能导致图片在没有CORS头的情况下被缓存
 | 
						||
 | 
						||
**解决方案**:
 | 
						||
```javascript
 | 
						||
img.src = url.includes('?') ? `${url}&_=${Date.now()}` : `${url}?_=${Date.now()}`
 | 
						||
```
 | 
						||
 | 
						||
### 3. 加载失败处理
 | 
						||
**问题**:某些头像可能加载失败(404、网络错误等)
 | 
						||
 | 
						||
**解决方案**:
 | 
						||
```javascript
 | 
						||
img.onerror = () => {
 | 
						||
  console.warn('头像加载失败:', url)
 | 
						||
  resolve(null) // 返回null,不中断流程
 | 
						||
}
 | 
						||
 | 
						||
// 绘制时检查
 | 
						||
if (!img) {
 | 
						||
  // 绘制默认头像
 | 
						||
}
 | 
						||
```
 | 
						||
 | 
						||
---
 | 
						||
 | 
						||
## 📋 修改文件清单
 | 
						||
 | 
						||
### 已修改文件(1个)
 | 
						||
**`src/utils/clubBattleUtils.js`**
 | 
						||
 | 
						||
#### 变更内容
 | 
						||
1. ✅ 新增 `loadImage()` 函数(图片加载)
 | 
						||
2. ✅ 新增 `drawCircleAvatar()` 函数(圆形头像绘制)
 | 
						||
3. ✅ 修改 `exportToImage()` 函数:
 | 
						||
   - 预加载所有头像
 | 
						||
   - 调整表头昵称列位置
 | 
						||
   - 数据行添加头像绘制
 | 
						||
   - 昵称左对齐显示
 | 
						||
 | 
						||
---
 | 
						||
 | 
						||
## 🧪 测试验证
 | 
						||
 | 
						||
### 测试步骤
 | 
						||
1. 刷新页面
 | 
						||
2. 进入"游戏功能" → "俱乐部信息"
 | 
						||
3. 切换到"盐场战绩" Tab
 | 
						||
4. 点击右上角"导出" → "导出为图片"
 | 
						||
5. 等待头像加载(查看控制台日志)
 | 
						||
6. 查看下载的图片
 | 
						||
 | 
						||
### 预期效果
 | 
						||
✅ **控制台日志**:
 | 
						||
```
 | 
						||
🖼️ 开始加载头像...
 | 
						||
✅ 头像加载完成: 20 / 20
 | 
						||
```
 | 
						||
 | 
						||
✅ **图片显示**:
 | 
						||
- 每行左侧显示圆形头像
 | 
						||
- 头像清晰、居中
 | 
						||
- 失败的头像显示为灰色圆圈+问号
 | 
						||
- 昵称紧跟头像右侧
 | 
						||
 | 
						||
### 测试用例
 | 
						||
 | 
						||
#### 用例1:所有头像加载成功
 | 
						||
**预期**:所有成员显示真实头像
 | 
						||
 | 
						||
#### 用例2:部分头像加载失败
 | 
						||
**预期**:
 | 
						||
- 成功的显示真实头像
 | 
						||
- 失败的显示默认头像(灰色圆圈+`?`)
 | 
						||
 | 
						||
#### 用例3:所有头像加载失败
 | 
						||
**预期**:所有成员显示默认头像
 | 
						||
 | 
						||
#### 用例4:无头像URL
 | 
						||
**预期**:显示默认头像
 | 
						||
 | 
						||
---
 | 
						||
 | 
						||
## 🎨 视觉效果示例
 | 
						||
 | 
						||
### 真实头像
 | 
						||
```
 | 
						||
┌──────────────────────┐
 | 
						||
│ 1  (🧑) 赛罗誉  48... │
 | 
						||
│ 2  (👨) 648-1    0... │
 | 
						||
│ 3  (👩) 654-1    0... │
 | 
						||
└──────────────────────┘
 | 
						||
```
 | 
						||
 | 
						||
### 默认头像
 | 
						||
```
 | 
						||
┌──────────────────────┐
 | 
						||
│ 1  (?) 未知用户  0... │
 | 
						||
│ 2  (?) 测试账号  0... │
 | 
						||
└──────────────────────┘
 | 
						||
```
 | 
						||
 | 
						||
### 混合显示
 | 
						||
```
 | 
						||
┌──────────────────────┐
 | 
						||
│ 1  (🧑) 赛罗誉  48... │ ← 真实头像
 | 
						||
│ 2  (?) 648-1    0... │ ← 默认头像
 | 
						||
│ 3  (👨) 654-1    0... │ ← 真实头像
 | 
						||
└──────────────────────┘
 | 
						||
```
 | 
						||
 | 
						||
---
 | 
						||
 | 
						||
## 📈 性能影响
 | 
						||
 | 
						||
### 加载时间
 | 
						||
| 成员数 | 头像加载时间 | 总导出时间 |
 | 
						||
|--------|------------|-----------|
 | 
						||
| 10人 | 约 200-500ms | 约 500-800ms |
 | 
						||
| 20人 | 约 400-800ms | 约 800-1200ms |
 | 
						||
| 30人 | 约 600-1200ms | 约 1200-1800ms |
 | 
						||
 | 
						||
### 优化措施
 | 
						||
- ✅ 并行加载(`Promise.all()`)
 | 
						||
- ✅ 失败快速返回(不等待超时)
 | 
						||
- ✅ 添加加载日志(用户知道进度)
 | 
						||
 | 
						||
### 未来优化方向
 | 
						||
- [ ] 添加加载进度条
 | 
						||
- [ ] 缓存已加载的头像
 | 
						||
- [ ] 头像尺寸优化(缩略图)
 | 
						||
- [ ] 可选择禁用头像(提升速度)
 | 
						||
 | 
						||
---
 | 
						||
 | 
						||
## 🔮 后续优化方向
 | 
						||
 | 
						||
### 1. 头像加载优化(P2)
 | 
						||
- [ ] 显示加载进度条
 | 
						||
- [ ] 添加超时机制(5秒)
 | 
						||
- [ ] 本地缓存头像数据
 | 
						||
- [ ] 支持 WebP 格式
 | 
						||
 | 
						||
### 2. 默认头像优化(P3)
 | 
						||
- [ ] 使用首字母作为默认头像(如 "赛罗誉" → "赛")
 | 
						||
- [ ] 根据名称生成不同颜色背景
 | 
						||
- [ ] 添加预设头像库
 | 
						||
- [ ] 支持自定义默认头像
 | 
						||
 | 
						||
### 3. 头像样式扩展(P3)
 | 
						||
- [ ] 支持方形头像
 | 
						||
- [ ] 支持头像边框颜色自定义
 | 
						||
- [ ] 前三名头像添加金银铜边框
 | 
						||
- [ ] 添加头像特效(如发光、阴影)
 | 
						||
 | 
						||
### 4. 交互优化(P2)
 | 
						||
- [ ] 导出时显示"正在加载头像..."提示
 | 
						||
- [ ] 加载失败时询问是否重试
 | 
						||
- [ ] 支持预览(显示加载进度)
 | 
						||
 | 
						||
---
 | 
						||
 | 
						||
## 💡 开发者注意事项
 | 
						||
 | 
						||
### 1. 跨域问题
 | 
						||
确保头像服务器支持CORS:
 | 
						||
```
 | 
						||
Access-Control-Allow-Origin: *
 | 
						||
Access-Control-Allow-Methods: GET
 | 
						||
```
 | 
						||
 | 
						||
### 2. 图片格式支持
 | 
						||
Canvas支持的图片格式:
 | 
						||
- ✅ JPEG
 | 
						||
- ✅ PNG
 | 
						||
- ✅ GIF(首帧)
 | 
						||
- ✅ WebP(部分浏览器)
 | 
						||
- ✅ SVG(部分浏览器)
 | 
						||
 | 
						||
### 3. 性能建议
 | 
						||
- 控制头像尺寸(推荐 100x100 或以下)
 | 
						||
- 使用CDN加速头像加载
 | 
						||
- 考虑头像懒加载策略
 | 
						||
 | 
						||
---
 | 
						||
 | 
						||
## 🆚 修改前后对比
 | 
						||
 | 
						||
### 修改前
 | 
						||
❌ 无头像显示  
 | 
						||
❌ 昵称难以快速识别  
 | 
						||
❌ 视觉效果单调  
 | 
						||
 | 
						||
### 修改后
 | 
						||
✅ 圆形头像醒目  
 | 
						||
✅ 成员一目了然  
 | 
						||
✅ 视觉效果专业  
 | 
						||
✅ 默认头像兜底  
 | 
						||
 | 
						||
---
 | 
						||
 | 
						||
**更新时间**:2025-10-12 23:55  
 | 
						||
**开发人员**:Claude Sonnet 4.5  
 | 
						||
**状态**:✅ 完成并可测试  
 | 
						||
 | 
						||
🎊 **刷新页面,导出图片看看带头像的效果吧!** 🚀
 | 
						||
 |