<template>
|
<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">
|
<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="page-header">
|
<div class="header-left">
|
<h2>
|
<el-icon :size="24"><InfoFilled /></el-icon>
|
实例详情
|
</h2>
|
<p class="text-muted">{{ instance.name }} ({{ instance.instanceId }})</p>
|
</div>
|
<el-button @click="$router.push('/')">
|
<el-icon><Back /></el-icon>
|
返回列表
|
</el-button>
|
</div>
|
|
<!-- 状态卡片 -->
|
<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) }}
|
</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>
|
|
<!-- 详细信息 -->
|
<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>
|
|
<!-- 操作按钮 -->
|
<el-card class="mt-4" shadow="never">
|
<div class="action-buttons">
|
<el-button
|
v-if="instance.status === 'Stopped' || instance.status === 'Error'"
|
type="success"
|
@click="handleStart"
|
>
|
<el-icon><VideoPlay /></el-icon>
|
启动
|
</el-button>
|
<el-button
|
v-if="instance.status === 'Running'"
|
type="warning"
|
@click="handleStop"
|
>
|
<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>
|
</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'
|
|
const route = useRoute()
|
|
const instance = ref<InstanceState | null>(null)
|
const loading = ref(true)
|
const errorMsg = ref('')
|
|
let refreshTimer: number | null = null
|
|
const id = route.params.id as string
|
|
async function loadInstance() {
|
try {
|
instance.value = await api.getInstance(id)
|
if (!instance.value) {
|
errorMsg.value = `实例 "${id}" 不存在`
|
}
|
} catch (err) {
|
console.error('加载实例失败:', err)
|
errorMsg.value = '加载实例失败,请查看控制台'
|
} finally {
|
loading.value = false
|
}
|
}
|
|
onMounted(() => {
|
loadInstance()
|
// 每2秒刷新一次状态
|
refreshTimer = window.setInterval(() => {
|
if (instance.value && instance.value.status !== 'Stopped') {
|
loadInstance()
|
}
|
}, 2000)
|
})
|
|
onUnmounted(() => {
|
if (refreshTimer !== null) {
|
clearInterval(refreshTimer)
|
}
|
})
|
|
async function handleStart() {
|
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)
|
ElMessage.error('启动失败,请查看控制台')
|
}
|
}
|
}
|
|
async function handleStop() {
|
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)
|
ElMessage.error('停止失败,请查看控制台')
|
}
|
}
|
}
|
|
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] || 'info'
|
}
|
|
function getStatusText(status: InstanceStatus): string {
|
const map: Record<InstanceStatus, string> = {
|
'Stopped': '已停止',
|
'Starting': '启动中',
|
'Running': '运行中',
|
'Stopping': '停止中',
|
'Error': '错误'
|
}
|
return map[status] || status
|
}
|
|
function getPlcTypeText(plcType: string): string {
|
const map: Record<string, string> = {
|
'S7200Smart': 'S7-200 Smart',
|
'S71200': 'S7-1200',
|
'S71500': 'S7-1500',
|
'S7300': 'S7-300',
|
'S7400': 'S7-400'
|
}
|
return map[plcType] || plcType
|
}
|
|
function formatDate(dateString: string | null): string {
|
if (!dateString) return '-'
|
const date = new Date(dateString)
|
return date.toLocaleString('zh-CN', {
|
year: 'numeric',
|
month: '2-digit',
|
day: '2-digit',
|
hour: '2-digit',
|
minute: '2-digit',
|
second: '2-digit'
|
})
|
}
|
</script>
|
|
<style scoped>
|
.page-header {
|
display: flex;
|
justify-content: space-between;
|
align-items: flex-start;
|
margin-bottom: 20px;
|
flex-wrap: wrap;
|
gap: 16px;
|
}
|
|
.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>
|