first commit

This commit is contained in:
jeremygan2021
2025-11-16 17:21:25 +08:00
commit a2682dc040
46 changed files with 5976 additions and 0 deletions

View File

@@ -0,0 +1,178 @@
{% extends "admin/base.html" %}
{% block title %}添加内容 - 墨水屏管理系统{% endblock %}
{% block content %}
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
<h1 class="h2"><i class="fas fa-plus-circle me-2"></i>添加内容</h1>
<div class="btn-toolbar mb-2 mb-md-0">
<a href="/admin/contents" class="btn btn-secondary">
<i class="fas fa-arrow-left me-1"></i> 返回内容列表
</a>
</div>
</div>
<div class="row">
<div class="col-lg-8">
<div class="card shadow-sm">
<div class="card-header bg-white py-3">
<h6 class="m-0 font-weight-bold text-primary">内容信息</h6>
</div>
<div class="card-body">
{% if error %}
<div class="alert alert-danger" role="alert">
<i class="fas fa-exclamation-triangle me-2"></i>{{ error }}
</div>
{% endif %}
<form method="post">
<div class="mb-3">
<label for="device_id" class="form-label">设备</label>
<select class="form-select" id="device_id" name="device_id" required>
<option value="">请选择设备</option>
{% for device in devices %}
<option value="{{ device.device_id }}" {% if selected_device == device.device_id %}selected{% endif %}>
{{ device.name }} ({{ device.device_id }})
</option>
{% endfor %}
</select>
<div class="form-text">选择要添加内容的设备</div>
</div>
<div class="mb-3">
<label for="title" class="form-label">标题</label>
<input type="text" class="form-control" id="title" name="title" required>
<div class="form-text">为内容起一个易于识别的标题</div>
</div>
<div class="mb-3">
<label for="description" class="form-label">描述</label>
<textarea class="form-control" id="description" name="description" rows="3"></textarea>
<div class="form-text">内容的详细描述(可选)</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="timezone" class="form-label">时区</label>
<select class="form-select" id="timezone" name="timezone">
<option value="Asia/Shanghai" selected>Asia/Shanghai (北京时间)</option>
<option value="UTC">UTC (协调世界时)</option>
<option value="America/New_York">America/New_York (纽约时间)</option>
<option value="Europe/London">Europe/London (伦敦时间)</option>
<option value="Asia/Tokyo">Asia/Tokyo (东京时间)</option>
</select>
<div class="form-text">选择设备所在时区</div>
</div>
<div class="col-md-6 mb-3">
<label for="time_format" class="form-label">时间格式</label>
<input type="text" class="form-control" id="time_format" name="time_format" value="%Y-%m-%d %H:%M">
<div class="form-text">时间显示格式,如:%Y-%m-%d %H:%M:%S</div>
</div>
</div>
<div class="mb-3">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="is_active" name="is_active" checked>
<label class="form-check-label" for="is_active">
启用此内容
</label>
<div class="form-text">创建后立即激活此内容</div>
</div>
</div>
<div class="mb-3">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="auto_push" name="auto_push" checked>
<label class="form-check-label" for="auto_push">
创建后自动推送到设备
</label>
<div class="form-text">创建完成后自动将内容推送到设备</div>
</div>
</div>
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
<a href="/admin/contents" class="btn btn-secondary me-md-2">
<i class="fas fa-times me-1"></i> 取消
</a>
<button type="submit" class="btn btn-primary">
<i class="fas fa-save me-1"></i> 创建内容
</button>
</div>
</form>
</div>
</div>
</div>
<div class="col-lg-4">
<div class="card shadow-sm mb-4">
<div class="card-header bg-white py-3">
<h6 class="m-0 font-weight-bold text-info">内容类型</h6>
</div>
<div class="card-body">
<div class="list-group mb-4">
<div class="list-group-item">
<div class="d-flex w-100 justify-content-between">
<h6 class="mb-1"><i class="fas fa-font me-2"></i>文本内容</h6>
<span class="badge bg-primary rounded-pill">当前</span>
</div>
<p class="mb-1">在此页面创建的是基本的文本内容,包括标题、描述和时间显示。</p>
<small>适用于简单文字信息展示</small>
</div>
<div class="list-group-item">
<div class="d-flex w-100 justify-content-between">
<h6 class="mb-1"><i class="fas fa-image me-2"></i>图片内容</h6>
<span class="badge bg-secondary rounded-pill">其他</span>
</div>
<p class="mb-1">如需创建图片内容,请使用"图片上传"功能,上传图片后系统会自动创建内容。</p>
<small>适用于复杂图形和图像展示</small>
</div>
</div>
<div class="alert alert-info" role="alert">
<i class="fas fa-info-circle me-2"></i> 创建内容后,系统将自动推送到对应设备。
</div>
</div>
</div>
<div class="card shadow-sm">
<div class="card-header bg-white py-3">
<h6 class="m-0 font-weight-bold text-warning">时间格式说明</h6>
</div>
<div class="card-body">
<h6 class="mb-3"><i class="fas fa-clock me-2"></i>常用时间格式代码</h6>
<div class="row g-2 mb-4">
<div class="col-6"><span class="badge bg-light text-dark">%Y</span> 四位年份 (2023)</div>
<div class="col-6"><span class="badge bg-light text-dark">%m</span> 两位月份 (01-12)</div>
<div class="col-6"><span class="badge bg-light text-dark">%d</span> 两位日期 (01-31)</div>
<div class="col-6"><span class="badge bg-light text-dark">%H</span> 24小时制小时 (00-23)</div>
<div class="col-6"><span class="badge bg-light text-dark">%M</span> 分钟 (00-59)</div>
<div class="col-6"><span class="badge bg-light text-dark">%S</span> 秒 (00-59)</div>
</div>
<h6 class="mb-3"><i class="fas fa-code me-2"></i>格式示例</h6>
<div class="list-group list-group-flush">
<div class="list-group-item px-0">
<div class="d-flex justify-content-between align-items-center">
<code>%Y-%m-%d %H:%M</code>
<span class="text-muted">2023-12-25 14:30</span>
</div>
</div>
<div class="list-group-item px-0">
<div class="d-flex justify-content-between align-items-center">
<code>%m/%d/%Y</code>
<span class="text-muted">12/25/2023</span>
</div>
</div>
<div class="list-group-item px-0">
<div class="d-flex justify-content-between align-items-center">
<code>%H:%M:%S</code>
<span class="text-muted">14:30:45</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,158 @@
{% extends "admin/base.html" %}
{% block title %}添加设备 - 墨水屏管理系统{% endblock %}
{% block content %}
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
<h1 class="h2"><i class="fas fa-plus-circle me-2"></i>添加设备</h1>
<div class="btn-toolbar mb-2 mb-md-0">
<a href="/admin/devices" class="btn btn-secondary">
<i class="fas fa-arrow-left me-1"></i> 返回设备列表
</a>
</div>
</div>
<div class="row">
<div class="col-lg-6">
<div class="card shadow-sm">
<div class="card-header bg-white py-3">
<h6 class="m-0 font-weight-bold text-primary">设备信息</h6>
</div>
<div class="card-body">
{% if error %}
<div class="alert alert-danger" role="alert">
<i class="fas fa-exclamation-triangle me-2"></i>{{ error }}
</div>
{% endif %}
<form method="post">
<div class="mb-3">
<label for="device_id" class="form-label">设备ID</label>
<input type="text" class="form-control" id="device_id" name="device_id" required>
<div class="form-text">设备的唯一标识符通常为ESP32的MAC地址或其他唯一ID</div>
</div>
<div class="mb-3">
<label for="name" class="form-label">设备名称</label>
<input type="text" class="form-control" id="name" name="name" required>
<div class="form-text">为设备起一个易于识别的名称</div>
</div>
<div class="mb-3">
<label for="scene" class="form-label">应用场景</label>
<select class="form-select" id="scene" name="scene" required>
<option value="">请选择应用场景</option>
<option value="office"><i class="fas fa-building me-1"></i>办公室</option>
<option value="meeting"><i class="fas fa-users me-1"></i>会议室</option>
<option value="reception"><i class="fas fa-concierge-bell me-1"></i>前台</option>
<option value="lobby"><i class="fas fa-door-open me-1"></i>大厅</option>
<option value="corridor"><i class="fas fa-route me-1"></i>走廊</option>
<option value="classroom"><i class="fas fa-chalkboard-teacher me-1"></i>教室</option>
<option value="other"><i class="fas fa-tag me-1"></i>其他</option>
</select>
<div class="form-text">选择设备所在的应用场景</div>
</div>
<div class="mb-3">
<label for="location" class="form-label">设备位置</label>
<input type="text" class="form-control" id="location" name="location" placeholder="例如:一楼大厅东侧">
<div class="form-text">可选,提供更详细的位置信息</div>
</div>
<div class="mb-3">
<label for="description" class="form-label">设备描述</label>
<textarea class="form-control" id="description" name="description" rows="3" placeholder="设备用途、特殊配置等"></textarea>
<div class="form-text">可选,提供设备的详细描述信息</div>
</div>
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
<a href="/admin/devices" class="btn btn-secondary me-md-2">
<i class="fas fa-times me-1"></i> 取消
</a>
<button type="submit" class="btn btn-primary">
<i class="fas fa-save me-1"></i> 添加设备
</button>
</div>
</form>
</div>
</div>
</div>
<div class="col-lg-6">
<div class="card shadow-sm mb-4">
<div class="card-header bg-white py-3">
<h6 class="m-0 font-weight-bold text-info">设备配置说明</h6>
</div>
<div class="card-body">
<h5 class="mb-3"><i class="fas fa-microchip me-2"></i>设备ID获取方法</h5>
<p>设备ID通常可以通过以下方式获取</p>
<div class="list-group mb-4">
<div class="list-group-item">
<div class="d-flex w-100 justify-content-between">
<h6 class="mb-1">ESP32的MAC地址</h6>
<small>推荐</small>
</div>
<p class="mb-1">格式如A1:B2:C3:D4:E5:F6</p>
<small>可通过串口输出或设备标签获取</small>
</div>
<div class="list-group-item">
<div class="d-flex w-100 justify-content-between">
<h6 class="mb-1">设备序列号</h6>
<small>备选</small>
</div>
<p class="mb-1">制造商提供的唯一序列号</p>
<small>确保在设备固件中一致</small>
</div>
<div class="list-group-item">
<div class="d-flex w-100 justify-content-between">
<h6 class="mb-1">自定义ID</h6>
<small>高级</small>
</div>
<p class="mb-1">自定义的唯一标识符</p>
<small>需确保在设备固件中一致</small>
</div>
</div>
</div>
</div>
<div class="card shadow-sm">
<div class="card-header bg-white py-3">
<h6 class="m-0 font-weight-bold text-warning">设备配置步骤</h6>
</div>
<div class="card-body">
<div class="process-steps">
<div class="step-item">
<div class="step-number">1</div>
<div class="step-text">在ESP32固件中设置设备ID</div>
</div>
<div class="step-item">
<div class="step-number">2</div>
<div class="step-text">配置MQTT连接参数</div>
</div>
<div class="step-item">
<div class="step-number">3</div>
<div class="step-text">确保设备能连接到MQTT服务器</div>
</div>
<div class="step-item">
<div class="step-number">4</div>
<div class="step-text">在此页面添加设备信息</div>
</div>
<div class="step-item">
<div class="step-number">5</div>
<div class="step-text">设备将自动连接并显示在设备列表中</div>
</div>
</div>
<div class="alert alert-info mt-4" role="alert">
<i class="fas fa-info-circle me-2"></i> 添加设备后,系统将自动生成设备密钥,用于设备认证。
</div>
<div class="alert alert-light mt-3" role="alert">
<h6 class="alert-heading"><i class="fas fa-lightbulb me-2"></i>提示</h6>
<p class="mb-0">确保设备ID唯一且与固件中设置的一致否则设备将无法正常连接到系统。</p>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

