| | |
| | | @page |
| | | @model IndexModel |
| | | @{ |
| | | ViewData["Title"] = "Home page"; |
| | | ViewData["Title"] = "实例列表"; |
| | | } |
| | | |
| | | <div class="text-center"> |
| | | <h1 class="display-4">Welcome</h1> |
| | | <p>Learn about <a href="https://docs.microsoft.com/aspnet/core">building Web apps with ASP.NET Core</a>.</p> |
| | | <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 仿真器实例 |
| | | </h2> |
| | | <p class="text-muted mb-0 mt-1">管理和监控 S7 PLC 仿真器实例</p> |
| | | </div> |
| | | <a asp-page="/Create" class="btn btn-primary"> |
| | | <i class="bi bi-plus-lg me-1"></i>创建实例 |
| | | </a> |
| | | </div> |
| | | |
| | | <div id="instancesContainer"> |
| | | <!-- Loading state --> |
| | | <div id="loadingState" class="text-center py-5"> |
| | | <div class="spinner-border text-primary" role="status"> |
| | | <span class="visually-hidden">加载中...</span> |
| | | </div> |
| | | <p class="mt-3 text-muted">正在加载实例列表...</p> |
| | | </div> |
| | | |
| | | <!-- Empty state --> |
| | | <div id="emptyState" class="empty-state d-none"> |
| | | <i class="bi bi-inbox"></i> |
| | | <h3>暂无实例</h3> |
| | | <p>点击上方"创建实例"按钮来创建您的第一个仿真器实例</p> |
| | | </div> |
| | | |
| | | <!-- Instances grid --> |
| | | <div id="instancesGrid" class="row row-cols-1 row-cols-md-2 row-cols-xl-3 g-4 d-none"></div> |
| | | </div> |
| | | |
| | | @section Scripts { |
| | | <script> |
| | | let autoRefreshInterval = null; |
| | | |
| | | document.addEventListener('DOMContentLoaded', function () { |
| | | loadInstances(); |
| | | startAutoRefresh(loadInstances, 5000); |
| | | }); |
| | | |
| | | async function loadInstances() { |
| | | try { |
| | | const instances = await apiCall(`${API_BASE_URL}/SimulatorInstances`); |
| | | renderInstances(instances); |
| | | } catch (error) { |
| | | console.error('Failed to load instances:', error); |
| | | document.getElementById('loadingState').classList.add('d-none'); |
| | | document.getElementById('emptyState').classList.remove('d-none'); |
| | | } |
| | | } |
| | | |
| | | function renderInstances(instances) { |
| | | const loadingState = document.getElementById('loadingState'); |
| | | const emptyState = document.getElementById('emptyState'); |
| | | const grid = document.getElementById('instancesGrid'); |
| | | |
| | | loadingState.classList.add('d-none'); |
| | | grid.classList.add('d-none'); |
| | | emptyState.classList.add('d-none'); |
| | | |
| | | if (!instances || instances.length === 0) { |
| | | emptyState.classList.remove('d-none'); |
| | | return; |
| | | } |
| | | |
| | | grid.classList.remove('d-none'); |
| | | grid.innerHTML = instances.map(instance => createInstanceCard(instance)).join(''); |
| | | } |
| | | |
| | | function createInstanceCard(instance) { |
| | | const statusClass = getStatusClass(instance.status); |
| | | const statusText = getStatusText(instance.status); |
| | | const plcTypeText = getPlcTypeText(instance.plcType); |
| | | const isRunning = instance.status === 'Running'; |
| | | const isStopped = instance.status === 'Stopped'; |
| | | |
| | | return ` |
| | | <div class="col"> |
| | | <div class="card instance-card h-100 ${statusClass}"> |
| | | <div class="card-header d-flex justify-content-between align-items-center"> |
| | | <h5 class="card-title mb-0">${escapeHtml(instance.instanceId)}</h5> |
| | | <span class="badge ${statusClass}">${statusText}</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">${escapeHtml(instance.name || '-')}</div> |
| | | </div> |
| | | <div class="col-6"> |
| | | <small class="instance-info-label">PLC型号</small> |
| | | <div class="instance-info-value">${plcTypeText}</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> |
| | | ${instance.startTime ? ` |
| | | <div class="row"> |
| | | <div class="col-12"> |
| | | <small class="instance-info-label">启动时间</small> |
| | | <div class="instance-info-value small">${formatDate(instance.startTime)}</div> |
| | | </div> |
| | | </div> |
| | | ` : ''} |
| | | ${instance.errorMessage ? ` |
| | | <div class="alert alert-danger alert-sm mt-2 mb-0 py-2 small"> |
| | | <i class="bi bi-exclamation-triangle-fill me-1"></i>${escapeHtml(instance.errorMessage)} |
| | | </div> |
| | | ` : ''} |
| | | </div> |
| | | </div> |
| | | <div class="card-footer bg-white"> |
| | | <div class="action-buttons d-flex gap-2"> |
| | | ${isRunning ? ` |
| | | <button class="btn btn-warning btn-sm flex-fill" onclick="stopInstance('${instance.instanceId}')" ${instance.status === 'Stopping' ? 'disabled' : ''}> |
| | | <i class="bi bi-stop-fill me-1"></i>停止 |
| | | </button> |
| | | ` : ''} |
| | | ${isStopped ? ` |
| | | <button class="btn btn-success btn-sm flex-fill" onclick="startInstance('${instance.instanceId}')" ${instance.status === 'Starting' ? 'disabled' : ''}> |
| | | <i class="bi bi-play-fill me-1"></i>启动 |
| | | </button> |
| | | ` : ''} |
| | | <a href="/Details?id=${encodeURIComponent(instance.instanceId)}" class="btn btn-info btn-sm text-white flex-fill"> |
| | | <i class="bi bi-info-circle-fill me-1"></i>详情 |
| | | </a> |
| | | <a href="/Edit?id=${encodeURIComponent(instance.instanceId)}" class="btn btn-primary btn-sm flex-fill"> |
| | | <i class="bi bi-pencil-fill me-1"></i>编辑 |
| | | </a> |
| | | <button class="btn btn-danger btn-sm" onclick="deleteInstance('${instance.instanceId}')"> |
| | | <i class="bi bi-trash-fill"></i> |
| | | </button> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | `; |
| | | } |
| | | |
| | | function getStatusClass(status) { |
| | | const map = { |
| | | 'Stopped': 'status-stopped', |
| | | 'Starting': 'status-starting', |
| | | 'Running': 'status-running', |
| | | 'Stopping': 'status-stopping', |
| | | 'Error': 'status-error' |
| | | }; |
| | | return map[status] || ''; |
| | | } |
| | | |
| | | async function startInstance(id) { |
| | | confirmAction(`确定要启动实例 "${id}" 吗?`, async () => { |
| | | try { |
| | | await apiCall(`${API_BASE_URL}/SimulatorInstances/${encodeURIComponent(id)}/start`, { |
| | | method: 'POST' |
| | | }); |
| | | showToast(`实例 "${id}" 启动命令已发送`, 'success'); |
| | | setTimeout(() => loadInstances(), 1000); |
| | | } catch (error) { |
| | | console.error('Failed to start instance:', error); |
| | | } |
| | | }); |
| | | } |
| | | |
| | | async function stopInstance(id) { |
| | | confirmAction(`确定要停止实例 "${id}" 吗?`, async () => { |
| | | try { |
| | | await apiCall(`${API_BASE_URL}/SimulatorInstances/${encodeURIComponent(id)}/stop`, { |
| | | method: 'POST' |
| | | }); |
| | | showToast(`实例 "${id}" 停止命令已发送`, 'success'); |
| | | setTimeout(() => loadInstances(), 1000); |
| | | } catch (error) { |
| | | console.error('Failed to stop instance:', error); |
| | | } |
| | | }); |
| | | } |
| | | |
| | | async function deleteInstance(id) { |
| | | confirmAction(`确定要删除实例 "${id}" 吗?此操作不可撤销!`, async () => { |
| | | try { |
| | | await apiCall(`${API_BASE_URL}/SimulatorInstances/${encodeURIComponent(id)}?deleteConfig=true`, { |
| | | method: 'DELETE' |
| | | }); |
| | | showToast(`实例 "${id}" 已删除`, 'success'); |
| | | loadInstances(); |
| | | } catch (error) { |
| | | console.error('Failed to delete instance:', error); |
| | | } |
| | | }); |
| | | } |
| | | |
| | | function escapeHtml(text) { |
| | | const div = document.createElement('div'); |
| | | div.textContent = text; |
| | | return div.innerHTML; |
| | | } |
| | | |
| | | // Stop auto-refresh when page is hidden |
| | | document.addEventListener('visibilitychange', function () { |
| | | if (document.hidden) { |
| | | stopAutoRefresh(); |
| | | } else { |
| | | startAutoRefresh(loadInstances, 5000); |
| | | } |
| | | }); |
| | | </script> |
| | | } |