feat: 顶部添加连接状态指示器,掉线时禁止编辑和新增操作
This commit is contained in:
BIN
xian_favor/__pycache__/__main__.cpython-310.pyc
Normal file
BIN
xian_favor/__pycache__/__main__.cpython-310.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user