| | |
| | | <template> |
| | | <div> |
| | | <div v-if="loading" class="text-center py-5"> |
| | | <div class="spinner-border text-primary" role="status"> |
| | | <span class="visually-hidden">加载中...</span> |
| | | </div> |
| | | <div v-if="loading" class="loading-container"> |
| | | <el-icon class="loading-icon" :size="40"><Loading /></el-icon> |
| | | <p>加载中...</p> |
| | | </div> |
| | | |
| | | <div v-else-if="errorMsg"> |
| | | <div class="alert alert-danger">{{ errorMsg }}</div> |
| | | <router-link to="/" class="btn btn-primary">返回列表</router-link> |
| | | <el-result icon="error" :title="errorMsg"> |
| | | <template #extra> |
| | | <el-button type="primary" @click="$router.push('/')">返回列表</el-button> |
| | | </template> |
| | | </el-result> |
| | | </div> |
| | | |
| | | <div v-else-if="instance"> |
| | | <div class="d-flex justify-content-between align-items-center mb-4"> |
| | | <div> |
| | | <h2 class="mb-0"> |
| | | <i class="bi bi-info-circle me-2"></i>实例详情 |
| | | <div class="page-header"> |
| | | <div class="header-left"> |
| | | <h2> |
| | | <el-icon :size="24"><InfoFilled /></el-icon> |
| | | 实例详情 |
| | | </h2> |
| | | <p class="text-muted mb-0 mt-1">{{ instance.name }} ({{ instance.instanceId }})</p> |
| | | <p class="text-muted">{{ instance.name }} ({{ instance.instanceId }})</p> |
| | | </div> |
| | | <router-link to="/" class="btn btn-outline-secondary"> |
| | | <i class="bi bi-arrow-left me-1"></i>返回列表 |
| | | </router-link> |
| | | <el-button @click="$router.push('/')"> |
| | | <el-icon><Back /></el-icon> |
| | | 返回列表 |
| | | </el-button> |
| | | </div> |
| | | |
| | | <!-- 状态卡片 --> |
| | | <div class="row mb-4"> |
| | | <div class="col-md-3"> |
| | | <div class="card text-center"> |
| | | <div class="card-body"> |
| | | <h6 class="card-subtitle mb-2 text-muted">状态</h6> |
| | | <h4 :class="['mb-0', getStatusClass(instance.status)]"> |
| | | <el-row :gutter="20" class="status-cards"> |
| | | <el-col :xs="12" :sm="6"> |
| | | <el-card shadow="hover" class="status-card"> |
| | | <el-statistic title="状态"> |
| | | <template #default> |
| | | <el-tag :type="getStatusTagType(instance.status)" size="large"> |
| | | {{ getStatusText(instance.status) }} |
| | | </h4> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <div class="col-md-3"> |
| | | <div class="card text-center"> |
| | | <div class="card-body"> |
| | | <h6 class="card-subtitle mb-2 text-muted">连接客户端</h6> |
| | | <h4 class="mb-0"> |
| | | <i class="bi bi-people-fill me-1"></i>{{ instance.clientCount }} |
| | | </h4> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <div class="col-md-3"> |
| | | <div class="card text-center"> |
| | | <div class="card-body"> |
| | | <h6 class="card-subtitle mb-2 text-muted">总请求数</h6> |
| | | <h4 class="mb-0">{{ instance.totalRequests }}</h4> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <div class="col-md-3"> |
| | | <div class="card text-center"> |
| | | <div class="card-body"> |
| | | <h6 class="card-subtitle mb-2 text-muted">端口</h6> |
| | | <h4 class="mb-0">{{ instance.port }}</h4> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </el-tag> |
| | | </template> |
| | | </el-statistic> |
| | | </el-card> |
| | | </el-col> |
| | | <el-col :xs="12" :sm="6"> |
| | | <el-card shadow="hover" class="status-card"> |
| | | <el-statistic title="连接客户端" :value="instance.clientCount"> |
| | | <template #suffix> |
| | | <el-icon><User /></el-icon> |
| | | </template> |
| | | </el-statistic> |
| | | </el-card> |
| | | </el-col> |
| | | <el-col :xs="12" :sm="6"> |
| | | <el-card shadow="hover" class="status-card"> |
| | | <el-statistic title="总请求数" :value="instance.totalRequests" /> |
| | | </el-card> |
| | | </el-col> |
| | | <el-col :xs="12" :sm="6"> |
| | | <el-card shadow="hover" class="status-card"> |
| | | <el-statistic title="端口" :value="instance.port" /> |
| | | </el-card> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <!-- 详细信息 --> |
| | | <div class="card mb-4"> |
| | | <div class="card-header"> |
| | | <h5 class="mb-0">基本信息</h5> |
| | | </div> |
| | | <div class="card-body"> |
| | | <table class="table table-bordered"> |
| | | <tbody> |
| | | <tr> |
| | | <th style="width: 30%">实例ID</th> |
| | | <td>{{ instance.instanceId }}</td> |
| | | </tr> |
| | | <tr> |
| | | <th>实例名称</th> |
| | | <td>{{ instance.name }}</td> |
| | | </tr> |
| | | <tr> |
| | | <th>PLC型号</th> |
| | | <td>{{ getPlcTypeText(instance.plcType) }}</td> |
| | | </tr> |
| | | <tr> |
| | | <th>监听端口</th> |
| | | <td>{{ instance.port }}</td> |
| | | </tr> |
| | | <tr v-if="instance.startTime"> |
| | | <th>启动时间</th> |
| | | <td>{{ formatDate(instance.startTime) }}</td> |
| | | </tr> |
| | | <tr v-if="instance.lastActivityTime"> |
| | | <th>最后活动时间</th> |
| | | <td>{{ formatDate(instance.lastActivityTime) }}</td> |
| | | </tr> |
| | | <tr v-if="instance.errorMessage"> |
| | | <th>错误信息</th> |
| | | <td class="text-danger">{{ instance.errorMessage }}</td> |
| | | </tr> |
| | | </tbody> |
| | | </table> |
| | | </div> |
| | | </div> |
| | | <el-card class="mt-4" shadow="never"> |
| | | <template #header> |
| | | <span class="card-header-title">基本信息</span> |
| | | </template> |
| | | <el-descriptions :column="2" border> |
| | | <el-descriptions-item label="实例ID">{{ instance.instanceId }}</el-descriptions-item> |
| | | <el-descriptions-item label="实例名称">{{ instance.name }}</el-descriptions-item> |
| | | <el-descriptions-item label="PLC型号">{{ getPlcTypeText(instance.plcType) }}</el-descriptions-item> |
| | | <el-descriptions-item label="监听端口">{{ instance.port }}</el-descriptions-item> |
| | | <el-descriptions-item v-if="instance.startTime" label="启动时间"> |
| | | {{ formatDate(instance.startTime) }} |
| | | </el-descriptions-item> |
| | | <el-descriptions-item v-if="instance.lastActivityTime" label="最后活动时间"> |
| | | {{ formatDate(instance.lastActivityTime) }} |
| | | </el-descriptions-item> |
| | | <el-descriptions-item v-if="instance.errorMessage" label="错误信息" :span="2"> |
| | | <el-text type="danger">{{ instance.errorMessage }}</el-text> |
| | | </el-descriptions-item> |
| | | </el-descriptions> |
| | | </el-card> |
| | | |
| | | <!-- 操作按钮 --> |
| | | <div class="card"> |
| | | <div class="card-body"> |
| | | <div class="d-flex gap-2"> |
| | | <button |
| | | <el-card class="mt-4" shadow="never"> |
| | | <div class="action-buttons"> |
| | | <el-button |
| | | v-if="instance.status === 'Stopped' || instance.status === 'Error'" |
| | | class="btn btn-success" |
| | | type="success" |
| | | @click="handleStart" |
| | | > |
| | | <i class="bi bi-play-fill me-1"></i>启动 |
| | | </button> |
| | | <button |
| | | <el-icon><VideoPlay /></el-icon> |
| | | 启动 |
| | | </el-button> |
| | | <el-button |
| | | v-if="instance.status === 'Running'" |
| | | class="btn btn-warning" |
| | | type="warning" |
| | | @click="handleStop" |
| | | > |
| | | <i class="bi bi-stop-fill me-1"></i>停止 |
| | | </button> |
| | | <router-link :to="`/edit/${instance.instanceId}`" class="btn btn-primary"> |
| | | <i class="bi bi-pencil-fill me-1"></i>编辑 |
| | | </router-link> |
| | | <router-link to="/" class="btn btn-outline-secondary"> |
| | | <i class="bi bi-arrow-left me-1"></i>返回列表 |
| | | </router-link> |
| | | <el-icon><VideoPause /></el-icon> |
| | | 停止 |
| | | </el-button> |
| | | <el-button type="primary" @click="$router.push(`/edit/${instance.instanceId}`)"> |
| | | <el-icon><Edit /></el-icon> |
| | | 编辑 |
| | | </el-button> |
| | | <el-button @click="$router.push('/')"> |
| | | <el-icon><Back /></el-icon> |
| | | 返回列表 |
| | | </el-button> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </el-card> |
| | | </div> |
| | | </div> |
| | | </template> |
| | |
| | | <script setup lang="ts"> |
| | | import { ref, onMounted, onUnmounted } from 'vue' |
| | | import { useRoute } from 'vue-router' |
| | | import { ElMessage, ElMessageBox } from 'element-plus' |
| | | import { |
| | | InfoFilled, |
| | | Back, |
| | | Loading, |
| | | User, |
| | | VideoPlay, |
| | | VideoPause, |
| | | Edit |
| | | } from '@element-plus/icons-vue' |
| | | import * as api from '../api' |
| | | import type { InstanceState, InstanceStatus } from '../types' |
| | | |
| | |
| | | }) |
| | | |
| | | async function handleStart() { |
| | | if (confirm(`确定要启动实例 "${id}" 吗?`)) { |
| | | try { |
| | | await ElMessageBox.confirm(`确定要启动实例 "${id}" 吗?`, '确认', { |
| | | confirmButtonText: '确定', |
| | | cancelButtonText: '取消', |
| | | type: 'info' |
| | | }) |
| | | await api.startInstance(id) |
| | | await loadInstance() |
| | | ElMessage.success('启动命令已发送') |
| | | } catch (err) { |
| | | if (err !== 'cancel') { |
| | | console.error('启动实例失败:', err) |
| | | alert('启动失败,请查看控制台') |
| | | ElMessage.error('启动失败,请查看控制台') |
| | | } |
| | | } |
| | | } |
| | | |
| | | async function handleStop() { |
| | | if (confirm(`确定要停止实例 "${id}" 吗?`)) { |
| | | try { |
| | | await ElMessageBox.confirm(`确定要停止实例 "${id}" 吗?`, '确认', { |
| | | confirmButtonText: '确定', |
| | | cancelButtonText: '取消', |
| | | type: 'warning' |
| | | }) |
| | | await api.stopInstance(id) |
| | | await loadInstance() |
| | | ElMessage.success('停止命令已发送') |
| | | } catch (err) { |
| | | if (err !== 'cancel') { |
| | | console.error('停止实例失败:', err) |
| | | alert('停止失败,请查看控制台') |
| | | ElMessage.error('停止失败,请查看控制台') |
| | | } |
| | | } |
| | | } |
| | | |
| | | function getStatusClass(status: InstanceStatus): string { |
| | | const map: Record<InstanceStatus, string> = { |
| | | 'Stopped': 'text-secondary', |
| | | 'Starting': 'text-info', |
| | | 'Running': 'text-success', |
| | | 'Stopping': 'text-warning', |
| | | 'Error': 'text-danger' |
| | | function getStatusTagType(status: InstanceStatus): 'success' | 'info' | 'warning' | 'danger' { |
| | | const map: Record<InstanceStatus, 'success' | 'info' | 'warning' | 'danger'> = { |
| | | 'Stopped': 'info', |
| | | 'Starting': 'info', |
| | | 'Running': 'success', |
| | | 'Stopping': 'warning', |
| | | 'Error': 'danger' |
| | | } |
| | | return map[status] || '' |
| | | return map[status] || 'info' |
| | | } |
| | | |
| | | function getStatusText(status: InstanceStatus): string { |
| | |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .card-subtitle { |
| | | font-size: 0.875rem; |
| | | font-weight: 600; |
| | | .page-header { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: flex-start; |
| | | margin-bottom: 20px; |
| | | flex-wrap: wrap; |
| | | gap: 16px; |
| | | } |
| | | |
| | | table th { |
| | | background-color: #f8f9fa; |
| | | .header-left h2 { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | margin: 0 0 8px 0; |
| | | } |
| | | |
| | | .text-muted { |
| | | color: #909399; |
| | | margin: 0; |
| | | } |
| | | |
| | | .loading-container { |
| | | text-align: center; |
| | | padding: 60px 0; |
| | | color: #909399; |
| | | } |
| | | |
| | | .loading-icon { |
| | | animation: spin 1s linear infinite; |
| | | } |
| | | |
| | | @keyframes spin { |
| | | from { |
| | | transform: rotate(0deg); |
| | | } |
| | | to { |
| | | transform: rotate(360deg); |
| | | } |
| | | } |
| | | |
| | | .status-cards { |
| | | margin-bottom: 20px; |
| | | } |
| | | |
| | | .status-card { |
| | | text-align: center; |
| | | } |
| | | |
| | | .card-header-title { |
| | | font-weight: 600; |
| | | font-size: 16px; |
| | | } |
| | | |
| | | .mt-4 { |
| | | margin-top: 16px; |
| | | } |
| | | |
| | | .action-buttons { |
| | | display: flex; |
| | | gap: 12px; |
| | | flex-wrap: wrap; |
| | | } |
| | | </style> |