feat: 实现WebSocket后台连接和信息面板集成

- 添加WebSocket配置到cad.js配置文件
- 创建websocketService.js实现完整WebSocket服务
- 集成WebSocket到main.js应用启动流程
- 移除InfoManagementPanel.vue所有硬编码数据
- 实现实时软件状态、操作日志、统计信息更新
- 优化面板样式匹配参考项目视觉效果
- 添加渐变按钮样式和深色主题一致性

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
sladro 2025-09-17 18:11:40 +08:00
parent 167b3fc6c8
commit 4d02c503ef
5 changed files with 693 additions and 227 deletions

View File

@ -9,9 +9,9 @@
right: -400px; /* 初始隐藏在右侧 */
width: 400px;
height: calc(100vh - 60px);
background: var(--color-bg-primary);
border-left: 1px solid var(--color-border-primary);
box-shadow: -4px 0 15px rgba(0, 0, 0, 0.1);
background: linear-gradient(135deg, #1e1e1e 0%, #2a2a2a 100%);
border-left: 1px solid rgba(255, 255, 255, 0.1);
box-shadow: -4px 0 15px rgba(0, 0, 0, 0.3);
z-index: 1000;
display: flex;
flex-direction: column;
@ -28,8 +28,8 @@
align-items: center;
justify-content: space-between;
padding: 15px 20px;
background: var(--color-bg-primary);
border-bottom: 1px solid var(--color-border-primary);
background: #1e1e1e;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
min-height: 60px;
}
@ -37,13 +37,13 @@
display: flex;
align-items: center;
gap: 10px;
color: var(--color-text-primary);
color: #ffffff;
font-weight: 600;
font-size: 16px;
}
.panel-title i {
color: var(--color-primary);
color: #2a5caa;
font-size: 18px;
}
@ -59,15 +59,15 @@
gap: 6px;
padding: 4px 8px;
border-radius: 4px;
background: var(--color-error-rgb-1);
color: var(--color-text-error);
background: rgba(244, 67, 54, 0.1);
color: #f44336;
font-size: 12px;
font-weight: 500;
}
.status-indicator.connected {
background: var(--color-success-rgb-1);
color: var(--color-text-success);
background: rgba(76, 175, 80, 0.1);
color: #4caf50;
}
.status-indicator i {
@ -77,7 +77,7 @@
.panel-close {
background: none;
border: none;
color: var(--color-text-secondary);
color: #cccccc;
cursor: pointer;
padding: 6px;
border-radius: 4px;
@ -85,8 +85,8 @@
}
.panel-close:hover {
background: var(--color-white-rgb-1);
color: var(--color-text-primary);
background: #2a2a2a;
color: #ffffff;
}
/* 面板内容区域 */
@ -100,8 +100,8 @@
/* 选项卡导航 */
.panel-tabs {
display: flex;
background: var(--color-bg-secondary);
border-bottom: 1px solid var(--color-border-primary);
background: #2a2a2a;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.tab-btn {
@ -113,7 +113,7 @@
padding: 12px 8px;
background: none;
border: none;
color: var(--color-text-secondary);
color: #cccccc;
font-size: 12px;
cursor: pointer;
transition: all 0.2s ease;
@ -121,14 +121,14 @@
}
.tab-btn:hover {
background: var(--color-white-rgb-05);
color: var(--color-text-primary);
background: rgba(255, 255, 255, 0.05);
color: #ffffff;
}
.tab-btn.active {
color: var(--color-primary);
border-bottom-color: var(--color-primary);
background: var(--color-bg-primary);
color: #2a5caa;
border-bottom-color: #2a5caa;
background: #1e1e1e;
}
.tab-btn i {
@ -155,16 +155,16 @@
display: flex;
align-items: center;
justify-content: space-between;
color: var(--color-text-primary);
color: #ffffff;
font-size: 14px;
font-weight: 600;
margin: 0 0 15px 0;
padding-bottom: 8px;
border-bottom: 1px solid var(--color-border-secondary);
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
}
.section-title i {
color: var(--color-primary);
color: #2a5caa;
margin-right: 8px;
}
@ -175,12 +175,12 @@
justify-content: center;
gap: 10px;
padding: 40px 20px;
color: var(--color-text-secondary);
color: #cccccc;
font-size: 14px;
}
.loading-state i {
color: var(--color-primary);
color: #2a5caa;
font-size: 16px;
}
@ -196,15 +196,15 @@
flex-direction: column;
gap: 12px;
padding: 16px;
background: var(--color-white-rgb-05);
border: 1px solid var(--color-border-secondary);
background: #2a2a2a;
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 8px;
transition: all 0.2s ease;
}
.software-item-backend:hover {
border-color: var(--color-border-primary);
background: var(--color-white-rgb-1);
border-color: rgba(255, 255, 255, 0.3);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
}
.software-info {
@ -220,7 +220,7 @@
display: flex;
align-items: center;
justify-content: center;
background: var(--color-primary-gradient);
background: linear-gradient(135deg, #2a5caa 0%, #3d7bd8 100%);
border-radius: 6px;
color: white;
font-size: 14px;
@ -232,19 +232,19 @@
}
.software-name-backend {
color: var(--color-text-primary);
color: #ffffff;
font-weight: 500;
font-size: 14px;
margin-bottom: 2px;
}
.software-status {
color: var(--color-text-secondary);
color: #cccccc;
font-size: 12px;
}
.software-status.running {
color: var(--color-text-success);
color: #4caf50;
}
.software-actions {
@ -271,41 +271,63 @@
}
.action-btn.start {
background: var(--color-success-gradient);
background: linear-gradient(135deg, #4caf50 0%, #66bb6a 100%);
color: white;
}
.action-btn.start:hover:not(:disabled) {
background: var(--color-success-gradient-hover);
background: linear-gradient(135deg, #66bb6a 0%, #81c784 100%);
transform: translateY(-1px);
box-shadow: 0 4px 8px var(--color-success-rgb-3);
box-shadow: 0 4px 8px rgba(76, 175, 80, 0.3);
}
.action-btn.stop {
background: var(--color-error-gradient);
background: linear-gradient(135deg, #f44336 0%, #ef5350 100%);
color: white;
}
.action-btn.stop:hover:not(:disabled) {
background: var(--color-error-gradient-hover);
background: linear-gradient(135deg, #ef5350 0%, #e57373 100%);
transform: translateY(-1px);
box-shadow: 0 4px 8px var(--color-error-rgb-3);
box-shadow: 0 4px 8px rgba(244, 67, 54, 0.3);
}
.action-btn.restart {
background: var(--color-warning-gradient);
background: linear-gradient(135deg, #ff9800 0%, #ffb74d 100%);
color: white;
}
.action-btn.restart:hover:not(:disabled) {
background: var(--color-warning-gradient-hover);
background: linear-gradient(135deg, #ffb74d 0%, #ffcc02 100%);
transform: translateY(-1px);
box-shadow: 0 4px 8px var(--color-warning-rgb-3);
box-shadow: 0 4px 8px rgba(255, 152, 0, 0.3);
}
.action-btn.refresh {
background: linear-gradient(135deg, #2196f3 0%, #42a5f5 100%);
color: white;
}
.action-btn.refresh:hover:not(:disabled) {
background: linear-gradient(135deg, #42a5f5 0%, #64b5f6 100%);
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(33, 150, 243, 0.3);
}
.action-btn.clear {
background: linear-gradient(135deg, #f44336 0%, #ef5350 100%);
color: white;
}
.action-btn.clear:hover:not(:disabled) {
background: linear-gradient(135deg, #ef5350 0%, #e57373 100%);
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(244, 67, 54, 0.3);
}
.action-btn:disabled {
background: var(--color-white-rgb-1);
color: var(--color-text-disabled);
background: #1a1a1a;
color: #999999;
cursor: not-allowed;
opacity: 0.6;
box-shadow: none;
@ -315,7 +337,7 @@
.refresh-btn {
background: none;
border: none;
color: var(--color-text-secondary);
color: #cccccc;
cursor: pointer;
padding: 4px;
border-radius: 4px;
@ -323,8 +345,8 @@
}
.refresh-btn:hover {
color: var(--color-primary);
background: var(--color-primary-rgb-1);
color: #2a5caa;
background: rgba(42, 92, 170, 0.1);
}
/* 日志面板样式 */
@ -348,10 +370,10 @@
.filter-select, .filter-input {
padding: 6px 10px;
border: 1px solid var(--color-border-secondary);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 4px;
background: var(--color-white-rgb-05);
color: var(--color-text-primary);
background: #2a2a2a;
color: #ffffff;
font-size: 12px;
}
@ -365,20 +387,20 @@
.filter-select:focus, .filter-input:focus {
outline: none;
border-color: var(--color-primary);
border-color: #2a5caa;
}
.logs-list {
max-height: 300px;
overflow-y: auto;
border: 1px solid var(--color-border-secondary);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 6px;
background: var(--color-white-rgb-05);
background: #2a2a2a;
}
.log-item {
padding: 10px 15px;
border-bottom: 1px solid var(--color-border-secondary);
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
transition: background 0.2s ease;
}
@ -387,7 +409,7 @@
}
.log-item:hover {
background: var(--color-white-rgb-1);
background: #1a1a1a;
}
.log-header {
@ -398,18 +420,18 @@
}
.log-operation {
color: var(--color-text-primary);
color: #ffffff;
font-weight: 500;
font-size: 13px;
}
.log-time {
color: var(--color-text-secondary);
color: #cccccc;
font-size: 11px;
}
.log-details {
color: var(--color-text-secondary);
color: #cccccc;
font-size: 12px;
line-height: 1.4;
}
@ -420,11 +442,11 @@
gap: 10px;
margin-top: 4px;
font-size: 11px;
color: var(--color-text-tertiary);
color: #888888;
}
.log-user {
color: var(--color-primary);
color: #2a5caa;
}
.log-status {
@ -435,13 +457,13 @@
}
.log-status.success {
background: var(--color-success-rgb-1);
color: var(--color-text-success);
background: rgba(76, 175, 80, 0.1);
color: #4caf50;
}
.log-status.failed {
background: var(--color-error-rgb-1);
color: var(--color-text-error);
background: rgba(244, 67, 54, 0.1);
color: #f44336;
}
.logs-pagination {
@ -451,34 +473,34 @@
gap: 15px;
margin-top: 15px;
padding-top: 15px;
border-top: 1px solid var(--color-border-secondary);
border-top: 1px solid rgba(255, 255, 255, 0.2);
}
.page-btn {
padding: 6px 12px;
border: 1px solid var(--color-border-secondary);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 4px;
background: var(--color-white-rgb-05);
color: var(--color-text-secondary);
background: #2a2a2a;
color: #cccccc;
cursor: pointer;
font-size: 12px;
transition: all 0.2s ease;
}
.page-btn:hover {
border-color: var(--color-primary);
color: var(--color-primary);
border-color: #2a5caa;
color: #2a5caa;
}
.page-btn:disabled {
background: var(--color-white-rgb-1);
color: var(--color-text-disabled);
background: #1a1a1a;
color: #999999;
cursor: not-allowed;
border-color: var(--color-border-secondary);
border-color: rgba(255, 255, 255, 0.2);
}
.page-info {
color: var(--color-text-secondary);
color: #cccccc;
font-size: 12px;
font-weight: 500;
}
@ -493,8 +515,8 @@
.stat-card {
padding: 15px;
background: var(--color-white-rgb-05);
border: 1px solid var(--color-border-secondary);
background: #2a2a2a;
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 8px;
text-align: center;
}
@ -503,23 +525,23 @@
display: block;
font-size: 24px;
font-weight: bold;
color: var(--color-primary);
color: #2a5caa;
margin-bottom: 5px;
}
.stat-label {
color: var(--color-text-secondary);
color: #cccccc;
font-size: 12px;
font-weight: 500;
}
.operation-types {
padding-top: 20px;
border-top: 1px solid var(--color-border-secondary);
border-top: 1px solid rgba(255, 255, 255, 0.2);
}
.operation-types h5 {
color: var(--color-text-primary);
color: #ffffff;
font-size: 13px;
font-weight: 600;
margin: 0 0 10px 0;
@ -533,10 +555,10 @@
.type-tag {
padding: 4px 8px;
background: var(--color-white-rgb-05);
border: 1px solid var(--color-border-secondary);
background: #2a2a2a;
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 4px;
color: var(--color-text-secondary);
color: #cccccc;
font-size: 11px;
font-weight: 500;
}

View File

@ -59,25 +59,25 @@
>
<div class="software-info">
<div class="software-icon-backend">
<i :class="software.icon"></i>
<i class="fas fa-desktop"></i>
</div>
<div class="software-details">
<div class="software-name-backend">{{ software.name }}</div>
<div class="software-status" :class="{ running: software.isRunning }">
{{ software.isRunning ? '运行中' : '已停止' }}
<div class="software-name-backend">{{ software.name || software.id }}</div>
<div class="software-status" :class="{ running: isRunning(software.status) }">
{{ getStatusText(software.status) }}
</div>
</div>
</div>
<div class="software-actions">
<button
v-if="!software.isRunning"
v-if="!isRunning(software.status)"
class="action-btn start"
@click="startSoftware(software.id)"
>
<i class="fas fa-play"></i>启动
</button>
<button
v-if="software.isRunning"
v-if="isRunning(software.status)"
class="action-btn stop"
@click="stopSoftware(software.id)"
>
@ -86,7 +86,7 @@
<button
class="action-btn restart"
@click="restartSoftware(software.id)"
:disabled="!software.isRunning"
:disabled="!isRunning(software.status)"
>
<i class="fas fa-redo"></i>重启
</button>
@ -104,10 +104,10 @@
<i class="fas fa-history"></i>操作日志
</h4>
<div class="logs-actions">
<button class="action-btn" @click="refreshLogs">
<button class="action-btn refresh" @click="refreshLogs">
<i class="fas fa-sync-alt"></i>刷新
</button>
<button class="action-btn" @click="clearLogs">
<button class="action-btn clear" @click="clearLogs">
<i class="fas fa-trash"></i>清理
</button>
</div>
@ -137,10 +137,10 @@
<div class="log-operation">{{ log.operation }}</div>
<div class="log-time">{{ formatTime(log.timestamp) }}</div>
</div>
<div class="log-details">{{ log.details }}</div>
<div class="log-details" v-if="log.details">{{ log.details }}</div>
<div class="log-meta">
<span class="log-user">{{ log.user }}</span>
<span class="log-status" :class="log.status">{{ log.status }}</span>
<span class="log-user" v-if="log.user_id">{{ log.user_id }}</span>
<span class="log-status" :class="log.status">{{ getLogStatusText(log.status) }}</span>
</div>
</div>
</div>
@ -165,20 +165,20 @@
<div class="stats-grid">
<div class="stat-card">
<span class="stat-value">{{ stats.totalOperations }}</span>
<span class="stat-label">操作次</span>
<span class="stat-value">{{ stats.totalOperations || 0 }}</span>
<span class="stat-label">日志</span>
</div>
<div class="stat-card">
<span class="stat-value">{{ stats.successfulOperations }}</span>
<span class="stat-label">成功操作</span>
<span class="stat-value">{{ stats.successfulOperations || 0 }}</span>
<span class="stat-label">用户操作</span>
</div>
<div class="stat-card">
<span class="stat-value">{{ stats.failedOperations }}</span>
<span class="stat-label">失败操作</span>
<span class="stat-value">{{ stats.failedOperations || 0 }}</span>
<span class="stat-label">错误日志</span>
</div>
<div class="stat-card">
<span class="stat-value">{{ stats.activeConnections }}</span>
<span class="stat-label">活动连接</span>
<span class="stat-value">{{ stats.activeConnections || 0 }}</span>
<span class="stat-label">系统操作</span>
</div>
</div>
@ -201,7 +201,8 @@
</template>
<script setup>
import { ref, computed } from 'vue'
import { ref, computed, onMounted, onUnmounted } from 'vue'
import websocketService from '@/services/websocketService'
// Props
defineProps({
@ -217,48 +218,17 @@ defineEmits(['close'])
//
const activeTab = ref('software')
//
const backendConnected = ref(true)
//
const softwareList = ref([
{
id: 'creo',
name: 'Creo',
icon: 'fas fa-cogs',
isRunning: true
},
{
id: 'revit',
name: 'Revit',
icon: 'fas fa-building',
isRunning: false
},
{
id: 'autocad',
name: 'AutoCAD',
icon: 'fas fa-drafting-compass',
isRunning: false
},
{
id: 'solidworks',
name: 'SolidWorks',
icon: 'fas fa-hammer',
isRunning: true
},
{
id: 'catia',
name: 'CATIA',
icon: 'fas fa-cube',
isRunning: false
},
{
id: 'pdms',
name: 'PDMS',
icon: 'fas fa-industry',
isRunning: false
}
])
// WebSocket
const backendConnected = ref(false)
const softwareList = ref([])
const logs = ref([])
const stats = ref({
totalOperations: 0,
successfulOperations: 0,
failedOperations: 0,
activeConnections: 0
})
const operationTypes = ref([])
//
const logTypeFilter = ref('')
@ -266,64 +236,6 @@ const operationFilter = ref('')
const currentPage = ref(1)
const logsPerPage = 10
//
const logs = ref([
{
id: 1,
operation: '启动Creo软件',
details: 'Creo软件已成功启动版本9.0.0.0',
timestamp: new Date(Date.now() - 5000),
user: '管理员',
status: 'success'
},
{
id: 2,
operation: '打开模型文件',
details: '成功打开模型文件assembly.asm',
timestamp: new Date(Date.now() - 15000),
user: '用户1',
status: 'success'
},
{
id: 3,
operation: '连接测试',
details: 'AutoCAD连接测试失败无法建立连接',
timestamp: new Date(Date.now() - 30000),
user: '系统',
status: 'failed'
},
{
id: 4,
operation: '模型分析',
details: '完成层级结构分析发现127个组件',
timestamp: new Date(Date.now() - 60000),
user: '用户2',
status: 'success'
},
{
id: 5,
operation: '导出文件',
details: '成功导出STEP格式文件',
timestamp: new Date(Date.now() - 120000),
user: '用户1',
status: 'success'
}
])
//
const stats = ref({
totalOperations: 156,
successfulOperations: 142,
failedOperations: 14,
activeConnections: 2
})
//
const operationTypes = ref([
'软件启动', '文件打开', '模型分析', '格式转换',
'数据导出', '连接测试', '系统配置', '用户管理'
])
//
const filteredLogs = computed(() => {
let filtered = logs.value
@ -350,65 +262,185 @@ const totalPages = computed(() => {
return Math.ceil(logs.value.length / logsPerPage)
})
// WebSocket
let listeners = []
const initListeners = () => {
//
const onStateChange = (state) => {
backendConnected.value = state.newState === 'CONNECTED'
}
websocketService.on('onStateChange', onStateChange)
listeners.push(['onStateChange', onStateChange])
//
const onSoftwareUpdate = (list) => {
softwareList.value = list
}
websocketService.on('onSoftwareUpdate', onSoftwareUpdate)
listeners.push(['onSoftwareUpdate', onSoftwareUpdate])
//
const onLogUpdate = (logData) => {
if (Array.isArray(logData)) {
logs.value = logData
} else if (logData.logs) {
logs.value = logData.logs
}
}
websocketService.on('onLogUpdate', onLogUpdate)
listeners.push(['onLogUpdate', onLogUpdate])
//
const onStatsUpdate = (newStats) => {
stats.value = {
totalOperations: newStats.total_logs || 0,
successfulOperations: newStats.user_operations || 0,
failedOperations: newStats.error_logs || 0,
activeConnections: newStats.active_connections || 0
}
}
websocketService.on('onStatsUpdate', onStatsUpdate)
listeners.push(['onStatsUpdate', onStatsUpdate])
//
const onOperationTypesUpdate = (types) => {
operationTypes.value = types
}
websocketService.on('onOperationTypesUpdate', onOperationTypesUpdate)
listeners.push(['onOperationTypesUpdate', onOperationTypesUpdate])
}
const removeListeners = () => {
listeners.forEach(([event, callback]) => {
websocketService.off(event, callback)
})
listeners = []
}
//
onMounted(() => {
initListeners()
//
backendConnected.value = websocketService.connectionState === 'CONNECTED'
softwareList.value = websocketService.softwareList
stats.value = websocketService.logStats || stats.value
operationTypes.value = websocketService.operationTypes
})
// WebSocket
onUnmounted(() => {
removeListeners()
})
//
const switchTab = (tab) => {
activeTab.value = tab
}
const refreshSoftwareList = () => {
//
websocketService.getSoftwareList()
}
const startSoftware = (id) => {
const software = softwareList.value.find(s => s.id === id)
if (software) {
software.isRunning = true
}
websocketService.startSoftware(id)
}
const stopSoftware = (id) => {
const software = softwareList.value.find(s => s.id === id)
if (software) {
software.isRunning = false
}
websocketService.stopSoftware(id)
}
const restartSoftware = (id) => {
const software = softwareList.value.find(s => s.id === id)
if (software) {
//
}
websocketService.restartSoftware(id)
}
const refreshLogs = () => {
//
const filters = {}
if (logTypeFilter.value) filters.log_type = logTypeFilter.value
if (operationFilter.value) filters.operation = operationFilter.value
filters.limit = logsPerPage
filters.offset = (currentPage.value - 1) * logsPerPage
websocketService.queryLogs(filters)
}
const clearLogs = () => {
logs.value = []
websocketService.cleanupLogs()
}
const refreshStats = () => {
//
websocketService.getLogStats()
websocketService.getOperationTypes()
}
//
const isRunning = (status) => {
return ['running', 'started', 'active', 'online', 'idle', 'ready', 'connected'].includes(status)
}
const getStatusText = (status) => {
const statusMap = {
'running': '运行中',
'stopped': '已停止',
'starting': '启动中...',
'stopping': '停止中...',
'restarting': '重启中...',
'started': '运行中',
'active': '运行中',
'online': '运行中',
'idle': '运行中',
'ready': '运行中',
'connected': '运行中',
'disconnected': '已停止',
'offline': '已停止',
'inactive': '已停止',
'terminated': '已停止',
'error': '错误',
'failed': '启动失败',
'unknown': '检测中...'
}
return statusMap[status] || '检测中...'
}
const getLogStatusText = (status) => {
const statusMap = {
'success': '成功',
'failed': '失败',
'pending': '进行中'
}
return statusMap[status] || '成功'
}
const formatTime = (timestamp) => {
return timestamp.toLocaleTimeString('zh-CN', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
})
if (!timestamp) return ''
try {
const date = new Date(timestamp)
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
})
} catch (error) {
return timestamp
}
}
const prevPage = () => {
if (currentPage.value > 1) {
currentPage.value--
refreshLogs()
}
}
const nextPage = () => {
if (currentPage.value < totalPages.value) {
currentPage.value++
refreshLogs()
}
}
</script>

View File

@ -6,6 +6,17 @@ const API_CONFIG = {
BASE_URL: import.meta.env.MODE === 'production' ? 'https://api.miany.com' : 'http://localhost',
TIMEOUT: 30000, // 30秒超时
// WebSocket配置
WEBSOCKET: {
BASE_URL: 'ws://localhost:8000',
ENDPOINT: '/api/v1/ws/connect',
CLIENT_ID: 'web_client',
USER_ID: 'admin',
RECONNECT_ATTEMPTS: 5,
RECONNECT_DELAY: 1000, // 1秒
HEARTBEAT_INTERVAL: 30000 // 30秒
},
// 通用API端点
COMMON_ENDPOINTS: {
health: '/health',
@ -240,6 +251,11 @@ export const getGeometryOptimizationDefaults = () => {
return API_CONFIG.GEOMETRY_OPTIMIZATION
}
// 获取WebSocket配置
export const getWebSocketConfig = () => {
return API_CONFIG.WEBSOCKET
}
// HTTP请求头配置
export const getRequestHeaders = () => {
return {

View File

@ -6,6 +6,7 @@ import 'element-plus/dist/index.css'
import App from './App.vue'
import router from './router'
import { useAuthStore } from './stores/auth'
import websocketService from './services/websocketService'
const app = createApp(App)
@ -17,4 +18,7 @@ app.use(ElementPlus)
const authStore = useAuthStore()
authStore.initAuth()
// 启动WebSocket连接
websocketService.connect()
app.mount('#app')

View File

@ -0,0 +1,392 @@
// WebSocket服务类 - 统一管理后台WebSocket连接
// 基于参考项目接口格式实现
import { getWebSocketConfig } from '@/config/cad'
class WebSocketService {
constructor() {
this.config = getWebSocketConfig()
this.ws = null
this.connectionState = 'DISCONNECTED' // CONNECTING, CONNECTED, DISCONNECTED, ERROR
this.reconnectAttempts = 0
this.heartbeatTimer = null
// 状态数据
this.softwareList = []
this.logs = []
this.logStats = null
this.operationTypes = []
this.connectedUsers = []
// 事件监听器
this.listeners = {
onOpen: [],
onMessage: [],
onClose: [],
onError: [],
onStateChange: [],
onSoftwareUpdate: [],
onLogUpdate: [],
onStatsUpdate: [],
onOperationTypesUpdate: []
}
console.log('WebSocketService 初始化完成')
}
// 获取WebSocket连接URL
getConnectionUrl() {
const { BASE_URL, ENDPOINT, CLIENT_ID, USER_ID } = this.config
return `${BASE_URL}${ENDPOINT}?client_id=${CLIENT_ID}&user_id=${USER_ID}`
}
// 连接WebSocket
connect() {
if (this.connectionState === 'CONNECTING' || this.connectionState === 'CONNECTED') {
return
}
const url = this.getConnectionUrl()
console.log('开始连接统一管理后台:', url)
this.setState('CONNECTING')
this.ws = new WebSocket(url)
this.ws.onopen = (event) => {
console.log('统一管理后台连接成功')
this.setState('CONNECTED')
this.reconnectAttempts = 0
this.startHeartbeat()
this.emit('onOpen', event)
// 连接成功后立即获取初始数据
setTimeout(() => {
this.getSoftwareList()
this.getLogStats()
this.getOperationTypes()
}, 500)
}
this.ws.onmessage = (event) => {
try {
const message = JSON.parse(event.data)
console.log('收到管理后台消息:', message)
this.handleMessage(message)
this.emit('onMessage', message)
} catch (error) {
console.error('解析管理后台消息失败:', error, event.data)
}
}
this.ws.onclose = (event) => {
console.log('统一管理后台连接关闭:', event)
this.setState('DISCONNECTED')
this.stopHeartbeat()
this.emit('onClose', event)
// 自动重连
if (!event.wasClean && this.reconnectAttempts < this.config.RECONNECT_ATTEMPTS) {
const delay = this.config.RECONNECT_DELAY * Math.pow(2, this.reconnectAttempts)
console.log(`统一管理后台将在 ${delay}ms 后重连,第 ${this.reconnectAttempts + 1} 次尝试`)
setTimeout(() => {
this.reconnectAttempts++
this.connect()
}, delay)
}
}
this.ws.onerror = (event) => {
console.error('统一管理后台连接错误:', event)
this.setState('ERROR')
this.emit('onError', event)
}
}
// 断开连接
disconnect() {
console.log('主动断开统一管理后台连接')
if (this.ws) {
this.ws.close(1000, 'Client disconnect')
this.ws = null
}
this.stopHeartbeat()
this.setState('DISCONNECTED')
}
// 发送消息
send(message) {
if (this.connectionState === 'CONNECTED' && this.ws) {
const jsonMessage = JSON.stringify(message)
console.log('发送管理后台消息:', message)
this.ws.send(jsonMessage)
return true
} else {
console.warn('统一管理后台未连接,消息发送失败:', message)
return false
}
}
// 处理收到的消息
handleMessage(message) {
switch (message.type) {
case 'info':
this.handleInfoMessage(message)
break
case 'heartbeat':
console.log('收到心跳响应')
break
case 'software_list_update':
this.handleSoftwareListUpdate(message)
break
case 'log_recorded':
console.log('操作日志已记录:', message.data)
break
case 'error':
console.error('管理后台错误:', message.message)
break
default:
console.log('未处理的消息类型:', message.type, message)
}
}
// 处理info类型消息
handleInfoMessage(message) {
if (message.data) {
if (message.data.software_list) {
this.softwareList = message.data.software_list
this.emit('onSoftwareUpdate', this.softwareList)
}
if (message.data.logs) {
this.logs = message.data.logs
this.emit('onLogUpdate', this.logs)
}
if (message.data.stats) {
this.logStats = message.data.stats
this.emit('onStatsUpdate', this.logStats)
}
if (message.data.operations) {
this.operationTypes = message.data.operations
this.emit('onOperationTypesUpdate', this.operationTypes)
}
if (message.data.connected_users) {
this.connectedUsers = message.data.connected_users
}
}
}
// 处理软件列表更新
handleSoftwareListUpdate(message) {
if (message.data && message.data.software_list) {
this.softwareList = message.data.software_list
console.log('软件列表已更新:', this.softwareList)
this.emit('onSoftwareUpdate', this.softwareList)
}
}
// API方法 - 心跳检测
ping() {
return this.send({ type: 'ping' })
}
// API方法 - 获取服务状态
getStatus() {
return this.send({ type: 'get_status' })
}
// API方法 - 获取软件列表
getSoftwareList() {
return this.send({ type: 'get_software_list' })
}
// API方法 - 启动软件
startSoftware(softwareId) {
if (!softwareId || typeof softwareId !== 'string' || softwareId.length === 0) {
console.error('启动软件失败: 无效的软件ID', softwareId)
return false
}
console.log('启动软件:', softwareId)
return this.send({
type: 'start_software',
software_id: softwareId
})
}
// API方法 - 停止软件
stopSoftware(softwareId) {
if (!softwareId || typeof softwareId !== 'string' || softwareId.length === 0) {
console.error('停止软件失败: 无效的软件ID', softwareId)
return false
}
console.log('停止软件:', softwareId)
return this.send({
type: 'stop_software',
software_id: softwareId
})
}
// API方法 - 重启软件
restartSoftware(softwareId) {
if (!softwareId || typeof softwareId !== 'string' || softwareId.length === 0) {
console.error('重启软件失败: 无效的软件ID', softwareId)
return false
}
console.log('重启软件:', softwareId)
return this.send({
type: 'restart_software',
software_id: softwareId
})
}
// API方法 - 记录操作日志
logOperation(operation, details = '', options = {}) {
if (!operation || typeof operation !== 'string' || operation.length === 0) {
console.error('记录日志失败: 操作名称不能为空')
return false
}
const logData = {
type: 'log_operation',
operation: operation.substring(0, 100), // 限制长度
details: details ? details.substring(0, 1000) : '', // 限制长度
action_type: options.action_type || 'execute',
target_object: options.target_object || '',
status: options.status || 'success',
duration: options.duration || 0,
operation_category: options.operation_category || '软件控制'
}
console.log('记录操作日志:', logData)
return this.send(logData)
}
// API方法 - 查询操作日志
queryLogs(filters = {}) {
const queryData = {
type: 'query_logs',
limit: filters.limit || 100,
offset: filters.offset || 0
}
// 添加可选的筛选条件
if (filters.log_type) queryData.log_type = filters.log_type
if (filters.operation) queryData.operation = filters.operation
if (filters.user_id_filter) queryData.user_id_filter = filters.user_id_filter
if (filters.level) queryData.level = filters.level
if (filters.start_time) queryData.start_time = filters.start_time
if (filters.end_time) queryData.end_time = filters.end_time
console.log('查询操作日志:', queryData)
return this.send(queryData)
}
// API方法 - 根据ID获取日志
getLogById(logId) {
if (!logId || typeof logId !== 'string' || logId.length === 0) {
console.error('获取日志失败: 无效的日志ID', logId)
return false
}
return this.send({
type: 'get_log_by_id',
log_id: logId
})
}
// API方法 - 获取日志统计信息
getLogStats() {
console.log('获取日志统计信息')
return this.send({ type: 'get_log_stats' })
}
// API方法 - 清理过期日志
cleanupLogs() {
console.log('清理过期日志')
return this.send({ type: 'cleanup_logs' })
}
// API方法 - 获取操作类型列表
getOperationTypes() {
console.log('获取操作类型列表')
return this.send({ type: 'get_operation_types' })
}
// 心跳检测
startHeartbeat() {
this.heartbeatTimer = setInterval(() => {
if (this.connectionState === 'CONNECTED') {
this.ping()
}
}, this.config.HEARTBEAT_INTERVAL)
}
stopHeartbeat() {
if (this.heartbeatTimer) {
clearInterval(this.heartbeatTimer)
this.heartbeatTimer = null
}
}
// 状态管理
setState(newState) {
if (this.connectionState !== newState) {
const oldState = this.connectionState
this.connectionState = newState
console.log(`统一管理后台状态变化: ${oldState} -> ${newState}`)
this.emit('onStateChange', { oldState, newState })
}
}
// 事件系统
on(event, callback) {
if (this.listeners[event]) {
this.listeners[event].push(callback)
} else {
console.warn('未知事件类型:', event)
}
}
off(event, callback) {
if (this.listeners[event]) {
const index = this.listeners[event].indexOf(callback)
if (index > -1) {
this.listeners[event].splice(index, 1)
}
}
}
emit(event, data) {
if (this.listeners[event]) {
this.listeners[event].forEach(callback => {
try {
callback(data)
} catch (error) {
console.error('事件回调执行失败:', event, error)
}
})
}
}
// 获取当前状态
getState() {
return {
connectionState: this.connectionState,
softwareList: this.softwareList,
logs: this.logs,
logStats: this.logStats,
operationTypes: this.operationTypes,
connectedUsers: this.connectedUsers
}
}
}
// 导出单例实例
export const websocketService = new WebSocketService()
// 默认导出
export default websocketService