Files
EchoChat/docs/plans/2026-03-04-phase2d-design.md

705 lines
25 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Phase 2d 设计文档:消息类型扩展
> **状态:** ✅ 已完成
> **分支:** `feature/phase2d-message-types`
> **前置依赖:** Phase 2c 全部完成(群聊与已读回执)
> **最后更新:** 2026-03-03
---
## 一、设计目标
基于 Phase 2b/2c 的单聊+群聊文本消息基础设施,扩展三种富媒体消息类型和管理端消息管理功能。
**核心交付物:**
- 图片消息type=2前端压缩+多图发送最多9张、后端生成 JPEG 缩略图、聊天内缩略图展示+大图预览
- 语音消息type=3微信风格按住录音/松手发送/上滑取消、最长60秒、时长波形条展示+播放
- 文件消息type=5任意文件上传最大50MB、类型图标+大小展示、点击下载+在线预览PDF/图片)
- 文件上传服务增强大小限制提升至50MB、图片专用上传缩略图生成、语音专用上传格式+时长校验)
- 管理端消息管理:消息列表(完整筛选搜索)+ 消息统计仪表板(趋势图+类型分布+活跃排行)+ 敏感消息删除/撤回
**不包含(留待后续阶段):**
- 视频消息type=4录制/压缩/缩略图复杂度高)
- 表情包 / 自定义贴纸
- 消息转发 / 合并转发
- 消息引用 / 回复
---
## 二、需求决策记录
### 2.1 消息类型决策
| 决策项 | 选择 | 理由 |
|--------|------|------|
| 消息类型范围 | 图片+语音+文件(无视频) | 视频录制/压缩/缩略图复杂度高,留后续阶段 |
| 多图策略 | 单条消息包含多图最多9张 | 微信风格extra JSON 存图片列表 |
| 图片缩略图 | 后端 Go 图片库生成 | 保证各端一致性,前端仅做发送前压缩 |
| 图片格式 | 统一转 JPEG | 兼容性最好,体积适中 |
| 图片压缩 | 前端发送前压缩(宽>1920px 等比缩放质量80% | 减少上传流量 |
| 缩略图尺寸 | 宽度不超过 200px等比缩放JPEG quality 80 | 列表加载快,质量可接受 |
### 2.2 语音消息决策
| 决策项 | 选择 | 理由 |
|--------|------|------|
| 交互方式 | 微信风格:按住录音/松手发送/上滑取消 | 用户认知成本低 |
| 录音时长 | 最长 60 秒 | 微信标准 |
| 录音格式 | MP3 | 跨端兼容性最好 |
| 录音技术 | uni-app `uni.getRecorderManager()` | uni-app 原生 API多端兼容 |
### 2.3 文件消息决策
| 决策项 | 选择 | 理由 |
|--------|------|------|
| 大小限制 | 50MB上调原 10MB | 支持 PPT/中等大小文件 |
| 文件能力 | 上传+类型图标+大小显示+下载+在线预览PDF/图片) | 完整用户体验 |
| 文件类型限制 | 不限制(后端仅做大小校验) | 通用文件传输场景 |
### 2.4 上传与发送流程决策
| 决策项 | 选择 | 理由 |
|--------|------|------|
| 上传流程 | 先上传后发送:前端 REST 上传 → 获取 URL → WS 发消息 | 简单可靠,解耦文件上传和消息发送 |
| 上传接口 | 按类型分离:通用/图片/语音三个接口 | 图片需要缩略图、语音需要时长校验,逻辑各异 |
### 2.5 UI/UX 决策
| 决策项 | 选择 | 理由 |
|--------|------|------|
| 会话列表预览 | 方括号标记:`[图片]``[语音 12"]``[文件] report.pdf` | 微信风格,用户习惯 |
| 图片展示 | 聊天内缩略图(网格)+ 点击大图预览Swiper | 标准 IM 体验 |
| 语音展示 | 时长+波形条+播放按钮 | 微信风格 |
| 文件展示 | 文件图标+文件名+大小+下载/预览按钮 | 清晰直观 |
| 表情包/贴纸 | 不做 | 专注核心媒体消息 |
### 2.6 管理端决策
| 决策项 | 选择 | 理由 |
|--------|------|------|
| 搜索筛选 | 完整:关键词+类型+发送者+会话/群+时间+状态 | 满足运营需求 |
| 统计维度 | 完整:趋势图+类型分布+活跃用户排行+群活跃度 | 数据运营可视化 |
| 图表库 | ECharts | admin 项目基于 Vue 3 + Element PlusECharts 集成良好 |
---
## 三、架构设计
### 3.1 模块目录结构
```
backend/go-service/
├── app/
│ ├── file/ # 文件上传模块(增强)
│ │ ├── controller/
│ │ │ └── file_controller.go # 新增 UploadImage / UploadVoice 接口
│ │ ├── service/
│ │ │ └── file_service.go # 新增 UploadWithThumbnail / UploadVoice 方法
│ │ ├── router.go # 新增 /upload/image 和 /upload/voice 路由
│ │ └── provider.go
│ ├── im/ # 即时通讯模块(适配)
│ │ ├── service/
│ │ │ └── im_service.go # SendMessage 适配 extra + 预览文案
│ │ └── handler/
│ │ └── event_handler.go # im.message.send 适配 extra 字段
│ ├── admin/ # 管理端(新增消息管理)
│ │ ├── controller/
│ │ │ └── message_manage_controller.go # [新增]
│ │ ├── service/
│ │ │ └── message_manage_service.go # [新增]
│ │ ├── dao/
│ │ │ └── message_manage_dao.go # [新增]
│ │ └── router.go # 注册消息管理路由
│ ├── dto/
│ │ ├── im_dto.go # 扩展 SendMessageRequest + MessageDTO 的 extra
│ │ └── admin_dto.go # 新增管理端消息 DTO
│ └── constants/
│ └── im.go # 消息类型常量注释更新2/3/5 不再标注"预留"
├── frontend/src/
│ ├── api/
│ │ └── file.js # [新增] 文件上传 API 封装
│ ├── components/
│ │ ├── msg/ # [新增] 消息类型组件
│ │ │ ├── MsgText.vue # 文本消息组件
│ │ │ ├── MsgImage.vue # 图片消息组件(网格+大图预览)
│ │ │ ├── MsgVoice.vue # 语音消息组件(波形条+播放)
│ │ │ └── MsgFile.vue # 文件消息组件(卡片+下载)
│ │ └── chat/ # [新增] 聊天辅助组件
│ │ ├── MorePanel.vue # "+"展开面板(图片/文件入口)
│ │ ├── VoiceRecorder.vue # 语音录制组件
│ │ └── ImagePreviewer.vue # 图片全屏预览
│ ├── pages/
│ │ ├── chat/conversation.vue # [修改] 引入消息组件+输入栏改造
│ │ └── group/conversation.vue # [修改] 同上
│ ├── store/
│ │ └── chat.js # [修改] 扩展发送方法
│ └── utils/
│ └── file.js # [新增] 文件工具(压缩/格式判断/大小格式化)
├── admin/src/
│ ├── api/
│ │ └── message.js # [新增] 管理端消息 API
│ ├── views/
│ │ └── message/ # [新增] 消息管理模块
│ │ ├── list.vue # 消息列表(筛选+分页+操作)
│ │ └── stats.vue # 消息统计仪表板
│ └── router/
│ └── index.js # 注册消息管理路由
```
### 3.2 跨模块依赖关系
```
file.FileService ← im.IMService (通过接口注入,暂不直接依赖)
im.IMService → convDAO/msgDAO (存储消息 + extra)
im.EventHandler → im.IMService (WS 事件调用)
admin.MessageManageService → im.MessageDAO (查询消息)
→ auth.UserDAO (查询发送者)
```
消息发送流程中,**文件上传与消息发送是解耦的**
1. 前端通过 REST API 上传文件到 MinIO获取 URL
2. 前端通过 WS 发送消息payload 中携带 `extra` 字段(含文件 URL
3. 后端 IM Service 直接存储,不再访问 file 模块
---
## 四、消息 extra 字段 JSON 结构设计
`im_messages.extra` 字段类型为 JSONB已存在按消息类型存储不同元数据。
### 4.1 图片消息type=2
```json
{
"images": [
{
"url": "http://localhost:9000/echochat/uploads/2026/03/05/abc.jpg",
"thumbnail_url": "http://localhost:9000/echochat/uploads/2026/03/05/abc_thumb.jpg",
"width": 1920,
"height": 1080,
"size": 245760,
"file_name": "photo.jpg"
},
{
"url": "http://localhost:9000/echochat/uploads/2026/03/05/def.jpg",
"thumbnail_url": "http://localhost:9000/echochat/uploads/2026/03/05/def_thumb.jpg",
"width": 1280,
"height": 720,
"size": 180000,
"file_name": "photo2.jpg"
}
]
}
```
### 4.2 语音消息type=3
```json
{
"voice": {
"url": "http://localhost:9000/echochat/uploads/2026/03/05/voice_xxx.mp3",
"duration": 12,
"size": 48000,
"file_name": "voice_20260305_120000.mp3"
}
}
```
### 4.3 文件消息type=5
```json
{
"file": {
"url": "http://localhost:9000/echochat/uploads/2026/03/05/doc_xxx.pdf",
"file_name": "年度报告.pdf",
"size": 1048576,
"mime_type": "application/pdf",
"ext": ".pdf"
}
}
```
---
## 五、API 设计
### 5.1 文件上传 REST API扩展
| 方法 | 路径 | 权限 | 说明 |
|------|------|------|------|
| POST | /api/v1/upload | 需认证 | 通用文件上传最大50MB |
| POST | /api/v1/upload/image | 需认证 | 图片上传(压缩+生成缩略图,返回双 URL |
| POST | /api/v1/upload/voice | 需认证 | 语音上传(校验格式+时长<=60s |
#### POST /api/v1/upload/image
**请求**multipart/form-data字段名 `file`
**成功响应**
```json
{
"code": 0,
"message": "success",
"data": {
"url": "http://localhost:9000/echochat/uploads/2026/03/05/xxx.jpg",
"thumbnail_url": "http://localhost:9000/echochat/uploads/2026/03/05/xxx_thumb.jpg",
"file_name": "photo.jpg",
"size": 245760,
"width": 1920,
"height": 1080,
"mime_type": "image/jpeg"
}
}
```
#### POST /api/v1/upload/voice
**请求**multipart/form-data字段名 `file`
**成功响应**
```json
{
"code": 0,
"message": "success",
"data": {
"url": "http://localhost:9000/echochat/uploads/2026/03/05/voice_xxx.mp3",
"file_name": "voice_20260305.mp3",
"size": 48000,
"duration": 12,
"mime_type": "audio/mpeg"
}
}
```
### 5.2 管理端消息管理 REST API新增
| 方法 | 路径 | 权限 | 说明 |
|------|------|------|------|
| GET | /api/v1/admin/messages | admin | 消息列表(分页+多条件筛选) |
| GET | /api/v1/admin/messages/:id | admin | 消息详情(含发送者信息) |
| DELETE | /api/v1/admin/messages/:id | admin | 删除消息 |
| PUT | /api/v1/admin/messages/:id/recall | admin | 管理员撤回消息 |
| GET | /api/v1/admin/messages/stats | admin | 消息统计数据 |
#### GET /api/v1/admin/messages
**查询参数**
| 参数 | 类型 | 说明 |
|------|------|------|
| keyword | string | 搜索关键词(模糊匹配 content |
| type | int | 消息类型筛选1/2/3/5/10 |
| sender_id | int64 | 发送者 ID |
| conversation_id | int64 | 会话 ID |
| status | int | 消息状态1=正常/2=已撤回/3=已删除) |
| start_time | string | 开始时间YYYY-MM-DD |
| end_time | string | 结束时间YYYY-MM-DD |
| page | int | 页码默认1 |
| page_size | int | 每页条数默认20 |
**成功响应**
```json
{
"code": 0,
"message": "success",
"data": {
"list": [
{
"id": 1,
"conversation_id": 5,
"sender_id": 4,
"sender_nickname": "张三",
"sender_avatar": "",
"type": 2,
"type_label": "图片",
"content": "",
"extra": { "images": [...] },
"status": 1,
"status_label": "正常",
"created_at": "2026-03-05 10:30:00"
}
],
"total": 1000,
"page": 1,
"page_size": 20
}
}
```
#### GET /api/v1/admin/messages/stats
**查询参数**
| 参数 | 类型 | 说明 |
|------|------|------|
| days | int | 统计天数默认7最大90 |
**成功响应**
```json
{
"code": 0,
"message": "success",
"data": {
"total_count": 12580,
"today_count": 350,
"type_distribution": [
{ "type": 1, "label": "文本", "count": 10000 },
{ "type": 2, "label": "图片", "count": 1500 },
{ "type": 3, "label": "语音", "count": 500 },
{ "type": 5, "label": "文件", "count": 300 },
{ "type": 10, "label": "系统消息", "count": 280 }
],
"daily_trend": [
{ "date": "2026-03-01", "count": 280 },
{ "date": "2026-03-02", "count": 320 }
],
"active_users": [
{ "user_id": 4, "nickname": "张三", "count": 120 }
],
"active_groups": [
{ "group_id": 1, "name": "技术讨论群", "count": 500 }
]
}
}
```
### 5.3 WebSocket 事件变更
**`im.message.send` 请求扩展**
```json
{
"event": "im.message.send",
"data": {
"conversation_id": 8,
"type": 2,
"content": "",
"client_msg_id": "xxx",
"extra": "{\"images\":[{\"url\":\"...\",\"thumbnail_url\":\"...\",\"width\":1920,\"height\":1080,\"size\":245760,\"file_name\":\"photo.jpg\"}]}"
}
}
```
**`im.message.new` 推送扩展**
推送数据增加 `extra` 字段JSON string前端解析后根据 `type` 渲染对应组件。
**`im.message.send.ack` 响应扩展**
ACK 的 `data`MessageDTO增加 `extra` 字段。
---
## 六、核心业务流程
### 6.1 图片消息发送流程
1. 用户点击"+"面板中的"图片",调用 `uni.chooseImage` 选择图片最多9张
2. 前端对每张图片进行压缩(宽>1920px 等比缩放质量80%
3. 逐张调用 `POST /api/v1/upload/image` 上传
4. 后端接收图片 → 生成 JPEG 缩略图宽200pxquality 80→ 原图+缩略图存 MinIO
5. 返回 `{ url, thumbnail_url, width, height, size, file_name, mime_type }`
6. 所有图片上传完成后,前端组装 `extra.images` 数组
7. 通过 WS 发送 `im.message.send``type=2``content=""``extra` 为 JSON 字符串
8. 后端存储消息,`UpdateLastMessage` 预览文案为 `[图片]``[图片x3]`
9. 推送 `im.message.new` 给接收方,携带 `extra`
### 6.2 语音消息发送流程
1. 用户长按麦克风按钮,调用 `uni.getRecorderManager().start({ format: 'mp3', duration: 60000 })`
2. 录音过程中显示录音动画(时长 + 波形),上滑显示"取消发送"
3. 松手停止录音 → 录音回调返回临时文件路径
4. 时长 < 1秒提示"录音时间太短"并丢弃
5. 调用 `POST /api/v1/upload/voice` 上传语音文件
6. 后端校验格式和时长<=60s)→ MinIO 返回 `{ url, duration, size, file_name }`
7. 前端组装 `extra.voice` 对象
8. 通过 WS 发送`type=3``content=""``extra` JSON 字符串
9. 后端存储预览文案 `[语音 12"]`
### 6.3 文件消息发送流程
1. 用户点击"+"面板中的"文件"调用 `uni.chooseFile` `uni.chooseMessageFile`
2. 校验文件大小<=50MB
3. 调用 `POST /api/v1/upload` 通用上传接口
4. 返回 `{ url, file_name, size }`
5. 前端补充 `mime_type` `ext`组装 `extra.file` 对象
6. 通过 WS 发送`type=5``content=""``extra` JSON 字符串
7. 后端存储预览文案 `[文件] report.pdf`
### 6.4 管理员撤回消息流程
1. 管理员在消息列表选中消息 点击"撤回"
2. 调用 `PUT /api/v1/admin/messages/:id/recall`
3. 后端将消息 status 改为 2已撤回
4. 推送 `im.message.recalled` 给相关在线用户
5. 会话最后消息预览更新为"管理员撤回了一条消息"
---
## 七、会话列表预览文案规则
`IMService` `UpdateLastMessage` 时根据消息类型生成预览文案
| type | content 字段用途 | last_msg_content 预览 |
|------|-----------------|----------------------|
| 1文本 | 消息正文 | 消息正文截断 |
| 2图片 | 空字符串 | `[图片]` / `[图片x3]`多图标数量 |
| 3语音 | 空字符串 | `[语音 12"]`duration extra 解析 |
| 5文件 | 空字符串 | `[文件] report.pdf`file_name extra 解析 |
| 10系统 | 系统消息正文 | 系统消息正文 |
---
## 八、前端页面规划
### 8.1 新增组件(前台用户端)
| 组件 | 路径 | 说明 |
|------|------|------|
| MsgText.vue | components/msg/ | 文本消息组件 conversation 提取 |
| MsgImage.vue | components/msg/ | 图片消息1张=大图2-4张=2列网格5-9张=3列网格点击放大 |
| MsgVoice.vue | components/msg/ | 语音消息播放按钮+波形条+时长宽度按时长缩放 |
| MsgFile.vue | components/msg/ | 文件消息文件图标+文件名+大小+下载/预览按钮 |
| MorePanel.vue | components/chat/ | 输入栏"+"展开面板图片文件两个入口 |
| VoiceRecorder.vue | components/chat/ | 语音录制长按录音+上滑取消+时长显示+波形动画 |
| ImagePreviewer.vue | components/chat/ | 图片全屏预览Swiper 左右滑+双指缩放 |
### 8.2 修改页面(前台用户端)
| 页面 | 修改内容 |
|------|----------|
| pages/chat/conversation.vue | 消息列表按 type 渲染不同组件输入栏增加"+"和麦克风按钮 |
| pages/group/conversation.vue | 同上 |
| store/chat.js | 扩展 sendMessage 支持 extra新增 sendImageMessage/sendVoiceMessage/sendFileMessage |
### 8.3 新增页面(管理端)
| 页面 | 路径 | 说明 |
|------|------|------|
| 消息列表 | views/message/list.vue | 多条件筛选+分页表格+撤回/删除操作 |
| 消息统计 | views/message/stats.vue | ECharts 图表趋势折线图+类型饼图+活跃排行柱状图 |
---
## 九、DTO 扩展规划
### 9.1 发送消息请求扩展
```go
// SendMessageRequest 发送消息请求WS 事件 im.message.send 的 data 字段)
type SendMessageRequest struct {
ConversationID int64 `json:"conversation_id"`
TargetUserID int64 `json:"target_user_id"`
Type int `json:"type"`
Content string `json:"content"`
ClientMsgID string `json:"client_msg_id"`
AtUserIDs []int64 `json:"at_user_ids"`
Extra string `json:"extra"` // [Phase 2d 新增] 扩展数据 JSON 字符串
}
```
### 9.2 消息 DTO 扩展
```go
// MessageDTO 消息传输对象(返回给前端)
type MessageDTO struct {
ID int64 `json:"id"`
ConversationID int64 `json:"conversation_id"`
SenderID int64 `json:"sender_id"`
Type int `json:"type"`
Content string `json:"content"`
Extra *string `json:"extra,omitempty"` // [Phase 2d 新增] 扩展数据 JSON
Status int `json:"status"`
ClientMsgID string `json:"client_msg_id"`
AtUserIDs []int64 `json:"at_user_ids"`
CreatedAt string `json:"created_at"`
}
```
### 9.3 文件上传结果 DTO 扩展
```go
// ImageUploadResult 图片上传结果
type ImageUploadResult struct {
URL string `json:"url"`
ThumbnailURL string `json:"thumbnail_url"`
FileName string `json:"file_name"`
Size int64 `json:"size"`
Width int `json:"width"`
Height int `json:"height"`
MimeType string `json:"mime_type"`
}
// VoiceUploadResult 语音上传结果
type VoiceUploadResult struct {
URL string `json:"url"`
FileName string `json:"file_name"`
Size int64 `json:"size"`
Duration int `json:"duration"`
MimeType string `json:"mime_type"`
}
```
### 9.4 管理端消息 DTO
```go
// AdminMessageListRequest 管理端消息列表查询
type AdminMessageListRequest struct {
Keyword string `form:"keyword"`
Type *int `form:"type"`
SenderID *int64 `form:"sender_id"`
ConversationID *int64 `form:"conversation_id"`
Status *int `form:"status"`
StartTime string `form:"start_time"`
EndTime string `form:"end_time"`
Page int `form:"page"`
PageSize int `form:"page_size"`
}
// AdminMessageDTO 管理端消息条目
type AdminMessageDTO struct {
ID int64 `json:"id"`
ConversationID int64 `json:"conversation_id"`
SenderID int64 `json:"sender_id"`
SenderNickname string `json:"sender_nickname"`
SenderAvatar string `json:"sender_avatar"`
Type int `json:"type"`
TypeLabel string `json:"type_label"`
Content string `json:"content"`
Extra *string `json:"extra,omitempty"`
Status int `json:"status"`
StatusLabel string `json:"status_label"`
CreatedAt string `json:"created_at"`
}
// AdminMessageStatsRequest 管理端消息统计请求
type AdminMessageStatsRequest struct {
Days int `form:"days"` // 统计天数默认7最大90
}
// AdminMessageStatsResponse 管理端消息统计响应
type AdminMessageStatsResponse struct {
TotalCount int64 `json:"total_count"`
TodayCount int64 `json:"today_count"`
TypeDistribution []TypeDistItem `json:"type_distribution"`
DailyTrend []DailyTrendItem `json:"daily_trend"`
ActiveUsers []ActiveUserItem `json:"active_users"`
ActiveGroups []ActiveGroupItem `json:"active_groups"`
}
type TypeDistItem struct {
Type int `json:"type"`
Label string `json:"label"`
Count int64 `json:"count"`
}
type DailyTrendItem struct {
Date string `json:"date"`
Count int64 `json:"count"`
}
type ActiveUserItem struct {
UserID int64 `json:"user_id"`
Nickname string `json:"nickname"`
Count int64 `json:"count"`
}
type ActiveGroupItem struct {
GroupID int64 `json:"group_id"`
Name string `json:"name"`
Count int64 `json:"count"`
}
```
---
## 十、技术要点
### 10.1 Go 缩略图生成
- 依赖`github.com/disintegration/imaging`
- 流程`imaging.Open(file)` `imaging.Resize(img, 200, 0, imaging.Lanczos)` `imaging.Encode(buf, thumb, imaging.JPEG, imaging.JPEGQuality(80))`
- 缩略图文件名原始 UUID + `_thumb` 后缀 `abc123_thumb.jpg`
- 原图同时进行 JPEG 转码如果不是 JPEG 格式
### 10.2 前端图片压缩
- H5 平台使用 Canvas 压缩创建 Canvas drawImage toBlob('image/jpeg', 0.8)
- 小程序/App 平台使用 `uni.compressImage({ src, quality: 80 })`
- 压缩策略宽度>1920px 等比缩放至 1920px质量 80%
### 10.3 语音录制
- uni-app RecorderManager API
- `start({ format: 'mp3', duration: 60000, sampleRate: 44100, numberOfChannels: 1 })`
- 监听 `onStop` 获取录音文件临时路径
- 时长计算:回调中的 `duration` 字段(毫秒),前端转秒显示
### 10.4 语音播放
- uni-app InnerAudioContext API
- `uni.createInnerAudioContext()``src = voiceUrl``play()`
- 全局单例:同一时间只允许一个语音播放
- 播放状态:空闲 → 加载中 → 播放中 → 播放完成
### 10.5 文件下载与预览
- 下载:`uni.downloadFile({ url })``uni.saveFile()` 或直接 `window.open(url)` (H5)
- 预览PDF 和图片通过 `uni.openDocument` 或浏览器新标签打开
- 文件类型图标映射根据扩展名匹配图标PDF/Word/Excel/PPT/ZIP/图片/音频/通用)
### 10.6 管理端 ECharts 集成
- `npm install echarts` 安装
- 使用 Vue 3 Composition API + `onMounted` 初始化
- 响应式:`window.resize``chart.resize()`
- 主题色与系统保持一致Primary #2563EB
---
## 十一、补充设计细节
### 11.1 消息内容安全
- 文件上传不做内容安全审查(留后续阶段)
- 管理端提供人工审查和撤回能力
- 上传文件类型不限制,但大小限制 50MB
### 11.2 多图布局规则
| 图片数量 | 布局 | 单图最大宽度 |
|----------|------|-------------|
| 1 张 | 自适应宽度(最大 500rpx | 500rpx |
| 2 张 | 2列1行 | 240rpx |
| 3 张 | 3列1行 | 160rpx |
| 4 张 | 2列2行 | 240rpx |
| 5-6 张 | 3列2行 | 160rpx |
| 7-9 张 | 3列3行 | 160rpx |
### 11.3 文件类型图标映射
| 扩展名 | 图标 | 颜色 |
|--------|------|------|
| .pdf | PDF 图标 | #E53E3E |
| .doc/.docx | Word 图标 | #2B6CB0 |
| .xls/.xlsx | Excel 图标 | #276749 |
| .ppt/.pptx | PPT 图标 | #C05621 |
| .zip/.rar/.7z | 压缩包图标 | #6B46C1 |
| .jpg/.png/.gif | 图片图标 | #38A169 |
| .mp3/.wav/.aac | 音频图标 | #D69E2E |
| 其他 | 通用文件图标 | #718096 |
### 11.4 语音波形条设计
- 宽度:按时长线性缩放,最小 120rpx1秒最大 500rpx60秒
- 高度:固定 60rpx
- 波形:使用 CSS 模拟随机高度的竖条5-6 个竖条)
- 播放时:从左到右逐条变色动画