Web优化

This commit is contained in:
Alex Yang
2025-11-28 23:59:58 +08:00
parent 8e2ea02a62
commit 3207510c91
8 changed files with 649 additions and 109 deletions

View File

@@ -166,6 +166,65 @@
text-shadow: 0 0 5px rgba(250, 204, 21, 0.3);
}
}
/* 状态过渡效果 */
.status-transition {
transition: all 0.3s ease-in-out;
}
/* 状态淡入动画 */
@keyframes status-fade-in {
0% {
opacity: 0;
transform: translateY(-5px);
}
100% {
opacity: 1;
transform: translateY(0);
}
}
/* 状态淡出动画 */
@keyframes status-fade-out {
0% {
opacity: 1;
transform: translateY(0);
}
100% {
opacity: 0;
transform: translateY(-5px);
}
}
/* 淡入类 */
.status-fade-in {
animation: status-fade-in 0.3s ease-in-out forwards;
}
/* 淡出类 */
.status-fade-out {
animation: status-fade-out 0.3s ease-in-out forwards;
}
/* 加载状态样式 */
.status-loading {
animation: status-pulse 1.5s ease-in-out infinite;
}
/* 状态脉冲动画 */
@keyframes status-pulse {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.7;
}
}
/* 保存按钮状态样式 */
#save-blacklist-status {
transition: all 0.3s ease-in-out;
}
</style>
</head>
<body class="bg-gray-50 text-dark font-sans">
@@ -739,6 +798,7 @@
<button id="save-rule-btn" class="px-4 py-2 bg-success text-white rounded-md hover:bg-success/90 transition-colors">
保存
</button>
<div id="save-rule-status" class="flex items-center text-sm"></div>
</div>
</div>
@@ -748,12 +808,13 @@
<thead>
<tr class="border-b border-gray-200">
<th class="text-left py-3 px-4 text-sm font-medium text-gray-500">规则</th>
<th class="text-center py-3 px-4 text-sm font-medium text-gray-500">状态</th>
<th class="text-right py-3 px-4 text-sm font-medium text-gray-500">操作</th>
</tr>
</thead>
<tbody id="rules-table-body">
<tr>
<td colspan="2" class="py-4 text-center text-gray-500">暂无规则</td>
<td colspan="3" class="py-4 text-center text-gray-500">暂无规则</td>
</tr>
</tbody>
</table>
@@ -775,11 +836,13 @@
<label for="blacklist-url" class="block text-sm font-medium text-gray-700 mb-1">URL</label>
<input type="text" id="blacklist-url" placeholder="输入黑名单URL" class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent">
</div>
<div class="flex items-end space-x-2">
<button id="save-blacklist-btn" class="px-4 py-2 bg-success text-white rounded-md hover:bg-success/90 transition-colors">
保存
</button>
<div id="save-blacklist-status" class="flex items-center text-sm"></div>
<div class="flex items-end">
<div class="flex items-center space-x-2">
<button id="save-blacklist-btn" class="px-4 py-2 bg-success text-white rounded-md hover:bg-success/90 transition-colors">
保存
</button>
<div id="save-blacklist-status" class="flex items-center text-sm"></div>
</div>
</div>
</div>
</div>

View File

