| | |
| | | <template> |
| | | <div |
| | | class="admin-page" |
| | | v-loading.fullscreen.lock="startActionLoading" |
| | | element-loading-text="正在启动实例,请稍候..." |
| | | > |
| | |
| | | </el-button> |
| | | </div> |
| | | |
| | | <el-row :gutter="20" class="status-cards"> |
| | | <el-col :xs="12" :sm="6"> |
| | | <el-card shadow="hover" class="status-card"> |
| | | <el-statistic title="状态"> |
| | | <template #default> |
| | | <el-tag :type="getStatusTagType(instance.status)" size="large"> |
| | | {{ getStatusText(instance.status) }} |
| | | </el-tag> |
| | | </template> |
| | | </el-statistic> |
| | | </el-card> |
| | | </el-col> |
| | | <el-col :xs="12" :sm="6"> |
| | | <el-card shadow="hover" class="status-card"> |
| | | <el-statistic title="连接客户端" :value="instance.clientCount"> |
| | | <template #suffix> |
| | | <el-icon><User /></el-icon> |
| | | </template> |
| | | </el-statistic> |
| | | </el-card> |
| | | </el-col> |
| | | <el-col :xs="12" :sm="6"> |
| | | <el-card shadow="hover" class="status-card"> |
| | | <el-statistic title="总请求数" :value="instance.totalRequests" /> |
| | | </el-card> |
| | | </el-col> |
| | | <el-col :xs="12" :sm="6"> |
| | | <el-card shadow="hover" class="status-card"> |
| | | <el-statistic title="端口" :value="instance.port" /> |
| | | </el-card> |
| | | </el-col> |
| | | </el-row> |
| | | <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="status-cards"> |
| | | <el-col :xs="12" :sm="6"> |
| | | <el-card shadow="hover" class="status-card panel-card"> |
| | | <el-statistic title="状态"> |
| | | <template #default> |
| | | <el-tag :type="getStatusTagType(instance.status)" size="large"> |
| | | {{ getStatusText(instance.status) }} |
| | | </el-tag> |
| | | </template> |
| | | </el-statistic> |
| | | </el-card> |
| | | </el-col> |
| | | <el-col :xs="12" :sm="6"> |
| | | <el-card shadow="hover" class="status-card panel-card"> |
| | | <el-statistic title="连接客户端" :value="instance.clientCount"> |
| | | <template #suffix> |
| | | <el-icon><User /></el-icon> |
| | | </template> |
| | | </el-statistic> |
| | | </el-card> |
| | | </el-col> |
| | | <el-col :xs="12" :sm="6"> |
| | | <el-card shadow="hover" class="status-card panel-card"> |
| | | <el-statistic title="总请求数" :value="instance.totalRequests" /> |
| | | </el-card> |
| | | </el-col> |
| | | <el-col :xs="12" :sm="6"> |
| | | <el-card shadow="hover" class="status-card panel-card"> |
| | | <el-statistic title="端口" :value="instance.port" /> |
| | | </el-card> |
| | | </el-col> |
| | | </el-row> |
| | | </div> |
| | | </section> |
| | | |
| | | <el-card class="mt-4" shadow="never"> |
| | | <section class="section-block detail-section"> |
| | | <div class="section-head"> |
| | | <div> |
| | | <h3 class="section-title">操作区</h3> |
| | | <p class="section-desc">左侧实例信息与操作,右侧实时 DB 数据</p> |
| | | </div> |
| | | </div> |
| | | <div class="section-body"> |
| | | <el-row :gutter="16" class="detail-main"> |
| | | <el-col :xs="24" :lg="8"> |
| | | <el-card class="panel-card" shadow="never"> |
| | | <template #header> |
| | | <span class="card-header-title">基本信息</span> |
| | | </template> |
| | | <el-descriptions :column="2" border> |
| | | <el-descriptions :column="1" border> |
| | | <el-descriptions-item label="实例ID">{{ instance.instanceId }}</el-descriptions-item> |
| | | <el-descriptions-item label="实例名称">{{ instance.name }}</el-descriptions-item> |
| | | <el-descriptions-item label="PLC型号">{{ getPlcTypeText(instance.plcType) }}</el-descriptions-item> |
| | |
| | | <el-descriptions-item v-if="instance.lastActivityTime" label="最后活动时间"> |
| | | {{ formatDate(instance.lastActivityTime) }} |
| | | </el-descriptions-item> |
| | | <el-descriptions-item v-if="instance.errorMessage" label="错误信息" :span="2"> |
| | | <el-descriptions-item v-if="instance.errorMessage" label="错误信息"> |
| | | <el-text type="danger">{{ instance.errorMessage }}</el-text> |
| | | </el-descriptions-item> |
| | | </el-descriptions> |
| | | </el-card> |
| | | |
| | | <el-card class="mt-4" shadow="never"> |
| | | <el-card class="panel-card left-actions" shadow="never"> |
| | | <div class="action-buttons"> |
| | | <el-button |
| | | v-if="instance.status === 'Stopped' || instance.status === 'Error'" |
| | | type="success" |
| | | |
| | | @click="handleStart" |
| | | > |
| | | <el-icon><VideoPlay /></el-icon> |
| | |
| | | <el-button |
| | | v-if="instance.status === 'Running'" |
| | | type="warning" |
| | | |
| | | @click="handleStop" |
| | | > |
| | | <el-icon><VideoPause /></el-icon> |
| | |
| | | </div> |
| | | </el-card> |
| | | |
| | | <el-card class="mt-4" shadow="never"> |
| | | </el-col> |
| | | <el-col :xs="24" :lg="16"> |
| | | <el-card class="panel-card" shadow="never"> |
| | | <template #header> |
| | | <div class="db-header"> |
| | | <span class="card-header-title">DB块实时数据</span> |
| | | <div class="db-toolbar"> |
| | | <el-switch v-model="autoRefreshDb" active-text="自动刷新" /> |
| | | <el-button size="small" @click="loadMemoryData(true)">手动刷新</el-button> |
| | | <el-button @click="loadMemoryData(true)">手动刷新</el-button> |
| | | </div> |
| | | </div> |
| | | </template> |
| | |
| | | <template #default> |
| | | <div v-if="deviceDbViews.length === 0" class="text-muted">当前设备模板未匹配到可显示的DB块</div> |
| | | <div v-else> |
| | | <el-tabs type="border-card" class="db-tabs"> |
| | | <el-tabs type="border-card" class="db-tabs"> |
| | | <el-tab-pane |
| | | v-for="view in deviceDbViews" |
| | | :key="view.templateDbNumber" |
| | |
| | | :key="`${view.templateDbNumber}-${group.key}`" |
| | | > |
| | | <template #label> |
| | | <el-tag :type="getFieldGroupTagType(group.key)" size="small">{{ group.key }}</el-tag> |
| | | <el-tag :type="getFieldGroupTagType(group.key)">{{ group.key }}</el-tag> |
| | | </template> |
| | | <div class="field-table-wrap"> |
| | | <el-table |
| | | :data="group.fields" |
| | | border |
| | | size="small" |
| | | class="field-table" |
| | | table-layout="auto" |
| | | empty-text="当前分组无字段映射" |
| | | > |
| | | <el-table-column prop="fieldKey" label="字段" min-width="140" /> |
| | | <el-table-column prop="address" label="地址" width="130" /> |
| | | <el-table-column prop="mappedDb" label="映射块" width="120"> |
| | | <el-table-column prop="fieldKey" label="字段" /> |
| | | <el-table-column prop="address" label="地址" /> |
| | | <el-table-column prop="mappedDb" label="映射块"> |
| | | <template #default="{ row }"> |
| | | <el-tag :type="getDbTagType(row.mappedDb)" size="small">{{ row.mappedDb }}</el-tag> |
| | | <el-tag :type="getDbTagType(row.mappedDb)">{{ row.mappedDb }}</el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="dataType" label="类型" width="90" /> |
| | | <el-table-column prop="direction" label="方向" width="130" /> |
| | | <el-table-column prop="value" label="当前值" min-width="220" /> |
| | | <el-table-column label="修改值" min-width="220"> |
| | | <el-table-column prop="dataType" label="类型" /> |
| | | <el-table-column prop="direction" label="方向" /> |
| | | <el-table-column prop="value" label="当前值" /> |
| | | <el-table-column label="修改值"> |
| | | <template #default="{ row }"> |
| | | <el-switch |
| | | v-if="row.dataType === 'Bool'" |
| | |
| | | @change="markFieldDirty(row)" |
| | | :disabled="!isFieldWritable(row)" |
| | | :controls="false" |
| | | style="width: 100%" |
| | | class="editable-control" |
| | | /> |
| | | <el-input |
| | | v-else |
| | | v-model="fieldEditValues[getFieldEditKey(row)]" |
| | | @input="markFieldDirty(row)" |
| | | :disabled="!isFieldWritable(row)" |
| | | class="editable-control" |
| | | /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="操作" width="90" fixed="right"> |
| | | <el-table-column label="操作" width="88" fixed="right"> |
| | | <template #default="{ row }"> |
| | | <el-button |
| | | type="primary" |
| | |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </div> |
| | | </el-tab-pane> |
| | | </el-tabs> |
| | | <template v-else-if="view.fields.length > 0"> |
| | | <div class="field-table-wrap"> |
| | | <el-table |
| | | v-else-if="view.fields.length > 0" |
| | | :data="view.fields" |
| | | border |
| | | size="small" |
| | | class="field-table" |
| | | table-layout="auto" |
| | | empty-text="当前DB块无字段映射" |
| | | > |
| | | <el-table-column prop="fieldKey" label="字段" min-width="140" /> |
| | | <el-table-column prop="address" label="地址" width="130" /> |
| | | <el-table-column prop="mappedDb" label="映射块" width="120"> |
| | | <el-table-column prop="fieldKey" label="字段" /> |
| | | <el-table-column prop="address" label="地址" /> |
| | | <el-table-column prop="mappedDb" label="映射块"> |
| | | <template #default="{ row }"> |
| | | <el-tag :type="getDbTagType(row.mappedDb)" size="small">{{ row.mappedDb }}</el-tag> |
| | | <el-tag :type="getDbTagType(row.mappedDb)">{{ row.mappedDb }}</el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="dataType" label="类型" width="90" /> |
| | | <el-table-column prop="direction" label="方向" width="130" /> |
| | | <el-table-column prop="value" label="当前值" min-width="220" /> |
| | | <el-table-column label="修改值" min-width="220"> |
| | | <el-table-column prop="dataType" label="类型" /> |
| | | <el-table-column prop="direction" label="方向" /> |
| | | <el-table-column prop="value" label="当前值" /> |
| | | <el-table-column label="修改值"> |
| | | <template #default="{ row }"> |
| | | <el-switch |
| | | v-if="row.dataType === 'Bool'" |
| | |
| | | @change="markFieldDirty(row)" |
| | | :disabled="!isFieldWritable(row)" |
| | | :controls="false" |
| | | style="width: 100%" |
| | | class="editable-control" |
| | | /> |
| | | <el-input |
| | | v-else |
| | | v-model="fieldEditValues[getFieldEditKey(row)]" |
| | | @input="markFieldDirty(row)" |
| | | :disabled="!isFieldWritable(row)" |
| | | class="editable-control" |
| | | /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="操作" width="90" fixed="right"> |
| | | <el-table-column label="操作" width="88" fixed="right"> |
| | | <template #default="{ row }"> |
| | | <el-button |
| | | type="primary" |
| | |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </div> |
| | | </template> |
| | | <div v-else class="text-muted">当前DB块无字段映射</div> |
| | | |
| | | <div class="card-header-title field-title">原始数据</div> |
| | |
| | | /> |
| | | </div> |
| | | </el-card> |
| | | </el-col> |
| | | </el-row> |
| | | </div> |
| | | </section> |
| | | </div> |
| | | </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; |
| | | } |
| | | |
| | | .loading-container { |
| | | text-align: center; |
| | | padding: 60px 0; |
| | | color: #909399; |
| | | } |
| | | |
| | | .loading-icon { |
| | | animation: spin 1s linear infinite; |
| | | } |
| | | |
| | | @keyframes spin { |
| | | from { |
| | | transform: rotate(0deg); |
| | | } |
| | | to { |
| | | transform: rotate(360deg); |
| | | } |
| | | } |
| | | |
| | | .status-cards { |
| | | margin-bottom: 20px; |
| | | margin-bottom: 8px; |
| | | } |
| | | |
| | | .status-card { |
| | |
| | | } |
| | | |
| | | .mt-4 { |
| | | margin-top: 16px; |
| | | margin-top: 14px; |
| | | } |
| | | |
| | | .action-buttons { |
| | | display: flex; |
| | | gap: 12px; |
| | | gap: 10px; |
| | | flex-wrap: wrap; |
| | | } |
| | | |
| | | .left-actions { |
| | | margin-top: 14px; |
| | | } |
| | | |
| | | .db-header { |
| | |
| | | |
| | | .db-content { |
| | | margin: 0; |
| | | max-height: 180px; |
| | | max-height: 168px; |
| | | overflow: auto; |
| | | white-space: pre-wrap; |
| | | font-family: Consolas, Monaco, 'Courier New', monospace; |
| | |
| | | } |
| | | |
| | | .db-tabs { |
| | | margin-top: 8px; |
| | | margin-top: 10px; |
| | | } |
| | | |
| | | .data-view-tabs { |
| | | margin-top: 8px; |
| | | margin-top: 10px; |
| | | } |
| | | |
| | | .db-raw-toolbar { |
| | |
| | | margin-top: 12px; |
| | | margin-bottom: 8px; |
| | | } |
| | | |
| | | .field-table-wrap { |
| | | width: 100%; |
| | | overflow-x: auto; |
| | | } |
| | | |
| | | :deep(.field-table) { |
| | | width: max-content; |
| | | min-width: 100%; |
| | | } |
| | | |
| | | :deep(.editable-control) { |
| | | width: 100%; |
| | | } |
| | | |
| | | :deep(.status-card .el-card__body) { |
| | | padding: 14px 16px; |
| | | } |
| | | |
| | | :deep(.action-buttons .el-button) { |
| | | min-width: 82px; |
| | | } |
| | | |
| | | :deep(.panel-card > .el-card__header) { |
| | | padding: 14px 18px; |
| | | } |
| | | |
| | | :deep(.panel-card > .el-card__body) { |
| | | padding: 14px 18px; |
| | | } |
| | | </style> |
| | | |