79
templates/admin/base.html Normal file
View File

@@ -0,0 +1,79 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}墨水屏管理系统{% endblock %}</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
<link rel="stylesheet" href="/static/admin/css/admin-enhanced.css">
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
<div class="container-fluid">
<a class="navbar-brand d-flex align-items-center" href="/admin">
<i class="fas fa-desktop me-2"></i>
墨水屏管理系统
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto">
<li class="nav-item">
<a class="nav-link {% if request.url.path == '/admin' %}active{% endif %}" href="/admin">
<i class="fas fa-tachometer-alt me-1"></i>仪表盘
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if '/admin/devices' in request.url.path %}active{% endif %}" href="/admin/devices">
<i class="fas fa-mobile-alt me-1"></i>设备管理
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if '/admin/contents' in request.url.path %}active{% endif %}" href="/admin/contents">
<i class="fas fa-file-image me-1"></i>内容管理
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if '/admin/upload' in request.url.path %}active{% endif %}" href="/admin/upload">
<i class="fas fa-upload me-1"></i>图片上传
</a>
</li>
</ul>
<div class="d-flex">
<button class="btn btn-outline-light me-2" id="themeToggle" title="切换主题">
<i class="fas fa-palette"></i>
</button>
<span class="navbar-text text-light">
<i class="far fa-clock me-1"></i>
<span id="currentTime"></span>
</span>
</div>
</div>
</div>
</nav>
<div class="container-fluid mt-4">
<div class="row">
<div class="col-12">
{% block content %}{% endblock %}
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script src="/static/admin/js/admin-enhanced.js"></script>
{% block scripts %}{% endblock %}
<script>
// 更新当前时间
function updateTime() {
const now = new Date();
document.getElementById('currentTime').textContent = now.toLocaleTimeString('zh-CN');
}
updateTime();
setInterval(updateTime, 1000);
</script>
</body>
</html>

View File

