Commit 6bc1bf31 by Performance System

1

parent cb5c7a46
# WebSocket 和实时同步功能清理总结
# WebSocket 和实时同步功能清理总结
## 🎯 清理目标
根据用户需求,多用户协作功能和实时同步功能在系统中没有实际使用,因此进行了全面清理,简化系统架构。
## 🗑️ 删除的文件
### 核心功能文件
-`src/utils/websocketClient.js` - WebSocket 客户端核心
-`src/utils/dataSyncManager.js` - 数据同步管理器
-`src/utils/syncInitializer.js` - 同步初始化器
-`src/components/RealtimeStatus.vue` - 实时状态组件
### 服务器文件
-`websocket-server.js` - WebSocket 服务器
-`websocket-server.cjs` - WebSocket 服务器 (CommonJS)
### 测试页面
-`public/cross-browser-sync-test.html` - 跨浏览器同步测试
-`public/data-sync-test.html` - 数据同步测试
-`public/sync-test.html` - 同步测试页面
-`public/test-sync.html` - 数据同步测试
### 文档文件
-`WebSocket修复总结.md` - WebSocket 修复文档
-`多用户同步测试指南.md` - 多用户同步指南
-`数据同步问题修复报告.md` - 数据同步修复报告
-`跨浏览器数据同步修复报告.md` - 跨浏览器同步报告
### 启动脚本
-`start-realtime.bat` - 实时同步启动脚本
## 🔧 修改的文件
### package.json
**删除的依赖**:
```json
"socket.io": "^4.8.1",
"socket.io-client": "^4.8.1"
```
**删除的脚本**:
```json
"server": "node websocket-server.js",
"dev:full": "concurrently \"npm run server\" \"npm run dev\"",
"start": "npm run build && npm run server"
```
**保留的脚本**:
```json
"start": "npm run build && npm run preview"
```
### src/views/user/UserPanel.vue
**删除的内容**:
- `<RealtimeStatus />` 组件引用
- `import RealtimeStatus from '@/components/RealtimeStatus.vue'`
- `import dataSyncManager from '@/utils/dataSyncManager.js'`
- `initializeRealtimeSync()` 函数调用
- 整个 `initializeRealtimeSync()` 函数实现
### start-system.ps1
**删除的内容**:
- WebSocket 服务器启动逻辑
- WebSocket 服务器健康检查
- WebSocket 服务器监控和重启逻辑
**保留的内容**:
- 前端开发服务器启动
- 前端服务器监控和重启
## 🎉 清理效果
### 简化的系统架构
```
┌─────────────────┐
│ 前端应用 │
│ Vue 3 SPA │
│ Element Plus │
└─────────────────┘
┌─────────────────┐
│ 本地存储 │
│ localStorage │
│ 数据持久化 │
└─────────────────┘
```
### 移除的复杂架构
```
❌ WebSocket 服务器
❌ Socket.IO 通信
❌ 实时数据同步
❌ 多用户协作
❌ 在线用户管理
❌ 变更历史记录
❌ 连接状态监控
```
## 📊 系统优化
### 性能提升
-**减少依赖** - 移除 socket.io 相关包
-**简化启动** - 只需启动前端服务器
-**降低复杂度** - 移除 WebSocket 连接逻辑
-**减少内存占用** - 无需维护连接状态
### 维护简化
-**代码量减少** - 删除约 1500+ 行代码
-**依赖减少** - 移除 2 个主要依赖包
-**部署简化** - 单一前端应用部署
-**调试简化** - 无需处理 WebSocket 连接问题
## 🚀 当前系统特性
### 保留的核心功能
-**用户认证** - 登录/登出功能
-**数据管理** - 用户和机构管理
-**图片上传** - 图片管理功能
-**权限控制** - 基于角色的权限
-**数据导入导出** - 备份和恢复
-**响应式界面** - 现代化 UI
### 数据存储方式
- **本地存储** - 使用 localStorage 持久化
- **单用户模式** - 每个浏览器独立数据
- **简单可靠** - 无网络依赖的数据管理
## 🎯 使用指南
### 启动系统
```bash
# 开发环境
npm run dev
# 生产环境
npm run build
npm run preview
# Docker 部署
docker compose up -d
```
### 访问地址
- **开发环境**: http://localhost:5173/
- **生产环境**: http://localhost:4173/
- **Docker 部署**: http://localhost:4001/
### 默认账号
- **管理员**: admin / admin123
- **张田田**: 13800138002 / 123456
- **陈锐屏**: 13800138001 / 123456
- **余芳飞**: 13800138003 / 123456
## 📋 验证清单
- ✅ 系统正常启动
- ✅ 用户登录功能正常
- ✅ 数据管理功能正常
- ✅ 图片上传功能正常
- ✅ 无 WebSocket 连接错误
- ✅ 无实时同步相关错误
- ✅ Docker 部署正常
- ✅ 构建过程无错误
## 🎉 清理完成
系统已成功移除所有 WebSocket 和实时同步相关功能,现在是一个简洁、高效的单用户绩效计分系统。所有核心业务功能保持完整,系统更加稳定和易于维护。
version: "3.8"
services:
scoring-app:
build:
......
......@@ -7,9 +7,7 @@
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"server": "node websocket-server.js",
"dev:full": "concurrently \"npm run server\" \"npm run dev\"",
"start": "npm run build && npm run server"
"start": "npm run build && npm run preview"
},
"dependencies": {
"@element-plus/icons-vue": "^2.1.0",
......@@ -18,8 +16,6 @@
"express": "^5.1.0",
"lodash-es": "^4.17.21",
"pinia": "^2.1.7",
"socket.io": "^4.8.1",
"socket.io-client": "^4.8.1",
"uuid": "^11.1.0",
"vue": "^3.3.8",
"vue-router": "^4.2.5",
......
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>数据同步测试</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 20px;
background-color: #f5f5f5;
}
.container {
max-width: 1200px;
margin: 0 auto;
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.status {
padding: 10px;
margin: 10px 0;
border-radius: 4px;
font-weight: bold;
}
.status.connected {
background-color: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.status.disconnected {
background-color: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.data-section {
margin: 20px 0;
padding: 15px;
border: 1px solid #ddd;
border-radius: 4px;
}
.data-section h3 {
margin-top: 0;
color: #333;
}
.data-item {
padding: 5px 0;
border-bottom: 1px solid #eee;
}
.data-item:last-child {
border-bottom: none;
}
.buttons {
margin: 20px 0;
}
.btn {
padding: 10px 20px;
margin: 5px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
.btn-primary {
background-color: #007bff;
color: white;
}
.btn-success {
background-color: #28a745;
color: white;
}
.btn-warning {
background-color: #ffc107;
color: #212529;
}
.btn-danger {
background-color: #dc3545;
color: white;
}
.log {
background-color: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 4px;
padding: 10px;
height: 200px;
overflow-y: auto;
font-family: monospace;
font-size: 12px;
}
.log-entry {
margin: 2px 0;
padding: 2px 0;
}
.log-entry.info {
color: #0066cc;
}
.log-entry.success {
color: #28a745;
}
.log-entry.warning {
color: #ffc107;
}
.log-entry.error {
color: #dc3545;
}
</style>
</head>
<body>
<div class="container">
<h1>数据同步测试页面</h1>
<div id="connectionStatus" class="status disconnected">
WebSocket: 未连接
</div>
<div class="buttons">
<button class="btn btn-primary" onclick="connectWebSocket()">连接WebSocket</button>
<button class="btn btn-warning" onclick="disconnectWebSocket()">断开连接</button>
<button class="btn btn-success" onclick="refreshData()">刷新数据</button>
<button class="btn btn-danger" onclick="clearLog()">清空日志</button>
</div>
<div class="data-section">
<h3>当前数据状态</h3>
<div id="dataDisplay">
<div class="data-item">用户数量: <span id="userCount">0</span></div>
<div class="data-item">机构数量: <span id="institutionCount">0</span></div>
<div class="data-item">在线用户: <span id="onlineCount">0</span></div>
<div class="data-item">最后更新: <span id="lastUpdate">从未</span></div>
</div>
</div>
<div class="data-section">
<h3>操作日志</h3>
<div id="log" class="log"></div>
</div>
</div>
<script src="https://cdn.socket.io/4.7.2/socket.io.min.js"></script>
<script>
let socket = null;
let isConnected = false;
function log(message, type = 'info') {
const logElement = document.getElementById('log');
const entry = document.createElement('div');
entry.className = `log-entry ${type}`;
entry.textContent = `[${new Date().toLocaleTimeString()}] ${message}`;
logElement.appendChild(entry);
logElement.scrollTop = logElement.scrollHeight;
}
function updateConnectionStatus(connected) {
isConnected = connected;
const statusElement = document.getElementById('connectionStatus');
if (connected) {
statusElement.textContent = 'WebSocket: 已连接';
statusElement.className = 'status connected';
} else {
statusElement.textContent = 'WebSocket: 未连接';
statusElement.className = 'status disconnected';
}
}
function updateDataDisplay() {
try {
const users = JSON.parse(localStorage.getItem('score_system_users') || '[]');
const institutions = JSON.parse(localStorage.getItem('score_system_institutions') || '[]');
document.getElementById('userCount').textContent = users.length;
document.getElementById('institutionCount').textContent = institutions.length;
document.getElementById('lastUpdate').textContent = new Date().toLocaleTimeString();
log(`数据更新: ${users.length}个用户, ${institutions.length}个机构`, 'success');
} catch (error) {
log(`数据读取失败: ${error.message}`, 'error');
}
}
function connectWebSocket() {
if (socket && isConnected) {
log('WebSocket已经连接', 'warning');
return;
}
try {
socket = io('http://localhost:3000');
socket.on('connect', () => {
updateConnectionStatus(true);
log('WebSocket连接成功', 'success');
// 登录到同步系统
socket.emit('user_login', {
id: 'test_user_' + Date.now(),
name: '测试用户',
role: 'user',
department: '测试部门',
browserInfo: {
userAgent: navigator.userAgent,
timestamp: new Date().toISOString()
}
});
});
socket.on('disconnect', () => {
updateConnectionStatus(false);
log('WebSocket连接断开', 'warning');
});
socket.on('connect_error', (error) => {
log(`连接错误: ${error.message}`, 'error');
});
socket.on('data_changed', (data) => {
log(`收到数据变更: ${data.change?.type || '未知类型'}`, 'info');
if (data.change && ['user_added', 'institution_added', 'data_saved'].includes(data.change.type)) {
setTimeout(updateDataDisplay, 100);
}
});
socket.on('online_users', (users) => {
document.getElementById('onlineCount').textContent = users.length;
log(`在线用户更新: ${users.length}人`, 'info');
});
socket.on('login_success', (data) => {
log(`登录成功: ${data.userInfo.name}`, 'success');
});
} catch (error) {
log(`连接失败: ${error.message}`, 'error');
}
}
function disconnectWebSocket() {
if (socket) {
socket.disconnect();
socket = null;
updateConnectionStatus(false);
log('手动断开WebSocket连接', 'warning');
}
}
function refreshData() {
updateDataDisplay();
log('手动刷新数据', 'info');
}
function clearLog() {
document.getElementById('log').innerHTML = '';
}
// 监听localStorage变化
window.addEventListener('storage', (event) => {
if (event.key && event.key.startsWith('score_system_')) {
log(`检测到localStorage变化: ${event.key}`, 'info');
updateDataDisplay();
}
});
// 监听自定义数据变更事件
window.addEventListener('dataChange', (event) => {
log(`收到数据变更事件: ${event.detail.type}`, 'info');
updateDataDisplay();
});
// 监听WebSocket数据变更事件
window.addEventListener('websocket_data_change', (event) => {
log(`收到WebSocket数据变更: ${event.detail.type}`, 'info');
updateDataDisplay();
});
// 页面加载时初始化
document.addEventListener('DOMContentLoaded', () => {
log('页面加载完成', 'info');
updateDataDisplay();
// 自动连接WebSocket
setTimeout(connectWebSocket, 1000);
});
</script>
</body>
</html>
<template>
<div class="realtime-status">
<!-- 连接状态指示器 -->
<div class="status-indicator">
<el-badge
:value="onlineUsers.length"
:type="connectionStatus.isConnected ? 'success' : 'danger'"
class="connection-badge"
>
<el-button
:type="connectionStatus.isConnected ? 'success' : 'danger'"
size="small"
circle
@click="showStatusDialog = true"
>
<el-icon>
<Connection v-if="connectionStatus.isConnected" />
<Close v-else />
</el-icon>
</el-button>
</el-badge>
<span class="status-text">
{{ connectionStatus.isConnected ? '已连接' : '未连接' }}
</span>
</div>
<!-- 状态详情对话框 -->
<el-dialog
v-model="showStatusDialog"
title="实时同步状态"
width="600px"
:before-close="handleClose"
>
<div class="status-details">
<!-- 连接信息 -->
<el-card class="status-card" shadow="never">
<template #header>
<div class="card-header">
<el-icon><Connection /></el-icon>
<span>连接状态</span>
</div>
</template>
<el-descriptions :column="2" border>
<el-descriptions-item label="连接状态">
<el-tag :type="connectionStatus.isConnected ? 'success' : 'danger'">
{{ connectionStatus.isConnected ? '已连接' : '未连接' }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="重连次数">
{{ connectionStatus.reconnectAttempts }}
</el-descriptions-item>
<el-descriptions-item label="最后更新">
{{ formatTime(connectionStatus.lastUpdate) }}
</el-descriptions-item>
<el-descriptions-item label="同步状态">
<el-tag :type="syncStatus.isSyncing ? 'warning' : 'success'">
{{ syncStatus.isSyncing ? '同步中' : '已同步' }}
</el-tag>
</el-descriptions-item>
</el-descriptions>
</el-card>
<!-- 在线用户 -->
<el-card class="status-card" shadow="never">
<template #header>
<div class="card-header">
<el-icon><User /></el-icon>
<span>在线用户 ({{ onlineUsers.length }})</span>
</div>
</template>
<div class="online-users">
<div
v-for="user in onlineUsers"
:key="user.socketId"
class="user-item"
>
<el-avatar :size="32" class="user-avatar">
{{ user.name.charAt(0) }}
</el-avatar>
<div class="user-info">
<div class="user-name">{{ user.name }}</div>
<div class="user-details">
<el-tag size="small" type="info">
{{ formatTime(user.loginTime) }}
</el-tag>
<el-tag size="small" type="success" v-if="user.sessionId">
会话: {{ user.sessionId.slice(-8) }}
</el-tag>
</div>
</div>
</div>
<el-empty v-if="onlineUsers.length === 0" description="暂无在线用户" />
</div>
</el-card>
<!-- 变更历史 -->
<el-card class="status-card" shadow="never">
<template #header>
<div class="card-header">
<el-icon><Clock /></el-icon>
<span>最近变更</span>
<el-button
size="small"
type="primary"
@click="refreshChangeHistory"
:loading="loadingHistory"
>
刷新
</el-button>
</div>
</template>
<div class="change-history">
<div
v-for="change in changeHistory.slice(0, 10)"
:key="change.id"
class="change-item"
>
<div class="change-icon">
<el-icon :color="getChangeTypeColor(change.type)">
<Edit v-if="change.type.includes('update')" />
<Plus v-else-if="change.type.includes('add')" />
<Delete v-else-if="change.type.includes('delete')" />
<Operation v-else />
</el-icon>
</div>
<div class="change-content">
<div class="change-title">
{{ getChangeTypeText(change.type) }}
</div>
<div class="change-details">
<span class="change-user">{{ change.userName }}</span>
<span class="change-time">{{ formatTime(change.timestamp) }}</span>
</div>
</div>
</div>
<el-empty v-if="changeHistory.length === 0" description="暂无变更记录" />
</div>
</el-card>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="refreshAll" :loading="refreshing">
刷新所有数据
</el-button>
<el-button type="primary" @click="showStatusDialog = false">
关闭
</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue';
import { Connection, Close, User, Clock, Edit, Plus, Delete, Operation } from '@element-plus/icons-vue';
import dataSyncManager from '../utils/dataSyncManager.js';
// 响应式数据
const showStatusDialog = ref(false);
const loadingHistory = ref(false);
const refreshing = ref(false);
// 从数据同步管理器获取状态
const connectionStatus = dataSyncManager.getConnectionStatus();
const syncStatus = dataSyncManager.getSyncStatus();
const data = dataSyncManager.getData();
// 计算属性
const onlineUsers = computed(() => data.onlineUsers);
const changeHistory = computed(() => data.changeHistory);
// 方法
const formatTime = (time) => {
if (!time) return '未知';
const date = new Date(time);
return date.toLocaleString('zh-CN');
};
const getChangeTypeColor = (type) => {
if (type.includes('update')) return '#409EFF';
if (type.includes('add')) return '#67C23A';
if (type.includes('delete')) return '#F56C6C';
return '#909399';
};
const getChangeTypeText = (type) => {
const typeMap = {
'update_user_score': '更新用户评分',
'update_workstation': '更新工作台',
'add_user': '添加用户',
'add_workstation': '添加工作台',
'delete_user': '删除用户',
'delete_workstation': '删除工作台'
};
return typeMap[type] || type;
};
const refreshChangeHistory = () => {
loadingHistory.value = true;
dataSyncManager.getChangeHistory(50);
setTimeout(() => {
loadingHistory.value = false;
}, 1000);
};
const refreshAll = () => {
refreshing.value = true;
dataSyncManager.getOnlineUsers();
dataSyncManager.getChangeHistory(50);
setTimeout(() => {
refreshing.value = false;
}, 1500);
};
const handleClose = (done) => {
done();
};
// 生命周期
onMounted(() => {
// 定期刷新在线用户
const interval = setInterval(() => {
if (connectionStatus.value.isConnected) {
dataSyncManager.getOnlineUsers();
}
}, 30000); // 每30秒刷新一次
// 清理定时器
onUnmounted(() => {
clearInterval(interval);
});
});
</script>
<style scoped>
.realtime-status {
display: flex;
align-items: center;
gap: 8px;
}
.status-indicator {
display: flex;
align-items: center;
gap: 8px;
}
.connection-badge {
cursor: pointer;
}
.status-text {
font-size: 12px;
color: #666;
}
.status-details {
display: flex;
flex-direction: column;
gap: 16px;
}
.status-card {
margin-bottom: 0;
}
.card-header {
display: flex;
align-items: center;
gap: 8px;
justify-content: space-between;
}
.card-header span {
font-weight: 500;
}
.online-users {
max-height: 200px;
overflow-y: auto;
}
.user-item {
display: flex;
align-items: center;
gap: 12px;
padding: 8px 0;
border-bottom: 1px solid #f0f0f0;
}
.user-item:last-child {
border-bottom: none;
}
.user-avatar {
flex-shrink: 0;
}
.user-info {
flex: 1;
}
.user-name {
font-weight: 500;
margin-bottom: 4px;
}
.user-details {
display: flex;
gap: 8px;
}
.change-history {
max-height: 300px;
overflow-y: auto;
}
.change-item {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 8px 0;
border-bottom: 1px solid #f0f0f0;
}
.change-item:last-child {
border-bottom: none;
}
.change-icon {
flex-shrink: 0;
margin-top: 2px;
}
.change-content {
flex: 1;
}
.change-title {
font-weight: 500;
margin-bottom: 4px;
}
.change-details {
display: flex;
gap: 12px;
font-size: 12px;
color: #666;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 8px;
}
</style>
import { reactive, ref } from 'vue';
import wsClient from './websocketClient.js';
import { ElMessage } from 'element-plus';
class DataSyncManager {
constructor() {
// 响应式数据
this.data = reactive({
users: [],
workstations: [],
onlineUsers: [],
changeHistory: []
});
// 连接状态
this.connectionStatus = ref({
isConnected: false,
reconnectAttempts: 0,
lastUpdate: null
});
// 同步状态
this.syncStatus = ref({
isSyncing: false,
lastSyncTime: null,
pendingChanges: 0
});
// 冲突解决队列
this.conflictQueue = [];
// 本地变更缓存
this.localChanges = new Map();
this.setupWebSocketListeners();
}
// 设置WebSocket事件监听
setupWebSocketListeners() {
// 连接状态变化
wsClient.on('connected', () => {
this.connectionStatus.value.isConnected = true;
this.connectionStatus.value.reconnectAttempts = 0;
console.log('数据同步管理器:WebSocket已连接');
});
wsClient.on('disconnected', () => {
this.connectionStatus.value.isConnected = false;
console.log('数据同步管理器:WebSocket已断开');
});
// 接收初始数据
wsClient.on('initial_data', (initialData) => {
this.handleInitialData(initialData);
});
// 数据变更
wsClient.on('data_changed', (changeData) => {
this.handleDataChange(changeData);
});
// 在线用户更新
wsClient.on('user_online', (user) => {
this.addOnlineUser(user);
});
wsClient.on('user_offline', (user) => {
this.removeOnlineUser(user);
});
wsClient.on('online_users', (users) => {
this.data.onlineUsers = users;
});
// 变更历史
wsClient.on('change_history', (history) => {
this.data.changeHistory = history;
});
}
// 处理初始数据
handleInitialData(initialData) {
console.log('接收初始数据:', initialData);
if (initialData.performanceData) {
this.data.users = initialData.performanceData.users || [];
this.data.workstations = initialData.performanceData.workstations || [];
}
this.data.onlineUsers = initialData.onlineUsers || [];
this.data.changeHistory = initialData.changeHistory || [];
this.connectionStatus.value.lastUpdate = new Date();
this.syncStatus.value.lastSyncTime = new Date();
ElMessage.success('数据同步完成');
}
// 处理数据变更
handleDataChange(changeData) {
console.log('处理数据变更:', changeData);
const { change, newData } = changeData;
// 检查是否是自己的变更
const isOwnChange = this.localChanges.has(change.id);
if (isOwnChange) {
this.localChanges.delete(change.id);
console.log('忽略自己的变更');
return;
}
// 应用数据变更
if (newData) {
this.applyDataUpdate(newData);
}
// 添加到变更历史
if (change) {
this.data.changeHistory.unshift(change);
// 保持历史记录在合理范围内
if (this.data.changeHistory.length > 100) {
this.data.changeHistory = this.data.changeHistory.slice(0, 50);
}
}
this.connectionStatus.value.lastUpdate = new Date();
}
// 应用数据更新
applyDataUpdate(newData) {
if (newData.users) {
this.data.users = [...newData.users];
}
if (newData.workstations) {
this.data.workstations = [...newData.workstations];
}
}
// 添加在线用户
addOnlineUser(user) {
const existingIndex = this.data.onlineUsers.findIndex(u => u.socketId === user.socketId);
if (existingIndex === -1) {
this.data.onlineUsers.push(user);
}
}
// 移除在线用户
removeOnlineUser(user) {
const index = this.data.onlineUsers.findIndex(u => u.socketId === user.socketId);
if (index > -1) {
this.data.onlineUsers.splice(index, 1);
}
}
// 更新用户数据
async updateUser(userData, oldData = null) {
const changeId = this.generateChangeId();
this.localChanges.set(changeId, true);
try {
// 乐观更新本地数据
const userIndex = this.data.users.findIndex(u => u.id === userData.id);
if (userIndex !== -1) {
this.data.users[userIndex] = { ...this.data.users[userIndex], ...userData };
}
// 发送到服务器
const success = wsClient.updateData('update_user_score', userData, oldData);
if (!success) {
// 回滚本地变更
if (oldData && userIndex !== -1) {
this.data.users[userIndex] = oldData;
}
throw new Error('发送更新失败');
}
return true;
} catch (error) {
this.localChanges.delete(changeId);
console.error('更新用户数据失败:', error);
ElMessage.error('更新失败: ' + error.message);
return false;
}
}
// 更新工作台数据
async updateWorkstation(workstationData, oldData = null) {
const changeId = this.generateChangeId();
this.localChanges.set(changeId, true);
try {
// 乐观更新本地数据
const wsIndex = this.data.workstations.findIndex(w => w.id === workstationData.id);
if (wsIndex !== -1) {
this.data.workstations[wsIndex] = { ...this.data.workstations[wsIndex], ...workstationData };
}
// 发送到服务器
const success = wsClient.updateData('update_workstation', workstationData, oldData);
if (!success) {
// 回滚本地变更
if (oldData && wsIndex !== -1) {
this.data.workstations[wsIndex] = oldData;
}
throw new Error('发送更新失败');
}
return true;
} catch (error) {
this.localChanges.delete(changeId);
console.error('更新工作台数据失败:', error);
ElMessage.error('更新失败: ' + error.message);
return false;
}
}
// 添加用户
async addUser(userData) {
const changeId = this.generateChangeId();
this.localChanges.set(changeId, true);
try {
const success = wsClient.updateData('add_user', userData);
if (!success) {
throw new Error('发送添加请求失败');
}
return true;
} catch (error) {
this.localChanges.delete(changeId);
console.error('添加用户失败:', error);
ElMessage.error('添加失败: ' + error.message);
return false;
}
}
// 添加工作台
async addWorkstation(workstationData) {
const changeId = this.generateChangeId();
this.localChanges.set(changeId, true);
try {
const success = wsClient.updateData('add_workstation', workstationData);
if (!success) {
throw new Error('发送添加请求失败');
}
return true;
} catch (error) {
this.localChanges.delete(changeId);
console.error('添加工作台失败:', error);
ElMessage.error('添加失败: ' + error.message);
return false;
}
}
// 删除用户
async deleteUser(userId) {
const changeId = this.generateChangeId();
this.localChanges.set(changeId, true);
try {
const success = wsClient.updateData('delete_user', { id: userId });
if (!success) {
throw new Error('发送删除请求失败');
}
return true;
} catch (error) {
this.localChanges.delete(changeId);
console.error('删除用户失败:', error);
ElMessage.error('删除失败: ' + error.message);
return false;
}
}
// 删除工作台
async deleteWorkstation(workstationId) {
const changeId = this.generateChangeId();
this.localChanges.set(changeId, true);
try {
const success = wsClient.updateData('delete_workstation', { id: workstationId });
if (!success) {
throw new Error('发送删除请求失败');
}
return true;
} catch (error) {
this.localChanges.delete(changeId);
console.error('删除工作台失败:', error);
ElMessage.error('删除失败: ' + error.message);
return false;
}
}
// 连接WebSocket
connect(serverUrl) {
wsClient.connect(serverUrl);
}
// 用户登录
login(userInfo) {
wsClient.login(userInfo);
}
// 获取在线用户
getOnlineUsers() {
wsClient.getOnlineUsers();
}
// 获取变更历史
getChangeHistory(limit = 50) {
wsClient.getChangeHistory(limit);
}
// 生成变更ID
generateChangeId() {
return Date.now().toString(36) + Math.random().toString(36).substr(2);
}
// 获取数据
getData() {
return this.data;
}
// 获取连接状态
getConnectionStatus() {
return this.connectionStatus;
}
// 获取同步状态
getSyncStatus() {
return this.syncStatus;
}
// 断开连接
disconnect() {
wsClient.disconnect();
this.connectionStatus.value.isConnected = false;
}
}
// 创建全局实例
const dataSyncManager = new DataSyncManager();
export default dataSyncManager;
/**
* 数据同步初始化器
* 统一管理WebSocket连接和数据同步逻辑
*/
import dataSyncManager from './dataSyncManager.js'
import { useAuthStore } from '@/store/auth.js'
import { useDataStore } from '@/store/data.js'
class SyncInitializer {
constructor() {
this.isInitialized = false
this.reconnectAttempts = 0
this.maxReconnectAttempts = 5
}
/**
* 初始化数据同步
*/
async initialize() {
if (this.isInitialized) {
console.log('数据同步已经初始化')
return
}
try {
const authStore = useAuthStore()
const dataStore = useDataStore()
if (!authStore.isAuthenticated) {
console.log('用户未登录,跳过数据同步初始化')
return
}
// 设置全局引用
window.dataSyncManager = dataSyncManager
window.dataStore = dataStore
// 连接WebSocket服务器
dataSyncManager.connect('http://localhost:3001')
// 准备用户信息
const userInfo = {
id: authStore.currentUser.id,
name: authStore.currentUser.name,
role: authStore.currentUser.role,
department: authStore.currentUser.department || '未知部门',
browserInfo: {
userAgent: navigator.userAgent,
timestamp: new Date().toISOString(),
url: window.location.href
}
}
// 登录到同步系统
dataSyncManager.login(userInfo)
// 设置数据变更监听
this.setupDataChangeListeners()
this.isInitialized = true
console.log('✅ 数据同步初始化完成')
} catch (error) {
console.error('数据同步初始化失败:', error)
this.scheduleReconnect()
}
}
/**
* 设置数据变更监听器
*/
setupDataChangeListeners() {
// 监听WebSocket数据变更
window.addEventListener('websocket_data_change', (event) => {
const { type, userName, timestamp } = event.detail
console.log(`收到WebSocket数据变更: ${type} (来自: ${userName})`)
// 触发数据重新加载
this.handleDataChange(type)
})
// 监听localStorage变更
window.addEventListener('storage', (event) => {
if (event.key && event.key.startsWith('score_system_')) {
console.log('检测到localStorage变化:', event.key)
this.handleDataChange('storage_change')
}
})
// 监听自定义数据变更事件
window.addEventListener('dataChange', (event) => {
const { type } = event.detail
console.log('收到自定义数据变更事件:', type)
this.handleDataChange(type)
})
}
/**
* 处理数据变更
*/
handleDataChange(type) {
try {
// 触发页面数据刷新
const refreshEvent = new CustomEvent('dataRefreshRequired', {
detail: {
type,
timestamp: new Date().toISOString()
}
})
window.dispatchEvent(refreshEvent)
// 如果是重要的数据变更,延迟再次刷新以确保数据一致性
if (['user_added', 'user_deleted', 'institution_added', 'institution_deleted'].includes(type)) {
setTimeout(() => {
window.dispatchEvent(new CustomEvent('dataRefreshRequired', {
detail: {
type: `${type}_delayed`,
timestamp: new Date().toISOString()
}
}))
}, 1000)
}
} catch (error) {
console.error('处理数据变更失败:', error)
}
}
/**
* 安排重连
*/
scheduleReconnect() {
if (this.reconnectAttempts < this.maxReconnectAttempts) {
this.reconnectAttempts++
const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000)
console.log(`${delay}ms后尝试重新连接 (${this.reconnectAttempts}/${this.maxReconnectAttempts})`)
setTimeout(() => {
this.isInitialized = false
this.initialize()
}, delay)
} else {
console.error('达到最大重连次数,停止重连')
}
}
/**
* 重置连接状态
*/
reset() {
this.isInitialized = false
this.reconnectAttempts = 0
}
/**
* 手动触发数据同步
*/
triggerSync() {
if (window.dataSyncManager) {
const dataStore = useDataStore()
// 广播当前数据状态
window.dataSyncManager.broadcastChange('data_saved', {
users: dataStore.getUsers(),
institutions: dataStore.getInstitutions(),
timestamp: new Date().toISOString()
})
console.log('手动触发数据同步')
}
}
/**
* 检查连接状态
*/
getStatus() {
return {
isInitialized: this.isInitialized,
isConnected: window.dataSyncManager?.connectionStatus?.value?.isConnected || false,
reconnectAttempts: this.reconnectAttempts,
onlineUsers: window.dataSyncManager?.data?.onlineUsers?.length || 0
}
}
}
// 创建单例实例
const syncInitializer = new SyncInitializer()
// 导出初始化函数和实例
export const initializeSync = () => syncInitializer.initialize()
export const resetSync = () => syncInitializer.reset()
export const triggerSync = () => syncInitializer.triggerSync()
export const getSyncStatus = () => syncInitializer.getStatus()
export default syncInitializer
import { io } from 'socket.io-client';
import { ElMessage, ElNotification } from 'element-plus';
class WebSocketClient {
constructor() {
this.socket = null;
this.isConnected = false;
this.reconnectAttempts = 0;
this.maxReconnectAttempts = 5;
this.reconnectDelay = 1000;
this.heartbeatInterval = null;
this.callbacks = new Map();
this.userInfo = null;
}
// 连接WebSocket服务器
connect(serverUrl = 'http://localhost:3000') {
try {
this.socket = io(serverUrl, {
transports: ['websocket', 'polling'],
timeout: 20000,
forceNew: true
});
this.setupEventListeners();
console.log('正在连接WebSocket服务器...');
} catch (error) {
console.error('WebSocket连接失败:', error);
ElMessage.error('连接服务器失败');
}
}
// 设置事件监听器
setupEventListeners() {
// 连接成功
this.socket.on('connect', () => {
console.log('WebSocket连接成功');
this.isConnected = true;
this.reconnectAttempts = 0;
// 开始心跳检测
this.startHeartbeat();
// 如果有用户信息,自动登录
if (this.userInfo) {
this.login(this.userInfo);
}
ElMessage.success('连接服务器成功');
this.emit('connected');
});
// 连接断开
this.socket.on('disconnect', (reason) => {
console.log('WebSocket连接断开:', reason);
this.isConnected = false;
this.stopHeartbeat();
ElMessage.warning('与服务器连接断开');
this.emit('disconnected', reason);
// 自动重连
if (reason !== 'io client disconnect') {
this.attemptReconnect();
}
});
// 连接错误
this.socket.on('connect_error', (error) => {
console.error('WebSocket连接错误:', error);
ElMessage.error('连接服务器失败');
this.emit('error', error);
});
// 接收初始数据
this.socket.on('initial_data', (data) => {
console.log('接收到初始数据:', data);
this.emit('initial_data', data);
});
// 数据变更通知
this.socket.on('data_changed', (data) => {
console.log('数据已变更:', data);
this.emit('data_changed', data);
// 显示变更通知
if (data.change && data.change.userName !== this.userInfo?.name) {
ElNotification({
title: '数据更新',
message: `${data.change.userName} 更新了数据`,
type: 'info',
duration: 3000
});
}
});
// 用户上线通知
this.socket.on('user_online', (user) => {
console.log('用户上线:', user);
this.emit('user_online', user);
ElNotification({
title: '用户上线',
message: `${user.name} 已上线`,
type: 'success',
duration: 2000
});
});
// 用户下线通知
this.socket.on('user_offline', (user) => {
console.log('用户下线:', user);
this.emit('user_offline', user);
ElNotification({
title: '用户下线',
message: `${user.name} 已下线`,
type: 'warning',
duration: 2000
});
});
// 在线用户列表
this.socket.on('online_users', (users) => {
this.emit('online_users', users);
});
// 变更历史
this.socket.on('change_history', (history) => {
this.emit('change_history', history);
});
}
// 用户登录
login(userInfo) {
this.userInfo = userInfo;
if (this.isConnected) {
const loginData = {
...userInfo,
browserInfo: this.getBrowserInfo()
};
this.socket.emit('user_login', loginData);
console.log('发送登录信息:', loginData);
}
}
// 发送数据更新
updateData(type, data, oldData = null) {
if (!this.isConnected) {
ElMessage.error('未连接到服务器');
return false;
}
const updateData = {
type,
data,
oldData,
timestamp: new Date()
};
this.socket.emit('data_update', updateData);
console.log('发送数据更新:', updateData);
return true;
}
// 获取在线用户列表
getOnlineUsers() {
if (this.isConnected) {
this.socket.emit('get_online_users');
}
}
// 获取变更历史
getChangeHistory(limit = 50) {
if (this.isConnected) {
this.socket.emit('get_change_history', limit);
}
}
// 开始心跳检测
startHeartbeat() {
this.heartbeatInterval = setInterval(() => {
if (this.isConnected) {
this.socket.emit('heartbeat');
}
}, 30000); // 每30秒发送一次心跳
}
// 停止心跳检测
stopHeartbeat() {
if (this.heartbeatInterval) {
clearInterval(this.heartbeatInterval);
this.heartbeatInterval = null;
}
}
// 尝试重连
attemptReconnect() {
if (this.reconnectAttempts < this.maxReconnectAttempts) {
this.reconnectAttempts++;
console.log(`尝试重连 (${this.reconnectAttempts}/${this.maxReconnectAttempts})...`);
setTimeout(() => {
if (this.socket) {
this.socket.connect();
}
}, this.reconnectDelay * this.reconnectAttempts);
} else {
console.log('重连次数已达上限');
ElMessage.error('无法连接到服务器,请刷新页面重试');
}
}
// 获取浏览器信息
getBrowserInfo() {
return {
userAgent: navigator.userAgent,
language: navigator.language,
platform: navigator.platform,
cookieEnabled: navigator.cookieEnabled,
onLine: navigator.onLine,
screen: {
width: screen.width,
height: screen.height,
colorDepth: screen.colorDepth
},
viewport: {
width: window.innerWidth,
height: window.innerHeight
},
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
timestamp: new Date()
};
}
// 事件监听
on(event, callback) {
if (!this.callbacks.has(event)) {
this.callbacks.set(event, []);
}
this.callbacks.get(event).push(callback);
}
// 移除事件监听
off(event, callback) {
if (this.callbacks.has(event)) {
const callbacks = this.callbacks.get(event);
const index = callbacks.indexOf(callback);
if (index > -1) {
callbacks.splice(index, 1);
}
}
}
// 触发事件
emit(event, data) {
if (this.callbacks.has(event)) {
this.callbacks.get(event).forEach(callback => {
try {
callback(data);
} catch (error) {
console.error('事件回调执行错误:', error);
}
});
}
}
// 断开连接
disconnect() {
this.stopHeartbeat();
if (this.socket) {
this.socket.disconnect();
this.socket = null;
}
this.isConnected = false;
this.userInfo = null;
console.log('WebSocket连接已断开');
}
// 获取连接状态
getConnectionStatus() {
return {
isConnected: this.isConnected,
reconnectAttempts: this.reconnectAttempts,
userInfo: this.userInfo
};
}
}
// 创建全局实例
const wsClient = new WebSocketClient();
export default wsClient;
......@@ -9,7 +9,6 @@
<p>负责机构:{{ userInstitutions.length }}</p>
</div>
<div class="header-actions">
<RealtimeStatus />
<el-button @click="handleLogout">退出登录</el-button>
</div>
</div>
......@@ -199,8 +198,7 @@ import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Search, Refresh } from '@element-plus/icons-vue'
import { useAuthStore } from '@/store/auth'
import { useDataStore } from '@/store/data'
import RealtimeStatus from '@/components/RealtimeStatus.vue'
import dataSyncManager from '@/utils/dataSyncManager.js'
/**
* 用户操作面板组件
......@@ -701,8 +699,7 @@ onMounted(() => {
router.push('/login')
}
// 初始化实时数据同步
initializeRealtimeSync()
// 调试:检查页面加载时的数据状态
console.log('=== 页面加载时数据状态 ===')
......@@ -716,29 +713,7 @@ onMounted(() => {
})
})
/**
* 初始化实时数据同步
*/
const initializeRealtimeSync = () => {
try {
// 连接WebSocket服务器
dataSyncManager.connect('http://localhost:3000')
// 用户登录到实时同步系统
const userInfo = {
id: authStore.currentUser.id,
name: authStore.currentUser.name,
department: authStore.currentUser.department || '未知部门'
}
dataSyncManager.login(userInfo)
console.log('实时数据同步已初始化')
} catch (error) {
console.error('初始化实时数据同步失败:', error)
ElMessage.warning('实时同步功能暂时不可用')
}
}
</script>
<style scoped>
......
@echo off
chcp 65001 >nul
echo ================================
echo 绩效计分系统 - 实时同步版本启动
echo ================================
echo.
echo [1/3] 检查Node.js环境...
node --version >nul 2>&1
if %errorlevel% neq 0 (
echo ❌ Node.js未安装,请先安装Node.js
pause
exit /b 1
)
npm --version >nul 2>&1
if %errorlevel% neq 0 (
echo ❌ npm未安装
pause
exit /b 1
)
echo ✅ Node.js环境检查通过
echo.
echo [2/3] 安装依赖...
npm install
if %errorlevel% neq 0 (
echo ❌ 依赖安装失败
pause
exit /b 1
)
echo ✅ 依赖安装完成
echo.
echo [3/3] 启动服务...
echo.
echo 🚀 正在启动前端开发服务器和WebSocket后端服务器...
echo.
echo 📋 服务信息:
echo - 前端开发服务器: http://localhost:5173
echo - WebSocket后端服务器: http://localhost:3000
echo - 实时数据同步: 已启用
echo.
echo 💡 提示:
echo - 可以在多个浏览器窗口中打开应用测试多用户同步
echo - 数据变更会实时同步到所有连接的客户端
echo - 右上角显示连接状态和在线用户数量
echo.
npm run dev:full
echo.
echo 服务已停止
pause
# 绩效计分系统启动脚本
# 用于启动前端服务器和WebSocket服务器
# 用于启动前端开发服务器
Write-Host "=== 绩效计分系统启动脚本 ===" -ForegroundColor Green
......@@ -50,15 +50,6 @@ if (-not (Test-Path "node_modules")) {
}
}
Write-Host "启动WebSocket服务器..." -ForegroundColor Yellow
$websocketJob = Start-Job -ScriptBlock {
Set-Location $using:PWD
node websocket-server.cjs
}
# 等待WebSocket服务器启动
Start-Sleep -Seconds 3
Write-Host "启动前端服务器..." -ForegroundColor Yellow
$frontendJob = Start-Job -ScriptBlock {
Set-Location $using:PWD
......@@ -78,12 +69,7 @@ try {
Write-Host "✗ 前端服务器启动失败" -ForegroundColor Red
}
try {
$websocketResponse = Invoke-WebRequest -Uri "http://localhost:4001" -Method HEAD -TimeoutSec 5 -ErrorAction Stop
Write-Host "✓ WebSocket服务器运行正常 (http://localhost:4001)" -ForegroundColor Green
} catch {
Write-Host "✗ WebSocket服务器启动失败" -ForegroundColor Red
}
Write-Host ""
Write-Host "=== 系统启动完成 ===" -ForegroundColor Green
......@@ -97,16 +83,8 @@ Write-Host "按 Ctrl+C 停止所有服务" -ForegroundColor Yellow
try {
while ($true) {
Start-Sleep -Seconds 30
# 检查作业状态
if ($websocketJob.State -eq "Failed" -or $websocketJob.State -eq "Stopped") {
Write-Host "WebSocket服务器已停止,重新启动..." -ForegroundColor Yellow
$websocketJob = Start-Job -ScriptBlock {
Set-Location $using:PWD
node websocket-server.cjs
}
}
if ($frontendJob.State -eq "Failed" -or $frontendJob.State -eq "Stopped") {
Write-Host "前端服务器已停止,重新启动..." -ForegroundColor Yellow
$frontendJob = Start-Job -ScriptBlock {
......@@ -117,8 +95,8 @@ try {
}
} finally {
Write-Host "停止所有服务..." -ForegroundColor Yellow
Stop-Job $websocketJob, $frontendJob -ErrorAction SilentlyContinue
Remove-Job $websocketJob, $frontendJob -ErrorAction SilentlyContinue
Stop-Job $frontendJob -ErrorAction SilentlyContinue
Remove-Job $frontendJob -ErrorAction SilentlyContinue
Get-Process -Name "node" -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue
Write-Host "所有服务已停止" -ForegroundColor Green
}
const express = require('express');
const cors = require('cors');
const path = require('path');
const http = require('http');
const socketIo = require('socket.io');
const { v4: uuidv4 } = require('uuid');
const app = express();
const server = http.createServer(app);
const io = socketIo(server, {
cors: {
origin: "*",
methods: ["GET", "POST"]
}
});
const PORT = process.env.PORT || 3000;
// 中间件
app.use(cors());
app.use(express.json());
app.use(express.static(path.join(__dirname, 'dist')));
// 在线用户管理
const onlineUsers = new Map();
const userSessions = new Map();
// 模拟数据存储
let performanceData = {
users: [
{ id: 1, name: '张三', department: '生产部', score: 3.5 },
{ id: 2, name: '李四', department: '质检部', score: 2.9 }
],
workstations: [
{ id: 1, name: '昆明市五华区爱牙口腔诊所', score: 3.5, status: '正常' },
{ id: 2, name: '五华区长青口腔诊疗所', score: 2.9, status: '待检查' },
{ id: 3, name: '昆明美奥云口腔医院有限公司安宁宁湖诊所', score: 0, status: '新建' }
]
};
// 数据变更历史
let changeHistory = [];
// WebSocket连接处理
io.on('connection', (socket) => {
console.log('用户连接:', socket.id);
// 用户登录
socket.on('user_login', (userData) => {
const sessionId = uuidv4();
const userInfo = {
id: userData.id || socket.id,
name: userData.name || '匿名用户',
socketId: socket.id,
sessionId: sessionId,
loginTime: new Date(),
lastActivity: new Date(),
browserInfo: userData.browserInfo || {}
};
// 存储用户信息
onlineUsers.set(socket.id, userInfo);
// 如果用户已有其他会话,添加到会话列表
if (!userSessions.has(userInfo.id)) {
userSessions.set(userInfo.id, []);
}
userSessions.get(userInfo.id).push(socket.id);
// 发送当前数据给新连接的用户
socket.emit('initial_data', {
performanceData,
onlineUsers: Array.from(onlineUsers.values()),
changeHistory: changeHistory.slice(-50) // 最近50条变更记录
});
// 广播用户上线消息
socket.broadcast.emit('user_online', userInfo);
console.log(`用户 ${userInfo.name} 已登录,会话ID: ${sessionId}`);
});
// 数据更新
socket.on('data_update', (updateData) => {
const user = onlineUsers.get(socket.id);
if (!user) return;
const timestamp = new Date();
const changeId = uuidv4();
// 记录变更
const change = {
id: changeId,
type: updateData.type,
data: updateData.data,
oldData: updateData.oldData,
userId: user.id,
userName: user.name,
timestamp: timestamp,
sessionId: user.sessionId
};
// 应用数据变更
applyDataChange(updateData);
// 添加到变更历史
changeHistory.push(change);
// 保持历史记录在合理范围内
if (changeHistory.length > 1000) {
changeHistory = changeHistory.slice(-500);
}
// 广播数据变更给所有连接的用户
io.emit('data_changed', {
change: change,
newData: performanceData
});
console.log(`数据更新 by ${user.name}:`, updateData.type);
});
// 心跳检测
socket.on('heartbeat', () => {
const user = onlineUsers.get(socket.id);
if (user) {
user.lastActivity = new Date();
onlineUsers.set(socket.id, user);
}
});
// 获取在线用户列表
socket.on('get_online_users', () => {
socket.emit('online_users', Array.from(onlineUsers.values()));
});
// 获取变更历史
socket.on('get_change_history', (limit = 50) => {
socket.emit('change_history', changeHistory.slice(-limit));
});
// 用户断开连接
socket.on('disconnect', () => {
const user = onlineUsers.get(socket.id);
if (user) {
// 从在线用户中移除
onlineUsers.delete(socket.id);
// 从用户会话中移除
if (userSessions.has(user.id)) {
const sessions = userSessions.get(user.id);
const index = sessions.indexOf(socket.id);
if (index > -1) {
sessions.splice(index, 1);
}
if (sessions.length === 0) {
userSessions.delete(user.id);
}
}
// 广播用户下线消息
socket.broadcast.emit('user_offline', user);
console.log(`用户 ${user.name} 已断开连接`);
}
});
});
// 应用数据变更
function applyDataChange(updateData) {
switch (updateData.type) {
case 'update_user_score':
const userIndex = performanceData.users.findIndex(u => u.id === updateData.data.id);
if (userIndex !== -1) {
performanceData.users[userIndex] = { ...performanceData.users[userIndex], ...updateData.data };
}
break;
case 'update_workstation':
const wsIndex = performanceData.workstations.findIndex(w => w.id === updateData.data.id);
if (wsIndex !== -1) {
performanceData.workstations[wsIndex] = { ...performanceData.workstations[wsIndex], ...updateData.data };
}
break;
case 'add_user':
updateData.data.id = Date.now(); // 简单的ID生成
performanceData.users.push(updateData.data);
break;
case 'add_workstation':
updateData.data.id = Date.now();
performanceData.workstations.push(updateData.data);
break;
case 'delete_user':
performanceData.users = performanceData.users.filter(u => u.id !== updateData.data.id);
break;
case 'delete_workstation':
performanceData.workstations = performanceData.workstations.filter(w => w.id !== updateData.data.id);
break;
}
}
// REST API 路由
app.get('/api/data', (req, res) => {
res.json(performanceData);
});
app.get('/api/online-users', (req, res) => {
res.json(Array.from(onlineUsers.values()));
});
app.get('/api/change-history', (req, res) => {
const limit = parseInt(req.query.limit) || 50;
res.json(changeHistory.slice(-limit));
});
app.post('/api/data', (req, res) => {
const updateData = req.body;
applyDataChange(updateData);
// 广播变更给所有WebSocket连接
io.emit('data_changed', {
change: {
id: uuidv4(),
type: updateData.type,
data: updateData.data,
timestamp: new Date(),
source: 'api'
},
newData: performanceData
});
res.json({ success: true, data: performanceData });
});
// 健康检查
app.get('/health', (req, res) => {
res.json({
status: 'healthy',
timestamp: new Date(),
onlineUsers: onlineUsers.size,
totalChanges: changeHistory.length
});
});
// SPA路由支持
app.get('*', (req, res) => {
res.sendFile(path.join(__dirname, 'dist', 'index.html'));
});
// 定期清理非活跃连接
setInterval(() => {
const now = new Date();
const timeout = 5 * 60 * 1000; // 5分钟超时
for (const [socketId, user] of onlineUsers.entries()) {
if (now - user.lastActivity > timeout) {
console.log(`清理非活跃用户: ${user.name}`);
onlineUsers.delete(socketId);
if (userSessions.has(user.id)) {
const sessions = userSessions.get(user.id);
const index = sessions.indexOf(socketId);
if (index > -1) {
sessions.splice(index, 1);
}
if (sessions.length === 0) {
userSessions.delete(user.id);
}
}
}
}
}, 60000); // 每分钟检查一次
server.listen(PORT, () => {
console.log(`\n🚀 WebSocket服务器运行在 http://localhost:${PORT}`);
console.log(`📡 支持实时数据同步`);
console.log(`👥 支持多用户多浏览器同步`);
console.log(`\n✅ 绩效计分系统已启动!`);
});
server.on('error', (err) => {
if (err.code === 'EADDRINUSE') {
console.log(`❌ 端口 ${PORT} 已被占用,请尝试其他端口`);
} else {
console.log('❌ 服务器启动失败:', err);
}
});
# 多用户多浏览器数据同步测试指南
++ /dev/null
# 多用户多浏览器数据同步测试指南
## 功能概述
绩效计分系统现已支持多用户多浏览器实时数据同步功能,包括:
- 🔄 **实时数据同步**:用户在任何浏览器中的数据变更会立即同步到所有其他连接的客户端
- 👥 **多用户支持**:支持多个用户同时在线,显示在线用户列表
- 📱 **多浏览器支持**:同一用户可以在多个浏览器/设备中同时使用
- 📊 **变更历史**:记录所有数据变更历史,包括操作者和时间
- 🔔 **实时通知**:用户上线/下线和数据变更的实时通知
- 💓 **连接监控**:实时显示连接状态和在线用户数量
## 启动系统
### 方法一:使用启动脚本(推荐)
```bash
# 双击运行
start-realtime.bat
```
### 方法二:手动启动
```bash
# 安装依赖
npm install
# 同时启动前端和后端
npm run dev:full
# 或分别启动
# 终端1:启动WebSocket后端服务器
npm run server
# 终端2:启动前端开发服务器
npm run dev
```
## 测试步骤
### 1. 基础连接测试
1. **启动系统**
- 运行 `start-realtime.bat`
- 等待前端和后端服务器都启动完成
2. **打开第一个浏览器窗口**
- 访问 http://localhost:5173
- 登录系统(用户名:admin,密码:admin123)
- 观察右上角的连接状态指示器(应显示绿色圆点和"已连接")
3. **检查连接状态**
- 点击右上角的连接状态按钮
- 查看连接详情对话框
- 确认显示"已连接"和当前用户信息
### 2. 多浏览器测试
1. **打开第二个浏览器窗口**
- 在同一浏览器中新开标签页,或使用不同浏览器
- 访问 http://localhost:5173
- 使用相同或不同的用户账号登录
2. **观察用户上线通知**
- 在第一个窗口中应该看到用户上线的通知
- 点击连接状态按钮,查看在线用户列表
- 应该显示2个在线用户
3. **测试多个浏览器**
- 可以打开Chrome、Firefox、Edge等不同浏览器
- 每个浏览器都登录系统
- 观察在线用户数量的变化
### 3. 数据同步测试
1. **测试评分更新同步**
- 在窗口A中修改某个机构的评分
- 观察窗口B中的数据是否立即更新
- 检查是否收到数据变更通知
2. **测试机构信息同步**
- 在窗口A中添加新机构
- 观察窗口B中是否立即显示新机构
- 在窗口B中修改机构信息
- 观察窗口A中的变化
3. **测试图片上传同步**
- 在窗口A中上传图片
- 观察窗口B中是否显示新上传的图片
- 检查图片数量统计是否同步更新
### 4. 变更历史测试
1. **查看变更记录**
- 点击连接状态按钮
- 切换到"最近变更"标签
- 查看所有数据变更的历史记录
2. **验证变更信息**
- 检查变更记录是否包含:
- 操作类型(添加、修改、删除)
- 操作者姓名
- 操作时间
- 变更内容
### 5. 连接稳定性测试
1. **网络中断测试**
- 暂时断开网络连接
- 观察连接状态变化
- 恢复网络连接
- 检查是否自动重连
2. **服务器重启测试**
- 停止后端服务器
- 观察前端的错误处理
- 重启后端服务器
- 检查客户端是否自动重连
3. **长时间连接测试**
- 保持多个浏览器窗口长时间打开
- 定期进行数据操作
- 观察连接是否稳定
## 测试场景
### 场景1:多用户协作
- 用户A负责添加机构信息
- 用户B负责上传图片
- 用户C负责评分
- 观察三个用户的操作如何实时同步
### 场景2:移动端测试
- 在手机浏览器中打开系统
- 与桌面浏览器进行数据同步测试
- 检查移动端的实时通知功能
### 场景3:大量数据测试
- 快速连续进行多个数据操作
- 观察同步性能和稳定性
- 检查是否有数据丢失或重复
## 预期结果
### 正常情况下应该看到:
1. **连接状态**
- 绿色连接指示器
- 正确的在线用户数量
- 稳定的WebSocket连接
2. **数据同步**
- 所有数据变更在1秒内同步到其他客户端
- 没有数据丢失或不一致
- 正确的变更通知
3. **用户体验**
- 流畅的实时更新
- 清晰的状态提示
- 友好的错误处理
### 异常情况处理:
1. **连接失败**
- 显示红色连接指示器
- 提示连接失败信息
- 自动尝试重连
2. **数据冲突**
- 显示冲突解决提示
- 保持数据一致性
- 记录冲突日志
## 故障排除
### 常见问题
1. **无法连接WebSocket服务器**
```
解决方案:
- 检查后端服务器是否启动(端口3000)
- 检查防火墙设置
- 确认端口没有被占用
```
2. **数据不同步**
```
解决方案:
- 刷新页面重新连接
- 检查浏览器控制台错误信息
- 重启后端服务器
```
3. **连接频繁断开**
```
解决方案:
- 检查网络稳定性
- 调整心跳检测间隔
- 检查服务器资源使用情况
```
### 调试信息
在浏览器控制台中可以看到:
- WebSocket连接状态
- 数据同步日志
- 错误信息和警告
## 性能指标
### 预期性能:
- **连接建立时间**:< 2秒
- **数据同步延迟**:< 1秒
- **支持并发用户**:50+
- **内存使用**:< 100MB(前端)
- **CPU使用**:< 5%(正常操作)
## 技术架构
### 前端技术栈:
- Vue 3 + Element Plus
- Socket.IO Client
- 响应式数据管理
### 后端技术栈:
- Node.js + Express
- Socket.IO Server
- 内存数据存储(可扩展到数据库)
### 通信协议:
- WebSocket(主要)
- HTTP轮询(备用)
- 自动降级和重连
## 下一步计划
1. **数据库集成**:将内存存储替换为PostgreSQL
2. **用户权限**:实现细粒度的权限控制
3. **离线支持**:支持离线操作和数据同步
4. **性能优化**:优化大量数据的同步性能
5. **监控面板**:添加系统监控和统计面板
# 数据同步问题修复报告
++ /dev/null
# 数据同步问题修复报告
## 🎯 问题概述
根据用户测试反馈,发现了数据同步功能的问题:
- Edge浏览器中新增用户"周二"后,Chrome浏览器中没有显示该用户
- 监控页面显示用户数量为5,说明数据已保存到localStorage
- WebSocket服务器正在运行,但数据同步没有正常工作
## 🔍 问题分析
### 1. 根本原因
- **WebSocket服务器数据存储问题**:WebSocket服务器维护的是自己的内存数据(performanceData),而不是localStorage数据
- **数据同步逻辑缺陷**:服务器接收到`data_update`事件后,没有正确处理localStorage数据的同步
- **前端刷新函数缺失**:管理员页面调用了`refreshStats()`函数,但该函数没有定义
### 2. 技术细节
- localStorage数据存储在客户端,每个浏览器实例都有独立的localStorage
- WebSocket服务器的`performanceData`是服务器内存数据,与localStorage无关
- 数据同步需要通过WebSocket事件通知其他客户端刷新localStorage数据
## 🛠️ 修复方案
### 1. 修复WebSocket服务器数据处理逻辑
**文件**: `websocket-server.cjs`
```javascript
// 修复前:试图修改服务器内存数据
case 'user_added':
case 'institution_added':
case 'data_saved':
// 这些事件主要用于通知其他客户端刷新数据
console.log('处理数据同步事件:', updateData.type);
break;
// 修复后:明确说明这些事件不需要修改服务器数据
case 'user_added':
case 'institution_added':
case 'data_saved':
console.log('处理数据同步事件:', updateData.type);
// 这些事件主要用于通知其他客户端刷新数据
// 不需要在服务器端修改数据,因为数据存储在localStorage中
break;
```
### 2. 添加缺失的refreshStats函数
**文件**: `src/views/admin/AdminPanel.vue`
```javascript
/**
* 刷新统计数据
*/
const refreshStats = () => {
// 强制刷新所有计算属性
forceRefresh()
// 重新加载数据存储
dataStore.loadFromStorage()
console.log('统计数据已刷新')
}
```
### 3. 数据同步流程优化
1. **发送端**(添加用户的浏览器):
- 保存数据到localStorage
- 通过WebSocket发送`data_update`事件
2. **WebSocket服务器**
- 接收`data_update`事件
- 广播`data_changed`事件给所有连接的客户端
3. **接收端**(其他浏览器):
- 接收`data_changed`事件
- 触发`dataRefreshRequired`事件
- 调用`refreshStats()`刷新页面数据
## 🧪 测试工具
创建了专门的测试页面:`public/data-sync-test.html`
### 功能特性:
- ✅ WebSocket连接状态监控
- ✅ 在线用户数量显示
- ✅ 实时数据统计(用户数、机构数)
- ✅ 测试添加用户/机构功能
- ✅ 数据变更事件监听
- ✅ 操作日志记录
- ✅ localStorage变化监听
### 使用方法:
1. 访问 `http://localhost:5173/data-sync-test.html`
2. 点击"连接WebSocket"建立连接
3. 使用"测试添加用户"和"测试添加机构"按钮测试同步
4. 在多个浏览器标签页中打开,验证数据同步效果
## 📋 验证步骤
### 1. 多浏览器测试
1. 在Edge浏览器中打开管理员页面
2. 在Chrome浏览器中打开管理员页面
3. 在Edge中添加新用户
4. 检查Chrome中是否自动显示新用户
### 2. 实时监控测试
1. 打开监控页面 `http://localhost:5173/sync-test.html`
2. 观察用户数量和在线状态
3. 在管理员页面进行操作
4. 检查监控页面是否实时更新
### 3. WebSocket连接测试
1. 打开浏览器开发者工具
2. 查看Console日志
3. 确认WebSocket连接成功
4. 确认数据变更事件正确发送和接收
## 🔧 技术架构
```
┌─────────────────┐ WebSocket ┌─────────────────┐
│ Edge浏览器 │ ◄──────────────► │ WebSocket服务器 │
│ localStorage │ │ (Node.js) │
└─────────────────┘ └─────────────────┘
│ WebSocket
┌─────────────────┐ ┌─────────────────┐
│ Chrome浏览器 │ ◄──────────────► │ 其他客户端 │
│ localStorage │ │ localStorage │
└─────────────────┘ └─────────────────┘
```
## ✅ 修复结果
- **数据同步正常**:不同浏览器间的数据变更能够实时同步
- **WebSocket连接稳定**:连接状态监控和自动重连机制工作正常
- **页面刷新及时**:数据变更后页面统计信息立即更新
- **错误处理完善**:添加了完整的错误日志和状态监控
## 🚀 后续优化建议
1. **数据持久化**:考虑将数据存储到数据库而不是localStorage
2. **冲突解决**:添加数据冲突检测和解决机制
3. **性能优化**:对频繁的数据同步事件进行防抖处理
4. **安全增强**:添加用户身份验证和权限控制
5. **监控完善**:添加更详细的系统监控和日志记录
## 📞 技术支持
如果在使用过程中遇到问题,请:
1. 检查WebSocket服务器是否正常运行
2. 查看浏览器控制台是否有错误信息
3. 使用测试页面验证数据同步功能
4. 检查网络连接和防火墙设置
# 跨浏览器数据同步修复报告
++ /dev/null
# 跨浏览器数据同步修复报告
## 🎯 问题概述
**核心问题**:不同浏览器之间无法共享localStorage数据,导致数据同步失效。
### 问题现象
1.**Edge浏览器**:添加用户"周二"成功,数据保存在Edge的localStorage中
2.**Chrome浏览器**:无法看到Edge中添加的用户"周二"
3.**监控页面**:显示用户数量为5,说明WebSocket服务器接收到了数据变更事件
### 根本原因
- **localStorage隔离**:每个浏览器都有独立的localStorage,无法跨浏览器共享
- **数据存储架构缺陷**:系统依赖localStorage作为主要数据存储,缺少真正的服务器端数据持久化
- **同步机制不完整**:WebSocket只负责事件通知,没有实现真正的数据同步
## 🛠️ 解决方案
### 1. 服务器端数据存储
**新增功能**:在WebSocket服务器中添加真实的系统数据存储
```javascript
// 真实数据存储 - 与前端localStorage格式一致
let systemData = {
users: [
{
id: 'admin',
name: '系统管理员',
phone: 'admin',
password: 'admin123',
role: 'admin',
institutions: []
}
],
institutions: [],
systemConfig: {
initialized: true,
version: '8.5.0',
hasDefaultData: false
}
};
```
### 2. 新增API接口
**文件**`websocket-server.cjs`
```javascript
// 获取系统数据
app.get('/api/system-data', (req, res) => {
res.json(systemData);
});
// 保存系统数据
app.post('/api/system-data', (req, res) => {
const { users, institutions, systemConfig } = req.body;
if (users) systemData.users = users;
if (institutions) systemData.institutions = institutions;
if (systemConfig) systemData.systemConfig = systemConfig;
// 广播数据变更给所有WebSocket连接
io.emit('system_data_changed', {
change: {
id: uuidv4(),
type: 'system_data_update',
timestamp: new Date(),
source: 'api'
},
newData: systemData
});
res.json({ success: true, data: systemData });
});
```
### 3. 服务器端数据同步管理器
**新文件**`src/utils/serverDataSync.js`
主要功能:
-**从服务器获取数据**`getSystemData()`
-**保存数据到服务器**`saveSystemData()`
-**双向同步**`syncToServer()``syncFromServer()`
-**连接状态检查**`checkServerConnection()`
-**定期同步**`startPeriodicSync()`
-**强制同步**`forceSync()`
### 4. 前端数据存储增强
**修改文件**`src/store/data.js`
```javascript
// 新增从服务器加载数据的函数
const loadFromServer = async () => {
try {
const systemData = await serverDataSync.getSystemData()
if (systemData.users) users.value = systemData.users
if (systemData.institutions) institutions.value = systemData.institutions
if (systemData.systemConfig) systemConfig.value = systemData.systemConfig
// 同步到localStorage
saveToStorage()
return true
} catch (error) {
console.warn('从服务器加载数据失败,将使用localStorage数据:', error)
return false
}
}
// 修改loadFromStorage函数,优先从服务器加载
const loadFromStorage = async () => {
// 首先尝试从服务器加载最新数据
const serverLoaded = await loadFromServer()
if (serverLoaded) {
console.log('✅ 已从服务器加载最新数据')
return
}
// 如果服务器加载失败,使用localStorage数据
// ... 原有的localStorage加载逻辑
}
```
### 5. 自动同步机制
**修改文件**`src/main.js`
```javascript
// 启用服务器数据同步
serverDataSync.enable()
// 启动定期同步
if (authStore.isAuthenticated) {
serverDataSync.startPeriodicSync(30000) // 每30秒同步一次
}
```
## 🧪 测试工具
### 跨浏览器同步测试页面
**文件**`public/cross-browser-sync-test.html`
**功能特性**
-**服务器连接检查**:实时监控服务器连接状态
-**数据加载/保存**:手动从服务器加载和保存数据
-**测试数据生成**:添加测试用户和机构
-**自动同步**:每10秒自动从服务器同步数据
-**浏览器信息显示**:显示当前浏览器信息
-**操作日志**:详细记录所有操作和错误
### 使用方法
1. **启动服务器**
```bash
node websocket-server.cjs
```
2. **打开测试页面**
```
http://localhost:5173/cross-browser-sync-test.html
```
3. **多浏览器测试**
- 在Edge浏览器中打开测试页面
- 在Chrome浏览器中打开测试页面
- 在一个浏览器中添加用户/机构
- 观察另一个浏览器是否自动同步数据
## 📋 数据同步流程
### 新增数据流程
```
1. 用户在Edge中添加数据
2. 数据保存到Edge的localStorage
3. 数据自动同步到服务器
4. 服务器广播数据变更事件
5. Chrome浏览器接收到事件
6. Chrome从服务器获取最新数据
7. Chrome更新本地localStorage
8. Chrome页面显示最新数据
```
### 页面加载流程
```
1. 用户打开页面
2. 系统尝试从服务器加载数据
3. 如果服务器连接成功:使用服务器数据
4. 如果服务器连接失败:使用localStorage数据
5. 启动定期同步(每30秒)
```
## ✅ 修复结果
### 解决的问题
- ✅ **跨浏览器数据同步**:不同浏览器能够实时同步数据
- ✅ **数据持久化**:数据保存在服务器端,不依赖单一浏览器
- ✅ **自动同步**:定期自动同步,无需手动操作
- ✅ **离线支持**:服务器不可用时仍可使用localStorage数据
- ✅ **实时更新**:数据变更后立即同步到所有客户端
### 技术架构
```
┌─────────────────┐ HTTP API ┌─────────────────┐
│ Edge浏览器 │ ◄──────────────► │ WebSocket服务器 │
│ localStorage │ │ systemData │
└─────────────────┘ └─────────────────┘
│ HTTP API
┌─────────────────┐ ┌─────────────────┐
│ Chrome浏览器 │ ◄──────────────► │ 其他客户端 │
│ localStorage │ │ localStorage │
└─────────────────┘ └─────────────────┘
```
## 🚀 使用指南
### 1. 开发环境测试
```bash
# 启动WebSocket服务器
node websocket-server.cjs
# 启动前端开发服务器
npm run dev
# 打开跨浏览器测试页面
http://localhost:5173/cross-browser-sync-test.html
```
### 2. 多浏览器验证
1. 在Edge中打开管理员页面,添加用户
2. 在Chrome中打开管理员页面,检查是否显示新用户
3. 使用测试页面验证自动同步功能
### 3. 故障排除
- **服务器连接失败**:检查WebSocket服务器是否运行在端口3000
- **数据不同步**:查看浏览器控制台错误信息
- **同步延迟**:正常情况下同步延迟不超过30秒
## 📞 技术支持
如果遇到问题:
1. 检查WebSocket服务器运行状态
2. 查看浏览器控制台日志
3. 使用测试页面验证服务器连接
4. 检查网络连接和防火墙设置
现在系统支持真正的跨浏览器数据同步!🎉
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment