MianyVue/src/components/layout/InfoManagementPanel.vue
sladro ad1d4fe2a1 feat: 信息面板增加日志导出JSON功能
- 在操作日志页面添加导出按钮
- 实现exportLogs方法,导出所有过滤后的日志
- 导出文件包含导出时间、筛选条件和完整日志数据
- 文件名使用时间戳格式:logs_export_YYYY-MM-DD.json

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-21 08:58:31 +08:00

473 lines
14 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

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

<template>
<div class="management-panel" :class="{ active: isVisible }">
<div class="panel-header">
<div class="panel-title">
<i class="fas fa-info-circle"></i>
<span>信息面板</span>
</div>
<div class="panel-status">
<div class="status-indicator" :class="{ connected: backendConnected }">
<i class="fas fa-circle"></i>
<span>{{ backendConnected ? '已连接' : '未连接' }}</span>
</div>
<button class="panel-close" @click="$emit('close')">
<i class="fas fa-times"></i>
</button>
</div>
</div>
<div class="panel-content">
<!-- 选项卡导航 -->
<div class="panel-tabs">
<button
class="tab-btn"
:class="{ active: activeTab === 'software' }"
@click="switchTab('software')"
>
<i class="fas fa-desktop"></i>软件控制
</button>
<button
class="tab-btn"
:class="{ active: activeTab === 'logs' }"
@click="switchTab('logs')"
>
<i class="fas fa-list-alt"></i>操作日志
</button>
<button
class="tab-btn"
:class="{ active: activeTab === 'stats' }"
@click="switchTab('stats')"
>
<i class="fas fa-chart-bar"></i>统计信息
</button>
</div>
<!-- 软件控制面板 -->
<div class="tab-panel" :class="{ active: activeTab === 'software' }">
<div class="panel-section">
<h4 class="section-title">
<i class="fas fa-list"></i>软件列表
<button class="refresh-btn" @click="refreshSoftwareList">
<i class="fas fa-sync-alt"></i>
</button>
</h4>
<div class="software-list">
<div
v-for="software in softwareList"
:key="software.id"
class="software-item-backend"
>
<div class="software-info">
<div class="software-icon-backend">
<i class="fas fa-desktop"></i>
</div>
<div class="software-details">
<div class="software-name-backend">{{ software.name || software.id }}</div>
<div class="software-status" :class="{ running: software.is_running }">
{{ software.is_running ? '运行中' : '已停止' }}
</div>
</div>
</div>
<div class="software-actions">
<button
v-if="!software.is_running"
class="action-btn start"
@click="startSoftware(software.id)"
>
<i class="fas fa-play"></i>启动
</button>
<button
v-if="software.is_running"
class="action-btn stop"
@click="stopSoftware(software.id)"
>
<i class="fas fa-stop"></i>停止
</button>
<button
class="action-btn restart"
@click="restartSoftware(software.id)"
:disabled="!software.is_running"
>
<i class="fas fa-redo"></i>重启
</button>
</div>
</div>
</div>
</div>
</div>
<!-- 操作日志面板 -->
<div class="tab-panel" :class="{ active: activeTab === 'logs' }">
<div class="panel-section">
<div class="logs-header">
<h4 class="section-title">
<i class="fas fa-history"></i>操作日志
</h4>
<div class="logs-actions">
<button class="action-btn refresh" @click="refreshLogs">
<i class="fas fa-sync-alt"></i>刷新
</button>
<button class="action-btn export" @click="exportLogs">
<i class="fas fa-download"></i>导出
</button>
<button class="action-btn clear" @click="clearLogs">
<i class="fas fa-trash"></i>清理
</button>
</div>
</div>
<div class="logs-filters">
<select class="filter-select" v-model="logTypeFilter">
<option value="">所有类型</option>
<option value="system_operation">系统操作</option>
<option value="user_operation">用户操作</option>
</select>
<input
type="text"
class="filter-input"
v-model="operationFilter"
placeholder="搜索操作..."
>
</div>
<div class="logs-list">
<div
v-for="log in filteredLogs"
:key="log.id"
class="log-item"
>
<div class="log-header">
<div class="log-operation">
<i class="fas fa-cogs" v-if="log.operation_category === 'CAD操作'"></i>
<i class="fas fa-desktop" v-else-if="log.operation_category === '软件控制'"></i>
<i class="fas fa-gear" v-else></i>
{{ log.operation }}
</div>
<div class="log-time">{{ formatTime(log.timestamp) }}</div>
</div>
<div class="log-details" v-if="log.details">{{ log.details }}</div>
<div class="log-meta">
<span class="log-category" v-if="log.operation_category">{{ log.operation_category }}</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>
<span class="log-duration" v-if="log.duration">{{ log.duration }}ms</span>
</div>
</div>
</div>
<div class="logs-pagination">
<button class="page-btn" @click="prevPage" :disabled="currentPage <= 1">上一页</button>
<span class="page-info">{{ currentPage }} / {{ totalPages }}</span>
<button class="page-btn" @click="nextPage" :disabled="currentPage >= totalPages">下一页</button>
</div>
</div>
</div>
<!-- 统计信息面板 -->
<div class="tab-panel" :class="{ active: activeTab === 'stats' }">
<div class="panel-section">
<h4 class="section-title">
<i class="fas fa-chart-pie"></i>统计概览
<button class="refresh-btn" @click="refreshStats">
<i class="fas fa-sync-alt"></i>
</button>
</h4>
<div class="stats-grid">
<div class="stat-card">
<span class="stat-value">{{ stats.totalOperations || 0 }}</span>
<span class="stat-label">总日志数</span>
</div>
<div class="stat-card">
<span class="stat-value">{{ stats.successfulOperations || 0 }}</span>
<span class="stat-label">用户操作</span>
</div>
<div class="stat-card">
<span class="stat-value">{{ stats.failedOperations || 0 }}</span>
<span class="stat-label">错误日志</span>
</div>
<div class="stat-card">
<span class="stat-value">{{ stats.activeConnections || 0 }}</span>
<span class="stat-label">系统操作</span>
</div>
</div>
<div class="operation-types">
<h5>支持的操作类型</h5>
<div class="types-list">
<span
v-for="type in operationTypes"
:key="type"
class="type-tag"
>
{{ type }}
</span>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue'
import websocketService from '@/services/websocketService'
// Props
defineProps({
isVisible: {
type: Boolean,
default: false
}
})
// Emits
defineEmits(['close'])
// 当前活动选项卡
const activeTab = ref('software')
// 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('')
const operationFilter = ref('')
const currentPage = ref(1)
const logsPerPage = 10
// 过滤后的完整日志
const allFilteredLogs = computed(() => {
let filtered = logs.value
if (logTypeFilter.value) {
filtered = filtered.filter(log =>
log.operation.includes(logTypeFilter.value === 'system_operation' ? '系统' : '用户')
)
}
if (operationFilter.value) {
filtered = filtered.filter(log =>
log.operation.toLowerCase().includes(operationFilter.value.toLowerCase())
)
}
return filtered
})
// 当前页显示的日志
const filteredLogs = computed(() => {
const start = (currentPage.value - 1) * logsPerPage
const end = start + logsPerPage
return allFilteredLogs.value.slice(start, end)
})
// 总页数
const totalPages = computed(() => {
return Math.ceil(allFilteredLogs.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].sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp))
} else if (logData.logs) {
logs.value = [...logData.logs].sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp))
}
}
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) => {
websocketService.startSoftware(id)
}
const stopSoftware = (id) => {
websocketService.stopSoftware(id)
}
const restartSoftware = (id) => {
websocketService.restartSoftware(id)
}
const refreshLogs = () => {
websocketService.queryLogs()
}
const clearLogs = () => {
websocketService.cleanupLogs()
}
const refreshStats = () => {
websocketService.getLogStats()
websocketService.getOperationTypes()
}
const exportLogs = () => {
// 导出数据结构
const exportData = {
export_time: new Date().toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
}),
total_logs: allFilteredLogs.value.length,
filters: {
type: logTypeFilter.value || 'all',
operation: operationFilter.value || 'all'
},
logs: allFilteredLogs.value
}
// 生成JSON字符串
const jsonStr = JSON.stringify(exportData, null, 2)
// 创建Blob对象
const blob = new Blob([jsonStr], { type: 'application/json' })
// 创建下载链接
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
// 生成带时间戳的文件名
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').split('T')[0]
link.download = `logs_export_${timestamp}.json`
link.href = url
// 触发下载
link.click()
// 清理URL对象
URL.revokeObjectURL(url)
}
// 辅助方法
const getLogStatusText = (status) => {
const statusMap = {
'success': '成功',
'failed': '失败',
'pending': '进行中'
}
return statusMap[status] || '成功'
}
const formatTime = (timestamp) => {
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 {
return timestamp
}
}
const prevPage = () => {
if (currentPage.value > 1) {
currentPage.value--
}
}
const nextPage = () => {
if (currentPage.value < totalPages.value) {
currentPage.value++
}
}
</script>
<style scoped>
/* 导入面板样式 */
@import '@/assets/styles/info-panel.css';
</style>