| | |
| | | <template> |
| | | <div |
| | | class="admin-page" |
| | | v-loading.fullscreen.lock="startActionLoading" |
| | | element-loading-text="正在启动实例,请稍候..." |
| | | > |
| | |
| | | <p class="text-muted">管理和监控 S7 PLC 模拟器实例</p> |
| | | </div> |
| | | <div class="header-right"> |
| | | <div class="stats"> |
| | | <span>运行中: {{ runningCount }} | 已停止: {{ stoppedCount }}</span> |
| | | <span v-if="errorCount > 0" class="error-text">| 错误: {{ errorCount }}</span> |
| | | </div> |
| | | <el-button type="primary" @click="$router.push('/create')"> |
| | | <el-button type="primary" class="create-btn" @click="$router.push('/create')"> |
| | | <el-icon><Plus /></el-icon> |
| | | 创建实例 |
| | | </el-button> |
| | | </div> |
| | | </div> |
| | | |
| | | <section class="section-block"> |
| | | <div class="section-head"> |
| | | <div> |
| | | <h3 class="section-title">信息区</h3> |
| | | <p class="section-desc">实例运行状态总览</p> |
| | | </div> |
| | | </div> |
| | | <div class="section-body"> |
| | | <el-row :gutter="12" class="summary-row"> |
| | | <el-col :xs="24" :sm="8"> |
| | | <el-card shadow="hover" class="summary-card running-card"> |
| | | <div class="summary-title">运行中</div> |
| | | <div class="summary-value">{{ runningCount }}</div> |
| | | </el-card> |
| | | </el-col> |
| | | <el-col :xs="24" :sm="8"> |
| | | <el-card shadow="hover" class="summary-card stopped-card"> |
| | | <div class="summary-title">已停止</div> |
| | | <div class="summary-value">{{ stoppedCount }}</div> |
| | | </el-card> |
| | | </el-col> |
| | | <el-col :xs="24" :sm="8"> |
| | | <el-card shadow="hover" class="summary-card error-card"> |
| | | <div class="summary-title">错误实例</div> |
| | | <div class="summary-value">{{ errorCount }}</div> |
| | | </el-card> |
| | | </el-col> |
| | | </el-row> |
| | | </div> |
| | | </section> |
| | | |
| | | <!-- Loading state --> |
| | | <div v-if="loading && instances.length === 0" class="loading-container"> |
| | |
| | | </el-empty> |
| | | |
| | | <!-- Instances grid --> |
| | | <el-row v-else :gutter="20"> |
| | | <el-col v-for="instance in instances" :key="instance.instanceId" :xs="24" :sm="12" :xl="8"> |
| | | <el-card class="instance-card" :class="getStatusClass(instance.status)" shadow="hover"> |
| | | <template #header> |
| | | <div class="card-header"> |
| | | <span class="instance-id">{{ instance.instanceId }}</span> |
| | | <el-tag :type="getStatusTagType(instance.status)"> |
| | | {{ getStatusText(instance.status) }} |
| | | </el-tag> |
| | | <section v-else class="section-block"> |
| | | <div class="section-head"> |
| | | <div> |
| | | <h3 class="section-title">操作区</h3> |
| | | <p class="section-desc">实例启动、停止、编辑与详情入口</p> |
| | | </div> |
| | | </div> |
| | | <div class="section-body"> |
| | | <el-row :gutter="14" class="instances-grid"> |
| | | <el-col |
| | | v-for="instance in instances" |
| | | :key="instance.instanceId" |
| | | :xs="24" |
| | | :sm="12" |
| | | :md="12" |
| | | :lg="8" |
| | | :xl="6" |
| | | > |
| | | <el-card class="instance-card panel-card" :class="getStatusClass(instance.status)" shadow="hover"> |
| | | <div class="card-glow"></div> |
| | | |
| | | <div class="card-top"> |
| | | <div class="card-title"> |
| | | <div class="instance-id-line"> |
| | | <span class="instance-id">{{ instance.instanceId }}</span> |
| | | <span class="instance-name">{{ instance.name || '未命名实例' }}</span> |
| | | <span class="instance-sub">PLC</span> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | |
| | | <div class="instance-info"> |
| | | <el-descriptions :column="2" size="small"> |
| | | <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 label="客户端"> |
| | | <el-icon><User /></el-icon> |
| | | {{ instance.clientCount || 0 }} |
| | | </el-descriptions-item> |
| | | <el-descriptions-item v-if="instance.startTime" label="启动时间" :span="2"> |
| | | {{ formatDate(instance.startTime) }} |
| | | </el-descriptions-item> |
| | | </el-descriptions> |
| | | |
| | | <el-alert |
| | | v-if="instance.errorMessage" |
| | | type="error" |
| | | :closable="false" |
| | | class="mt-2" |
| | | show-icon |
| | | > |
| | | {{ instance.errorMessage }} |
| | | </el-alert> |
| | | <el-tag :type="getStatusTagType(instance.status)" effect="dark" round> |
| | | {{ getStatusText(instance.status) }} |
| | | </el-tag> |
| | | </div> |
| | | |
| | | <template #footer> |
| | | <div class="card-footer"> |
| | | <div class="meta-row"> |
| | | <div class="meta-chip"> |
| | | <span class="chip-label">PLC</span> |
| | | <span class="chip-value">{{ getPlcTypeText(instance.plcType) }}</span> |
| | | </div> |
| | | <div class="meta-chip"> |
| | | <span class="chip-label">端口</span> |
| | | <span class="chip-value">{{ instance.port || '-' }}</span> |
| | | </div> |
| | | <div class="meta-chip"> |
| | | <span class="chip-label">客户端</span> |
| | | <span class="chip-value"> |
| | | <el-icon><User /></el-icon> |
| | | {{ instance.clientCount || 0 }} |
| | | </span> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="time-row"> |
| | | <span class="time-label">启动时间</span> |
| | | <span class="time-value">{{ instance.startTime ? formatDate(instance.startTime) : '-' }}</span> |
| | | </div> |
| | | |
| | | <el-alert |
| | | v-if="instance.errorMessage" |
| | | type="error" |
| | | :closable="false" |
| | | class="mt-2" |
| | | show-icon |
| | | > |
| | | {{ instance.errorMessage }} |
| | | </el-alert> |
| | | |
| | | <div class="card-footer"> |
| | | <div class="main-actions"> |
| | | <el-button |
| | | v-if="instance.status === 'Running'" |
| | | type="warning" |
| | |
| | | <el-icon><Edit /></el-icon> |
| | | 编辑 |
| | | </el-button> |
| | | <el-button type="danger" @click="handleDelete(instance.instanceId)"> |
| | | <el-icon><Delete /></el-icon> |
| | | </el-button> |
| | | </div> |
| | | </template> |
| | | </el-card> |
| | | </el-col> |
| | | </el-row> |
| | | <el-button class="delete-btn" type="danger" @click="handleDelete(instance.instanceId)"> |
| | | <el-icon><Delete /></el-icon> |
| | | </el-button> |
| | | </div> |
| | | </el-card> |
| | | </el-col> |
| | | </el-row> |
| | | </div> |
| | | </section> |
| | | </div> |
| | | </template> |
| | | |
| | |
| | | </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; |
| | | } |
| | | |
| | | .header-right { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 16px; |
| | | gap: 10px; |
| | | flex-wrap: wrap; |
| | | } |
| | | |
| | | .stats { |
| | | color: #606266; |
| | | font-size: 14px; |
| | | .create-btn { |
| | | padding-inline: 18px; |
| | | } |
| | | |
| | | .error-text { |
| | | color: #f56c6c; |
| | | .summary-row { |
| | | margin-top: -2px; |
| | | margin-bottom: 2px; |
| | | } |
| | | |
| | | .loading-container { |
| | | text-align: center; |
| | | padding: 60px 0; |
| | | color: #909399; |
| | | .instances-grid { |
| | | max-width: 1500px; |
| | | } |
| | | |
| | | .loading-icon { |
| | | animation: spin 1s linear infinite; |
| | | .summary-card { |
| | | border-radius: 12px; |
| | | border: 1px solid #dbe2ea; |
| | | overflow: hidden; |
| | | } |
| | | |
| | | @keyframes spin { |
| | | from { |
| | | transform: rotate(0deg); |
| | | } |
| | | to { |
| | | transform: rotate(360deg); |
| | | } |
| | | .summary-title { |
| | | color: #64748b; |
| | | font-size: 13px; |
| | | margin-bottom: 6px; |
| | | } |
| | | |
| | | .summary-value { |
| | | font-size: 28px; |
| | | font-weight: 700; |
| | | line-height: 1; |
| | | } |
| | | |
| | | .running-card { |
| | | background: linear-gradient(180deg, #f0fdf4 0%, #ffffff 100%); |
| | | } |
| | | |
| | | .running-card .summary-value { |
| | | color: #15803d; |
| | | } |
| | | |
| | | .stopped-card { |
| | | background: linear-gradient(180deg, #f8fafc 0%, #ffffff 100%); |
| | | } |
| | | |
| | | .stopped-card .summary-value { |
| | | color: #334155; |
| | | } |
| | | |
| | | .error-card { |
| | | background: linear-gradient(180deg, #fef2f2 0%, #ffffff 100%); |
| | | } |
| | | |
| | | .error-card .summary-value { |
| | | color: #b91c1c; |
| | | } |
| | | |
| | | .instance-card { |
| | | margin-bottom: 20px; |
| | | transition: all 0.3s; |
| | | margin-bottom: 10px; |
| | | position: relative; |
| | | border-radius: 12px; |
| | | border: 1px solid #dbe2ea; |
| | | transition: all 0.25s; |
| | | overflow: hidden; |
| | | } |
| | | |
| | | .instance-card:hover { |
| | | transform: translateY(-4px); |
| | | transform: translateY(-3px); |
| | | box-shadow: 0 14px 26px rgba(15, 23, 42, 0.1); |
| | | } |
| | | |
| | | .card-header { |
| | | .card-glow { |
| | | position: absolute; |
| | | top: -56px; |
| | | right: -56px; |
| | | width: 100px; |
| | | height: 100px; |
| | | border-radius: 50%; |
| | | background: radial-gradient(circle, rgba(14, 116, 244, 0.16), transparent 70%); |
| | | pointer-events: none; |
| | | } |
| | | |
| | | .card-title { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | flex-direction: column; |
| | | gap: 1px; |
| | | min-width: 0; |
| | | } |
| | | |
| | | .instance-id { |
| | | font-weight: 600; |
| | | font-size: 16px; |
| | | font-size: 15px; |
| | | color: #0f172a; |
| | | white-space: nowrap; |
| | | } |
| | | |
| | | .instance-info { |
| | | margin-bottom: 16px; |
| | | .instance-name { |
| | | color: #64748b; |
| | | font-size: 12px; |
| | | overflow: hidden; |
| | | text-overflow: ellipsis; |
| | | white-space: nowrap; |
| | | } |
| | | |
| | | .card-footer { |
| | | .instance-id-line { |
| | | display: flex; |
| | | align-items: baseline; |
| | | gap: 6px; |
| | | min-width: 0; |
| | | } |
| | | |
| | | .instance-sub { |
| | | color: #94a3b8; |
| | | font-size: 10px; |
| | | } |
| | | |
| | | .card-top { |
| | | margin-top: 2px; |
| | | display: flex; |
| | | justify-content: space-between; |
| | | gap: 8px; |
| | | align-items: flex-start; |
| | | } |
| | | |
| | | .meta-row { |
| | | margin-top: 8px; |
| | | display: flex; |
| | | gap: 6px; |
| | | flex-wrap: wrap; |
| | | } |
| | | |
| | | .card-footer .el-button { |
| | | flex: 1; |
| | | min-width: 60px; |
| | | .meta-chip { |
| | | border: 1px solid #e2e8f0; |
| | | border-radius: 999px; |
| | | padding: 4px 8px; |
| | | background: #f8fafc; |
| | | display: inline-flex; |
| | | align-items: center; |
| | | gap: 6px; |
| | | } |
| | | |
| | | .status-stopped { border-left: 4px solid #909399; } |
| | | .status-starting { border-left: 4px solid #409eff; } |
| | | .status-running { border-left: 4px solid #67c23a; } |
| | | .status-stopping { border-left: 4px solid #e6a23c; } |
| | | .status-error { border-left: 4px solid #f56c6c; } |
| | | .chip-label { |
| | | font-size: 10px; |
| | | line-height: 1; |
| | | color: #64748b; |
| | | padding: 1px 5px; |
| | | border-radius: 999px; |
| | | background: #e2e8f0; |
| | | } |
| | | |
| | | .chip-value { |
| | | font-size: 12px; |
| | | line-height: 1; |
| | | color: #0f172a; |
| | | display: inline-flex; |
| | | align-items: center; |
| | | gap: 4px; |
| | | } |
| | | |
| | | .time-row { |
| | | margin-top: 7px; |
| | | margin-bottom: 8px; |
| | | display: flex; |
| | | justify-content: space-between; |
| | | gap: 8px; |
| | | font-size: 11px; |
| | | } |
| | | |
| | | .time-label { |
| | | color: #64748b; |
| | | } |
| | | |
| | | .time-value { |
| | | color: #0f172a; |
| | | } |
| | | |
| | | .card-footer { |
| | | border-top: 1px dashed #dbe2ea; |
| | | padding-top: 8px; |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | gap: 10px; |
| | | } |
| | | |
| | | .main-actions { |
| | | display: flex; |
| | | gap: 6px; |
| | | flex-wrap: wrap; |
| | | } |
| | | |
| | | .main-actions .el-button { |
| | | border-radius: 8px; |
| | | height: 28px; |
| | | padding: 0 10px; |
| | | } |
| | | |
| | | .delete-btn { |
| | | border-radius: 8px; |
| | | height: 28px; |
| | | padding: 0 10px; |
| | | } |
| | | |
| | | .mt-2 { |
| | | margin-top: 8px; |
| | | } |
| | | </style> |
| | | |
| | | |