wanshenmean
2026-03-19 c493779a8504fe1eb548c865ff268a7f7436ec01
Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Web/src/views/HomeView.vue
@@ -1,123 +1,194 @@
<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 仿真器实例
<template>
  <div
    class="admin-page"
    v-loading.fullscreen.lock="startActionLoading"
    element-loading-text="正在启动实例,请稍候..."
  >
    <div class="page-header">
      <div class="header-left">
        <h2>
          <el-icon :size="24"><Cpu /></el-icon>
          S7 PLC 模拟器实例
        </h2>
        <p class="text-muted mb-0 mt-1">管理和监控 S7 PLC 仿真器实例</p>
        <p class="text-muted">管理和监控 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 class="header-right">
        <el-button type="primary" class="create-btn" @click="$router.push('/create')">
          <el-icon><Plus /></el-icon>
          创建实例
        </el-button>
      </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>
    <section class="section-block">
      <div class="section-head">
        <div>
          <h3 class="section-title">信息区</h3>
          <p class="section-desc">实例运行状态总览</p>
        </div>
      </div>
      <p class="mt-3 text-muted">正在加载实例列表...</p>
      <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-icon class="loading-icon" :size="40"><Loading /></el-icon>
      <p>正在加载实例列表...</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>
    <el-empty v-else-if="instances.length === 0" description="暂无实例">
      <el-button type="primary" @click="$router.push('/create')">创建第一个实例</el-button>
    </el-empty>
    <!-- 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>
    <section v-else class="section-block">
      <div class="section-head">
        <div>
          <h3 class="section-title">操作区</h3>
          <p class="section-desc">实例启动、停止、编辑与详情入口</p>
        </div>
      </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>
            <el-tag :type="getStatusTagType(instance.status)" effect="dark" round>
              {{ getStatusText(instance.status) }}
            </el-tag>
          </div>
          <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"
                @click="handleStop(instance.instanceId)"
              >
                <el-icon><VideoPause /></el-icon>
                停止
              </el-button>
              <el-button
                v-if="instance.status === 'Stopped'"
                type="success"
                @click="handleStart(instance.instanceId)"
              >
                <el-icon><VideoPlay /></el-icon>
                启动
              </el-button>
              <el-button type="info" @click="$router.push(`/details/${instance.instanceId}`)">
                <el-icon><InfoFilled /></el-icon>
                详情
              </el-button>
              <el-button type="primary" @click="$router.push(`/edit/${instance.instanceId}`)">
                <el-icon><Edit /></el-icon>
                编辑
              </el-button>
            </div>
            <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 setup lang="ts">
import { onMounted, onUnmounted } from 'vue'
import { onMounted, onUnmounted, ref } from 'vue'
import { storeToRefs } from 'pinia'
import { useInstancesStore } from '../stores/instances'
import { ElMessage, ElMessageBox } from 'element-plus'
import {
  Cpu,
  Plus,
  Loading,
  User,
  VideoPause,
  VideoPlay,
  InfoFilled,
  Edit,
  Delete
} from '@element-plus/icons-vue'
const store = useInstancesStore()
const { instances, loading, runningCount, stoppedCount, errorCount } = storeToRefs(store)
const startActionLoading = ref(false)
onMounted(() => {
  store.loadInstances()
@@ -128,28 +199,52 @@
  store.stopAutoRefresh()
})
function handleStart(id: string) {
  if (confirm(`确定要启动实例 "${id}" 吗?`)) {
    store.startInstance(id).catch(() => {
      alert('启动失败,请查看控制台')
async function handleStart(id: string) {
  try {
    await ElMessageBox.confirm(`确定要启动实例 "${id}" 吗?`, '确认', {
      confirmButtonText: '确定',
      cancelButtonText: '取消',
      type: 'info'
    })
    startActionLoading.value = true
    await store.startInstance(id)
    ElMessage.success('启动命令已发送')
  } catch (err) {
    if (err !== 'cancel') {
      ElMessage.error('启动失败,请查看控制台')
    }
  } finally {
    startActionLoading.value = false
  }
}
function handleStop(id: string) {
  if (confirm(`确定要停止实例 "${id}" 吗?`)) {
    store.stopInstance(id).catch(() => {
      alert('停止失败,请查看控制台')
  ElMessageBox.confirm(`确定要停止实例 "${id}" 吗?`, '确认', {
    confirmButtonText: '确定',
    cancelButtonText: '取消',
    type: 'warning'
  }).then(() => {
    store.stopInstance(id).then(() => {
      ElMessage.success('停止命令已发送')
    }).catch(() => {
      ElMessage.error('停止失败,请查看控制台')
    })
  }
  })
}
function handleDelete(id: string) {
  if (confirm(`确定要删除实例 "${id}" 吗?此操作不可撤销!`)) {
    store.deleteInstance(id).catch(() => {
      alert('删除失败,请查看控制台')
  ElMessageBox.confirm(`确定要删除实例 "${id}" 吗?此操作不可撤销!`, '警告', {
    confirmButtonText: '确定',
    cancelButtonText: '取消',
    type: 'error'
  }).then(() => {
    store.deleteInstance(id).then(() => {
      ElMessage.success('实例已删除')
    }).catch(() => {
      ElMessage.error('删除失败,请查看控制台')
    })
  }
  })
}
function getStatusClass(status: string): string {
@@ -161,6 +256,17 @@
    'Error': 'status-error'
  }
  return map[status] || ''
}
function getStatusTagType(status: string): 'success' | 'info' | 'warning' | 'danger' {
  const map: Record<string, 'success' | 'info' | 'warning' | 'danger'> = {
    'Stopped': 'info',
    'Starting': 'info',
    'Running': 'success',
    'Stopping': 'warning',
    'Error': 'danger'
  }
  return map[status] || 'info'
}
function getStatusText(status: string): string {
@@ -200,52 +306,220 @@
</script>
<style scoped>
.instance-card {
  transition: transform 0.2s, box-shadow 0.2s;
.header-left h2 {
  margin: 0;
}
.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 {
.header-right {
  display: flex;
  align-items: center;
  gap: 10px;
  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;
.create-btn {
  padding-inline: 18px;
}
.empty-state i {
  font-size: 4rem;
  margin-bottom: 1rem;
.summary-row {
  margin-top: -2px;
  margin-bottom: 2px;
}
.empty-state h3 {
  margin-bottom: 0.5rem;
.instances-grid {
  max-width: 1500px;
}
.alert-sm {
  font-size: 0.875rem;
.summary-card {
  border-radius: 12px;
  border: 1px solid #dbe2ea;
  overflow: hidden;
}
.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: 10px;
  position: relative;
  border-radius: 12px;
  border: 1px solid #dbe2ea;
  transition: all 0.25s;
  overflow: hidden;
}
.instance-card:hover {
  transform: translateY(-3px);
  box-shadow: 0 14px 26px rgba(15, 23, 42, 0.1);
}
.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;
  flex-direction: column;
  gap: 1px;
  min-width: 0;
}
.instance-id {
  font-weight: 600;
  font-size: 15px;
  color: #0f172a;
  white-space: nowrap;
}
.instance-name {
  color: #64748b;
  font-size: 12px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.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;
}
.meta-chip {
  border: 1px solid #e2e8f0;
  border-radius: 999px;
  padding: 4px 8px;
  background: #f8fafc;
  display: inline-flex;
  align-items: center;
  gap: 6px;
}
.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>