@@ -0,0 +1,295 @@
{% extends "admin/base.html" %}
{% block title %}内容详情 - {{ content.title }} - 墨水屏管理系统{% endblock %}
{% block content %}
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
<h1 class="h2"><i class="fas fa-file-alt me-2"></i>内容详情 - {{ content.title }}</h1>
<div class="btn-toolbar mb-2 mb-md-0">
<a href="/admin/contents" class="btn btn-secondary me-2">
<i class="fas fa-arrow-left me-1"></i> 返回内容列表
</a>
<a href="/admin/devices/{{ device.device_id }}" class="btn btn-primary">
<i class="fas fa-tv me-1"></i> 查看设备
</a>
</div>
</div>
<div class="row">
<!-- 内容信息 -->
<div class="col-lg-6 mb-4">
<div class="card shadow-sm">
<div class="card-header bg-white py-3">
<h6 class="m-0 font-weight-bold text-primary"><i class="fas fa-info-circle me-2"></i>内容信息</h6>
</div>
<div class="card-body">
<table class="table table-borderless table-hover">
<tr>
<th width="30%"><i class="fas fa-tv me-2"></i>设备</th>
<td><a href="/admin/devices/{{ device.device_id }}">{{ device.name }} ({{ device.device_id }})</a></td>
</tr>
<tr>
<th><i class="fas fa-heading me-2"></i>标题</th>
<td>{{ content.title }}</td>
</tr>
<tr>
<th><i class="fas fa-code-branch me-2"></i>版本</th>
<td><span class="badge bg-info">v{{ content.version }}</span></td>
</tr>
<tr>
<th><i class="fas fa-align-left me-2"></i>描述</th>
<td>{{ content.description or '无' }}</td>
</tr>
<tr>
<th><i class="fas fa-toggle-on me-2"></i>状态</th>
<td>
{% if content.is_active %}
<span class="badge bg-success"><i class="fas fa-check-circle me-1"></i>活跃</span>
{% else %}
<span class="badge bg-secondary"><i class="fas fa-pause-circle me-1"></i>禁用</span>
{% endif %}
</td>
</tr>
<tr>
<th><i class="fas fa-globe me-2"></i>时区</th>
<td>{{ content.timezone }}</td>
</tr>
<tr>
<th><i class="fas fa-clock me-2"></i>时间格式</th>
<td><code>{{ content.time_format }}</code></td>
</tr>
<tr>
<th><i class="fas fa-calendar-plus me-2"></i>创建时间</th>
<td>{{ content.created_at.strftime('%Y-%m-%d %H:%M:%S') }}</td>
</tr>
</table>
</div>
</div>
</div>
<!-- 内容操作 -->
<div class="col-lg-6 mb-4">
<div class="card shadow-sm">
<div class="card-header bg-white py-3">
<h6 class="m-0 font-weight-bold text-info"><i class="fas fa-cogs me-2"></i>内容操作</h6>
</div>
<div class="card-body">
<div class="d-grid gap-2">
<button class="btn btn-success" onclick="pushContent('{{ device.device_id }}', {{ content.version }})">
<i class="fas fa-paper-plane me-2"></i> 推送到设备
</button>
<button class="btn btn-warning" onclick="toggleContentStatus('{{ device.device_id }}', {{ content.version }})">
{% if content.is_active %}
<i class="fas fa-pause me-2"></i> 禁用内容
{% else %}
<i class="fas fa-play me-2"></i> 启用内容
{% endif %}
</button>
<a href="/admin/upload?device_id={{ device.device_id }}&version={{ content.version }}" class="btn btn-info">
<i class="fas fa-image me-2"></i> 上传图片
</a>
<button class="btn btn-danger" onclick="deleteContent('{{ device.device_id }}', {{ content.version }})">
<i class="fas fa-trash me-2"></i> 删除内容
</button>
</div>
<div class="mt-4">
<h6 class="mb-3"><i class="fas fa-chart-line me-2"></i>推送历史</h6>
<div class="list-group list-group-flush">
<div class="list-group-item px-0 d-flex justify-content-between align-items-center">
<span><i class="fas fa-history me-2"></i>最后推送时间</span>
<span class="badge bg-primary rounded-pill">{{ content.last_pushed_at.strftime('%Y-%m-%d %H:%M') if content.last_pushed_at else '从未' }}</span>
</div>
<div class="list-group-item px-0 d-flex justify-content-between align-items-center">
<span><i class="fas fa-sync-alt me-2"></i>推送次数</span>
<span class="badge bg-info rounded-pill">{{ content.push_count or 0 }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 图片预览 -->
{% if content.image_path %}
<div class="card shadow-sm mb-4">
<div class="card-header bg-white py-3">
<h6 class="m-0 font-weight-bold text-primary"><i class="fas fa-image me-2"></i>图片预览</h6>
</div>
<div class="card-body text-center">
<div class="position-relative d-inline-block">
<img src="{{ content.image_path }}" alt="内容图片" class="img-fluid rounded shadow-sm" style="max-height: 400px;">
<div class="position-absolute top-0 end-0 p-2">
<a href="{{ content.image_path }}" target="_blank" class="btn btn-sm btn-light bg-white rounded-circle">
<i class="fas fa-expand"></i>
</a>
</div>
</div>
<div class="mt-3">
<button class="btn btn-sm btn-outline-secondary" onclick="downloadImage('{{ content.image_path }}')">
<i class="fas fa-download me-1"></i> 下载图片
</button>
</div>
</div>
</div>
{% endif %}
<!-- 布局配置 -->
{% if layout_config %}
<div class="card shadow-sm mb-4">
<div class="card-header bg-white py-3">
<h6 class="m-0 font-weight-bold text-primary"><i class="fas fa-th-large me-2"></i>布局配置</h6>
</div>
<div class="card-body">
<div class="bg-light p-3 rounded">
<pre class="mb-0">{{ layout_config | tojson(indent=2) }}</pre>
</div>
<div class="mt-3">
<button class="btn btn-sm btn-outline-primary" onclick="copyLayoutConfig()">
<i class="fas fa-copy me-1"></i> 复制配置
</button>
</div>
</div>
</div>
{% endif %}
<!-- 设备信息 -->
<div class="card shadow-sm">
<div class="card-header bg-white py-3">
<h6 class="m-0 font-weight-bold text-primary"><i class="fas fa-tv me-2"></i>设备信息</h6>
</div>
<div class="card-body">
<table class="table table-borderless table-hover">
<tr>
<th width="30%"><i class="fas fa-barcode me-2"></i>设备ID</th>
<td><code>{{ device.device_id }}</code></td>
</tr>
<tr>
<th><i class="fas fa-tag me-2"></i>设备名称</th>
<td>{{ device.name }}</td>
</tr>
<tr>
<th><i class="fas fa-map-marker-alt me-2"></i>应用场景</th>
<td>{{ device.scene }}</td>
</tr>
<tr>
<th><i class="fas fa-toggle-on me-2"></i>状态</th>
<td>
{% if device.is_active %}
{% if device.is_online %}
<span class="badge bg-success"><i class="fas fa-wifi me-1"></i>在线</span>
{% else %}
<span class="badge bg-warning"><i class="fas fa-wifi-slash me-1"></i>离线</span>
{% endif %}
{% else %}
<span class="badge bg-secondary"><i class="fas fa-ban me-1"></i>禁用</span>
{% endif %}
</td>
</tr>
<tr>
<th><i class="fas fa-clock me-2"></i>最后上线</th>
<td>{{ device.last_online.strftime('%Y-%m-%d %H:%M:%S') if device.last_online else '从未' }}</td>
</tr>
</table>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
function pushContent(deviceId, version) {
if (confirm('确定要推送此内容到设备吗?')) {
fetch(`/api/devices/${deviceId}/update`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
version: version
})
})
.then(response => response.json())
.then(data => {
showToast('内容推送成功', 'success');
})
.catch(error => {
console.error('Error:', error);
showToast('内容推送失败', 'error');
});
}
}
function toggleContentStatus(deviceId, version) {
const action = event.target.textContent.includes('启用') ? '启用' : '禁用';
if (confirm(`确定要${action}此内容吗?`)) {
fetch(`/api/contents/${deviceId}/${version}/toggle`, {
method: 'POST'
})
.then(response => response.json())
.then(data => {
showToast(`内容${action}成功`, 'success');
location.reload();
})
.catch(error => {
console.error('Error:', error);
showToast(`内容${action}失败`, 'error');
});
}
}
function deleteContent(deviceId, version) {
if (confirm('确定要删除此内容吗?此操作不可恢复!')) {
fetch(`/api/contents/${deviceId}/${version}`, {
method: 'DELETE'
})
.then(response => response.json())
.then(data => {
showToast('内容删除成功', 'success');
window.location.href = `/admin/devices/${deviceId}`;
})
.catch(error => {
console.error('Error:', error);
showToast('内容删除失败', 'error');
});
}
}
function downloadImage(imagePath) {
const link = document.createElement('a');
link.href = imagePath;
link.download = imagePath.split('/').pop();
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
function copyLayoutConfig() {
const configText = document.querySelector('pre').textContent;
navigator.clipboard.writeText(configText).then(() => {
showToast('布局配置已复制到剪贴板', 'success');
}).catch(err => {
console.error('复制失败:', err);
showToast('复制失败', 'error');
});
}
function showToast(message, type) {
// 创建一个简单的toast通知而不是使用alert
const toast = document.createElement('div');
toast.className = `alert alert-${type === 'success' ? 'success' : 'danger'} position-fixed top-0 end-0 m-3`;
toast.style.zIndex = '1050';
toast.innerHTML = `<i class="fas fa-${type === 'success' ? 'check-circle' : 'exclamation-circle'} me-2"></i>${message}`;
document.body.appendChild(toast);
// 3秒后自动移除
setTimeout(() => {
toast.style.opacity = '0';
toast.style.transition = 'opacity 0.5s';
setTimeout(() => {
document.body.removeChild(toast);
}, 500);
}, 3000);
}
</script>
{% endblock %}

View File

@@ -0,0 +1,236 @@
{% extends "admin/base.html" %}
{% block title %}内容管理 - 墨水屏管理系统{% endblock %}
{% block content %}
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
<h1 class="h2"><i class="fas fa-file-alt me-2"></i>内容管理</h1>
<div class="btn-toolbar mb-2 mb-md-0">
<div class="btn-group me-2">
<a href="/admin/contents/add" class="btn btn-primary">
<i class="fas fa-plus me-1"></i> 添加内容
</a>
<a href="/admin/upload" class="btn btn-success">
<i class="fas fa-upload me-1"></i> 上传图片
</a>
</div>
</div>
</div>
<!-- 设备筛选 -->
<div class="row mb-3">
<div class="col-12">
<div class="card shadow-sm">
<div class="card-header bg-white py-3">
<h6 class="m-0 font-weight-bold text-primary">筛选条件</h6>
</div>
<div class="card-body">
<form method="get" class="row g-3">
<div class="col-md-6">
<label for="device_id" class="form-label">按设备筛选</label>
<select class="form-select" id="device_id" name="device_id" onchange="this.form.submit()">
<option value="">所有设备</option>
{% for device in devices %}
<option value="{{ device.device_id }}" {% if filtered and device.device_id == device_id %}selected{% endif %}>
{{ device.name }} ({{ device.device_id }})
</option>
{% endfor %}
</select>
</div>
<div class="col-md-3">
<label for="content_type" class="form-label">内容类型</label>
<select class="form-select" id="content_type" name="content_type" onchange="this.form.submit()">
<option value="">全部类型</option>
<option value="image" {% if content_type == 'image' %}selected{% endif %}>图片内容</option>
<option value="text" {% if content_type == 'text' %}selected{% endif %}>文本内容</option>
</select>
</div>
<div class="col-md-3">
<label for="status" class="form-label">状态</label>
<select class="form-select" id="status" name="status" onchange="this.form.submit()">
<option value="">全部状态</option>
<option value="active" {% if status == 'active' %}selected{% endif %}>活跃</option>
<option value="inactive" {% if status == 'inactive' %}selected{% endif %}>禁用</option>
</select>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- 内容列表 -->
{% if content_list %}
<div class="card shadow-sm">
<div class="card-header bg-white py-3">
<h6 class="m-0 font-weight-bold text-primary">内容列表</h6>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover align-middle table-sortable" data-sort-order="asc">
<thead>
<tr>
<th data-sort="device">设备 <i class="fas fa-sort text-muted"></i></th>
<th data-sort="title">标题 <i class="fas fa-sort text-muted"></i></th>
<th data-sort="version">版本 <i class="fas fa-sort text-muted"></i></th>
<th data-sort="type">类型 <i class="fas fa-sort text-muted"></i></th>
<th data-sort="status">状态 <i class="fas fa-sort text-muted"></i></th>
<th data-sort="created_at">创建时间 <i class="fas fa-sort text-muted"></i></th>
<th>操作</th>
</tr>
</thead>
<tbody>
{% for item in content_list %}
<tr>
<td data-column="device">
<a href="/admin/devices/{{ item.content.device_id }}" class="text-decoration-none">
<i class="fas fa-mobile-alt me-1"></i>
{{ item.device.name if item.device else item.content.device_id }}
</a>
</td>
<td data-column="title">
<a href="/admin/devices/{{ item.content.device_id }}/contents/{{ item.content.version }}" class="text-decoration-none fw-bold">
{{ item.content.title }}
</a>
</td>
<td data-column="version">
<span class="badge bg-light text-dark">v{{ item.content.version }}</span>
</td>
<td data-column="type">
{% if item.content.image_path %}
<span class="badge bg-info">
<i class="fas fa-image me-1"></i>图片
</span>
{% else %}
<span class="badge bg-secondary">
<i class="fas fa-font me-1"></i>文本
</span>
{% endif %}
</td>
<td data-column="status">
{% if item.content.is_active %}
<span class="badge bg-success">
<i class="fas fa-check-circle me-1"></i>活跃
</span>
{% else %}
<span class="badge bg-secondary">
<i class="fas fa-times-circle me-1"></i>禁用
</span>
{% endif %}
</td>
<td data-column="created_at">
<span title="{{ item.content.created_at.strftime('%Y-%m-%d %H:%M:%S') }}">
{{ item.content.created_at.strftime('%Y-%m-%d %H:%M') }}
</span>
</td>
<td>
<div class="btn-group btn-group-sm" role="group">
<a href="/admin/devices/{{ item.content.device_id }}/contents/{{ item.content.version }}" class="btn btn-outline-info" title="查看详情">
<i class="fas fa-eye"></i>
</a>
<button class="btn btn-outline-success" onclick="pushContent('{{ item.content.device_id }}', {{ item.content.version }})" title="推送到设备">
<i class="fas fa-paper-plane"></i>
</button>
<button class="btn btn-outline-warning" onclick="duplicateContent('{{ item.content.device_id }}', {{ item.content.version }})" title="复制内容">
<i class="fas fa-copy"></i>
</button>
<button class="btn btn-outline-danger" onclick="deleteContent('{{ item.content.device_id }}', {{ item.content.version }})" title="删除内容">
<i class="fas fa-trash"></i>
</button>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% else %}
<div class="card shadow-sm">
<div class="card-body text-center py-5">
<i class="fas fa-file-alt fa-4x text-muted mb-4"></i>
<h5 class="card-title">暂无内容</h5>
<p class="card-text">
{% if filtered %}
该设备还没有任何内容。
<a href="/admin/contents/add?device_id={{ device_id }}" class="btn btn-primary btn-lg">
<i class="fas fa-plus me-2"></i> 添加内容
</a>
{% else %}
系统中还没有任何内容。
<a href="/admin/contents/add" class="btn btn-primary btn-lg">
<i class="fas fa-plus me-2"></i> 添加第一个内容
</a>
{% endif %}
</p>
</div>
</div>
{% endif %}
{% endblock %}
{% block scripts %}
<script>
function pushContent(deviceId, version) {
if (confirm('确定要推送此内容到设备吗?')) {
fetch(`/api/devices/${deviceId}/update`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
version: version
})
})
.then(response => response.json())
.then(data => {
alert('内容推送成功');
})
.catch(error => {
console.error('Error:', error);
alert('内容推送失败');
});
}
}
function duplicateContent(deviceId, version) {
if (confirm('确定要复制此内容吗?')) {
fetch(`/api/devices/${deviceId}/contents/${version}/duplicate`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.then(data => {
alert('内容复制成功');
window.location.reload();
})
.catch(error => {
console.error('Error:', error);
alert('内容复制失败');
});
}
}
function deleteContent(deviceId, version) {
if (confirm('确定要删除此内容吗?此操作不可恢复!')) {
fetch(`/api/devices/${deviceId}/contents/${version}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.then(data => {
alert('内容删除成功');
window.location.reload();
})
.catch(error => {
console.error('Error:', error);
alert('内容删除失败');
});
}
}
</script>
{% endblock %}

View File

@@ -0,0 +1,470 @@
{% extends "admin/base.html" %}
{% block title %}仪表盘 - 墨水屏管理系统{% endblock %}
{% block content %}
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
<h1 class="h2"><i class="fas fa-chart-line me-2"></i>仪表盘</h1>
<div class="btn-toolbar mb-2 mb-md-0">
<div class="btn-group me-2">
<button type="button" class="btn btn-sm btn-outline-secondary" id="refreshDashboard">
<i class="fas fa-sync-alt"></i> 刷新
</button>
<button type="button" class="btn btn-sm btn-outline-primary" id="themeToggle" title="切换主题">
<i class="fas fa-snowflake"></i> 圣诞主题
</button>
</div>
</div>
</div>
<!-- 统计卡片 -->
<div class="row mb-4">
<div class="col-xl-3 col-md-6 mb-4">
<div class="card card-stats bg-primary text-white shadow-sm">
<div class="card-body">
<div class="row">
<div class="col-5">
<div class="icon-big text-center">
<i class="fas fa-desktop"></i>
</div>
</div>
<div class="col-7">
<div class="numbers">
<p class="card-category">设备总数</p>
<h3 class="card-title">{{ device_count }}</h3>
</div>
</div>
</div>
</div>
<div class="card-footer">
<div class="stats">
<i class="fas fa-clock"></i> 最近更新: {{ last_update or '刚刚' }}
</div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6 mb-4">
<div class="card card-stats bg-success text-white shadow-sm">
<div class="card-body">
<div class="row">
<div class="col-5">
<div class="icon-big text-center">
<i class="fas fa-wifi"></i>
</div>
</div>
<div class="col-7">
<div class="numbers">
<p class="card-category">活跃设备</p>
<h3 class="card-title">{{ active_device_count }}</h3>
</div>
</div>
</div>
</div>
<div class="card-footer">
<div class="stats">
<i class="fas fa-signal"></i> 在线率: {{ ((active_device_count / device_count * 100) | round(1) if device_count > 0 else 0) }}%
</div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6 mb-4">
<div class="card card-stats bg-info text-white shadow-sm">
<div class="card-body">
<div class="row">
<div class="col-5">
<div class="icon-big text-center">
<i class="fas fa-images"></i>
</div>
</div>
<div class="col-7">
<div class="numbers">
<p class="card-category">内容总数</p>
<h3 class="card-title">{{ content_count }}</h3>
</div>
</div>
</div>
</div>
<div class="card-footer">
<div class="stats">
<i class="fas fa-database"></i> 存储空间: {{ storage_usage or '未知' }}
</div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6 mb-4">
<div class="card card-stats bg-warning text-white shadow-sm">
<div class="card-body">
<div class="row">
<div class="col-5">
<div class="icon-big text-center">
<i class="fas fa-play-circle"></i>
</div>
</div>
<div class="col-7">
<div class="numbers">
<p class="card-category">活跃内容</p>
<h3 class="card-title">{{ active_content_count }}</h3>
</div>
</div>
</div>
</div>
<div class="card-footer">
<div class="stats">
<i class="fas fa-chart-line"></i> 活跃率: {{ ((active_content_count / content_count * 100) | round(1) if content_count > 0 else 0) }}%
</div>
</div>
</div>
</div>
</div>
<div class="row">
<!-- 最近上线的设备 -->
<div class="col-lg-6 mb-4">
<div class="card shadow-sm">
<div class="card-header bg-white py-3 d-flex flex-row align-items-center justify-content-between">
<h6 class="m-0 font-weight-bold text-primary">
<i class="fas fa-history me-2"></i>最近上线的设备
</h6>
<a href="/admin/devices" class="btn btn-sm btn-primary">
<i class="fas fa-list me-1"></i>查看全部
</a>
</div>
<div class="card-body">
{% if recent_devices %}
<div class="table-responsive">
<table class="table table-hover">
<thead class="table-light">
<tr>
<th><i class="fas fa-barcode me-1"></i>设备ID</th>
<th><i class="fas fa-tag me-1"></i>名称</th>
<th><i class="fas fa-toggle-on me-1"></i>状态</th>
<th><i class="fas fa-clock me-1"></i>最后上线</th>
</tr>
</thead>
<tbody>
{% for device in recent_devices %}
<tr>
<td><a href="/admin/devices/{{ device.device_id }}" class="text-decoration-none"><code>{{ device.device_id }}</code></a></td>
<td>{{ device.name }}</td>
<td>
{% if device.is_online %}
<span class="badge bg-success"><i class="fas fa-wifi me-1"></i>在线</span>
{% else %}
<span class="badge bg-secondary"><i class="fas fa-wifi-slash me-1"></i>离线</span>
{% endif %}
</td>
<td>{{ device.last_online.strftime('%Y-%m-%d %H:%M') if device.last_online else '从未' }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="text-center py-4">
<i class="fas fa-inbox fa-3x text-muted mb-3"></i>
<p class="text-muted">暂无设备</p>
</div>
{% endif %}
</div>
</div>
</div>
<!-- 最近创建的内容 -->
<div class="col-lg-6 mb-4">
<div class="card shadow-sm">
<div class="card-header bg-white py-3 d-flex flex-row align-items-center justify-content-between">
<h6 class="m-0 font-weight-bold text-primary">
<i class="fas fa-clock me-2"></i>最近创建的内容
</h6>
<a href="/admin/contents" class="btn btn-sm btn-primary">
<i class="fas fa-list me-1"></i>查看全部
</a>
</div>
<div class="card-body">
{% if recent_contents %}
<div class="table-responsive">
<table class="table table-hover">
<thead class="table-light">
<tr>
<th><i class="fas fa-barcode me-1"></i>设备ID</th>
<th><i class="fas fa-heading me-1"></i>标题</th>
<th><i class="fas fa-code-branch me-1"></i>版本</th>
<th><i class="fas fa-calendar me-1"></i>创建时间</th>
</tr>
</thead>
<tbody>
{% for content in recent_contents %}
<tr>
<td><a href="/admin/devices/{{ content.device_id }}" class="text-decoration-none"><code>{{ content.device_id }}</code></a></td>
<td><a href="/admin/devices/{{ content.device_id }}/contents/{{ content.version }}" class="text-decoration-none">{{ content.title }}</a></td>
<td><span class="badge bg-info">v{{ content.version }}</span></td>
<td>{{ content.created_at.strftime('%Y-%m-%d %H:%M') }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="text-center py-4">
<i class="fas fa-inbox fa-3x text-muted mb-3"></i>
<p class="text-muted">暂无内容</p>
</div>
{% endif %}
</div>
</div>
</div>
</div>
<!-- 快捷操作 -->
<div class="row">
<div class="col-12">
<div class="card shadow-sm">
<div class="card-header bg-white py-3">
<h6 class="m-0 font-weight-bold text-primary">
<i class="fas fa-rocket me-2"></i>快捷操作
</h6>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-3 mb-3">
<a href="/admin/devices/add" class="btn btn-primary btn-lg btn-block">
<i class="fas fa-plus me-2"></i> 添加设备
</a>
</div>
<div class="col-md-3 mb-3">
<a href="/admin/contents/add" class="btn btn-info btn-lg btn-block">
<i class="fas fa-plus me-2"></i> 添加内容
</a>
</div>
<div class="col-md-3 mb-3">
<a href="/admin/upload" class="btn btn-success btn-lg btn-block">
<i class="fas fa-upload me-2"></i> 上传图片
</a>
</div>
<div class="col-md-3 mb-3">
<a href="/admin/devices" class="btn btn-warning btn-lg btn-block">
<i class="fas fa-tv me-2"></i> 设备管理
</a>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 系统状态 -->
<div class="row mt-4">
<div class="col-12">
<div class="card shadow-sm">
<div class="card-header bg-white py-3">
<h6 class="m-0 font-weight-bold text-primary">
<i class="fas fa-heartbeat me-2"></i>系统状态
</h6>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-4 mb-3">
<div class="d-flex align-items-center">
<div class="flex-shrink-0">
<i class="fas fa-database fa-2x text-primary"></i>
</div>
<div class="flex-grow-1 ms-3">
<h6 class="mb-1">数据库</h6>
<div class="progress" style="height: 8px;">
<div class="progress-bar bg-success" role="progressbar" style="width: 100%" aria-valuenow="100" aria-valuemin="0" aria-valuemax="100"></div>
</div>
<small class="text-muted">连接正常</small>
</div>
</div>
</div>
<div class="col-md-4 mb-3">
<div class="d-flex align-items-center">
<div class="flex-shrink-0">
<i class="fas fa-network-wired fa-2x text-info"></i>
</div>
<div class="flex-grow-1 ms-3">
<h6 class="mb-1">MQTT服务</h6>
<div class="progress" style="height: 8px;">
<div class="progress-bar bg-success" role="progressbar" style="width: 100%" aria-valuenow="100" aria-valuemin="0" aria-valuemax="100"></div>
</div>
<small class="text-muted">运行中</small>
</div>
</div>
</div>
<div class="col-md-4 mb-3">
<div class="d-flex align-items-center">
<div class="flex-shrink-0">
<i class="fas fa-microchip fa-2x text-warning"></i>
</div>
<div class="flex-grow-1 ms-3">
<h6 class="mb-1">存储空间</h6>
<div class="progress" style="height: 8px;">
<div class="progress-bar bg-warning" role="progressbar" style="width: 45%" aria-valuenow="45" aria-valuemin="0" aria-valuemax="100"></div>
</div>
<small class="text-muted">已使用 45%</small>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
// 刷新仪表盘
document.getElementById('refreshDashboard').addEventListener('click', function() {
const icon = this.querySelector('i');
icon.classList.add('fa-spin');
setTimeout(() => {
location.reload();
}, 500);
});
// 主题切换
document.getElementById('themeToggle').addEventListener('click', function() {
const body = document.body;
const isChristmas = body.classList.contains('christmas-theme');
if (isChristmas) {
body.classList.remove('christmas-theme');
localStorage.setItem('theme', 'default');
this.innerHTML = '<i class="fas fa-snowflake"></i> 圣诞主题';
showToast('已切换到默认主题', 'info');
} else {
body.classList.add('christmas-theme');
localStorage.setItem('theme', 'christmas');
this.innerHTML = '<i class="fas fa-sun"></i> 默认主题';
showToast('已切换到圣诞主题', 'success');
// 添加圣诞特效
addChristmasEffects();
}
});
// 页面加载时检查主题设置
document.addEventListener('DOMContentLoaded', function() {
const savedTheme = localStorage.getItem('theme');
if (savedTheme === 'christmas') {
document.body.classList.add('christmas-theme');
document.getElementById('themeToggle').innerHTML = '<i class="fas fa-sun"></i> 默认主题';
addChristmasEffects();
}
});
// 添加圣诞特效
function addChristmasEffects() {
// 创建雪花效果
createSnowflakes();
// 添加圣诞装饰
addChristmasDecorations();
}
// 创建雪花效果
function createSnowflakes() {
// 如果已经存在雪花容器,先移除
const existingSnowflakes = document.getElementById('snowflakes-container');
if (existingSnowflakes) {
existingSnowflakes.remove();
}
// 创建雪花容器
const snowflakesContainer = document.createElement('div');
snowflakesContainer.id = 'snowflakes-container';
snowflakesContainer.style.position = 'fixed';
snowflakesContainer.style.top = '0';
snowflakesContainer.style.left = '0';
snowflakesContainer.style.width = '100%';
snowflakesContainer.style.height = '100%';
snowflakesContainer.style.pointerEvents = 'none';
snowflakesContainer.style.zIndex = '999';
snowflakesContainer.style.overflow = 'hidden';
// 创建雪花
for (let i = 0; i < 50; i++) {
const snowflake = document.createElement('div');
snowflake.className = 'snowflake';
snowflake.innerHTML = '❄';
snowflake.style.position = 'absolute';
snowflake.style.top = Math.random() * 100 + '%';
snowflake.style.left = Math.random() * 100 + '%';
snowflake.style.fontSize = Math.random() * 10 + 10 + 'px';
snowflake.style.opacity = Math.random() * 0.7 + 0.3;
snowflake.style.animation = `fall ${Math.random() * 5 + 5}s linear infinite`;
snowflake.style.animationDelay = Math.random() * 5 + 's';
snowflakesContainer.appendChild(snowflake);
}
document.body.appendChild(snowflakesContainer);
// 添加雪花下落动画
const style = document.createElement('style');
style.textContent = `
@keyframes fall {
from { transform: translateY(-100px); }
to { transform: translateY(calc(100vh + 100px)); }
}
`;
document.head.appendChild(style);
}
// 添加圣诞装饰
function addChristmasDecorations() {
// 在页面顶部添加圣诞装饰横幅
const header = document.querySelector('nav.navbar');
if (header && !header.classList.contains('christmas-decorated')) {
header.classList.add('christmas-decorated');
// 创建圣诞装饰
const decorations = document.createElement('div');
decorations.className = 'christmas-decorations';
decorations.style.position = 'absolute';
decorations.style.top = '0';
decorations.style.left = '0';
decorations.style.width = '100%';
decorations.style.height = '10px';
decorations.style.background = 'linear-gradient(90deg, #ff0000, #00ff00, #ff0000, #00ff00, #ff0000)';
decorations.style.zIndex = '10';
header.style.position = 'relative';
header.appendChild(decorations);
// 添加圣诞帽到logo
const logo = document.querySelector('.navbar-brand');
if (logo && !logo.querySelector('.christmas-hat')) {
const hat = document.createElement('span');
hat.className = 'christmas-hat';
hat.innerHTML = '🎅';
hat.style.marginLeft = '5px';
logo.appendChild(hat);
}
}
}
// Toast通知函数
function showToast(message, type) {
const toast = document.createElement('div');
toast.className = `alert alert-${type === 'success' ? 'success' : type === 'warning' ? 'warning' : type === 'info' ? 'info' : 'danger'} position-fixed top-0 end-0 m-3`;
toast.style.zIndex = '1050';
toast.innerHTML = `<i class="fas fa-${type === 'success' ? 'check-circle' : type === 'warning' ? 'exclamation-triangle' : type === 'info' ? 'info-circle' : 'exclamation-circle'} me-2"></i>${message}`;
document.body.appendChild(toast);
// 3秒后自动移除
setTimeout(() => {
toast.style.opacity = '0';
toast.style.transition = 'opacity 0.5s';
setTimeout(() => {
document.body.removeChild(toast);
}, 500);
}, 3000);
}
</script>
{% endblock %}

View File

@@ -0,0 +1,277 @@
{% extends "admin/base.html" %}
{% block title %}设备详情 - {{ device.name }} - 墨水屏管理系统{% endblock %}
{% block content %}
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
<h1 class="h2"><i class="fas fa-tv me-2"></i>设备详情 - {{ device.name }}</h1>
<div class="btn-toolbar mb-2 mb-md-0">
<a href="/admin/devices" class="btn btn-secondary me-2">
<i class="fas fa-arrow-left me-1"></i> 返回设备列表
</a>
<a href="/admin/devices/{{ device.device_id }}/contents/add" class="btn btn-primary">
<i class="fas fa-plus me-1"></i> 添加内容
</a>
</div>
</div>
<div class="row">
<!-- 设备信息 -->
<div class="col-lg-6 mb-4">
<div class="card shadow-sm">
<div class="card-header bg-white py-3">
<h6 class="m-0 font-weight-bold text-primary"><i class="fas fa-info-circle me-2"></i>设备信息</h6>
</div>
<div class="card-body">
<table class="table table-borderless table-hover">
<tr>
<th width="30%"><i class="fas fa-barcode me-2"></i>设备ID</th>
<td><code>{{ device.device_id }}</code></td>
</tr>
<tr>
<th><i class="fas fa-tag me-2"></i>设备名称</th>
<td>{{ device.name }}</td>
</tr>
<tr>
<th><i class="fas fa-map-marker-alt me-2"></i>应用场景</th>
<td>{{ device.scene }}</td>
</tr>
<tr>
<th><i class="fas fa-toggle-on me-2"></i>状态</th>
<td>
{% if device.is_active %}
{% if device.is_online %}
<span class="badge bg-success"><i class="fas fa-wifi me-1"></i>在线</span>
{% else %}
<span class="badge bg-warning"><i class="fas fa-wifi-slash me-1"></i>离线</span>
{% endif %}
{% else %}
<span class="badge bg-secondary"><i class="fas fa-ban me-1"></i>禁用</span>
{% endif %}
</td>
</tr>
<tr>
<th><i class="fas fa-clock me-2"></i>最后上线</th>
<td>{{ device.last_online.strftime('%Y-%m-%d %H:%M:%S') if device.last_online else '从未' }}</td>
</tr>
<tr>
<th><i class="fas fa-calendar-plus me-2"></i>创建时间</th>
<td>{{ device.created_at.strftime('%Y-%m-%d %H:%M:%S') }}</td>
</tr>
</table>
</div>
</div>
</div>
<!-- 设备操作 -->
<div class="col-lg-6 mb-4">
<div class="card shadow-sm">
<div class="card-header bg-white py-3">
<h6 class="m-0 font-weight-bold text-info"><i class="fas fa-cogs me-2"></i>设备操作</h6>
</div>
<div class="card-body">
<div class="d-grid gap-2">
<a href="/admin/devices/{{ device.device_id }}/contents/add" class="btn btn-primary">
<i class="fas fa-plus me-2"></i> 添加内容
</a>
<a href="/admin/upload?device_id={{ device.device_id }}" class="btn btn-success">
<i class="fas fa-upload me-2"></i> 上传图片
</a>
<button class="btn btn-warning" onclick="refreshDevice('{{ device.device_id }}')">
<i class="fas fa-sync me-2"></i> 刷新设备状态
</button>
<button class="btn btn-info" onclick="rebootDevice('{{ device.device_id }}')">
<i class="fas fa-power-off me-2"></i> 重启设备
</button>
</div>
<div class="mt-4">
<h6 class="mb-3"><i class="fas fa-chart-line me-2"></i>设备统计</h6>
<div class="list-group list-group-flush">
<div class="list-group-item px-0 d-flex justify-content-between align-items-center">
<span><i class="fas fa-file-alt me-2"></i>内容总数</span>
<span class="badge bg-primary rounded-pill">{{ contents|length }}</span>
</div>
<div class="list-group-item px-0 d-flex justify-content-between align-items-center">
<span><i class="fas fa-check-circle me-2"></i>活跃内容</span>
<span class="badge bg-success rounded-pill">{{ contents|selectattr('is_active')|list|length }}</span>
</div>
<div class="list-group-item px-0 d-flex justify-content-between align-items-center">
<span><i class="fas fa-image me-2"></i>图片内容</span>
<span class="badge bg-info rounded-pill">{{ contents|selectattr('image_path')|list|length }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 内容列表 -->
<div class="card shadow-sm">
<div class="card-header bg-white py-3 d-flex flex-row align-items-center justify-content-between">
<h6 class="m-0 font-weight-bold text-primary"><i class="fas fa-file-alt me-2"></i>内容列表</h6>
<div>
<button class="btn btn-sm btn-outline-secondary me-2" onclick="toggleContentList()">
<i class="fas fa-compress-alt me-1"></i> 折叠/展开
</button>
<a href="/admin/devices/{{ device.device_id }}/contents/add" class="btn btn-sm btn-primary">
<i class="fas fa-plus me-1"></i> 添加内容
</a>
</div>
</div>
<div class="card-body" id="contentList">
{% if contents %}
<div class="table-responsive">
<table class="table table-hover">
<thead class="table-light">
<tr>
<th><i class="fas fa-code-branch me-1"></i>版本</th>
<th><i class="fas fa-heading me-1"></i>标题</th>
<th><i class="fas fa-file-image me-1"></i>类型</th>
<th><i class="fas fa-toggle-on me-1"></i>状态</th>
<th><i class="fas fa-calendar me-1"></i>创建时间</th>
<th><i class="fas fa-cogs me-1"></i>操作</th>
</tr>
</thead>
<tbody>
{% for content in contents %}
<tr>
<td><span class="badge bg-info">v{{ content.version }}</span></td>
<td><a href="/admin/devices/{{ device.device_id }}/contents/{{ content.version }}" class="text-decoration-none">{{ content.title }}</a></td>
<td>
{% if content.image_path %}
<span class="badge bg-info"><i class="fas fa-image me-1"></i>图片</span>
{% else %}
<span class="badge bg-secondary"><i class="fas fa-font me-1"></i>文本</span>
{% endif %}
</td>
<td>
{% if content.is_active %}
<span class="badge bg-success"><i class="fas fa-check-circle me-1"></i>活跃</span>
{% else %}
<span class="badge bg-secondary"><i class="fas fa-pause-circle me-1"></i>禁用</span>
{% endif %}
</td>
<td>{{ content.created_at.strftime('%Y-%m-%d %H:%M') }}</td>
<td>
<div class="btn-group btn-group-sm" role="group">
<a href="/admin/devices/{{ device.device_id }}/contents/{{ content.version }}" class="btn btn-outline-info" title="查看">
<i class="fas fa-eye"></i>
</a>
<button class="btn btn-outline-success" onclick="pushContent('{{ device.device_id }}', {{ content.version }})" title="推送">
<i class="fas fa-paper-plane"></i>
</button>
<a href="/admin/upload?device_id={{ device.device_id }}&version={{ content.version }}" class="btn btn-outline-primary" title="上传图片">
<i class="fas fa-image"></i>
</a>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="text-center py-5">
<i class="fas fa-folder-open fa-3x text-muted mb-3"></i>
<h5>暂无内容</h5>
<p class="text-muted">此设备还没有任何内容。</p>
<a href="/admin/devices/{{ device.device_id }}/contents/add" class="btn btn-primary">
<i class="fas fa-plus me-1"></i> 添加第一个内容
</a>
</div>
{% endif %}
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
function refreshDevice(deviceId) {
showToast('正在刷新设备状态...', 'info');
fetch(`/api/devices/${deviceId}/status`, {
method: 'GET'
})
.then(response => response.json())
.then(data => {
if (data.online) {
showToast('设备在线', 'success');
} else {
showToast('设备离线', 'warning');
}
location.reload();
})
.catch(error => {
console.error('Error:', error);
showToast('获取设备状态失败', 'error');
});
}
function rebootDevice(deviceId) {
if (confirm('确定要重启设备吗?设备可能需要几分钟才能重新上线。')) {
showToast('正在发送重启命令...', 'info');
fetch(`/api/devices/${deviceId}/reboot`, {
method: 'POST'
})
.then(response => response.json())
.then(data => {
showToast('重启命令已发送', 'success');
})
.catch(error => {
console.error('Error:', error);
showToast('发送重启命令失败', 'error');
});
}
}
function pushContent(deviceId, version) {
if (confirm('确定要推送此内容到设备吗?')) {
showToast('正在推送内容...', 'info');
fetch(`/api/devices/${deviceId}/update`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
version: version
})
})
.then(response => response.json())
.then(data => {
showToast('内容推送成功', 'success');
})
.catch(error => {
console.error('Error:', error);
showToast('内容推送失败', 'error');
});
}
}
function toggleContentList() {
const contentList = document.getElementById('contentList');
contentList.classList.toggle('d-none');
}
function showToast(message, type) {
// 创建一个简单的toast通知而不是使用alert
const toast = document.createElement('div');
toast.className = `alert alert-${type === 'success' ? 'success' : type === 'warning' ? 'warning' : type === 'info' ? 'info' : 'danger'} position-fixed top-0 end-0 m-3`;
toast.style.zIndex = '1050';
toast.innerHTML = `<i class="fas fa-${type === 'success' ? 'check-circle' : type === 'warning' ? 'exclamation-triangle' : type === 'info' ? 'info-circle' : 'exclamation-circle'} me-2"></i>${message}`;
document.body.appendChild(toast);
// 3秒后自动移除
setTimeout(() => {
toast.style.opacity = '0';
toast.style.transition = 'opacity 0.5s';
setTimeout(() => {
document.body.removeChild(toast);
}, 500);
}, 3000);
}
</script>
{% endblock %}

View File

@@ -0,0 +1,148 @@
{% extends "admin/base.html" %}
{% block title %}设备管理 - 墨水屏管理系统{% endblock %}
{% block content %}
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
<h1 class="h2"><i class="fas fa-mobile-alt me-2"></i>设备管理</h1>
<div class="btn-toolbar mb-2 mb-md-0">
<div class="btn-group me-2">
<button type="button" class="btn btn-sm btn-outline-secondary dropdown-toggle" data-bs-toggle="dropdown">
<i class="fas fa-filter me-1"></i> 筛选
</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="#" data-filter="all">全部设备</a></li>
<li><a class="dropdown-item" href="#" data-filter="online">在线设备</a></li>
<li><a class="dropdown-item" href="#" data-filter="offline">离线设备</a></li>
<li><a class="dropdown-item" href="#" data-filter="disabled">禁用设备</a></li>
</ul>
</div>
<a href="/admin/devices/add" class="btn btn-primary">
<i class="fas fa-plus me-1"></i> 添加设备
</a>
</div>
</div>
{% if devices %}
<div class="card shadow-sm">
<div class="card-header bg-white py-3">
<h6 class="m-0 font-weight-bold text-primary">设备列表</h6>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover align-middle table-sortable" data-sort-order="asc">
<thead>
<tr>
<th data-sort="device_id">设备ID <i class="fas fa-sort text-muted"></i></th>
<th data-sort="name">名称 <i class="fas fa-sort text-muted"></i></th>
<th data-sort="scene">场景 <i class="fas fa-sort text-muted"></i></th>
<th data-sort="status">状态 <i class="fas fa-sort text-muted"></i></th>
<th data-sort="last_online">最后上线 <i class="fas fa-sort text-muted"></i></th>
<th>操作</th>
</tr>
</thead>
<tbody>
{% for device in devices %}
<tr data-status="{% if device.is_active %}{% if device.is_online %}online{% else %}offline{% endif %}{% else %}disabled{% endif %}">
<td data-column="device_id">
<a href="/admin/devices/{{ device.device_id }}" class="text-decoration-none fw-bold">
{{ device.device_id }}
</a>
</td>
<td data-column="name">{{ device.name }}</td>
<td data-column="scene">
<span class="badge bg-light text-dark">{{ device.scene }}</span>
</td>
<td data-column="status">
{% if device.is_active %}
{% if device.is_online %}
<span class="badge bg-success">
<i class="fas fa-circle me-1" style="font-size: 0.5rem;"></i>在线
</span>
{% else %}
<span class="badge bg-warning">
<i class="fas fa-circle me-1" style="font-size: 0.5rem;"></i>离线
</span>
{% endif %}
{% else %}
<span class="badge bg-secondary">
<i class="fas fa-circle me-1" style="font-size: 0.5rem;"></i>禁用
</span>
{% endif %}
</td>
<td data-column="last_online">
{% if device.last_online %}
<span title="{{ device.last_online.strftime('%Y-%m-%d %H:%M:%S') }}">
{{ device.last_online.strftime('%Y-%m-%d %H:%M') }}
</span>
{% else %}
<span class="text-muted">从未</span>
{% endif %}
</td>
<td>
<div class="btn-group btn-group-sm" role="group">
<a href="/admin/devices/{{ device.device_id }}" class="btn btn-outline-info" title="查看详情">
<i class="fas fa-eye"></i>
</a>
<a href="/admin/devices/{{ device.device_id }}/contents/add" class="btn btn-outline-success" title="添加内容">
<i class="fas fa-plus"></i>
</a>
<button class="btn btn-outline-primary" onclick="refreshDevice('{{ device.device_id }}')" title="刷新状态">
<i class="fas fa-sync-alt"></i>
</button>
<button class="btn btn-outline-danger" onclick="deleteDevice('{{ device.device_id }}')" title="删除设备">
<i class="fas fa-trash"></i>
</button>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% else %}
<div class="card shadow-sm">
<div class="card-body text-center py-5">
<i class="fas fa-mobile-alt fa-4x text-muted mb-4"></i>
<h5 class="card-title">暂无设备</h5>
<p class="card-text">您还没有添加任何设备。</p>
<a href="/admin/devices/add" class="btn btn-primary btn-lg">
<i class="fas fa-plus me-2"></i> 添加第一个设备
</a>
</div>
</div>
{% endif %}
{% endblock %}
{% block scripts %}
<script>
// 设备筛选功能
document.addEventListener('DOMContentLoaded', function() {
const filterButtons = document.querySelectorAll('[data-filter]');
const deviceRows = document.querySelectorAll('tbody tr');
filterButtons.forEach(button => {
button.addEventListener('click', function(e) {
e.preventDefault();
const filter = this.getAttribute('data-filter');
// 更新按钮状态
filterButtons.forEach(btn => btn.classList.remove('active'));
this.classList.add('active');
// 筛选设备
deviceRows.forEach(row => {
if (filter === 'all') {
row.style.display = '';
} else {
const status = row.getAttribute('data-status');
row.style.display = status === filter ? '' : 'none';
}
});
});
});
});
</script>
{% endblock %}

View File

@@ -0,0 +1,288 @@
{% extends "admin/base.html" %}
{% block title %}图片上传 - 墨水屏管理系统{% endblock %}
{% block content %}
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
<h1 class="h2"><i class="fas fa-image me-2"></i>图片上传</h1>
<div class="btn-toolbar mb-2 mb-md-0">
<a href="/admin/contents" class="btn btn-secondary">
<i class="fas fa-arrow-left me-1"></i> 返回内容列表
</a>
</div>
</div>
<div class="row">
<div class="col-lg-8">
<div class="card shadow-sm">
<div class="card-header bg-white py-3">
<h6 class="m-0 font-weight-bold text-primary">图片上传</h6>
</div>
<div class="card-body">
{% if error %}
<div class="alert alert-danger" role="alert">
<i class="fas fa-exclamation-triangle me-2"></i>{{ error }}
</div>
{% endif %}
<form method="post" enctype="multipart/form-data" id="uploadForm">
<div class="mb-3">
<label for="device_id" class="form-label">设备</label>
<select class="form-select" id="device_id" name="device_id" required>
<option value="">请选择设备</option>
{% for device in devices %}
<option value="{{ device.device_id }}">{{ device.name }} ({{ device.device_id }})</option>
{% endfor %}
</select>
<div class="form-text">选择要上传图片的设备</div>
</div>
<div class="mb-3">
<label for="version" class="form-label">内容版本</label>
<select class="form-select" id="version" name="version">
<option value="">创建新版本</option>
</select>
<div class="form-text">选择现有版本或创建新版本</div>
</div>
<div class="mb-3">
<label for="title" class="form-label">内容标题</label>
<input type="text" class="form-control" id="title" name="title" placeholder="输入内容标题">
<div class="form-text">为图片内容添加描述性标题</div>
</div>
<div class="mb-3">
<label for="image" class="form-label">图片文件</label>
<div class="upload-area" id="uploadArea">
<input type="file" class="form-control" id="image" name="image" accept="image/*" required>
<div class="upload-overlay">
<div class="upload-content">
<i class="fas fa-cloud-upload-alt fa-3x text-muted mb-3"></i>
<p class="mb-2">拖拽图片到此处或点击选择</p>
<p class="text-muted small">支持JPG、PNG、BMP等常见图片格式</p>
</div>
</div>
</div>
</div>
<!-- 图片预览 -->
<div class="mb-3" id="imagePreview" style="display: none;">
<label class="form-label">图片预览</label>
<div class="preview-container">
<img id="previewImg" src="#" alt="图片预览" class="img-fluid">
<div class="preview-actions">
<button type="button" class="btn btn-sm btn-outline-danger" id="removeImage">
<i class="fas fa-trash"></i> 移除
</button>
</div>
</div>
</div>
<div class="mb-3">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="autoPush" name="autoPush" checked>
<label class="form-check-label" for="autoPush">
上传后自动推送到设备
</label>
<div class="form-text">上传完成后自动将内容推送到设备</div>
</div>
</div>
<div class="mb-3">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="enhanceContrast" name="enhanceContrast" checked>
<label class="form-check-label" for="enhanceContrast">
增强图片对比度
</label>
<div class="form-text">自动优化图片对比度以适应墨水屏显示</div>
</div>
</div>
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
<a href="/admin/contents" class="btn btn-secondary me-md-2">
<i class="fas fa-times me-1"></i> 取消
</a>
<button type="submit" class="btn btn-primary" id="uploadBtn">
<i class="fas fa-upload me-1"></i> 上传图片
</button>
</div>
</form>
</div>
</div>
</div>
<div class="col-lg-4">
<div class="card shadow-sm mb-4">
<div class="card-header bg-white py-3">
<h6 class="m-0 font-weight-bold text-info">图片处理说明</h6>
</div>
<div class="card-body">
<h5 class="mb-3"><i class="fas fa-info-circle me-2"></i>支持的图片格式</h5>
<div class="row g-2 mb-4">
<div class="col-6"><span class="badge bg-light text-dark">JPEG/JPG</span></div>
<div class="col-6"><span class="badge bg-light text-dark">PNG</span></div>
<div class="col-6"><span class="badge bg-light text-dark">BMP</span></div>
<div class="col-6"><span class="badge bg-light text-dark">WEBP</span></div>
</div>
<h5 class="mb-3"><i class="fas fa-cogs me-2"></i>图片处理流程</h5>
<div class="process-steps">
<div class="step-item">
<div class="step-number">1</div>
<div class="step-text">上传原始图片</div>
</div>
<div class="step-item">
<div class="step-number">2</div>
<div class="step-text">自动转换为黑白图像</div>
</div>
<div class="step-item">
<div class="step-number">3</div>
<div class="step-text">调整大小以适应墨水屏</div>
</div>
<div class="step-item">
<div class="step-number">4</div>
<div class="step-text">优化对比度</div>
</div>
<div class="step-item">
<div class="step-number">5</div>
<div class="step-text">保存处理后的图片</div>
</div>
</div>
<h5 class="mb-3 mt-4"><i class="fas fa-desktop me-2"></i>墨水屏规格</h5>
<ul class="list-group list-group-flush">
<li class="list-group-item d-flex justify-content-between align-items-center px-0">
分辨率
<span class="badge bg-primary rounded-pill">400×300 像素</span>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center px-0">
显示模式
<span class="badge bg-secondary rounded-pill">黑白</span>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center px-0">
刷新时间
<span class="badge bg-info rounded-pill">约2-3秒</span>
</li>
</ul>
</div>
</div>
<div class="card shadow-sm">
<div class="card-header bg-white py-3">
<h6 class="m-0 font-weight-bold text-warning">使用提示</h6>
</div>
<div class="card-body">
<div class="alert alert-light border-0">
<ul class="mb-0">
<li class="mb-2"><i class="fas fa-check-circle text-success me-2"></i>建议上传高对比度的图片,以获得更好的显示效果</li>
<li class="mb-2"><i class="fas fa-check-circle text-success me-2"></i>图片内容应简洁明了,避免过多细节</li>
<li class="mb-2"><i class="fas fa-check-circle text-success me-2"></i>文字内容应使用较大字号,确保可读性</li>
<li><i class="fas fa-check-circle text-success me-2"></i>上传后会自动处理为适合墨水屏显示的格式</li>
</ul>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
document.addEventListener('DOMContentLoaded', function() {
const deviceSelect = document.getElementById('device_id');
const versionSelect = document.getElementById('version');
const imageInput = document.getElementById('image');
const imagePreview = document.getElementById('imagePreview');
const previewImg = document.getElementById('previewImg');
const uploadForm = document.getElementById('uploadForm');
const uploadBtn = document.getElementById('uploadBtn');
const uploadArea = document.getElementById('uploadArea');
const removeImageBtn = document.getElementById('removeImage');
// 拖拽上传功能
uploadArea.addEventListener('dragover', function(e) {
e.preventDefault();
this.classList.add('drag-over');
});
uploadArea.addEventListener('dragleave', function(e) {
e.preventDefault();
this.classList.remove('drag-over');
});
uploadArea.addEventListener('drop', function(e) {
e.preventDefault();
this.classList.remove('drag-over');
if (e.dataTransfer.files.length) {
imageInput.files = e.dataTransfer.files;
showImagePreview(e.dataTransfer.files[0]);
}
});
// 点击上传区域触发文件选择
uploadArea.addEventListener('click', function() {
imageInput.click();
});
// 移除图片
removeImageBtn.addEventListener('click', function() {
imageInput.value = '';
imagePreview.style.display = 'none';
});
// 设备选择变化时,加载该设备的内容版本
deviceSelect.addEventListener('change', function() {
const deviceId = this.value;
if (!deviceId) {
versionSelect.innerHTML = '<option value="">创建新版本</option>';
return;
}
// 获取设备的内容版本
fetch(`/api/devices/${deviceId}/contents`)
.then(response => response.json())
.then(data => {
versionSelect.innerHTML = '<option value="">创建新版本</option>';
data.contents.forEach(content => {
const option = document.createElement('option');
option.value = content.version;
option.textContent = `v${content.version} - ${content.title}`;
versionSelect.appendChild(option);
});
})
.catch(error => {
console.error('Error:', error);
versionSelect.innerHTML = '<option value="">创建新版本</option>';
});
});
// 图片选择变化时,显示预览
imageInput.addEventListener('change', function() {
if (this.files && this.files[0]) {
showImagePreview(this.files[0]);
} else {
imagePreview.style.display = 'none';
}
});
// 显示图片预览
function showImagePreview(file) {
if (file.type.startsWith('image/')) {
const reader = new FileReader();
reader.onload = function(e) {
previewImg.src = e.target.result;
imagePreview.style.display = 'block';
};
reader.readAsDataURL(file);
}
}
// 表单提交时,显示加载状态
uploadForm.addEventListener('submit', function(e) {
uploadBtn.disabled = true;
uploadBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span>上传中...';
});
});
</script>
{% endblock %}