- 在操作日志页面添加导出按钮 - 实现exportLogs方法,导出所有过滤后的日志 - 导出文件包含导出时间、筛选条件和完整日志数据 - 文件名使用时间戳格式:logs_export_YYYY-MM-DD.json 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
473 lines
14 KiB
Vue
473 lines
14 KiB
Vue
<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> |