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