feat: 设备名称超链接到详情页; 新增设备详情页面展示完整信息

This commit is contained in:
2026-05-20 11:05:53 +08:00
parent 3da8f5a089
commit 50a49c1bcd
4 changed files with 293 additions and 28 deletions

View File

@@ -11,10 +11,11 @@ from rest_framework_simplejwt.views import (
TokenRefreshView,
)
from device_management.views import home_page
from device_management.views import home_page, device_detail
urlpatterns = [
path('', home_page, name='home'),
path('device/<int:device_id>/', device_detail, name='device_detail'),
path('admin/', admin.site.urls),
path('api/', include('device_management.urls')),
path('api/token/', TokenObtainPairView.as_view(), name='token_obtain_pair'),

View File

@@ -0,0 +1,268 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ device.device_name }} - 设备详情</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
.gradient-bg { background: linear-gradient(135deg, #1e3a5f 0%, #2d5a87 50%, #1e3a5f 100%); }
.status-normal { background: #dcfce7; color: #166534; }
.status-warning { background: #fef9c3; color: #854d0e; }
.status-offline { background: #f3f4f6; color: #6b7280; }
.status-repair { background: #fee2e2; color: #991b1b; }
.status-scrap { background: #e5e7eb; color: #374151; text-decoration: line-through; }
</style>
</head>
<body class="bg-gray-50 min-h-screen">
<header class="gradient-bg text-white shadow-lg">
<div class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-5">
<div class="flex items-center justify-between">
<div class="flex items-center space-x-4">
<a href="/" class="text-blue-200 hover:text-white">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"/>
</svg>
</a>
<div>
<h1 class="text-xl font-bold">{{ device.device_name }}</h1>
<p class="text-blue-200 text-sm">{{ device.location|default:"-" }}</p>
</div>
</div>
<div class="flex items-center space-x-3">
<a href="/admin/device_management/device/{{ device.id }}/change/" class="bg-white/20 hover:bg-white/30 text-white text-sm px-4 py-2 rounded-lg transition-colors">编辑</a>
<a href="/" class="text-blue-200 hover:text-white text-sm">返回列表</a>
</div>
</div>
</div>
</header>
<main class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<!-- 基本信息 -->
<div class="bg-white rounded-xl shadow-sm border border-gray-100 p-6 mb-6">
<h2 class="text-base font-bold text-gray-800 mb-4 flex items-center">
<span class="w-1 h-5 bg-blue-500 rounded-full mr-3"></span>
基本信息
</h2>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<div>
<p class="text-xs text-gray-500 uppercase tracking-wider">状态</p>
<p class="mt-1">
<span class="status-{{ device.status }} text-xs font-medium px-2 py-1 rounded-full">
{{ device.get_status_display }}
</span>
</p>
</div>
<div>
<p class="text-xs text-gray-500 uppercase tracking-wider">品牌</p>
<p class="mt-1 text-gray-800 font-medium">{{ device.brand|default:"-" }}</p>
</div>
<div>
<p class="text-xs text-gray-500 uppercase tracking-wider">型号</p>
<p class="mt-1 text-gray-800 font-medium">{{ device.model|default:"-" }}</p>
</div>
<div>
<p class="text-xs text-gray-500 uppercase tracking-wider">地点</p>
<p class="mt-1 text-gray-800">{{ device.location|default:"-" }}</p>
</div>
<div>
<p class="text-xs text-gray-500 uppercase tracking-wider">楼栋/机柜</p>
<p class="mt-1 text-gray-800">
{{ device.building|default:"-" }}
{% if device.floor %} {{ device.floor }}{% endif %}
{% if device.cabinet %} / {{ device.cabinet }}{% endif %}
</p>
</div>
<div>
<p class="text-xs text-gray-500 uppercase tracking-wider">MAC地址</p>
<p class="mt-1 font-mono text-sm text-gray-800">{{ device.mac_address|default:"-" }}</p>
</div>
<div>
<p class="text-xs text-gray-500 uppercase tracking-wider">运维人员</p>
<p class="mt-1 text-gray-800">{{ device.responsible_person|default:"-" }}</p>
</div>
<div>
<p class="text-xs text-gray-500 uppercase tracking-wider">启用日期</p>
<p class="mt-1 text-gray-800">{{ device.enable_date|default:"-" }}</p>
</div>
<div>
<p class="text-xs text-gray-500 uppercase tracking-wider">服役时长</p>
<p class="mt-1 text-gray-800 font-medium">{{ device.service_duration_days }} 天</p>
</div>
<div>
<p class="text-xs text-gray-500 uppercase tracking-wider">保修到期</p>
<p class="mt-1 text-gray-800">{{ device.warranty_expire|default:"-" }}</p>
</div>
<div>
<p class="text-xs text-gray-500 uppercase tracking-wider">最后巡检</p>
<p class="mt-1 text-gray-800">{{ device.last_inspection_date|default:"-" }}</p>
</div>
</div>
</div>
<!-- IP地址 -->
<div class="bg-white rounded-xl shadow-sm border border-gray-100 p-6 mb-6">
<h2 class="text-base font-bold text-gray-800 mb-4 flex items-center">
<span class="w-1 h-5 bg-green-500 rounded-full mr-3"></span>
IP地址
<span class="ml-2 text-xs bg-gray-100 text-gray-500 px-2 py-0.5 rounded-full">{{ device_ips|length }}</span>
</h2>
{% if device_ips %}
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr class="bg-gray-50 text-gray-500 text-xs uppercase tracking-wider">
<th class="px-3 py-2 text-left">IP地址</th>
<th class="px-3 py-2 text-left">类型</th>
<th class="px-3 py-2 text-left">主IP</th>
<th class="px-3 py-2 text-left">子网掩码</th>
<th class="px-3 py-2 text-left">网关</th>
<th class="px-3 py-2 text-left">VLAN</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100">
{% for ip in device_ips %}
<tr class="hover:bg-gray-50">
<td class="px-3 py-2 font-mono">{{ ip.ip_address }}</td>
<td class="px-3 py-2 text-gray-600">{{ ip.ip_type|default:"-" }}</td>
<td class="px-3 py-2">
{% if ip.is_primary %}
<span class="text-xs bg-green-100 text-green-700 px-2 py-0.5 rounded-full"></span>
{% else %}
<span class="text-xs text-gray-400"></span>
{% endif %}
</td>
<td class="px-3 py-2 font-mono text-gray-600">{{ ip.subnet_mask|default:"-" }}</td>
<td class="px-3 py-2 font-mono text-gray-600">{{ ip.gateway|default:"-" }}</td>
<td class="px-3 py-2 text-gray-600">{{ ip.vlan_id|default:"-" }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<p class="text-gray-400 text-sm">暂无IP地址</p>
{% endif %}
</div>
<!-- 序列号 -->
<div class="bg-white rounded-xl shadow-sm border border-gray-100 p-6 mb-6">
<h2 class="text-base font-bold text-gray-800 mb-4 flex items-center">
<span class="w-1 h-5 bg-blue-500 rounded-full mr-3"></span>
序列号
<span class="ml-2 text-xs bg-gray-100 text-gray-500 px-2 py-0.5 rounded-full">{{ device_serials|length }}</span>
</h2>
{% if device_serials %}
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr class="bg-gray-50 text-gray-500 text-xs uppercase tracking-wider">
<th class="px-3 py-2 text-left">序列号</th>
<th class="px-3 py-2 text-left">类型</th>
<th class="px-3 py-2 text-left">主序列号</th>
<th class="px-3 py-2 text-left">备注</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100">
{% for serial in device_serials %}
<tr class="hover:bg-gray-50">
<td class="px-3 py-2 font-mono">{{ serial.serial_number }}</td>
<td class="px-3 py-2 text-gray-600">{{ serial.serial_type|default:"-" }}</td>
<td class="px-3 py-2">
{% if serial.is_primary %}
<span class="text-xs bg-green-100 text-green-700 px-2 py-0.5 rounded-full"></span>
{% else %}
<span class="text-xs text-gray-400"></span>
{% endif %}
</td>
<td class="px-3 py-2 text-gray-600">{{ serial.remark|default:"-" }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<p class="text-gray-400 text-sm">暂无序列号</p>
{% endif %}
</div>
<!-- 维修记录 -->
<div class="bg-white rounded-xl shadow-sm border border-gray-100 p-6 mb-6">
<h2 class="text-base font-bold text-gray-800 mb-4 flex items-center">
<span class="w-1 h-5 bg-orange-500 rounded-full mr-3"></span>
维修记录
<span class="ml-2 text-xs bg-gray-100 text-gray-500 px-2 py-0.5 rounded-full">{{ maintenance_records|length }}</span>
</h2>
{% if maintenance_records %}
<div class="space-y-4">
{% for record in maintenance_records %}
<div class="border border-gray-100 rounded-lg p-4 hover:border-gray-200">
<div class="flex items-start justify-between mb-2">
<span class="text-sm font-medium text-gray-800">{{ record.maintenance_date }}</span>
<span class="text-xs text-gray-500">
{% if record.cost %}费用: ¥{{ record.cost }}{% endif %}
{% if record.maintenance_by %} · {{ record.maintenance_by }}{% endif %}
</span>
</div>
{% if record.fault_description %}
<div class="mb-2">
<p class="text-xs text-gray-500">故障描述</p>
<p class="text-sm text-gray-700">{{ record.fault_description }}</p>
</div>
{% endif %}
{% if record.repair_content %}
<div class="mb-2">
<p class="text-xs text-gray-500">维修内容</p>
<p class="text-sm text-gray-700">{{ record.repair_content }}</p>
</div>
{% endif %}
{% if record.replaced_parts %}
<div>
<p class="text-xs text-gray-500">更换配件</p>
<p class="text-sm text-gray-700">{{ record.replaced_parts }}</p>
</div>
{% endif %}
</div>
{% endfor %}
</div>
{% else %}
<p class="text-gray-400 text-sm">暂无维修记录</p>
{% endif %}
</div>
<!-- 附件 -->
<div class="bg-white rounded-xl shadow-sm border border-gray-100 p-6">
<h2 class="text-base font-bold text-gray-800 mb-4 flex items-center">
<span class="w-1 h-5 bg-purple-500 rounded-full mr-3"></span>
附件
<span class="ml-2 text-xs bg-gray-100 text-gray-500 px-2 py-0.5 rounded-full">{{ attachments|length }}</span>
</h2>
{% if attachments %}
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{% for att in attachments %}
<a href="{{ att.file.url }}" target="_blank" class="border border-gray-100 rounded-lg p-4 hover:border-blue-300 hover:bg-blue-50 transition-colors block">
<div class="flex items-center space-x-3">
<svg class="w-8 h-8 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
</svg>
<div class="flex-1 min-w-0">
<p class="text-sm font-medium text-gray-700 truncate">{{ att.file.name }}</p>
<p class="text-xs text-gray-400">{{ att.uploaded_at|date:"Y-m-d" }}</p>
</div>
</div>
</a>
{% endfor %}
</div>
{% else %}
<p class="text-gray-400 text-sm">暂无附件</p>
{% endif %}
</div>
</main>
<footer class="border-t border-gray-200 mt-8">
<div class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-6 text-center text-sm text-gray-400">
© 2026 视频主设备管理系统 · Django REST Framework
</div>
</footer>
</body>
</html>

View File

@@ -92,8 +92,6 @@
<tr class="bg-gray-50 text-gray-500 text-xs uppercase tracking-wider">
<th class="px-4 py-3 text-left font-medium">设备名称</th>
<th class="px-4 py-3 text-left font-medium">型号</th>
<th class="px-4 py-3 text-left font-medium">主IP</th>
<th class="px-4 py-3 text-left font-medium">主序列号</th>
<th class="px-4 py-3 text-left font-medium">状态</th>
<th class="px-4 py-3 text-left font-medium">运维人员</th>
<th class="px-4 py-3 text-left font-medium">服役天数</th>
@@ -104,34 +102,12 @@
{% for device in devices %}
<tr class="device-row transition-colors">
<td class="px-4 py-3">
<div class="font-medium text-gray-800">{{ device.device_name }}</div>
<div class="font-medium">
<a href="/device/{{ device.id }}/" class="text-blue-600 hover:text-blue-800">{{ device.device_name }}</a>
</div>
<div class="text-xs text-gray-400">{{ device.brand|default:"" }}</div>
</td>
<td class="px-4 py-3 text-gray-600">{{ device.model|default:"-" }}</td>
<td class="px-4 py-3">
{% with primary_ip=device.ips.all|first %}
{% if primary_ip and primary_ip.is_primary %}
<code class="text-xs bg-gray-100 px-1.5 py-0.5 rounded">{{ primary_ip.ip_address }}</code>
{% else %}
{% for ip in device.ips.all %}
{% if ip.is_primary %}
<code class="text-xs bg-gray-100 px-1.5 py-0.5 rounded">{{ ip.ip_address }}</code>
{% endif %}
{% empty %}
<span class="text-gray-400">-</span>
{% endfor %}
{% endif %}
{% endwith %}
</td>
<td class="px-4 py-3">
{% for serial in device.serials.all %}
{% if serial.is_primary %}
<code class="text-xs bg-gray-100 px-1.5 py-0.5 rounded">{{ serial.serial_number }}</code>
{% endif %}
{% empty %}
<span class="text-gray-400">-</span>
{% endfor %}
</td>
<td class="px-4 py-3">
<span class="status-{{ device.status }} text-xs font-medium px-2 py-1 rounded-full">
{{ device.get_status_display }}

View File

@@ -50,6 +50,26 @@ def home_page(request):
}
return render(request, 'device_management/index.html', context)
def device_detail(request, device_id):
from django.shortcuts import get_object_or_404
device = get_object_or_404(Device, id=device_id)
device_serials = device.serials.all()
device_ips = device.ips.all()
maintenance_records = device.maintenance_records.order_by('-maintenance_date')
attachments = device.attachments.order_by('-uploaded_at')
context = {
'device': device,
'device_serials': device_serials,
'device_ips': device_ips,
'maintenance_records': maintenance_records,
'attachments': attachments,
}
return render(request, 'device_management/detail.html', context)
from .models import (
Device, DeviceSerial, DeviceIP, MaintenanceRecord, DeviceAttachment
)