<template>
|
<div>
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
<div>
|
<h2 class="mb-0">
|
<i class="bi bi-cpu-fill me-2"></i>S7 PLC 仿真器实例
|
</h2>
|
<p class="text-muted mb-0 mt-1">管理和监控 S7 PLC 仿真器实例</p>
|
</div>
|
<div class="d-flex align-items-center gap-3">
|
<div class="text-muted small">
|
运行中: {{ runningCount }} | 已停止: {{ stoppedCount }}
|
<span v-if="errorCount > 0" class="text-danger">| 错误: {{ errorCount }}</span>
|
</div>
|
<router-link to="/create" class="btn btn-primary">
|
<i class="bi bi-plus-lg me-1"></i>创建实例
|
</router-link>
|
</div>
|
</div>
|
|
<!-- Loading state -->
|
<div v-if="loading && instances.length === 0" class="text-center py-5">
|
<div class="spinner-border text-primary" role="status">
|
<span class="visually-hidden">加载中...</span>
|
</div>
|
<p class="mt-3 text-muted">正在加载实例列表...</p>
|
</div>
|
|
<!-- Empty state -->
|
<div v-else-if="instances.length === 0" class="empty-state">
|
<i class="bi bi-inbox"></i>
|
<h3>暂无实例</h3>
|
<p>点击上方"创建实例"按钮来创建您的第一个仿真器实例</p>
|
</div>
|
|
<!-- Instances grid -->
|
<div v-else class="row row-cols-1 row-cols-md-2 row-cols-xl-3 g-4">
|
<div v-for="instance in instances" :key="instance.instanceId" class="col">
|
<div :class="['card', 'instance-card', 'h-100', getStatusClass(instance.status)]">
|
<div class="card-header d-flex justify-content-between align-items-center">
|
<h5 class="card-title mb-0">{{ instance.instanceId }}</h5>
|
<span :class="['badge', getStatusClass(instance.status)]">
|
{{ getStatusText(instance.status) }}
|
</span>
|
</div>
|
<div class="card-body">
|
<div class="instance-info mb-3">
|
<div class="row mb-2">
|
<div class="col-6">
|
<small class="instance-info-label">名称</small>
|
<div class="instance-info-value">{{ instance.name || '-' }}</div>
|
</div>
|
<div class="col-6">
|
<small class="instance-info-label">PLC型号</small>
|
<div class="instance-info-value">{{ getPlcTypeText(instance.plcType) }}</div>
|
</div>
|
</div>
|
<div class="row mb-2">
|
<div class="col-6">
|
<small class="instance-info-label">端口</small>
|
<div class="instance-info-value">{{ instance.port || '-' }}</div>
|
</div>
|
<div class="col-6">
|
<small class="instance-info-label">客户端</small>
|
<div class="instance-info-value">
|
<i class="bi bi-people-fill me-1"></i>{{ instance.clientCount || 0 }}
|
</div>
|
</div>
|
</div>
|
<div v-if="instance.startTime" class="row">
|
<div class="col-12">
|
<small class="instance-info-label">启动时间</small>
|
<div class="instance-info-value small">{{ formatDate(instance.startTime) }}</div>
|
</div>
|
</div>
|
<div v-if="instance.errorMessage" class="alert alert-danger alert-sm mt-2 mb-0 py-2 small">
|
<i class="bi bi-exclamation-triangle-fill me-1"></i>{{ instance.errorMessage }}
|
</div>
|
</div>
|
</div>
|
<div class="card-footer bg-white">
|
<div class="action-buttons d-flex gap-2">
|
<button
|
v-if="instance.status === 'Running'"
|
class="btn btn-warning btn-sm flex-fill"
|
@click="handleStop(instance.instanceId)"
|
>
|
<i class="bi bi-stop-fill me-1"></i>停止
|
</button>
|
<button
|
v-if="instance.status === 'Stopped'"
|
class="btn btn-success btn-sm flex-fill"
|
@click="handleStart(instance.instanceId)"
|
>
|
<i class="bi bi-play-fill me-1"></i>启动
|
</button>
|
<router-link :to="`/details/${instance.instanceId}`" class="btn btn-info btn-sm text-white flex-fill">
|
<i class="bi bi-info-circle-fill me-1"></i>详情
|
</router-link>
|
<router-link :to="`/edit/${instance.instanceId}`" class="btn btn-primary btn-sm flex-fill">
|
<i class="bi bi-pencil-fill me-1"></i>编辑
|
</router-link>
|
<button class="btn btn-danger btn-sm" @click="handleDelete(instance.instanceId)">
|
<i class="bi bi-trash-fill"></i>
|
</button>
|
</div>
|
</div>
|
</div>
|
</div>
|
</div>
|
</div>
|
</template>
|
|
<script setup lang="ts">
|
import { onMounted, onUnmounted } from 'vue'
|
import { storeToRefs } from 'pinia'
|
import { useInstancesStore } from '../stores/instances'
|
|
const store = useInstancesStore()
|
const { instances, loading, runningCount, stoppedCount, errorCount } = storeToRefs(store)
|
|
onMounted(() => {
|
store.loadInstances()
|
store.startAutoRefresh(2000)
|
})
|
|
onUnmounted(() => {
|
store.stopAutoRefresh()
|
})
|
|
function handleStart(id: string) {
|
if (confirm(`确定要启动实例 "${id}" 吗?`)) {
|
store.startInstance(id).catch(() => {
|
alert('启动失败,请查看控制台')
|
})
|
}
|
}
|
|
function handleStop(id: string) {
|
if (confirm(`确定要停止实例 "${id}" 吗?`)) {
|
store.stopInstance(id).catch(() => {
|
alert('停止失败,请查看控制台')
|
})
|
}
|
}
|
|
function handleDelete(id: string) {
|
if (confirm(`确定要删除实例 "${id}" 吗?此操作不可撤销!`)) {
|
store.deleteInstance(id).catch(() => {
|
alert('删除失败,请查看控制台')
|
})
|
}
|
}
|
|
function getStatusClass(status: string): string {
|
const map: Record<string, string> = {
|
'Stopped': 'status-stopped',
|
'Starting': 'status-starting',
|
'Running': 'status-running',
|
'Stopping': 'status-stopping',
|
'Error': 'status-error'
|
}
|
return map[status] || ''
|
}
|
|
function getStatusText(status: string): string {
|
const map: Record<string, 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>
|
.instance-card {
|
transition: transform 0.2s, box-shadow 0.2s;
|
}
|
|
.instance-card:hover {
|
transform: translateY(-2px);
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
}
|
|
.instance-info-label {
|
color: #6c757d;
|
font-size: 0.75rem;
|
text-transform: uppercase;
|
letter-spacing: 0.5px;
|
}
|
|
.instance-info-value {
|
font-weight: 500;
|
}
|
|
.action-buttons {
|
flex-wrap: wrap;
|
}
|
|
.status-stopped { border-left: 4px solid #6c757d; }
|
.status-starting { border-left: 4px solid #0dcaf0; }
|
.status-running { border-left: 4px solid #198754; }
|
.status-stopping { border-left: 4px solid #ffc107; }
|
.status-error { border-left: 4px solid #dc3545; }
|
|
.empty-state {
|
text-align: center;
|
padding: 4rem 2rem;
|
color: #6c757d;
|
}
|
|
.empty-state i {
|
font-size: 4rem;
|
margin-bottom: 1rem;
|
}
|
|
.empty-state h3 {
|
margin-bottom: 0.5rem;
|
}
|
|
.alert-sm {
|
font-size: 0.875rem;
|
}
|
</style>
|