@@ -17,6 +17,9 @@ function updateStatus(url, status, message) {
const statusElement = document.getElementById(`update-status-${encodeURIComponent(url)}`);
if (!statusElement) return;
// 清除之前的所有类
statusElement.classList.remove('status-transition', 'status-loading', 'status-success', 'status-error', 'status-fade-in', 'status-fade-out');
let statusHTML = '';
switch (status) {
@@ -33,12 +36,90 @@ function updateStatus(url, status, message) {
statusHTML = '<span class="text-gray-400">-</span>';
}
// 强制重排,确保过渡效果生效
void statusElement.offsetWidth;
// 设置新的HTML内容
statusElement.innerHTML = statusHTML;
// 如果是成功或失败状态3秒后恢复默认状态
// 添加过渡类和对应状态类
statusElement.classList.add('status-transition');
// 如果不是默认状态,添加淡入动画和对应状态类
if (status !== 'default') {
statusElement.classList.add('status-fade-in');
statusElement.classList.add(`status-${status}`);
}
// 如果是成功或失败状态3秒后渐变消失
if (status === 'success' || status === 'error') {
setTimeout(() => {
updateStatus(url, 'default');
// 添加淡出类
statusElement.classList.add('status-fade-out');
// 等待淡出动画完成后切换到默认状态
setTimeout(() => {
// 清除所有类
statusElement.classList.remove('status-transition', 'status-loading', 'status-success', 'status-error', 'status-fade-in', 'status-fade-out');
// 设置默认状态
statusElement.innerHTML = '<span class="text-gray-400">-</span>';
}, 300); // 与CSS动画持续时间一致
}, 3000);
}
}
// 更新规则状态显示函数
function updateRuleStatus(rule, status, message) {
const statusElement = document.getElementById(`rule-status-${encodeURIComponent(rule)}`);
if (!statusElement) return;
// 清除之前的所有类
statusElement.classList.remove('status-transition', 'status-loading', 'status-success', 'status-error', 'status-fade-in', 'status-fade-out');
let statusHTML = '';
switch (status) {
case 'loading':
statusHTML = '<span class="text-blue-500"><i class="fa fa-spinner fa-spin"></i> 处理中...</span>';
break;
case 'success':
statusHTML = `<span class="text-green-500"><i class="fa fa-check"></i> ${message || '成功'}</span>`;
break;
case 'error':
statusHTML = `<span class="text-red-500"><i class="fa fa-times"></i> ${message || '失败'}</span>`;
break;
default:
statusHTML = '<span class="text-gray-400">-</span>';
}
// 强制重排,确保过渡效果生效
void statusElement.offsetWidth;
// 设置新的HTML内容
statusElement.innerHTML = statusHTML;
// 添加过渡类和对应状态类
statusElement.classList.add('status-transition');
// 如果不是默认状态,添加淡入动画和对应状态类
if (status !== 'default') {
statusElement.classList.add('status-fade-in');
statusElement.classList.add(`status-${status}`);
}
// 如果是成功或失败状态3秒后渐变消失
if (status === 'success' || status === 'error') {
setTimeout(() => {
// 添加淡出类
statusElement.classList.add('status-fade-out');
// 等待淡出动画完成后切换到默认状态
setTimeout(() => {
// 清除所有类
statusElement.classList.remove('status-transition', 'status-loading', 'status-success', 'status-error', 'status-fade-in', 'status-fade-out');
// 设置默认状态
statusElement.innerHTML = '<span class="text-gray-400">-</span>';
}, 300); // 与CSS动画持续时间一致
}, 3000);
}
}
@@ -248,7 +329,7 @@ function updateRulesTable(rules) {
if (rules.length === 0) {
const emptyRow = document.createElement('tr');
emptyRow.innerHTML = '<td colspan="2" class="py-4 text-center text-gray-500">暂无规则</td>';
emptyRow.innerHTML = '<td colspan="3" class="py-4 text-center text-gray-500">暂无规则</td>';
tbody.appendChild(emptyRow);
return;
}
@@ -268,6 +349,11 @@ function updateRulesTable(rules) {
tdRule.className = 'py-3 px-4';
tdRule.textContent = rule;
const tdStatus = document.createElement('td');
tdStatus.className = 'py-3 px-4 text-center';
tdStatus.id = `rule-status-${encodeURIComponent(rule)}`;
tdStatus.innerHTML = '<span class="text-gray-400">-</span>';
const tdAction = document.createElement('td');
tdAction.className = 'py-3 px-4 text-right';
@@ -289,7 +375,9 @@ function updateRulesTable(rules) {
};
tdAction.appendChild(deleteBtn);
tr.appendChild(tdRule);
tr.appendChild(tdStatus);
tr.appendChild(tdAction);
fragment.appendChild(tr);
});
@@ -300,7 +388,7 @@ function updateRulesTable(rules) {
// 如果有更多规则,添加提示
if (rules.length > maxRulesToShow) {
const infoRow = document.createElement('tr');
infoRow.innerHTML = `<td colspan="2" class="py-4 text-center text-gray-500">显示前 ${maxRulesToShow} 条规则,共 ${rules.length} 条</td>`;
infoRow.innerHTML = `<td colspan="3" class="py-4 text-center text-gray-500">显示前 ${maxRulesToShow} 条规则,共 ${rules.length} 条</td>`;
tbody.appendChild(infoRow);
}
}
@@ -336,6 +424,9 @@ async function handleDeleteRule(e) {
}
try {
// 显示加载状态
updateRuleStatus(rule, 'loading');
console.log('Sending DELETE request to /api/shield');
const response = await fetch('/api/shield', {
method: 'DELETE',
@@ -348,28 +439,47 @@ async function handleDeleteRule(e) {
console.log('Response status:', response.status);
console.log('Response ok:', response.ok);
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Failed to delete rule: ${response.status} ${errorText}`);
// 解析服务器响应
let responseData;
try {
responseData = await response.json();
} catch (jsonError) {
responseData = {};
}
const responseData = await response.json();
console.log('Response data:', responseData);
showNotification('规则删除成功', 'success');
console.log('Current rules type:', currentRulesType);
// 根据当前显示的规则类型重新加载对应的规则列表
if (currentRulesType === 'local') {
console.log('Reloading local rules');
loadLocalRules();
// 根据服务器响应判断是否成功
if (response.ok && responseData.status === 'success') {
// 显示成功状态
updateRuleStatus(rule, 'success', '已删除');
showNotification('规则删除成功', 'success');
console.log('Current rules type:', currentRulesType);
// 延迟重新加载规则列表和统计信息,让用户能看到成功状态
setTimeout(() => {
// 根据当前显示的规则类型重新加载对应的规则列表
if (currentRulesType === 'local') {
console.log('Reloading local rules');
loadLocalRules();
} else {
console.log('Reloading remote rules');
loadRemoteRules();
}
// 重新加载统计信息
loadShieldStats();
}, 3000);
} else {
console.log('Reloading remote rules');
loadRemoteRules();
const errorMessage = responseData.error || responseData.message || `删除规则失败: ${response.status}`;
// 显示错误状态
updateRuleStatus(rule, 'error', errorMessage);
throw new Error(errorMessage);
}
// 重新加载统计信息
loadShieldStats();
} catch (error) {
console.error('Error deleting rule:', error);
// 显示错误状态
updateRuleStatus(rule, 'error', error.message);
showNotification('删除规则失败: ' + error.message, 'error');
}
}
@@ -377,12 +487,26 @@ async function handleDeleteRule(e) {
// 添加新规则
async function handleAddRule() {
const rule = document.getElementById('new-rule').value.trim();
const statusElement = document.getElementById('save-rule-status');
if (!rule) {
showNotification('规则不能为空', 'error');
return;
}
try {
// 清除之前的所有类
statusElement.classList.remove('status-transition', 'status-loading', 'status-success', 'status-error', 'status-fade-in', 'status-fade-out');
// 显示加载状态
statusElement.innerHTML = '<i class="fa fa-spinner fa-spin text-blue-500"></i> 正在添加...';
// 强制重排,确保过渡效果生效
void statusElement.offsetWidth;
// 添加过渡类和加载状态类
statusElement.classList.add('status-transition', 'status-fade-in', 'status-loading');
const response = await fetch('/api/shield', {
method: 'POST',
headers: {
@@ -391,20 +515,86 @@ async function handleAddRule() {
body: JSON.stringify({ rule })
});
if (!response.ok) {
throw new Error('Failed to add rule');
// 解析服务器响应
let responseData;
try {
responseData = await response.json();
} catch (jsonError) {
responseData = {};
}
showNotification('规则添加成功', 'success');
// 清空输入框
document.getElementById('new-rule').value = '';
// 重新加载规则
loadLocalRules();
// 重新加载统计信息
loadShieldStats();
// 根据服务器响应判断是否成功
if (response.ok && responseData.status === 'success') {
// 清除之前的所有类
statusElement.classList.remove('status-transition', 'status-loading', 'status-success', 'status-error', 'status-fade-in', 'status-fade-out');
// 显示成功状态
statusElement.innerHTML = '<span class="text-green-500"><i class="fa fa-check"></i> 成功</span>';
// 强制重排,确保过渡效果生效
void statusElement.offsetWidth;
// 添加过渡类和成功状态类
statusElement.classList.add('status-transition', 'status-fade-in', 'status-success');
showNotification('规则添加成功', 'success');
// 清空输入框
document.getElementById('new-rule').value = '';
// 延迟重新加载规则和统计信息,让用户能看到成功状态
setTimeout(() => {
// 重新加载规则
loadLocalRules();
// 重新加载统计信息
loadShieldStats();
}, 3000);
} else {
// 清除之前的所有类
statusElement.classList.remove('status-transition', 'status-loading', 'status-success', 'status-error', 'status-fade-in', 'status-fade-out');
// 显示失败状态
const errorMessage = responseData.error || responseData.message || '添加规则失败';
statusElement.innerHTML = `<span class="text-red-500"><i class="fa fa-times"></i> ${errorMessage}</span>`;
// 强制重排,确保过渡效果生效
void statusElement.offsetWidth;
// 添加过渡类和错误状态类
statusElement.classList.add('status-transition', 'status-fade-in', 'status-error');
showNotification(errorMessage, 'error');
}
} catch (error) {
console.error('Error adding rule:', error);
showNotification('添加规则失败', 'error');
// 清除之前的所有类
statusElement.classList.remove('status-transition', 'status-loading', 'status-success', 'status-error', 'status-fade-in', 'status-fade-out');
// 显示错误状态
const errorMessage = error.message || '添加规则失败';
statusElement.innerHTML = `<span class="text-red-500"><i class="fa fa-times"></i> ${errorMessage}</span>`;
// 强制重排,确保过渡效果生效
void statusElement.offsetWidth;
// 添加过渡类和错误状态类
statusElement.classList.add('status-transition', 'status-fade-in', 'status-error');
showNotification(errorMessage, 'error');
} finally {
// 3秒后渐变消失
setTimeout(() => {
// 添加淡出类
statusElement.classList.add('status-fade-out');
// 等待淡出动画完成后清除状态
setTimeout(() => {
// 清除所有类
statusElement.classList.remove('status-transition', 'status-loading', 'status-success', 'status-error', 'status-fade-in', 'status-fade-out');
// 清空状态显示
statusElement.innerHTML = '';
}, 300); // 与CSS动画持续时间一致
}, 3000);
}
}
@@ -618,19 +808,42 @@ async function handleUpdateBlacklist(e) {
body: JSON.stringify(updatedBlacklists)
});
if (!updateResponse.ok) {
const errorText = await updateResponse.text();
throw new Error(`更新失败: ${updateResponse.status} ${errorText}`);
// 解析服务器响应
let responseData;
try {
responseData = await updateResponse.json();
} catch (jsonError) {
responseData = {};
}
// 显示成功状态
updateStatus(url, 'success');
// 重新加载黑名单
loadRemoteBlacklists();
// 重新加载统计信息
loadShieldStats();
showNotification('黑名单更新成功', 'success');
// 根据服务器响应判断是否成功
if (updateResponse.ok && (responseData.status === 'success' || !responseData.status)) {
// 显示成功状态
updateStatus(url, 'success');
// 显示通知
showNotification('黑名单更新成功', 'success');
// 延迟重新加载黑名单和统计信息,让用户能看到成功状态
setTimeout(() => {
// 重新加载黑名单
loadRemoteBlacklists();
// 重新加载统计信息
loadShieldStats();
}, 3000);
} else {
// 显示失败状态
updateStatus(url, 'error', responseData.error || responseData.message || `更新失败: ${updateResponse.status}`);
showNotification(`黑名单更新失败: ${responseData.error || responseData.message || updateResponse.status}`, 'error');
// 延迟重新加载黑名单和统计信息
setTimeout(() => {
// 重新加载黑名单
loadRemoteBlacklists();
// 重新加载统计信息
loadShieldStats();
}, 3000);
}
} catch (error) {
console.error('更新黑名单失败:', error);
// 显示错误状态
@@ -661,6 +874,13 @@ async function handleDeleteBlacklist(e) {
}
try {
// 获取当前行元素
const tr = btn.closest('tr');
if (!tr) {
console.error('未找到行元素');
return;
}
// 显示加载状态
updateStatus(url, 'loading');
@@ -684,19 +904,58 @@ async function handleDeleteBlacklist(e) {
body: JSON.stringify(updatedBlacklists)
});
if (!updateResponse.ok) {
const errorText = await updateResponse.text();
throw new Error(`删除失败: ${updateResponse.status} ${errorText}`);
// 解析服务器响应
let responseData;
try {
responseData = await updateResponse.json();
} catch (jsonError) {
responseData = {};
}
// 显示成功状态
updateStatus(url, 'success', '已删除');
// 重新加载黑名单
loadRemoteBlacklists();
// 重新加载统计信息
loadShieldStats();
showNotification('黑名单删除成功', 'success');
// 根据服务器响应判断是否成功
if (updateResponse.ok && responseData.status === 'success') {
// 显示成功状态
updateStatus(url, 'success', '已删除');
// 显示通知
showNotification('黑名单删除成功', 'success');
// 延迟后渐变移除该行
setTimeout(() => {
// 添加渐变移除类
tr.style.transition = 'all 0.3s ease-in-out';
tr.style.opacity = '0';
tr.style.transform = 'translateX(-10px)';
tr.style.height = tr.offsetHeight + 'px';
tr.style.overflow = 'hidden';
// 等待过渡效果完成后,隐藏该行
setTimeout(() => {
tr.style.display = 'none';
// 延迟重新加载黑名单和统计信息,确保视觉效果完成
setTimeout(() => {
// 重新加载黑名单
loadRemoteBlacklists();
// 重新加载统计信息
loadShieldStats();
}, 100);
}, 300);
}, 3000);
} else {
// 显示失败状态
const errorMessage = responseData.error || responseData.message || `删除失败: ${updateResponse.status}`;
updateStatus(url, 'error', errorMessage);
showNotification(errorMessage, 'error');
// 延迟重新加载黑名单和统计信息
setTimeout(() => {
// 重新加载黑名单
loadRemoteBlacklists();
// 重新加载统计信息
loadShieldStats();
}, 3000);
}
} catch (error) {
console.error('删除黑名单失败:', error);
// 显示错误状态
@@ -761,19 +1020,43 @@ async function handleToggleBlacklist(e) {
body: JSON.stringify(updatedBlacklists)
});
if (!updateResponse.ok) {
const errorText = await updateResponse.text();
throw new Error(`更新状态失败: ${updateResponse.status} ${errorText}`);
// 解析服务器响应
let responseData;
try {
responseData = await updateResponse.json();
} catch (jsonError) {
responseData = {};
}
// 显示成功状态
updateStatus(url, 'success', currentEnabled ? '已禁用' : '已启用');
// 重新加载黑名单
loadRemoteBlacklists();
// 重新加载统计信息
loadShieldStats();
showNotification(`黑名单已${currentEnabled ? '禁用' : '启用'}`, 'success');
// 根据服务器响应判断是否成功
if (updateResponse.ok && responseData.status === 'success') {
// 显示成功状态
updateStatus(url, 'success', currentEnabled ? '已禁用' : '已启用');
// 显示通知
showNotification(`黑名单已${currentEnabled ? '禁用' : '启用'}`, 'success');
// 延迟重新加载黑名单和统计信息,让用户能看到成功状态
setTimeout(() => {
// 重新加载黑名单
loadRemoteBlacklists();
// 重新加载统计信息
loadShieldStats();
}, 3000);
} else {
// 显示失败状态
const errorMessage = responseData.error || responseData.message || `更新状态失败: ${updateResponse.status}`;
updateStatus(url, 'error', errorMessage);
showNotification(errorMessage, 'error');
// 延迟重新加载黑名单和统计信息
setTimeout(() => {
// 重新加载黑名单
loadRemoteBlacklists();
// 重新加载统计信息
loadShieldStats();
}, 3000);
}
} catch (error) {
console.error('启用/禁用黑名单失败:', error);
// 显示错误状态
@@ -811,9 +1094,18 @@ async function handleAddBlacklist(event) {
}
try {
// 清除之前的所有类
statusElement.classList.remove('status-transition', 'status-loading', 'status-success', 'status-error', 'status-fade-in', 'status-fade-out');
// 显示加载状态
statusElement.innerHTML = '<i class="fa fa-spinner fa-spin text-blue-500"></i> 正在添加...';
// 强制重排,确保过渡效果生效
void statusElement.offsetWidth;
// 添加过渡类和加载状态类
statusElement.classList.add('status-transition', 'status-fade-in', 'status-loading');
// 发送添加请求
const response = await fetch('/api/shield/blacklists', {
method: 'POST',
@@ -823,40 +1115,82 @@ async function handleAddBlacklist(event) {
body: JSON.stringify({ name, url })
});
if (!response.ok) {
// 尝试从响应中获取更详细的错误信息
let errorMessage = '添加黑名单失败';
try {
const errorData = await response.json();
if (errorData.error) {
errorMessage = errorData.error;
}
} catch (jsonError) {
// 忽略JSON解析错误
}
throw new Error(errorMessage);
// 解析服务器响应
let responseData;
try {
responseData = await response.json();
} catch (jsonError) {
responseData = {};
}
// 显示成功状态
statusElement.innerHTML = '<span class="text-green-500"><i class="fa fa-check"></i> 成功</span>';
showNotification('黑名单添加成功', 'success');
// 清空输入框
if (nameInput) nameInput.value = '';
if (urlInput) urlInput.value = '';
// 重新加载黑名单
loadRemoteBlacklists();
// 重新加载统计信息
loadShieldStats();
// 根据服务器响应判断是否成功
if (response.ok && (responseData.status === 'success' || !responseData.status)) {
// 清除之前的所有类
statusElement.classList.remove('status-transition', 'status-loading', 'status-success', 'status-error', 'status-fade-in', 'status-fade-out');
// 显示成功状态
statusElement.innerHTML = '<span class="text-green-500"><i class="fa fa-check"></i> 成功</span>';
// 强制重排,确保过渡效果生效
void statusElement.offsetWidth;
// 添加过渡类和成功状态类
statusElement.classList.add('status-transition', 'status-fade-in', 'status-success');
showNotification('黑名单添加成功', 'success');
// 清空输入框
if (nameInput) nameInput.value = '';
if (urlInput) urlInput.value = '';
// 重新加载黑名单
loadRemoteBlacklists();
// 重新加载统计信息
loadShieldStats();
} else {
// 清除之前的所有类
statusElement.classList.remove('status-transition', 'status-loading', 'status-success', 'status-error', 'status-fade-in', 'status-fade-out');
// 显示失败状态
const errorMessage = responseData.error || responseData.message || `添加失败: ${response.status}`;
statusElement.innerHTML = `<span class="text-red-500"><i class="fa fa-times"></i> ${errorMessage}</span>`;
// 强制重排,确保过渡效果生效
void statusElement.offsetWidth;
// 添加过渡类和错误状态类
statusElement.classList.add('status-transition', 'status-fade-in', 'status-error');
showNotification(errorMessage, 'error');
}
} catch (error) {
console.error('Error adding blacklist:', error);
// 清除之前的所有类
statusElement.classList.remove('status-transition', 'status-loading', 'status-success', 'status-error', 'status-fade-in', 'status-fade-out');
// 显示错误状态
statusElement.innerHTML = `<span class="text-red-500"><i class="fa fa-times"></i> 失败</span>`;
showNotification(error.message || '添加黑名单失败', 'error');
const errorMessage = error.message || '添加黑名单失败';
statusElement.innerHTML = `<span class="text-red-500"><i class="fa fa-times"></i> ${errorMessage}</span>`;
// 强制重排,确保过渡效果生效
void statusElement.offsetWidth;
// 添加过渡类和错误状态类
statusElement.classList.add('status-transition', 'status-fade-in', 'status-error');
showNotification(errorMessage, 'error');
} finally {
// 3秒后清除状态显示
// 3秒后渐变消失
setTimeout(() => {
statusElement.innerHTML = '';
// 添加淡出类
statusElement.classList.add('status-fade-out');
// 等待淡出动画完成后清除状态
setTimeout(() => {
// 清除所有类
statusElement.classList.remove('status-transition', 'status-loading', 'status-success', 'status-error', 'status-fade-in', 'status-fade-out');
// 清空状态显示
statusElement.innerHTML = '';
}, 300); // 与CSS动画持续时间一致
}, 3000);
}
}