feat: 顶部添加连接状态指示器,掉线时禁止编辑和新增操作

This commit is contained in:
2026-04-20 18:10:37 +08:00
parent 94efc524c6
commit ca7dc10e92
10 changed files with 14541 additions and 73 deletions

Binary file not shown.

View File

@@ -768,6 +768,85 @@ INDEX_TEMPLATE = '''
.sidebar a { color: #adb5bd; text-decoration: none; padding: 10px 20px; display: block; }
.sidebar a:hover, .sidebar a.active { background: #495057; color: #fff; }
.content { padding: 20px; }
/* 连接状态指示器 */
.connection-status {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
transition: all 0.3s ease;
}
.connection-status.online {
background: linear-gradient(135deg, #28a745 0%, #20c997 100%);
color: white;
}
.connection-status.offline {
background: linear-gradient(135deg, #dc3545 0%, #c82333 100%);
color: white;
}
.connection-status.connecting {
background: linear-gradient(135deg, #ffc107 0%, #fd7e14 100%);
color: #333;
}
.connection-status .status-icon {
margin-right: 8px;
}
.connection-status .status-text {
font-size: 14px;
font-weight: 500;
}
.connection-status.offline .status-text {
animation: pulse 1.5s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.6; }
}
/* 离线遮罩 */
.offline-overlay {
position: fixed;
top: 40px;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.3);
z-index: 9998;
display: none;
cursor: not-allowed;
}
.offline-overlay.show {
display: block;
}
.offline-overlay .offline-message {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: white;
padding: 30px 50px;
border-radius: 10px;
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
text-align: center;
}
.offline-overlay .offline-message h3 {
color: #dc3545;
margin-bottom: 10px;
}
.offline-overlay .offline-message p {
color: #666;
}
/* 内容区域偏移(为状态栏留空间) */
.content.with-status-bar {
padding-top: 60px;
}
.card { margin-bottom: 8px; transition: transform 0.2s; }
.card:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0,0,0,0.15); }
.card-body { padding: 8px 12px; }
@@ -793,6 +872,23 @@ INDEX_TEMPLATE = '''
</style>
</head>
<body>
<!-- 连接状态指示器 -->
<div class="connection-status online" id="connectionStatus">
<span class="status-icon"><i class="bi bi-wifi"></i></span>
<span class="status-text">已连接</span>
</div>
<!-- 离线遮罩 -->
<div class="offline-overlay" id="offlineOverlay">
<div class="offline-message">
<h3><i class="bi bi-wifi-off"></i> 服务器连接已断开</h3>
<p>无法进行编辑和新增操作,请检查网络连接</p>
<button class="btn btn-primary mt-3" onclick="checkConnection()">
<i class="bi bi-arrow-repeat"></i> 重新连接
</button>
</div>
</div>
<div class="container-fluid">
<div class="row">
<!-- 侧边栏 -->
@@ -821,7 +917,7 @@ INDEX_TEMPLATE = '''
</div>
<!-- 主内容 -->
<div class="col-md-10 content">
<div class="col-md-10 content with-status-bar">
<!-- 提醒栏 -->
<div id="reminderBar" class="alert alert-warning alert-dismissible fade show mb-3" style="display:none;" role="alert">
<i class="bi bi-bell-fill"></i>
@@ -1392,6 +1488,92 @@ let currentFilter = { type: '', status: '', starred: null };
let currentSort = { sort_by: '', sort_order: '' };
let currentPage = 1;
const pageSize = 20;
// ============ 连接状态检测 ============
let isOnline = true;
let connectionCheckTimer = null;
const CONNECTION_CHECK_INTERVAL = 5000; // 5秒检测一次
const CONNECTION_TIMEOUT = 3000; // 3秒超时
function updateConnectionStatus(status) {
const statusEl = document.getElementById('connectionStatus');
const overlayEl = document.getElementById('offlineOverlay');
statusEl.classList.remove('online', 'offline', 'connecting');
statusEl.classList.add(status);
const iconEl = statusEl.querySelector('.status-icon');
const textEl = statusEl.querySelector('.status-text');
if (status === 'online') {
iconEl.innerHTML = '<i class="bi bi-wifi"></i>';
textEl.textContent = '已连接';
overlayEl.classList.remove('show');
isOnline = true;
} else if (status === 'offline') {
iconEl.innerHTML = '<i class="bi bi-wifi-off"></i>';
textEl.textContent = '连接已断开';
overlayEl.classList.add('show');
isOnline = false;
} else if (status === 'connecting') {
iconEl.innerHTML = '<i class="bi bi-arrow-repeat"></i>';
textEl.textContent = '正在连接...';
isOnline = false;
}
}
async function checkConnection() {
updateConnectionStatus('connecting');
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), CONNECTION_TIMEOUT);
const res = await fetch(`${API_BASE}/stats`, {
signal: controller.signal,
headers: { 'Cache-Control': 'no-cache' }
});
clearTimeout(timeoutId);
if (res.ok) {
updateConnectionStatus('online');
// 连接恢复后刷新数据
if (!isOnline) {
refreshData();
}
} else {
updateConnectionStatus('offline');
}
} catch (e) {
updateConnectionStatus('offline');
}
}
function startConnectionCheck() {
// 立即检测一次
checkConnection();
// 定时检测
connectionCheckTimer = setInterval(checkConnection, CONNECTION_CHECK_INTERVAL);
}
function stopConnectionCheck() {
if (connectionCheckTimer) {
clearInterval(connectionCheckTimer);
connectionCheckTimer = null;
}
}
// 离线时禁止操作
function checkOnlineBeforeAction(actionName) {
if (!isOnline) {
alert('服务器连接已断开,无法执行此操作');
return false;
}
return true;
}
function debounce(fn, delay) {
let timer;
return function(...args) {
@@ -1402,6 +1584,9 @@ function debounce(fn, delay) {
// 初始化
document.addEventListener('DOMContentLoaded', async () => {
// 启动连接状态检测
startConnectionCheck();
// 确保初始状态清空
document.getElementById('searchInput').value = '';
document.getElementById('typeFilter').value = '';
@@ -1750,6 +1935,9 @@ async function editDraft(draftId) {
// 发布草稿(转为正式条目)
async function publishDraft(draftId) {
// 离线检查
if (!checkOnlineBeforeAction('发布草稿')) return;
if (!confirm('确认发布这条草稿?')) return;
const res = await fetch(`${API_BASE}/drafts/${draftId}/publish`, { method: 'POST' });
@@ -1766,6 +1954,9 @@ async function publishDraft(draftId) {
// 删除草稿
async function deleteDraft(draftId) {
// 离线检查
if (!checkOnlineBeforeAction('删除草稿')) return;
if (!confirm('确认删除这条草稿?')) return;
const res = await fetch(`${API_BASE}/drafts/${draftId}`, { method: 'DELETE' });
@@ -1857,6 +2048,9 @@ function hideDraftIndicator() {
}
function showAddModal(type) {
// 离线检查
if (!checkOnlineBeforeAction('添加数据')) return;
// 设置类型
document.getElementById('addType').value = type;
@@ -1901,6 +2095,9 @@ function showAddModal(type) {
// 添加条目
async function addItem() {
// 离线检查
if (!checkOnlineBeforeAction('添加数据')) return;
const type = document.getElementById('addType').value;
const data = {
type,
@@ -1940,12 +2137,18 @@ async function addItem() {
// 完成待办
async function completeItem(id) {
// 离线检查
if (!checkOnlineBeforeAction('完成待办')) return;
await fetch(`${API_BASE}/items/${id}/done`, { method: 'POST' });
refreshData();
}
// 重新打开待办
async function reopenItem(id) {
// 离线检查
if (!checkOnlineBeforeAction('重新打开待办')) return;
const res = await fetch(`${API_BASE}/items/${id}/reopen`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -1963,6 +2166,9 @@ async function reopenItem(id) {
let convertItemData = null;
async function showConvertModal(itemId) {
// 离线检查
if (!checkOnlineBeforeAction('转换数据')) return;
const res = await fetch(`${API_BASE}/items/${itemId}`);
const data = await res.json();
@@ -2034,6 +2240,9 @@ async function executeConvert() {
// 删除条目
async function deleteItem(id) {
// 离线检查
if (!checkOnlineBeforeAction('删除数据')) return;
if (!confirm('确认删除?')) return;
await fetch(`${API_BASE}/items/${id}`, { method: 'DELETE' });
refreshData();
@@ -2149,6 +2358,9 @@ function editModalHideHandler(e) {
}
async function openEditModal(id) {
// 离线检查
if (!checkOnlineBeforeAction('编辑数据')) return;
currentDetailId = id;
const res = await fetch(`${API_BASE}/items/${id}`);
const data = await res.json();
@@ -2265,6 +2477,9 @@ function updateEditFieldsByType(type) {
// 保存编辑
async function saveEdit() {
// 离线检查
if (!checkOnlineBeforeAction('保存修改')) return;
const id = document.getElementById('editId').value;
const type = document.getElementById('editType').value; // 从下拉框获取新类型
@@ -2305,6 +2520,9 @@ async function saveEdit() {
// 切换重点关注状态
async function toggleStar(id) {
// 离线检查
if (!checkOnlineBeforeAction('切换关注状态')) return;
const res = await fetch(`${API_BASE}/items/${id}/star`, { method: 'POST' });
if (res.ok) {
refreshData();
@@ -2541,6 +2759,9 @@ async function loadTagManagerList() {
}
async function createTag() {
// 离线检查
if (!checkOnlineBeforeAction('创建标签')) return;
const name = document.getElementById('newTagName').value.trim();
if (!name) return;
@@ -2699,6 +2920,9 @@ async function processAIInput() {
}
async function confirmAIAdd() {
// 离线检查
if (!checkOnlineBeforeAction('AI添加')) return;
if (!aiParsedData) return;
const res = await fetch(`${API_BASE}/items`, {
@@ -2820,6 +3044,9 @@ async function hideTrash() {
}
async function restoreItem(id) {
// 离线检查
if (!checkOnlineBeforeAction('恢复数据')) return;
if (!confirm('确认恢复这条数据?')) return;
const res = await fetch(`${API_BASE}/items/${id}/restore`, { method: 'POST' });
@@ -2834,6 +3061,9 @@ async function restoreItem(id) {
}
async function deletePermanently(id) {
// 离线检查
if (!checkOnlineBeforeAction('彻底删除')) return;
if (!confirm('确认彻底删除这条数据?此操作不可恢复!')) return;
const res = await fetch(`${API_BASE}/items/${id}/permanent`, { method: 'DELETE' });
@@ -2848,6 +3078,9 @@ async function deletePermanently(id) {
}
async function emptyTrash() {
// 离线检查
if (!checkOnlineBeforeAction('清空回收站')) return;
if (!confirm('确认清空回收站?此操作不可恢复!')) return;
const res = await fetch(`${API_BASE}/trash`, { method: 'DELETE' });
@@ -2906,6 +3139,9 @@ async function loadBackupList() {
}
async function createManualBackup() {
// 离线检查
if (!checkOnlineBeforeAction('创建备份')) return;
const res = await fetch(`${API_BASE}/backups`, { method: 'POST' });
const data = await res.json();
@@ -2919,7 +3155,10 @@ async function createManualBackup() {
}
async function restoreBackup(name) {
if (!confirm(`确认恢复备份 "${name}"\n当前数据将被覆盖!`)) return;
// 离线检查
if (!checkOnlineBeforeAction('恢复备份')) return;
if (!confirm(`确认恢复备份 "${name}"\\n当前数据将被覆盖`)) return;
const res = await fetch(`${API_BASE}/backups/${name}`, { method: 'POST' });
const data = await res.json();
@@ -3004,6 +3243,9 @@ async function loadEmailManagerList() {
}
async function createEmail() {
// 离线检查
if (!checkOnlineBeforeAction('添加邮箱')) return;
const emailAddr = document.getElementById('newEmailAddr').value.trim();
const emailName = document.getElementById('newEmailName').value.trim();
@@ -3099,6 +3341,9 @@ async function showSendEmailModal(itemId) {
}
async function sendItemEmail() {
// 离线检查
if (!checkOnlineBeforeAction('发送邮件')) return;
const itemId = document.getElementById('sendEmailItemId').value;
let emailAddr = document.getElementById('sendEmailSelect').value;