From cde6ad77663a80d78d77568428a6287b53347716 Mon Sep 17 00:00:00 2001
From: wanshenmean <cathay_xy@163.com>
Date: 星期四, 19 三月 2026 17:19:55 +0800
Subject: [PATCH] feat: 新增API路由缓存预热并完善机器人消息日志

---
 Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Web/src/views/RobotClientsView.vue |  452 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 452 insertions(+), 0 deletions(-)

diff --git a/Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Web/src/views/RobotClientsView.vue b/Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Web/src/views/RobotClientsView.vue
new file mode 100644
index 0000000..693aa25
--- /dev/null
+++ b/Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Web/src/views/RobotClientsView.vue
@@ -0,0 +1,452 @@
+锘�<template>
+  <div class="admin-page robot-admin-page">
+    <div class="page-header">
+      <div>
+        <h2>鏈烘鎵嬪鎴风绠$悊鍙�</h2>
+        <p class="text-muted">澶氬疄渚嬭繛鎺ョ洰鏍囨湇鍔$銆佹秷鎭敹鍙戠洃鎺�</p>
+      </div>
+      <div class="toolbar-actions">
+        <el-button :loading="refreshing" @click="loadStatus">鍒锋柊</el-button>
+        <el-button type="danger" :loading="stoppingAll" @click="handleStopAll">鍋滄鍏ㄩ儴</el-button>
+      </div>
+    </div>
+
+    <el-row :gutter="12" class="stats-row">
+      <el-col :xs="24" :sm="8">
+        <el-card shadow="hover" class="stat-card">
+          <el-statistic title="杩愯瀹炰緥" :value="status?.runningServerCount ?? 0" />
+        </el-card>
+      </el-col>
+      <el-col :xs="24" :sm="8">
+        <el-card shadow="hover" class="stat-card">
+          <el-statistic title="瀹炰緥鎬绘暟" :value="status?.servers.length ?? 0" />
+        </el-card>
+      </el-col>
+      <el-col :xs="24" :sm="8">
+        <el-card shadow="hover" class="stat-card">
+          <el-statistic title="鍦ㄧ嚎杩炴帴鎬绘暟" :value="totalConnectedCount" />
+        </el-card>
+      </el-col>
+    </el-row>
+
+    <el-card shadow="never" class="create-card">
+      <template #header>
+        <span>鏂板瀹㈡埛绔疄渚�</span>
+      </template>
+      <el-form :inline="true" :model="startForm" class="create-form">
+        <el-form-item label="瀹炰緥ID">
+          <el-input v-model="startForm.serverId" placeholder="robot-client-1" style="width: 180px" />
+        </el-form-item>
+        <el-form-item label="鏈嶅姟绔湴鍧�">
+          <el-input v-model="startForm.listenIp" placeholder="127.0.0.1" style="width: 160px" />
+        </el-form-item>
+        <el-form-item label="鏈嶅姟绔鍙�">
+          <el-input-number v-model="startForm.listenPort" :min="1" :max="65535" />
+        </el-form-item>
+        <el-form-item label="鏈湴绔彛">
+          <el-input-number v-model="startForm.localPort" :min="1" :max="65535" />
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" :loading="starting" @click="handleStart">杩炴帴瀹炰緥</el-button>
+        </el-form-item>
+      </el-form>
+    </el-card>
+
+    <el-row :gutter="12" class="main-row">
+      <el-col :xs="24" :lg="8">
+        <el-card shadow="never" class="panel-card">
+          <template #header>
+            <span>瀹㈡埛绔疄渚嬪垪琛�</span>
+          </template>
+          <el-table
+            :data="status?.servers || []"
+            border
+            size="small"
+            highlight-current-row
+            :row-class-name="serverRowClassName"
+            @row-click="onServerRowClick"
+          >
+            <el-table-column prop="serverId" label="瀹炰緥ID" min-width="120" />
+            <el-table-column label="鐩爣鏈嶅姟绔�" min-width="130">
+              <template #default="{ row }">{{ row.listenIp }}:{{ row.listenPort }}</template>
+            </el-table-column>
+            <el-table-column prop="localPort" label="鏈湴绔彛" width="100" />
+            <el-table-column prop="connectedCount" label="杩炴帴" width="70" />
+            <el-table-column label="鎿嶄綔" width="90">
+              <template #default="{ row }">
+                <el-button link type="danger" @click.stop="handleStopOne(row.serverId)">鍋滄</el-button>
+              </template>
+            </el-table-column>
+          </el-table>
+        </el-card>
+      </el-col>
+
+      <el-col :xs="24" :lg="16">
+        <el-card v-if="selectedServer" shadow="never" class="panel-card">
+          <template #header>
+            <div class="panel-header">
+              <span>瀹炰緥璇︽儏锛歿{ selectedServer.serverId }}</span>
+              <el-tag :type="selectedServer.running ? 'success' : 'info'">
+                {{ selectedServer.running ? '杩愯涓�' : '宸插仠姝�' }}
+              </el-tag>
+            </div>
+          </template>
+
+          <el-descriptions :column="4" border class="desc-block">
+            <el-descriptions-item label="鏈嶅姟绔湴鍧�">{{ selectedServer.listenIp }}</el-descriptions-item>
+            <el-descriptions-item label="鏈嶅姟绔鍙�">{{ selectedServer.listenPort }}</el-descriptions-item>
+            <el-descriptions-item label="鏈湴绔彛">{{ selectedServer.localPort }}</el-descriptions-item>
+            <el-descriptions-item label="杩炴帴鐘舵��">{{ selectedServer.connectedCount > 0 ? '宸茶繛鎺�' : '鏈繛鎺�' }}</el-descriptions-item>
+          </el-descriptions>
+
+          <div class="message-actions">
+            <el-button type="primary" @click="openMessageCenter">
+              娑堟伅涓績
+            </el-button>
+          </div>
+
+          <el-card shadow="never" class="connection-card">
+            <template #header>
+              <span>杩炴帴鍒楄〃</span>
+            </template>
+            <el-table :data="selectedServer.clients || []" border size="small">
+              <el-table-column prop="clientId" label="杩炴帴ID" width="100" />
+              <el-table-column prop="remoteEndPoint" label="杩滅鍦板潃" min-width="170" />
+              <el-table-column label="鐘舵��" width="100">
+                <template #default="{ row }">
+                  <el-tag :type="row.connected ? 'success' : 'danger'">{{ row.connected ? '鍦ㄧ嚎' : '绂荤嚎' }}</el-tag>
+                </template>
+              </el-table-column>
+              <el-table-column prop="connectedAt" label="杩炴帴鏃堕棿" min-width="170" />
+              <el-table-column prop="lastReceivedAt" label="鏈�杩戞帴鏀�" min-width="170" />
+              <el-table-column prop="lastSentAt" label="鏈�杩戝彂閫�" min-width="170" />
+              <el-table-column prop="lastError" label="鏈�鍚庨敊璇�" min-width="190" />
+            </el-table>
+          </el-card>
+        </el-card>
+
+        <el-empty v-else description="璇烽�夋嫨宸︿晶瀹㈡埛绔疄渚嬫煡鐪嬭鎯�" />
+      </el-col>
+    </el-row>
+
+    <el-dialog
+      v-model="messageCenterVisible"
+      title="娑堟伅涓績"
+      width="78vw"
+      destroy-on-close
+    >
+      <el-card shadow="never" class="message-send-card">
+        <template #header>
+          <span>鍙戦�佹寚浠�</span>
+        </template>
+        <div class="send-row">
+          <el-input v-model="sendMessage" placeholder="渚嬪 Pickbattery,1" />
+          <el-button type="primary" :loading="sending" @click="handleSend">鍙戦��</el-button>
+        </div>
+      </el-card>
+
+      <el-tabs v-model="messageTab">
+        <el-tab-pane
+          :label="`鎺ユ敹娑堟伅 (${selectedServer?.receivedMessages?.length || 0})`"
+          name="received"
+        >
+          <div class="message-toolbar">
+            <el-button
+              link
+              type="danger"
+              :loading="clearingMessages"
+              @click="handleClearMessages"
+            >
+              娓呯┖娑堟伅
+            </el-button>
+          </div>
+          <el-table :data="selectedServer?.receivedMessages || []" border size="small" max-height="430">
+            <el-table-column prop="receivedAt" label="鎺ユ敹鏃堕棿" min-width="170" />
+            <el-table-column prop="clientId" label="杩炴帴ID" width="100" />
+            <el-table-column prop="remoteEndPoint" label="杩滅鍦板潃" min-width="170" />
+            <el-table-column prop="message" label="娑堟伅鍐呭" min-width="250" />
+          </el-table>
+        </el-tab-pane>
+        <el-tab-pane
+          :label="`鍙戦�佹秷鎭� (${selectedServer?.sentMessages?.length || 0})`"
+          name="sent"
+        >
+          <el-table :data="selectedServer?.sentMessages || []" border size="small" max-height="430">
+            <el-table-column prop="sentAt" label="鍙戦�佹椂闂�" min-width="170" />
+            <el-table-column prop="clientId" label="杩炴帴ID" width="100" />
+            <el-table-column prop="remoteEndPoint" label="杩滅鍦板潃" min-width="170" />
+            <el-table-column prop="message" label="娑堟伅鍐呭" min-width="250" />
+          </el-table>
+        </el-tab-pane>
+      </el-tabs>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { computed, onMounted, onUnmounted, ref } from 'vue'
+import { ElMessage } from 'element-plus'
+import {
+  clearRobotClientReceivedMessages,
+  getRobotClientStatus,
+  sendRobotClientMessage,
+  startRobotClients,
+  stopRobotClients
+} from '../api'
+import type {
+  RobotClientStartRequest,
+  RobotClientStatusResponse,
+  RobotServerStatusItem
+} from '../types'
+
+const startForm = ref<RobotClientStartRequest>({
+  serverId: 'robot-client-1',
+  listenIp: '127.0.0.1',
+  listenPort: 2000,
+  localPort: 2001
+})
+
+const status = ref<RobotClientStatusResponse | null>(null)
+const selectedServerId = ref('')
+
+const refreshing = ref(false)
+const starting = ref(false)
+const stoppingAll = ref(false)
+const sending = ref(false)
+const clearingMessages = ref(false)
+const messageCenterVisible = ref(false)
+const messageTab = ref<'received' | 'sent'>('received')
+
+const sendMessage = ref('')
+
+let timer: number | null = null
+
+const totalConnectedCount = computed(() =>
+  (status.value?.servers || []).reduce((sum, x) => sum + (x.connectedCount || 0), 0)
+)
+
+const selectedServer = computed<RobotServerStatusItem | null>(() => {
+  if (!status.value) return null
+  return status.value.servers.find(x => x.serverId === selectedServerId.value) || null
+})
+
+async function loadStatus() {
+  refreshing.value = true
+  try {
+    status.value = await getRobotClientStatus()
+    if ((status.value.servers || []).length > 0) {
+      const exists = status.value.servers.some(x => x.serverId === selectedServerId.value)
+      if (!exists) {
+        selectedServerId.value = status.value.servers[0].serverId
+      }
+    } else {
+      selectedServerId.value = ''
+    }
+  } catch (error) {
+    console.error(error)
+  } finally {
+    refreshing.value = false
+  }
+}
+
+function onServerRowClick(row: RobotServerStatusItem) {
+  selectedServerId.value = row.serverId
+}
+
+function serverRowClassName(args: { row: RobotServerStatusItem }) {
+  return args.row.serverId === selectedServerId.value ? 'is-selected-row' : ''
+}
+
+function openMessageCenter() {
+  if (!selectedServer.value) {
+    ElMessage.warning('璇峰厛閫夋嫨涓�涓鎴风瀹炰緥')
+    return
+  }
+
+  messageTab.value = 'received'
+  messageCenterVisible.value = true
+}
+
+async function handleStart() {
+  if (!startForm.value.serverId.trim()) {
+    ElMessage.warning('璇疯緭鍏ュ疄渚婭D')
+    return
+  }
+
+  starting.value = true
+  try {
+    status.value = await startRobotClients({
+      ...startForm.value,
+      serverId: startForm.value.serverId.trim()
+    })
+    selectedServerId.value = startForm.value.serverId.trim()
+    ElMessage.success('瀹㈡埛绔疄渚嬭繛鎺ユ垚鍔�')
+  } catch (error) {
+    console.error(error)
+    ElMessage.error('杩炴帴澶辫触锛岃妫�鏌ユ湇鍔$鍦板潃鍜岀鍙�')
+  } finally {
+    starting.value = false
+  }
+}
+
+async function handleStopAll() {
+  stoppingAll.value = true
+  try {
+    await stopRobotClients()
+    await loadStatus()
+    ElMessage.success('鍏ㄩ儴瀹㈡埛绔疄渚嬪凡鍋滄')
+  } catch (error) {
+    console.error(error)
+    ElMessage.error('鍋滄澶辫触锛岃鏌ョ湅鏃ュ織')
+  } finally {
+    stoppingAll.value = false
+  }
+}
+
+async function handleStopOne(serverId: string) {
+  try {
+    await stopRobotClients(serverId)
+    await loadStatus()
+    ElMessage.success(`瀹炰緥 ${serverId} 宸插仠姝)
+  } catch (error) {
+    console.error(error)
+    ElMessage.error('鍋滄瀹炰緥澶辫触')
+  }
+}
+
+async function handleSend() {
+  if (!selectedServer.value) {
+    ElMessage.warning('璇峰厛閫夋嫨涓�涓鎴风瀹炰緥')
+    return
+  }
+
+  if (!sendMessage.value.trim()) {
+    ElMessage.warning('璇疯緭鍏ュ彂閫佸唴瀹�')
+    return
+  }
+
+  sending.value = true
+  try {
+    await sendRobotClientMessage({
+      serverId: selectedServer.value.serverId,
+      clientId: null,
+      message: sendMessage.value.trim()
+    })
+    ElMessage.success('鍙戦�佹垚鍔�')
+  } catch (error) {
+    console.error(error)
+    ElMessage.error('鍙戦�佸け璐ワ紝璇锋鏌ヨ繛鎺ョ姸鎬�')
+  } finally {
+    sending.value = false
+  }
+}
+
+async function handleClearMessages() {
+  if (!selectedServer.value) {
+    ElMessage.warning('璇峰厛閫夋嫨涓�涓鎴风瀹炰緥')
+    return
+  }
+
+  clearingMessages.value = true
+  try {
+    await clearRobotClientReceivedMessages(selectedServer.value.serverId)
+    await loadStatus()
+    ElMessage.success('鎺ユ敹娑堟伅宸叉竻绌�')
+  } catch (error) {
+    console.error(error)
+    ElMessage.error('娓呯┖澶辫触锛岃鏌ョ湅鏃ュ織')
+  } finally {
+    clearingMessages.value = false
+  }
+}
+
+onMounted(async () => {
+  await loadStatus()
+  timer = window.setInterval(() => {
+    loadStatus()
+  }, 2000)
+})
+
+onUnmounted(() => {
+  if (timer !== null) {
+    clearInterval(timer)
+  }
+})
+</script>
+
+<style scoped>
+.robot-admin-page {
+  display: flex;
+  flex-direction: column;
+  gap: 14px;
+}
+
+.toolbar-actions {
+  display: flex;
+  gap: 8px;
+}
+
+.stats-row {
+  margin-bottom: 0;
+}
+
+.stat-card {
+  min-height: 92px;
+}
+
+.create-card,
+.panel-card {
+  border-radius: 8px;
+}
+
+.create-form {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 8px 12px;
+}
+
+.main-row {
+  margin-top: 0;
+}
+
+.panel-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+
+.desc-block {
+  margin-bottom: 12px;
+}
+
+.connection-card {
+  margin-top: 8px;
+}
+
+.message-send-card {
+  margin-bottom: 8px;
+}
+
+.send-row {
+  display: flex;
+  gap: 8px;
+  align-items: center;
+  flex-wrap: wrap;
+}
+
+.message-actions {
+  display: flex;
+  gap: 10px;
+  margin-bottom: 8px;
+}
+
+.message-toolbar {
+  display: flex;
+  justify-content: flex-end;
+  margin-bottom: 6px;
+}
+
+:deep(.is-selected-row td) {
+  background-color: #ecf5ff !important;
+}
+</style>

--
Gitblit v1.9.3