11.5 Web UI 图像功能

UI 设计概述

在 Web 客户端中添加图像上传功能需要考虑:

  1. 用户体验:直观的上传流程和预览
  2. 错误处理:清晰的错误提示
  3. 性能优化:大文件的处理
  4. 兼容性:支持不同浏览器

UI 布局

1
2
3
4
5
┌─────────────────────────────────────────────┐
│ [🖼️] [输入框.....................] [发送] │
└─────────────────────────────────────────────┘

图像上传按钮

预览模式:

1
2
3
4
5
6
7
8
9
┌─────────────────────────────────────────────┐
│ [×] ┌─────────┐ │
│ │ │ [输入框..........] [发送] │
│ │ 预览图 │ │
│ │ │ │
│ └─────────┘ │
└─────────────────────────────────────────────┘

移除按钮

HTML 结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<!-- public/index.html -->

<div class="message-input-container">
<!-- 图像上传按钮 -->
<button id="imageUploadBtn" class="icon-button" title="上传图片">
🖼️
</button>

<!-- 隐藏的文件输入 -->
<input
type="file"
id="imageInput"
accept="image/png,image/jpeg,image/gif,image/webp"
style="display: none;"
/>

<!-- 图像预览区域 -->
<div id="imagePreview" class="image-preview" style="display: none;">
<button id="removeImageBtn" class="remove-image-btn" title="移除图片">
×
</button>
<img id="previewImg" src="" alt="预览" />
</div>

<!-- 文本输入框 -->
<textarea
id="messageInput"
placeholder="输入消息..."
rows="1"
></textarea>

<!-- 发送按钮 -->
<button id="sendBtn" class="send-btn">发送</button>
</div>

CSS 样式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
/* public/css/style.css */

/* 图像上传按钮 */
.icon-button {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
padding: 8px;
color: #666;
transition: color 0.2s;
}

.icon-button:hover {
color: #007bff;
}

/* 图像预览容器 */
.image-preview {
position: relative;
display: inline-block;
margin-right: 10px;
vertical-align: bottom;
}

/* 预览图像 */
.image-preview img {
max-width: 100px;
max-height: 100px;
border-radius: 8px;
object-fit: cover;
border: 2px solid #ddd;
}

/* 移除按钮 */
.remove-image-btn {
position: absolute;
top: -8px;
right: -8px;
background: #dc3545;
color: white;
border: none;
border-radius: 50%;
width: 24px;
height: 24px;
cursor: pointer;
font-size: 18px;
line-height: 1;
display: flex;
align-items: center;
justify-content: center;
}

.remove-image-btn:hover {
background: #c82333;
}

/* 消息输入容器 */
.message-input-container {
display: flex;
align-items: flex-end;
padding: 15px;
border-top: 1px solid #ddd;
gap: 10px;
}

/* 输入框 */
#messageInput {
flex: 1;
border: 1px solid #ddd;
border-radius: 8px;
padding: 10px;
resize: none;
font-family: inherit;
font-size: 14px;
}

#messageInput:focus {
outline: none;
border-color: #007bff;
}

JavaScript 实现

状态管理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// public/js/app.js

class ChatApp {
constructor() {
this.selectedImage = null; // 存储选中的图像
this.ws = null;
this.sessionId = null;
this.init();
}

init() {
this.initWebSocket();
this.initImageUpload();
this.initMessageInput();
}
}

图像上传初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
// public/js/app.js

initImageUpload() {
const uploadBtn = document.getElementById('imageUploadBtn');
const imageInput = document.getElementById('imageInput');
const previewContainer = document.getElementById('imagePreview');
const previewImg = document.getElementById('previewImg');
const removeBtn = document.getElementById('removeImageBtn');

// 点击上传按钮
uploadBtn.addEventListener('click', () => {
imageInput.click();
});

// 文件选择事件
imageInput.addEventListener('change', async (e) => {
const file = e.target.files[0];
if (!file) return;

try {
// 验证文件类型
if (!file.type.startsWith('image/')) {
throw new Error('请选择图像文件');
}

// 验证文件大小 (10MB)
const maxSize = 10 * 1024 * 1024;
if (file.size > maxSize) {
throw new Error(
`图像文件过大 (${(file.size / 1024 / 1024).toFixed(2)}MB),` +
`最大支持 10MB`
);
}

// 读取文件为 Data URL
const dataUrl = await this.readFileAsDataURL(file);

// 存储图像数据
this.selectedImage = this.parseDataUrl(dataUrl);

// 显示预览
previewImg.src = dataUrl;
previewContainer.style.display = 'inline-block';

// 清空 input,允许重新选择同一文件
imageInput.value = '';

} catch (error) {
this.showError(error.message);
imageInput.value = '';
}
});

// 移除图像
removeBtn.addEventListener('click', () => {
this.selectedImage = null;
previewContainer.style.display = 'none';
previewImg.src = '';
imageInput.value = '';
});
}

