Initial commit: DataTool backend, frontend and Docker

This commit is contained in:
liu
2026-01-31 00:51:14 +08:00
commit 59bb8e16f5
69 changed files with 9449 additions and 0 deletions

View File

@@ -0,0 +1,890 @@
# 数据传输助手 - 开发文档
## 拆分文档目录01/02/03…
- [01 - 整体架构与核心概念](docs/01-整体架构与核心概念.md)
- [02 - 房间管理(创建 / 加入 / 退出)](docs/02-房间管理(创建-加入-退出).md)
- [03 - WebSocket连接管理连接 / 订阅 / 心跳 / 重连)](docs/03-WebSocket连接管理连接-订阅-心跳-重连).md)
- [04 - 消息协议与消息分发统一Message模型](docs/04-消息协议与消息分发统一Message模型.md)
- [05 - 文本传输(含大文本分片)](docs/05-文本传输(含大文本分片).md)
- [06 - 文件传输(拖拽 / 分片 / 进度 / 下载)](docs/06-文件传输(拖拽-分片-进度-下载).md)
- [07 - 图片预览Base64内联显示](docs/07-图片预览Base64内联显示.md)
- [08 - 剪贴板集成(读取 / 粘贴 / 降级)](docs/08-剪贴板集成(读取-粘贴-降级).md)
- [09 - 在线用户列表(房间成员与系统消息)](docs/09-在线用户列表(房间成员与系统消息).md)
- [10 - 历史记录(本地存储 / 清空 / 导出)](docs/10-历史记录(本地存储-清空-导出).md)
- [11 - 数据库设计(可选:持久化与审计)](docs/11-数据库设计(可选:持久化与审计).md)
- [12 - 部署与环境配置(后端 / 前端 / Nginx](docs/12-部署与环境配置(后端-前端-Nginx.md)
- [13 - 安全与风控(基础措施与可选增强)](docs/13-安全与风控(基础措施与可选增强).md)
- [14 - 开发计划与里程碑Phase 1~4](docs/14-开发计划与里程碑Phase1-4.md)
## 一、项目概述
### 1.1 项目背景
在VNC远程桌面、内网环境或安全隔离场景中由于剪贴板共享被禁用或系统限制用户无法直接进行复制粘贴操作。本工具提供一个基于浏览器的轻量级数据传输方案通过WebSocket实现多端实时数据同步。
### 1.2 应用场景
- VNC/远程桌面环境的数据传输
- 内外网隔离环境下的文件/文本交换
- 临时性的跨设备数据共享
- 无法使用U盘或即时通讯工具的场景
### 1.3 技术栈
| 层级 | 技术 | 版本建议 |
|------|------|----------|
| 后端 | Spring Boot | 2.7.x / 3.x |
| 后端 | Java | 11 / 17 |
| 实时通信 | WebSocket (STOMP) | - |
| 前端 | Vue | 3.x |
| UI组件库 | Element Plus | 2.x |
| 构建工具 | Vite | 4.x |
---
## 二、系统架构
### 2.1 架构图
```
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ 发送端浏览器 │◄───►│ WebSocket │◄───►│ 接收端浏览器 │
│ (Vue + Element)│ │ 服务器(Java) │ │ (Vue + Element)│
└─────────────────┘ └─────────────────┘ └─────────────────┘
│ │ │
└───────────────────────┴───────────────────────┘
同一房间号(room)
```
### 2.2 核心概念
- **房间(Room)**数据传输的隔离空间通过6位数字房间号标识
- **会话(Session)**WebSocket连接实例
- **消息(Message)**:传输的数据单元,支持文本、文件元数据、二进制分片
---
## 三、功能设计
### 3.1 功能模块
```
├── 房间管理
│ ├── 创建房间自动生成6位房间号
│ ├── 加入房间(输入房间号)
│ └── 退出房间
├── 数据传输
│ ├── 文本传输(支持大文本,自动分片)
│ ├── 文件传输(支持拖拽上传、进度显示)
│ ├── 图片预览Base64内联显示
│ └── 剪贴板读取(读取系统剪贴板内容)
├── 连接管理
│ ├── 在线用户列表
│ ├── 连接状态指示
│ └── 心跳保活
└── 历史记录
├── 消息历史(本地存储)
├── 清空记录
└── 导出记录
```
### 3.2 页面布局
```
┌─────────────────────────────────────────────────────────────┐
│ 数据传输助手 [连接状态] │
├─────────────────────────────────────────────────────────────┤
│ ┌─────────────┐ ┌───────────────────────────────────────┐ │
│ │ 房间信息 │ │ 消息展示区 │ │
│ │ ───────── │ │ ┌─────────────────────────────────┐ │ │
│ │ 房间号: │ │ │ [系统] xxx 加入了房间 │ │ │
│ │ 123456 │ │ │ [我] 这是一段文本消息... │ │ │
│ │ │ │ │ [对方] 收到,这是回复... │ │ │
│ │ ───────── │ │ │ [文件] 文档.pdf (2.5MB) [下载] │ │ │
│ │ 在线用户 │ │ └─────────────────────────────────┘ │ │
│ │ ● 用户A │ │ │ │
│ │ ○ 用户B │ └───────────────────────────────────────┘ │
│ │ │ ┌───────────────────────────────────────┐ │
│ │ ───────── │ │ [粘贴/拖拽区域] │ │
│ │ 快捷操作 │ │ 支持: 文本粘贴、文件拖拽、剪贴板读取 │ │
│ │ [清空] │ └───────────────────────────────────────┘ │
│ │ [导出] │ ┌────────────────────┐ ┌──────────────┐ │ │
│ └─────────────┘ │ 输入框... │ │ 发送 [▶] │ │ │
│ └────────────────────┘ └──────────────┘ │ │
└─────────────────────────────────────────────────────────────┘
```
---
## 四、数据库设计
### 4.1 实体设计(可选,支持消息持久化)
```sql
-- 房间表
CREATE TABLE room (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
room_code VARCHAR(6) UNIQUE NOT NULL COMMENT '房间号',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
expires_at TIMESTAMP COMMENT '过期时间',
is_active TINYINT DEFAULT 1
);
-- 消息记录表(可选,用于审计或消息回放)
CREATE TABLE message (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
room_code VARCHAR(6) NOT NULL,
sender_id VARCHAR(64) NOT NULL,
sender_name VARCHAR(32),
msg_type TINYINT COMMENT '1-文本 2-文件 3-图片',
content TEXT COMMENT '文本内容或文件元数据JSON',
file_size BIGINT COMMENT '文件大小',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_room_time (room_code, created_at)
);
```
---
## 五、接口设计
### 5.1 WebSocket 端点
| 端点 | 说明 |
|------|------|
| `/ws/data-transfer` | WebSocket连接入口 |
| `/topic/room/{roomCode}` | 房间广播频道(订阅) |
| `/app/room/{roomCode}/join` | 加入房间(发送) |
| `/app/room/{roomCode}/leave` | 离开房间(发送) |
| `/app/room/{roomCode}/message` | 发送消息(发送) |
| `/app/room/{roomCode}/file/chunk` | 发送文件分片(发送) |
### 5.2 消息协议
#### 基础消息格式
```json
{
"type": "TEXT | FILE | IMAGE | SYSTEM | CHUNK",
"senderId": "uuid",
"senderName": "用户昵称",
"timestamp": 1706345600000,
"payload": {}
}
```
#### 文本消息
```json
{
"type": "TEXT",
"senderId": "uuid",
"senderName": "用户A",
"timestamp": 1706345600000,
"payload": {
"content": "要传输的文本内容",
"isChunk": false,
"chunkIndex": 0,
"totalChunks": 1
}
}
```
#### 文件元数据消息
```json
{
"type": "FILE",
"senderId": "uuid",
"senderName": "用户A",
"timestamp": 1706345600000,
"payload": {
"fileId": "uuid",
"fileName": "document.pdf",
"fileSize": 2621440,
"mimeType": "application/pdf",
"totalChunks": 10
}
}
```
#### 文件分片消息
```json
{
"type": "CHUNK",
"senderId": "uuid",
"payload": {
"fileId": "uuid",
"chunkIndex": 0,
"data": "base64EncodedChunkData"
}
}
```
#### 系统消息
```json
{
"type": "SYSTEM",
"payload": {
"event": "USER_JOIN | USER_LEAVE | ERROR",
"message": "xxx 加入了房间",
"userList": [{"id": "uuid", "name": "用户A"}]
}
}
```
---
## 六、后端实现
### 6.1 项目结构
```
data-transfer-server/
├── src/main/java/com/example/datatransfer/
│ ├── config/
│ │ ├── WebSocketConfig.java # WebSocket配置
│ │ └── CorsConfig.java # 跨域配置
│ ├── controller/
│ │ └── WebSocketController.java # WebSocket消息处理器
│ ├── service/
│ │ ├── RoomService.java # 房间管理
│ │ ├── MessageService.java # 消息处理
│ │ └── FileTransferService.java # 文件传输管理
│ ├── model/
│ │ ├── Message.java # 消息实体
│ │ ├── Room.java # 房间实体
│ │ └── FileChunk.java # 文件分片
│ ├── handler/
│ │ └── CustomHandshakeHandler.java # 握手处理器
│ └── DataTransferApplication.java
├── src/main/resources/
│ └── application.yml
└── pom.xml
```
### 6.2 核心代码示例
#### WebSocketConfig.java
```java
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/topic");
config.setApplicationDestinationPrefixes("/app");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws/data-transfer")
.setAllowedOriginPatterns("*")
.withSockJS();
}
}
```
#### WebSocketController.java
```java
@Controller
public class WebSocketController {
@Autowired
private SimpMessagingTemplate messagingTemplate;
@Autowired
private RoomService roomService;
// 加入房间
@MessageMapping("/room/{roomCode}/join")
public void joinRoom(@DestinationVariable String roomCode,
@Payload JoinRequest request,
SimpMessageHeaderAccessor headerAccessor) {
String sessionId = headerAccessor.getSessionId();
roomService.joinRoom(roomCode, sessionId, request.getUserName());
// 广播用户加入消息
Message systemMsg = Message.builder()
.type(MessageType.SYSTEM)
.payload(Map.of(
"event", "USER_JOIN",
"message", request.getUserName() + " 加入了房间",
"userList", roomService.getUserList(roomCode)
))
.build();
messagingTemplate.convertAndSend("/topic/room/" + roomCode, systemMsg);
}
// 发送文本消息
@MessageMapping("/room/{roomCode}/message")
public void sendMessage(@DestinationVariable String roomCode,
@Payload Message message) {
message.setTimestamp(System.currentTimeMillis());
messagingTemplate.convertAndSend("/topic/room/" + roomCode, message);
}
// 发送文件分片
@MessageMapping("/room/{roomCode}/file/chunk")
public void sendFileChunk(@DestinationVariable String roomCode,
@Payload FileChunk chunk) {
messagingTemplate.convertAndSend(
"/topic/room/" + roomCode + "/file/" + chunk.getFileId(),
chunk
);
}
}
```
#### RoomService.java
```java
@Service
public class RoomService {
// 内存存储生产环境可改用Redis
private ConcurrentHashMap<String, Room> rooms = new ConcurrentHashMap<>();
public String createRoom() {
String roomCode = generateRoomCode();
Room room = new Room(roomCode);
rooms.put(roomCode, room);
return roomCode;
}
public void joinRoom(String roomCode, String sessionId, String userName) {
Room room = rooms.computeIfAbsent(roomCode, k -> new Room(k));
room.addUser(sessionId, userName);
}
public void leaveRoom(String roomCode, String sessionId) {
Room room = rooms.get(roomCode);
if (room != null) {
room.removeUser(sessionId);
if (room.isEmpty()) {
rooms.remove(roomCode);
}
}
}
private String generateRoomCode() {
// 生成6位数字房间号
Random random = new Random();
return String.format("%06d", random.nextInt(1000000));
}
}
```
---
## 七、前端实现
### 7.1 项目结构
```
data-transfer-web/
├── public/
├── src/
│ ├── api/
│ │ └── websocket.js # WebSocket封装
│ ├── components/
│ │ ├── RoomPanel.vue # 房间信息面板
│ │ ├── MessageList.vue # 消息列表
│ │ ├── MessageInput.vue # 消息输入
│ │ ├── FileDropZone.vue # 文件拖拽区域
│ │ └── UserList.vue # 在线用户列表
│ ├── views/
│ │ ├── HomeView.vue # 首页(创建/加入房间)
│ │ └── RoomView.vue # 房间页面
│ ├── stores/
│ │ └── room.js # Pinia状态管理
│ ├── utils/
│ │ ├── fileChunker.js # 文件分片工具
│ │ └── clipboard.js # 剪贴板工具
│ ├── App.vue
│ └── main.js
├── index.html
├── package.json
└── vite.config.js
```
### 7.2 核心代码示例
#### WebSocket封装 (websocket.js)
```javascript
import SockJS from 'sockjs-client'
import { Client } from '@stomp/stompjs'
class WebSocketService {
constructor() {
this.client = null
this.subscriptions = new Map()
}
connect(url, onConnect, onError) {
this.client = new Client({
webSocketFactory: () => new SockJS(url),
reconnectDelay: 5000,
heartbeatIncoming: 4000,
heartbeatOutgoing: 4000,
})
this.client.onConnect = onConnect
this.client.onStompError = onError
this.client.activate()
}
subscribe(destination, callback) {
if (this.client && this.client.connected) {
const subscription = this.client.subscribe(destination, (message) => {
callback(JSON.parse(message.body))
})
this.subscriptions.set(destination, subscription)
}
}
send(destination, body) {
if (this.client && this.client.connected) {
this.client.publish({
destination,
body: JSON.stringify(body)
})
}
}
disconnect() {
this.subscriptions.forEach(sub => sub.unsubscribe())
this.subscriptions.clear()
if (this.client) {
this.client.deactivate()
}
}
}
export default new WebSocketService()
```
#### 房间页面 (RoomView.vue)
```vue
<template>
<div class="room-container">
<el-container>
<!-- 左侧边栏 -->
<el-aside width="250px">
<RoomPanel
:roomCode="roomCode"
:userList="userList"
@leave="handleLeave"
/>
</el-aside>
<!-- 主内容区 -->
<el-main>
<MessageList
:messages="messages"
ref="messageList"
/>
<FileDropZone
@file-selected="handleFileSelect"
@text-pasted="handleTextPaste"
/>
<MessageInput
v-model="inputText"
@send="sendTextMessage"
@paste="handlePaste"
/>
</el-main>
</el-container>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import WebSocketService from '@/api/websocket'
import RoomPanel from '@/components/RoomPanel.vue'
import MessageList from '@/components/MessageList.vue'
import MessageInput from '@/components/MessageInput.vue'
import FileDropZone from '@/components/FileDropZone.vue'
const route = useRoute()
const router = useRouter()
const roomCode = route.params.code
const messages = ref([])
const userList = ref([])
const inputText = ref('')
const myId = ref('')
onMounted(() => {
connectWebSocket()
})
onUnmounted(() => {
WebSocketService.disconnect()
})
const connectWebSocket = () => {
WebSocketService.connect('/ws/data-transfer', () => {
// 订阅房间消息
WebSocketService.subscribe(`/topic/room/${roomCode}`, (msg) => {
handleMessage(msg)
})
// 加入房间
WebSocketService.send(`/app/room/${roomCode}/join`, {
userName: `用户${Math.random().toString(36).substr(2, 4)}`
})
})
}
const handleMessage = (msg) => {
switch (msg.type) {
case 'SYSTEM':
if (msg.payload.userList) {
userList.value = msg.payload.userList
}
messages.value.push({
type: 'system',
content: msg.payload.message
})
break
case 'TEXT':
messages.value.push({
type: msg.senderId === myId.value ? 'me' : 'other',
sender: msg.senderName,
content: msg.payload.content,
time: msg.timestamp
})
break
case 'FILE':
messages.value.push({
type: 'file',
sender: msg.senderName,
fileName: msg.payload.fileName,
fileSize: msg.payload.fileSize,
fileId: msg.payload.fileId
})
break
}
}
const sendTextMessage = () => {
if (!inputText.value.trim()) return
WebSocketService.send(`/app/room/${roomCode}/message`, {
type: 'TEXT',
senderId: myId.value,
payload: {
content: inputText.value,
isChunk: false
}
})
inputText.value = ''
}
const handleFileSelect = async (file) => {
// 发送文件元数据
const fileId = generateUUID()
const chunkSize = 64 * 1024 // 64KB分片
const totalChunks = Math.ceil(file.size / chunkSize)
WebSocketService.send(`/app/room/${roomCode}/message`, {
type: 'FILE',
senderId: myId.value,
payload: {
fileId,
fileName: file.name,
fileSize: file.size,
mimeType: file.type,
totalChunks
}
})
// 发送文件分片
for (let i = 0; i < totalChunks; i++) {
const chunk = file.slice(i * chunkSize, (i + 1) * chunkSize)
const base64 = await fileToBase64(chunk)
WebSocketService.send(`/app/room/${roomCode}/file/chunk`, {
fileId,
chunkIndex: i,
data: base64.split(',')[1]
})
}
}
const handleTextPaste = (text) => {
inputText.value = text
}
const handleLeave = () => {
WebSocketService.send(`/app/room/${roomCode}/leave`, {})
router.push('/')
}
</script>
```
#### 文件拖拽组件 (FileDropZone.vue)
```vue
<template>
<div
class="file-drop-zone"
:class="{ 'drag-over': isDragOver }"
@dragover.prevent="isDragOver = true"
@dragleave="isDragOver = false"
@drop.prevent="handleDrop"
@paste="handlePaste"
tabindex="0"
>
<el-icon><Upload /></el-icon>
<span>拖拽文件到此处或按 Ctrl+V 粘贴</span>
<el-button @click="readClipboard" size="small" type="primary">
读取剪贴板
</el-button>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { Upload } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
const emit = defineEmits(['file-selected', 'text-pasted'])
const isDragOver = ref(false)
const handleDrop = (e) => {
isDragOver.value = false
const files = e.dataTransfer.files
if (files.length > 0) {
emit('file-selected', files[0])
}
}
const handlePaste = (e) => {
const items = e.clipboardData.items
for (let item of items) {
if (item.kind === 'file') {
const file = item.getAsFile()
emit('file-selected', file)
} else if (item.kind === 'string' && item.type === 'text/plain') {
item.getAsString(text => emit('text-pasted', text))
}
}
}
const readClipboard = async () => {
try {
const text = await navigator.clipboard.readText()
emit('text-pasted', text)
ElMessage.success('已读取剪贴板内容')
} catch (err) {
ElMessage.error('无法读取剪贴板,请手动粘贴')
}
}
</script>
<style scoped>
.file-drop-zone {
border: 2px dashed #dcdfe6;
border-radius: 8px;
padding: 20px;
text-align: center;
transition: all 0.3s;
margin: 10px 0;
}
.file-drop-zone.drag-over {
border-color: #409eff;
background: #f0f9ff;
}
</style>
```
---
## 八、部署配置
### 8.1 后端部署 (application.yml)
```yaml
server:
port: 8080
spring:
websocket:
message-buffer-size: 8192
# 如需消息持久化,配置数据库
datasource:
url: jdbc:mysql://localhost:3306/data_transfer
username: root
password: xxx
# 文件传输配置
transfer:
chunk-size: 65536 # 64KB分片
max-file-size: 104857600 # 100MB
room-expire-hours: 24 # 房间过期时间
```
### 8.2 前端部署 (vite.config.js)
```javascript
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
server: {
proxy: {
'/ws': {
target: 'http://localhost:8080',
ws: true,
changeOrigin: true
},
'/api': {
target: 'http://localhost:8080',
changeOrigin: true
}
}
},
build: {
outDir: 'dist',
assetsDir: 'assets'
}
})
```
### 8.3 Nginx配置生产环境
```nginx
server {
listen 80;
server_name your-domain.com;
location / {
root /path/to/data-transfer-web/dist;
index index.html;
try_files $uri $uri/ /index.html;
}
location /ws {
proxy_pass http://localhost:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
location /api {
proxy_pass http://localhost:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
```
---
## 九、安全考虑
### 9.1 安全措施
| 风险 | 解决方案 |
|------|----------|
| 房间号暴力破解 | 6位数字+过期机制,限制尝试频率 |
| 大文件攻击 | 限制单文件100MB限制房间总传输量 |
| XSS攻击 | 消息内容HTML转义使用textContent |
| 数据泄露 | 房间自动过期,服务端不持久化敏感内容 |
| WebSocket劫持 | 握手时验证Origin使用WSS加密 |
### 9.2 可选增强
- 添加房间密码
- 端到端加密WebCrypto API
- IP白名单限制
- 操作日志审计
---
## 十、开发计划
### 10.1 里程碑
| 阶段 | 时间 | 交付物 |
|------|------|--------|
| Phase 1 | 3天 | 基础WebSocket连接、文本传输、房间管理 |
| Phase 2 | 3天 | 文件传输(分片)、进度显示、拖拽上传 |
| Phase 3 | 2天 | 剪贴板集成、图片预览、历史记录 |
| Phase 4 | 2天 | UI优化、响应式适配、测试修复 |
### 10.2 依赖清单
**后端 (pom.xml)**
```xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>sockjs-client</artifactId>
<version>1.5.1</version>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>stomp-websocket</artifactId>
<version>2.3.4</version>
</dependency>
</dependencies>
```
**前端 (package.json)**
```json
{
"dependencies": {
"vue": "^3.3.4",
"vue-router": "^4.2.4",
"pinia": "^2.1.6",
"element-plus": "^2.3.14",
"@stomp/stompjs": "^7.0.0",
"sockjs-client": "^1.6.1"
},
"devDependencies": {
"vite": "^4.4.9",
"@vitejs/plugin-vue": "^4.3.4"
}
}
```
---
## 十一、附录
### 11.1 参考文档
- [Spring WebSocket官方文档](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#websocket)
- [STOMP协议规范](https://stomp.github.io/)
- [Element Plus文档](https://element-plus.org/)
- [Vue 3文档](https://vuejs.org/)
### 11.2 常见问题
**Q: 文件传输大小限制?**
A: 默认100MB可通过配置调整。超大文件建议分片后逐片发送。
**Q: 支持多文件同时传输吗?**
A: 支持每个文件有独立fileId可并行传输。
**Q: 断线重连如何处理?**
STOMP客户端内置重连机制重连后需重新加入房间。
**Q: 内网环境如何部署?**
A: 打包后部署到内网服务器确保客户端能访问WebSocket端口。
---
**文档版本**: v1.0
**编写日期**: 2026-01-27
**作者**: AI Assistant

View File

@@ -0,0 +1,28 @@
# 01 - 整体架构与核心概念
## 目标与范围
- **目标**:在 VNC/远程桌面、内网隔离等场景下,提供基于浏览器的轻量数据传输能力,通过 WebSocket 实现多端实时同步。
- **范围**:文本、文件(分片)、图片预览、剪贴板读取/粘贴、在线用户列表、历史记录(本地)。
## 架构概览
```
发送端浏览器(Vue) <——WebSocket(STOMP/SockJS)——> 服务端(Spring Boot) <——> 接收端浏览器(Vue)
└—— 同一房间号(roomCode)隔离广播
```
## 核心概念
- **房间Room**:数据传输隔离空间,通过 **6 位数字** `roomCode` 标识。
- **会话Session**WebSocket 连接实例,通常以 `sessionId` 识别。
- **消息Message**:传输数据单元,支持 `TEXT/FILE/IMAGE/SYSTEM/CHUNK`
## 关键数据流
- **加入房间**:客户端连接成功后发送 `/app/room/{roomCode}/join` → 服务端更新房间用户 → 广播 `SYSTEM(USER_JOIN + userList)``/topic/room/{roomCode}`
- **发送消息**:客户端发送 `/app/room/{roomCode}/message`TEXT/FILE/IMAGE/SYSTEM→ 服务端补齐时间戳并广播到 `/topic/room/{roomCode}`
- **文件分片**:客户端发送 `/app/room/{roomCode}/file/chunk`CHUNK→ 服务端转发到 `/topic/room/{roomCode}/file/{fileId}`(或同房间通道内约定字段分发)。
## 非功能性要求NFR
- **可用性**:断线自动重连;重连后需重新 join。
- **性能**:大文本/大文件分片;限制单文件大小与分片大小;避免 UI 卡顿(增量渲染/节流)。
- **安全**默认不落库房间过期XSS 防护;可选 WSS/Origin 校验。

View File

@@ -0,0 +1,39 @@
# 02 - 房间管理(创建 / 加入 / 退出)
## 功能目标
- **创建房间**:自动生成 6 位数字房间号 `roomCode`
- **加入房间**:输入房间号加入;服务端维护在线用户列表。
- **退出房间**:主动退出或断线退出;房间空则销毁或等待过期。
## 前端Vue
- **页面/组件**
- `HomeView.vue`:创建房间、加入房间(输入 `roomCode`)。
- `RoomView.vue`:展示房间信息与离开操作入口。
- `RoomPanel.vue`:显示房间号、在线用户、退出按钮、快捷操作(清空/导出等)。
- **交互细节**
- 加入房间前校验 `roomCode` 为 6 位数字。
- join/leave 成功后在消息区插入系统提示“xxx加入/离开”)。
## 后端Spring Boot
- **核心服务:`RoomService`**
- `createRoom()`:生成 `roomCode` 并创建房间。
- `joinRoom(roomCode, sessionId, userName)`:绑定用户到房间。
- `leaveRoom(roomCode, sessionId)`:移除用户;若空房间则移除。
- (可选)`expireRoom()`:定时清理过期房间(按 `transfer.room-expire-hours`)。
- **消息入口STOMP**
- `/app/room/{roomCode}/join`:加入房间
- `/app/room/{roomCode}/leave`:离开房间
## 协议与数据
- **JoinRequest**
- `userName`:用户昵称(前端生成或用户输入)
- **系统消息SYSTEM**
- `payload.event``USER_JOIN | USER_LEAVE | ERROR`
- `payload.message`:提示文本
- `payload.userList`:在线用户列表(数组)
## 边界与注意点
- **房间是否需要“先创建后加入”**:文档示例允许 `computeIfAbsent`,即加入时若不存在会自动创建(实现上需确定产品规则)。
- **断线离开**:建议监听会话断开事件,同步触发离房逻辑并广播 `USER_LEAVE`
- **防暴力猜测**:限制 join 尝试频率(可选增强)。

View File

@@ -0,0 +1,49 @@
# 03 - WebSocket连接管理连接 / 订阅 / 心跳 / 重连)
## 功能目标
- 建立浏览器到服务端的 WebSocket 连接SockJS + STOMP
- 订阅房间广播通道,实时接收消息。
- 具备心跳保活与自动重连UI 显示连接状态。
## 前端Vue
- **封装层**`src/api/websocket.js`
- `connect(url, onConnect, onError)`:创建 STOMP Client启用 `reconnectDelay` 与心跳参数。
- `subscribe(destination, callback)`:按 destination 保存订阅,便于统一退订。
- `send(destination, body)`publish JSON 消息。
- `disconnect()`:取消订阅并关闭连接。
- **连接时序(建议)**
1. connect 成功
2. subscribe `/topic/room/{roomCode}`
3. send `/app/room/{roomCode}/join`
4. 开始收发消息
- **连接状态 UI**
- 显示:连接中 / 已连接 / 断开 / 重连中
- 重连成功后:自动重新 subscribe + 重新 join
## 后端Spring Boot
- **WebSocket 配置**
- 端点:`/ws/data-transfer`
- broker`/topic`
- 应用前缀:`/app`
- SockJS`.withSockJS()`
- **跨域与握手**
- `setAllowedOriginPatterns("*")`(开发期)
- 生产期建议:限制 Origin + 启用 WSS
- **心跳**
- 与前端 STOMP 心跳保持一致(如需更严格,改用外部 broker 或自定义心跳策略)
## 协议与通道
- 订阅(接收)
- `/topic/room/{roomCode}`:房间内通用消息广播
- `/topic/room/{roomCode}/file/{fileId}`:某文件分片通道(可选设计)
- 发送(服务端处理)
- `/app/room/{roomCode}/join`
- `/app/room/{roomCode}/leave`
- `/app/room/{roomCode}/message`
- `/app/room/{roomCode}/file/chunk`
## 边界与注意点
- **重连后的状态恢复**:需要重新 join 才能恢复在线用户列表与系统消息一致性。
- **订阅泄漏**:房间切换、页面卸载必须 disconnect/退订,避免重复订阅造成重复消息。
- **代理/反代**:生产 Nginx 需支持 Upgrade`/ws` 走 ws 反代。

View File

@@ -0,0 +1,97 @@
# 04 - 消息协议与消息分发(统一 Message 模型)
## 功能目标
- 定义统一消息格式(便于扩展与兼容)。
- 服务端负责房间维度的消息广播/转发。
- 前端按 `type` 分发渲染与业务处理。
## 协议定义
### 基础消息格式
```json
{
"type": "TEXT | FILE | IMAGE | SYSTEM | CHUNK",
"senderId": "uuid",
"senderName": "用户昵称",
"timestamp": 1706345600000,
"payload": {}
}
```
### 文本消息TEXT
```json
{
"type": "TEXT",
"senderId": "uuid",
"senderName": "用户A",
"timestamp": 1706345600000,
"payload": {
"content": "要传输的文本内容",
"isChunk": false,
"chunkIndex": 0,
"totalChunks": 1
}
}
```
### 文件元数据FILE
```json
{
"type": "FILE",
"senderId": "uuid",
"senderName": "用户A",
"timestamp": 1706345600000,
"payload": {
"fileId": "uuid",
"fileName": "document.pdf",
"fileSize": 2621440,
"mimeType": "application/pdf",
"totalChunks": 10
}
}
```
### 文件分片CHUNK
```json
{
"type": "CHUNK",
"senderId": "uuid",
"payload": {
"fileId": "uuid",
"chunkIndex": 0,
"data": "base64EncodedChunkData"
}
}
```
### 系统消息SYSTEM
```json
{
"type": "SYSTEM",
"payload": {
"event": "USER_JOIN | USER_LEAVE | ERROR",
"message": "xxx 加入了房间",
"userList": [{"id": "uuid", "name": "用户A"}]
}
}
```
## 前端分发Vue
- **入口**`RoomView``handleMessage(msg)` 或等价逻辑。
- **分发规则(示例)**
- `SYSTEM`:更新 `userList`,并向消息列表追加系统提示。
- `TEXT`:追加文本消息(区分 me/other
- `FILE`:追加文件卡片(显示名称、大小、下载按钮、进度)。
- `CHUNK`:写入文件缓存并更新进度;完成后合并为 Blob。
## 后端分发Spring Boot
- **控制器**`WebSocketController`
- `/message`:广播常规消息到 `/topic/room/{roomCode}`
- `/file/chunk`:转发分片到文件通道或同房间通道
- **补齐与校验(建议)**
- 服务端补齐 `timestamp`,必要时校验 `type/payload` 的字段完整性、大小限制与频率限制。
## 边界与注意点
- **通道设计一致性**:若采用“文件独立 topic”前端需额外 subscribe若统一走房间 topic需在 payload 内携带 fileId 并做路由分发。
- **大消息**:避免一次性发送超大 payload统一走分片策略。
- **安全**:前端渲染文本必须转义,禁止把用户内容当 HTML 渲染。

View File

@@ -0,0 +1,36 @@
# 05 - 文本传输(含大文本分片)
## 功能目标
- 支持在房间内发送/接收文本消息。
- 支持大文本自动分片(避免单包过大导致失败或卡顿)。
## 前端Vue
- **输入与发送**
- `MessageInput.vue`:输入框 + 发送按钮;支持 Ctrl+V 粘贴填充。
- 发送前校验:非空、长度上限(建议)。
- **大文本分片(建议实现方式)**
- 当文本长度超过阈值(如 8KB/32KB
- 生成 `messageId`(可复用 `senderId + timestamp` 或 UUID
- 拆分为 `totalChunks`
- 逐段发送 `TEXT`,携带 `isChunk=true/chunkIndex/totalChunks`,并在 payload 中携带 `messageId`
- 接收端按 `messageId` 缓存分片并重组,重组完成后再写入消息列表。
- **展示与防护**
- 文本内容按纯文本展示(转义),禁止 `v-html`
## 后端Spring Boot
- **处理策略**
- 默认透传广播即可:在 `/app/room/{roomCode}/message` 收到后补齐 `timestamp` 并广播。
- (可选)限流/长度限制:防刷屏与恶意超大文本。
## 协议与数据
- `type=TEXT`
- `payload` 推荐字段:
- `content`:文本内容(单片)
- `isChunk`:是否为分片
- `chunkIndex/totalChunks`:分片序号与总片数
- `messageId`:(建议新增)用于重组与去重
## 边界与注意点
- **重组内存**:前端缓存分片需设定超时清理,避免长期占用内存。
- **乱序/丢片**WebSocket 一般有序但重连/异常情况下仍可能丢失;可以在 UI 上提示“分片未完整接收”。

View File

@@ -0,0 +1,42 @@
# 06 - 文件传输(拖拽 / 分片 / 进度 / 下载)
## 功能目标
- 支持拖拽上传、粘贴文件、发送文件元数据与分片内容。
- 接收端显示进度并可下载(本地重组 Blob
## 前端Vue
- **入口组件**
- `FileDropZone.vue`:拖拽、粘贴;触发 `file-selected` 事件。
- **分片与发送**
- `fileChunker.js`:按 `chunkSize` 切片文档示例64KB
- 发送流程:
1. 生成 `fileId`
2. 发送 `FILE` 元数据到 `/app/room/{roomCode}/message`
3. 循环发送 `CHUNK``/app/room/{roomCode}/file/chunk`
- **进度显示(建议)**
- 发送端:`sentChunks / totalChunks`
- 接收端:`receivedChunks / totalChunks`
- 通过消息列表中的文件卡片展示(进度条 + 状态:接收中/完成/失败)。
- **接收与下载(建议实现方式)**
- 建立 `fileId -> { meta, chunks[] }` 缓存
- 收到所有分片后,按顺序拼接为 `Uint8Array`/Blob
- 使用 `URL.createObjectURL(blob)` 生成下载链接
## 后端Spring Boot
- **转发策略**
- 元数据:作为 `FILE` 消息广播到房间 topic。
- 分片:作为 `CHUNK` 消息转发(可用独立 topic 或统一 topic
- **限制与风控(建议)**
- 单文件最大值(默认 100MB可配置
- 分片大小(默认 64KB可配置
- 频率限制(防止刷分片)。
## 协议与数据
- `FILE.payload``fileId/fileName/fileSize/mimeType/totalChunks`
- `CHUNK.payload``fileId/chunkIndex/data(base64)`
## 边界与注意点
- **Base64 膨胀**Base64 会增加体积(约 33%),大文件会更慢;如需优化可改二进制 WebSocket后续增强
- **并行多文件**:以 `fileId` 隔离缓存即可并行UI 需支持多文件卡片。
- **断线续传**当前协议未定义续传可选增强chunk ack / 断点续传)。

View File

@@ -0,0 +1,29 @@
# 07 - 图片预览Base64 内联显示)
## 功能目标
- 在消息列表中对图片进行缩略图预览与放大查看。
- 支持通过“文件传输通道”或“图片专用消息”传输图片数据。
## 前端Vue
- **识别规则**
- `mimeType` 满足 `image/*` 时按图片渲染。
- **展示形态(建议)**
- 消息卡片中显示缩略图(限制最大宽高)。
- 点击后弹窗预览Element Plus `el-dialog`/`el-image` 预览)。
- **传输策略(两种可选)**
- **复用文件分片**(推荐一致性):图片走 `FILE + CHUNK`,接收端完成后生成 Blob 并预览。
- **小图直发**:当图片小于阈值(如 200KB时发送 `IMAGE`payload 直接携带 base64需限制大小
## 后端Spring Boot
- 默认透传(与 FILE/CHUNK 一致);在生产环境建议增加大小限制与频率限制。
## 协议与数据(建议)
- 若使用 `IMAGE`
- `payload.data`base64不带 dataURL 前缀或带前缀需约定)
- `payload.mimeType`:如 `image/png`
- `payload.fileName`:(可选)
## 边界与注意点
- **性能**:图片 base64 可能导致消息体很大,优先走分片。
- **安全**:只当作图片二进制展示,不执行任何脚本;避免把 payload 当 HTML 渲染。

View File

@@ -0,0 +1,26 @@
# 08 - 剪贴板集成(读取 / 粘贴 / 降级)
## 功能目标
- 支持 Ctrl+V 粘贴文本/文件到传输区域。
- 支持按钮主动读取系统剪贴板文本(在权限允许的情况下)。
- 在权限受限或浏览器不支持时提供清晰降级提示。
## 前端Vue
- **粘贴事件处理**
- 监听 `paste`,遍历 `e.clipboardData.items`
- `kind=file`:提取 File走文件传输流程
- `kind=string && type=text/plain`:提取文本,填充输入框或直接发送
- **主动读取剪贴板**
- `navigator.clipboard.readText()`:读取文本并 emit `text-pasted`
- 失败提示:“无法读取剪贴板,请手动粘贴”(常见于非 HTTPS、权限未授予、浏览器策略限制
- **组件建议**
- `FileDropZone.vue`:承载粘贴与读取按钮
- `utils/clipboard.js`:封装权限判断与异常处理(便于复用)
## 后端Spring Boot
- 无需专门接口支持;剪贴板只影响前端如何生成 `TEXT/FILE` 消息。
## 边界与注意点
- **HTTPS/权限**`navigator.clipboard` 通常要求安全上下文HTTPS/localhost与用户手势。
- **可用性**:粘贴是最稳妥的降级方式,读取按钮只是增强能力。

View File

@@ -0,0 +1,30 @@
# 09 - 在线用户列表(房间成员与系统消息)
## 功能目标
- 在房间侧边栏展示在线用户列表。
- 用户加入/离开时,实时更新列表并显示系统提示。
## 前端Vue
- **组件**
- `UserList.vue`:渲染 `userList`(在线/离线样式可选)。
- `RoomPanel.vue`:承载房间号与用户列表。
- **更新机制**
- 订阅房间 topic 后,收到 `SYSTEM` 且携带 `payload.userList` 时更新 `userList`
- 在消息区插入 `payload.message` 作为系统提示。
## 后端Spring Boot
- **RoomService**
- 存储结构(示例):`roomCode -> { sessionId -> userName }`
- `getUserList(roomCode)`:返回数组(包含 id/name
- **广播时机**
- join 成功后广播 `SYSTEM(USER_JOIN + userList)`
- leave 成功后广播 `SYSTEM(USER_LEAVE + userList)`
## 协议与数据
- `SYSTEM.payload.userList`
- `[{ "id": "uuid/sessionId", "name": "用户A" }, ...]`
## 边界与注意点
- **用户身份**:示例使用 `sessionId` 当 id如需“跨重连保持身份”需引入客户端生成的 stableId可选增强
- **断线处理**:需要在服务端捕获 disconnect 事件,触发 leave 并广播列表更新。

View File

@@ -0,0 +1,26 @@
# 10 - 历史记录(本地存储 / 清空 / 导出)
## 功能目标
- 保存消息历史(默认本地存储,不依赖服务端)。
- 支持清空当前房间记录、导出记录(便于审计或留存)。
## 前端Vue
- **本地存储策略(建议)**
-`roomCode` 分桶保存:`history:{roomCode}`
- 存储介质:
- 少量文本:`localStorage`
- 含文件/图片元数据与大量消息:建议 `IndexedDB`(可选)
- 存储内容建议只存元数据与文本,不存大块二进制(避免爆仓)。
- **功能入口**
- `RoomPanel.vue`:提供“清空”“导出”按钮。
- **导出格式(建议)**
- JSON包含 `type/sender/timestamp/payload摘要`
- 或文本:便于粘贴到工单/邮件
## 后端Spring Boot
- 默认无需支持;若开启服务端持久化则由后端提供查询/导出(可选增强)。
## 边界与注意点
- **隐私**:本地存储可能包含敏感信息;需明确提示用户可手动清空。
- **容量**:浏览器存储空间有限;建议设定最大条数/最大时间窗与自动淘汰策略。

View File

@@ -0,0 +1,299 @@
# 15 - UI 设计规范与交互原则DataTool
> 本文作为 DataTool 项目前端的统一 UI 设计规范与交互基线,后续所有功能开发应在不违背本规范的前提下扩展。若有必要变更,请同步更新本文件。
---
## 1. 产品定位与使用场景
- **产品类型**:轻量级、基于浏览器的数据传输工具
- **核心场景**:在 VNC / 远程桌面 / 内网隔离等环境中,通过 6 位房间号在多端之间快速传文本、文件、图片和剪贴板内容
- **受众角色**:开发 / 运维 / 支持工程师、内网用户
- **体验方向****稳定、可靠、低干扰、专业、轻量**
---
## 2. 整体设计原则
- **一致性优先**
- 所有页面沿用统一的色板、字体、圆角与阴影体系。
- 所有按钮、输入框、消息卡片、文件卡片必须复用同一套组件样式,不允许在局部自行定义“新样式”。
- **信息层次清晰**
- 强调房间号与连接状态,其次是消息内容与文件进度,最后是系统提示和辅助信息。
- 使用颜色 / 字号 / 粗细 / 间距来区分重要程度,避免靠纯颜色或纯位置。
- **状态可感知**
- WebSocket 连接状态、房间加入/退出、文件发送与接收进度必须在 UI 中可见且明确。
- 所有异步操作(发送消息、上传文件、读取剪贴板等)都有“进行中 / 成功 / 失败”的视觉反馈。
- **轻量与性能**
- 大量消息场景下保持滚动流畅,避免复杂阴影和高成本动画。
- 组件优先考虑简单、可扩展的布局,预留虚拟列表等性能优化空间。
- **可访问与可维护**
- 遵守基本对比度、可聚焦与键盘可操作性。
- 使用统一的设计 token颜色/间距/字号),便于全局调整。
---
## 3. 视觉设计规范
### 3.1 色彩体系(建议映射到 CSS 变量 / Tailwind token
- **品牌主色Primary**
- 基准色:`#2563EB`(蓝,类似 Tailwind `blue-600`
- Hover`#1D4ED8``blue-700`
- Active`#1E40AF``blue-800`
- 主要用于:主按钮、主操作高亮、房间号强调、连接状态“已连接”标识。
- **强调色Success / Danger / Warning**
- Success`#16A34A``green-600`)—— 文件传输完成、连接成功、操作成功提示。
- Danger`#DC2626``red-600`)—— 错误、连接失败、限制触发。
- Warning`#F97316``orange-500`)—— 限制接近阈值、网络不稳定等。
- **中性色(背景 / 边框 / 文本)**
- 页面背景:`#F8FAFC``slate-50`
- 卡片背景:`#FFFFFF`
- 边框:`#E2E8F0``slate-200`
- 分割线:`#E5E7EB``gray-200`
- 主文本:`#0F172A``slate-900`
- 次级文本:`#475569``slate-600`
- 弱化文本/占位:`#9CA3AF``gray-400`)及以上,不使用更浅灰值作为正文。
- **系统消息与提示色**
- 系统消息背景:`#EFF6FF``blue-50`),文字使用 `#1D4ED8`
- 提示条Banner背景 `#F1F5F9``slate-100`),根据状态加左侧色条(蓝/绿/橙/红)。
> 所有新页面不得新增“第二主色”若确有需要先在本文件补充“辅助色Secondary”章节再使用。
### 3.2 字体与排版
- **字体族**
- 优先:`system-ui, -apple-system, BlinkMacSystemFont, "SF Pro SC", "PingFang SC", "Microsoft YaHei", sans-serif`
- **字号层级(桌面端基线)**
- 标题 H124px / 32px用于页面主标题如“房间 123456”
- 标题 H220px / 28px用于分区标题在线用户、消息区、文件传输
- 标题 H316px / 24px用于小模块标题、卡片标题
- 正文14px / 22px首选行高 1.6
- 标签/次要信息12px / 18px谨慎使用保证可读性
- **文字对齐**
- 文本与输入框左对齐,数字型信息(如进度百分比)可以右对齐。
- **行宽与段落**
- 文本区域最大宽度建议控制在 6575 个汉字以内,避免极长单行。
### 3.3 圆角、阴影与描边
- **圆角**
- 输入框/按钮:`4px`
- 卡片(消息卡片、文件卡片):`8px`
- 悬浮面板 / 弹窗:`12px`
- **阴影**
- 默认卡片:无阴影,仅边框(`border-slate-200`
- 悬浮/高层级元素(如弹窗、悬浮面板):轻量阴影 `0 10px 25px rgba(15,23,42,0.08)`
- **描边**
- 主按钮默认无边框仅用色块Hover 时可增加内阴影或细微深色。
- 输入框:有 1px 描边,聚焦时描边色切换为主色并增加轻微光晕。
### 3.4 间距与栅格
- 页面左右内边距:桌面端 `24px`,窄屏端 `16px`
- 区块间距(模块与模块之间):`24px`
- 元素垂直间距(标题与内容 / 行与行):`812px`
- 列表项内边距:上下 `8px` / 左右 `12px`
- 使用 4 的倍数作为统一间距刻度:`4 / 8 / 12 / 16 / 24 / 32`
---
## 4. 布局与信息架构
### 4.1 整体布局结构
- **桌面端(≥ 1024px**
- 顶部:窄高度顶部栏,包含项目 Logo/名称、当前房间号、连接状态指示、小范围操作(帮助、设置)。
- 主区:左右双栏布局。
- 左侧(约 2530% 宽度):`RoomPanel`,包含房间信息、在线用户列表、基础操作(退出、清空、导出)。
- 右侧(约 7075% 宽度):消息与文件显示区域(消息列表 + 文件/图片预览),底部为输入与文件操作区域。
- **窄屏端(< 1024px**
- 顶部栏保留,主内容上下布局:
- 默认展示消息区域,在线用户通过折叠面板或底部抽屉查看。
- 拖拽上传在窄屏上使用明显的上传按钮 + 粘贴提示,拖拽仅作为增强能力。
### 4.2 关键页面说明
- **首页 `HomeView`**
- 中心内容区居中显示:
- 左:创建房间卡片(展示自动生成 6 位房间号 + “创建并进入”按钮)
- 右:加入房间卡片(房间号输入框 + 加入按钮)
- 明确提示“数据不落库 / 仅当前会话可见”等安全特性(简短描述)。
- 若已有最近加入的房间记录,可在下方列出“最近使用的房间”列表(历史记录相关)。
- **房间页 `RoomView`**
- 顶部显示:
- 房间号(可点击复制)+ 小标签(如“临时房间”)
- 连接状态:圆点 + 文本(连接中/已连接/断开/重连中)
- 左侧 `RoomPanel`
- 在线用户列表(昵称 + 状态点),当前用户行高亮。
- 简要统计信息:在线人数、当前传输中文件数量等。
- 操作按钮:退出房间(弱化但明确)、清空本地历史、导出历史。
- 右侧主区:
- 上方消息与系统提示列表含文本、图片、文件卡片、SYSTEM 消息)。
- 下方:输入区域(文本输入 + 发送按钮 + 附件/文件按钮 + 剪贴板按钮)。
### 4.3 消息列表布局
- **文本消息**
- 根据发送者区分左右对齐(可选),也可统一左对齐,通过头像首字母+昵称区分。
- 时间戳放置在右下角,使用次级文字色。
- **系统消息**
- 居中对齐,使用浅色条带样式,与普通消息明显区分。
- 内容如“xxx 加入房间 / 离开房间 / 连接已重连”等。
- **文件与图片消息**
- 使用卡片样式,包含文件名、大小、进度条、状态图标以及操作按钮(下载/打开)。
---
## 5. 核心组件规范
> 实现时建议将以下元素封装为 Vue 组件,并在使用前优先复用。
### 5.1 按钮(`Button`
- **尺寸**
- 大:高度 4044px主要操作如“创建房间”“加入房间”“发送”
- 中:高度 3236px列表操作、弹窗确认等
- 小:高度 2832px标签型按钮、图标按钮
- **类型**
- 主按钮Primary填充主色白色文字圆角 4px。
- 次按钮Secondary白底 + 主色描边 + 主色文字。
- 危险按钮Danger红色填充 + 白色文字,仅用于删除/退出等操作。
- 文本按钮Ghost/Text透明背景 + 主色文字,用于低权重操作。
- **状态**
- 默认 / Hover / Active / Disabled 四态。
- Hover亮度 + 边框/阴影轻微变化,不允许大幅缩放导致布局抖动。
- Disabled降低对比度并移除悬浮态鼠标指针改为默认。
### 5.2 输入与表单(`Input`, `Textarea`, `FormField`
- **样式**
- 高度 3640px左右有 1216px 内边距。
- 边框颜色:默认 `#E2E8F0`,聚焦 `primary`
- 占位文字使用 `gray-400`,不可与正常正文颜色相同。
- **校验**
- 房间号输入框限制为 6 位数字,错误时在输入框下方展示红色错误文案,并将边框颜色切换为 `red-500`
- 提交按钮在校验不通过时为禁用状态。
### 5.3 标签与状态(`Tag`, `Badge`, `StatusDot`
- 用于表示连接状态、房间类型、文件状态等。
- 状态点尺寸 810px放置在用户名左侧或右上角。
- 颜色与状态映射保持统一(如:绿色=在线,灰色=离线,橙色=异常)。
### 5.4 消息气泡与系统消息(`MessageItem`, `SystemMessage`
- 消息气泡:
- 背景使用白色或略带浅灰,边框微弱(可选)。
- 内部包含:昵称(可选)/ 消息内容 / 时间戳。
- 系统消息:
- 使用全宽度条带,背景为 `blue-50``slate-100`,文字居中。
- 与气泡在视觉上区分开(不使用“对话气泡”样式)。
### 5.5 文件卡片与进度条(`FileMessage`, `ProgressBar`
- **文件卡片内容**
- 文件名(可省略中间,用省略号显示)+ 文件大小 + 文件类型图标。
- 进度条(发送端、接收端皆可见)+ 状态标签(传输中 / 已完成 / 失败)。
- 操作按钮:下载、打开所在文件夹(浏览器可行范围内)、重新下载(失败时)。
- **进度条样式**
- 高度 46px圆角 999px。
- 背景条颜色 `slate-200`,前景条使用主色或状态色。
### 5.6 图片预览卡片(`ImageMessage`, `ImagePreviewModal`
- 消息列表中展示缩略图(固定宽高比,如 4:3点击后打开大图预览。
- 大图使用居中遮罩弹窗,背景半透明黑色,支持点击空白处关闭或 Esc 关闭。
### 5.7 Toast 与通知(`Toast`, `AlertBanner`
- Toast 适合短暂状态提示(发送成功、复制成功),出现在右上角或右下角,自动消失。
- AlertBanner 适合持久的提醒(当前使用 HTTP 协议、传输大小接近上限等),固定在内容区顶部。
---
## 6. 关键交互流程规范
### 6.1 加入 / 创建房间
- **创建房间**
- 点击“创建房间”后立即显示加载状态(按钮禁用 + Loading 图标)。
- 创建成功后自动跳转到房间页,并以系统消息提示“你已创建并进入房间 123456”。
- **加入房间**
- 用户输入 6 位房间号,实时校验;不足 6 位或包含非数字字符时按钮禁用。
- 加入失败(房间不存在/连接错误)时:
- 在输入框下方显示清晰错误文案。
- 可用 Toast 或 Banner 补充说明。
### 6.2 WebSocket 连接与状态恢复
- 顶部栏右上区域固定显示连接状态:
- 连接中:黄色点 + “连接中…”
- 已连接:绿色点 + “已连接”
- 重连中:橙色点 + “重连中…”
- 已断开:红色点 + “已断开”
- 当状态从“断开/重连中”恢复到“已连接”时:
- 插入一条 SYSTEM 消息说明“已重新连接,已重新加入房间”。
### 6.3 消息发送与错误处理
- 按下 Enter 发送消息Shift+Enter 换行(在文本框旁明确提示)。
- 发送过程中,发送按钮进入 Loading 状态并禁用,避免重复点击。
- 若发送失败(网络中断等):
- 消息在列表中使用“失败”样式(如左侧红色竖线 + 灰度文字)。
- 提供“重试发送”小按钮。
### 6.4 文件拖拽上传 / 粘贴
- **拖拽区域**
- 在输入区域上方或消息列表顶部提供明显的拖拽提示区域(浅虚线边框 + 上传图标)。
- 拖拽文件进入页面时,高亮整个拖拽区域,并在靠顶部位置显示提示条(如“释放鼠标以发送文件到房间 123456”
- **粘贴文件 / 剪贴板**
- 当用户使用 Ctrl+V 粘贴文件或图片时,提示一次确认(可选):“是否将剪贴板中的图片/文件发送到当前房间?”。
- 剪贴板权限被浏览器限制时,弹出说明性对话框,引导用户手动拖拽或选择文件。
### 6.5 历史记录与本地存储
- 在房间页的 `RoomPanel` 中提供“历史记录”入口,可选择:
- 清空当前房间历史(弹出确认对话框)。
- 导出当前房间历史(文件命名建议:`DataTool-房间号-时间戳.json`)。
- 刷新页面后自动加载当前房间的本地历史,并通过淡入动画呈现。
---
## 7. 可访问性与通用 UX 要求
- **颜色对比度**
- 正文文本与背景对比度 ≥ 4.5:1次级文本不低于 3:1。
- **键盘导航**
- 所有按钮和可点击卡片必须可通过 Tab 聚焦,按 Enter/Space 触发。
- 焦点样式清晰可见(外发光或描边),不依赖颜色变化微弱的 hover 效果。
- **ARIA 与语义化**
- 图标按钮(仅图标无文字)添加 `aria-label`
- 使用语义标签(`<main>`, `<header>`, `<nav>`, `<section>`)构建布局。
- **动画与动效**
- 常规动效时长控制在 150250ms 内。
- 支持 `prefers-reduced-motion`,在该设置下减弱或关闭动画。
---
## 8. 动画与微交互规则
- **Hover 与点击**
- 按钮、卡片 hover 使用颜色/阴影变化,不使用大幅 scale 动画。
- 点击反馈可使用轻微按压效果(阴影变小/颜色略深)。
- **加载与进度**
- 使用细线进度条或简洁 Spinner不使用大型覆盖式 Loading 遮罩,避免阻塞操作。
- **列表更新**
- 新消息出现时可使用轻微淡入效果(不超过 150ms避免大幅滑入动画。
---
## 9. 实现与扩展建议
- 建议在前端建立统一的设计 token`dt-color-primary`, `dt-radius-card`, `dt-spacing-md` 等),由基础样式文件或 Tailwind 配置统一生成。
- 新增页面或组件时:
- **必须** 复用现有颜色、字体、间距与组件模式。
- 如需突破现有规范(例如新增“标签页导航组件”),请先在本文件新增对应的组件规范小节,再在代码中实现。
- 后续 Phase 3/4 中的剪贴板、图片预览、历史记录等功能应基于本规范中的组件文件卡片、图片卡片、Toast、Modal 等)组合实现,不单独发明新样式。