From c493779a8504fe1eb548c865ff268a7f7436ec01 Mon Sep 17 00:00:00 2001
From: wanshenmean <cathay_xy@163.com>
Date: 星期四, 19 三月 2026 11:43:36 +0800
Subject: [PATCH] feat: 集成机械手客户端并重构模拟器前端工作台
---
Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Web/src/views/HomeView.vue | 566 ++++++++++++++++++++++++++++++++++++++++++--------------
1 files changed, 420 insertions(+), 146 deletions(-)
diff --git a/Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Web/src/views/HomeView.vue b/Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Web/src/views/HomeView.vue
index 9a6cbf9..06cb505 100644
--- a/Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Web/src/views/HomeView.vue
+++ b/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="姝e湪鍚姩瀹炰緥锛岃绋嶅��..."
+ >
+ <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">姝e湪鍔犺浇瀹炰緥鍒楄〃...</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>姝e湪鍔犺浇瀹炰緥鍒楄〃...</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>
+
--
Gitblit v1.9.3