文件读取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// public/js/app.js

/**
* 读取文件为 Data URL
*/
readFileAsDataURL(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();

reader.onload = (e) => {
resolve(e.target.result);
};

reader.onerror = () => {
reject(new Error('文件读取失败'));
};

reader.readAsDataURL(file);
});
}

/**
* 解析 Data URL
*/
parseDataUrl(dataUrl) {
// Data URL 格式: data:image/png;base64,iVBORw0KG...
const match = dataUrl.match(/^data:([^;]+);base64,(.+)$/);

if (!match) {
throw new Error('无效的图像格式');
}

return {
mediaType: match[1], // image/png
data: match[2] // base64 数据
};
}

发送多模态消息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// public/js/app.js

sendMessage() {
const input = document.getElementById('messageInput');
const text = input.value.trim();

// 构建消息内容
let content;

if (this.selectedImage && text) {
// 文本 + 图像
content = [
{ type: 'text', text: text },
{
type: 'image',
mediaType: this.selectedImage.mediaType,
data: this.selectedImage.data
}
];
} else if (this.selectedImage) {
// 纯图像
content = {
type: 'image',
mediaType: this.selectedImage.mediaType,
data: this.selectedImage.data
};
} else if (text) {
// 纯文本
content = text;
} else {
return; // 没有内容
}

// 发送消息
const message = {
type: 'chat.send',
sessionId: this.sessionId,
content: content
};

this.ws.send(JSON.stringify(message));

// 清空输入
input.value = '';

// 移除图像预览
if (this.selectedImage) {
this.selectedImage = null;
document.getElementById('imagePreview').style.display = 'none';
document.getElementById('previewImg').src = '';
}
}

错误提示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// public/js/app.js

/**
* 显示错误消息
*/
showError(message) {
// 创建提示元素
const toast = document.createElement('div');
toast.className = 'error-toast';
toast.textContent = message;
toast.style.cssText = `
position: fixed;
bottom: 20px;
right: 20px;
background: #dc3545;
color: white;
padding: 12px 20px;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
animation: slideIn 0.3s ease-out;
`;

document.body.appendChild(toast);

// 3秒后自动移除
setTimeout(() => {
toast.style.animation = 'slideOut 0.3s ease-out';
setTimeout(() => toast.remove(), 300);
}, 3000);
}

// 添加动画样式
const style = document.createElement('style');
style.textContent = `
@keyframes slideIn {
from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
@keyframes slideOut {
from { transform: translateX(0); opacity: 1; }
to { transform: translateX(100%); opacity: 0; }
}
`;
document.head.appendChild(style);

完整流程

用户上传图像流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
1. 点击 🖼️ 按钮

2. 打开文件选择对话框

3. 选择图像文件

4. 验证文件类型 (image/*)

5. 验证文件大小 (≤10MB)

6. FileReader 读取文件

7. 获取 Data URL

8. 解析为 ImageContent

9. 显示预览图

10. 可选:输入文本描述

11. 点击发送按钮

12. 构建多模态消息

13. 通过 WebSocket 发送

14. 清空输入状态

移除图像流程

1
2
3
4
5
6
7
8
9
1. 点击 × 按钮

2. 清空 selectedImage

3. 隐藏预览容器

4. 清空预览图 src

5. 重置文件 input

浏览器兼容性

功能 Chrome Firefox Safari Edge
FileReader API
Data URL
File API
WebSocket

最低版本要求:

  • Chrome 90+
  • Firefox 88+
  • Safari 14+
  • Edge 90+

性能优化

图像压缩(可选)

对于大图像,可以在客户端进行压缩:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/**
* 压缩图像
*/
async function compressImage(file, maxWidth = 1920, maxHeight = 1080, quality = 0.9) {
return new Promise((resolve) => {
const img = new Image();
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');

img.onload = () => {
// 计算缩放比例
let width = img.width;
let height = img.height;

if (width > maxWidth || height > maxHeight) {
const ratio = Math.min(maxWidth / width, maxHeight / height);
width *= ratio;
height *= ratio;
}

// 调整画布大小
canvas.width = width;
canvas.height = height;

// 绘制图像
ctx.drawImage(img, 0, 0, width, height);

// 转换为 Blob
canvas.toBlob(
(blob) => resolve(new File([blob], file.name, { type: file.type })),
file.type,
quality
);
};

img.src = URL.createObjectURL(file);
});
}

小结

本节介绍了 Web UI 图像功能的完整实现:

  • HTML 结构和 CSS 样式
  • 图像上传和预览
  • 文件验证和错误处理
  • 多模态消息发送
  • 浏览器兼容性
  • 性能优化建议

导航

上一篇: 11.4 多模态消息协议

下一篇: 11.6 CLI 图像命令