diff --git a/.trae/documents/修复保存按钮错位问题.md b/.trae/documents/修复保存按钮错位问题.md new file mode 100644 index 0000000..b62d072 --- /dev/null +++ b/.trae/documents/修复保存按钮错位问题.md @@ -0,0 +1,37 @@ +# 修复保存按钮错位问题 + +## 问题分析 +远程黑名单管理部分的保存按钮错位,原因是: +- 添加黑名单表单使用了Grid布局(`grid grid-cols-1 md:grid-cols-3 gap-4`) +- 前两个子元素包含label和input,高度较高 +- 第三个子元素只有按钮和状态显示,没有label,高度较矮 +- 导致按钮在垂直方向上与前两个输入框不对齐 + +## 解决方案 +修改添加黑名单表单的HTML结构,确保三个子元素在垂直方向上对齐: + +1. **为第三个子元素添加垂直对齐** + - 确保保存按钮容器与前两个输入框容器在垂直方向上对齐 + - 可以选择顶部对齐或中间对齐,保持视觉一致性 + +2. **统一子元素结构** + - 为第三个子元素添加一个隐藏的label,确保结构一致 + - 或者调整Grid布局,使第三个子元素的内容垂直居中 + +3. **调整容器样式** + - 修改保存按钮容器的样式,确保垂直对齐 + - 可以使用`items-start`或`items-center`属性 + +## 实现步骤 + +1. 修改`add-blacklist-form`的HTML结构 + - 为第三个子元素添加`items-start`类,使其与前两个子元素的顶部对齐 + - 或者添加一个隐藏的label,确保结构一致 + +2. 测试修改后的效果 + - 确保保存按钮与前两个输入框在垂直方向上对齐 + - 保持响应式布局,在不同屏幕尺寸下都能正常显示 + +## 预期效果 + +保存按钮将与前两个输入框在垂直方向上对齐,消除错位现象,提升表单的视觉一致性和用户体验。 \ No newline at end of file diff --git a/.trae/documents/修复远程黑名单管理保存按钮对齐问题.md b/.trae/documents/修复远程黑名单管理保存按钮对齐问题.md new file mode 100644 index 0000000..699b234 --- /dev/null +++ b/.trae/documents/修复远程黑名单管理保存按钮对齐问题.md @@ -0,0 +1,20 @@ +## 问题分析 +从截图可以看出,远程黑名单管理的保存按钮与输入框在垂直方向上没有对齐,按钮位置偏下。 + +## 原因分析 +1. 当前使用grid布局分为3列,前两列包含label和input,第三列包含空label和按钮容器 +2. 空label虽然没有内容,但仍然占据了空间(mb-1 margin),导致按钮被推到下方 +3. 按钮容器使用了`items-center`,但整体位置受label影响 + +## 修复方案 +修改第三列的HTML结构,移除空label,直接放置按钮容器,并使用flex布局的对齐属性确保按钮与输入框垂直对齐。 + +## 具体修改 +1. 打开`/root/dns/static/index.html`文件 +2. 找到远程黑名单管理的添加表单部分(第839-847行) +3. 修改第三列的HTML结构,移除空label,直接放置按钮容器 +4. 使用flex布局的`items-end`属性确保按钮与输入框底部对齐 +5. 调整按钮的margin-top为0,确保与输入框精确对齐 + +## 预期效果 +保存按钮与输入框在垂直方向上精确对齐,整体布局更加美观和专业。 \ No newline at end of file diff --git a/.trae/documents/修改远程黑名单管理界面,添加状态渐变效果.md b/.trae/documents/修改远程黑名单管理界面,添加状态渐变效果.md new file mode 100644 index 0000000..2760439 --- /dev/null +++ b/.trae/documents/修改远程黑名单管理界面,添加状态渐变效果.md @@ -0,0 +1,29 @@ +# 修改远程黑名单管理界面,添加状态渐变效果 + +## 问题分析 +当前远程黑名单管理界面的状态更新功能已经实现了3秒后恢复默认状态的功能,但没有添加渐变效果,用户体验不够流畅。 + +## 解决方案 +修改`updateStatus`函数,为状态元素添加渐变效果,确保状态更新时的平滑过渡。 + +## 实现步骤 + +1. **修改`updateStatus`函数** + - 为状态元素添加CSS过渡效果 + - 使用`classList`操作来添加/移除渐变类 + - 确保状态更新时的平滑过渡 + +2. **添加CSS渐变样式** + - 在index.html中添加状态渐变的CSS样式 + - 为不同状态(loading、success、error)添加不同的渐变效果 + +3. **测试修改后的功能** + - 确保状态更新时有平滑的渐变效果 + - 确保3秒后状态能正常恢复默认 + +## 预期效果 +远程黑名单管理界面的状态更新将具有平滑的渐变效果,包括: +- 加载状态的蓝色渐变效果 +- 成功状态的绿色渐变效果 +- 错误状态的红色渐变效果 +- 所有状态在3秒后平滑消失,恢复默认状态 \ No newline at end of file diff --git a/.trae/documents/根据服务器日志判断操作成功状态.md b/.trae/documents/根据服务器日志判断操作成功状态.md new file mode 100644 index 0000000..475f624 --- /dev/null +++ b/.trae/documents/根据服务器日志判断操作成功状态.md @@ -0,0 +1,33 @@ +# 根据服务器日志判断操作成功状态 + +## 问题分析 +当前代码中,部分操作只检查了HTTP响应状态码,没有检查服务器返回的`status`字段,这可能导致在某些情况下无法正确判断操作是否成功。根据服务器端代码,所有成功的响应都会返回`{"status": "success"}`,因此需要确保前端代码在所有操作中都检查这个字段。 + +## 解决方案 +修改以下函数,确保它们都根据服务器返回的`status`字段判断操作是否成功: + +1. **handleDeleteBlacklist** (line 653-716) + - 添加对服务器响应数据中`status`字段的检查 + - 确保只有当`status`为`success`时才认为删除成功 + +2. **handleToggleBlacklist** (line 719-793) + - 添加对服务器响应数据中`status`字段的检查 + - 确保只有当`status`为`success`时才认为切换状态成功 + +3. **handleAddRule** (line 378-409) + - 添加对服务器响应数据中`status`字段的检查 + - 确保只有当`status`为`success`时才认为添加规则成功 + +4. **handleDeleteRule** (line 309-375) + - 添加对服务器响应数据中`status`字段的检查 + - 确保只有当`status`为`success`时才认为删除规则成功 + +## 实现步骤 +1. 修改`handleDeleteBlacklist`函数,添加响应数据解析和status字段检查 +2. 修改`handleToggleBlacklist`函数,添加响应数据解析和status字段检查 +3. 修改`handleAddRule`函数,添加响应数据解析和status字段检查 +4. 修改`handleDeleteRule`函数,添加响应数据解析和status字段检查 +5. 测试所有操作,确保它们都能正确根据服务器响应判断成功或失败 + +## 预期效果 +所有操作(添加、更新、删除、切换状态)都会根据服务器返回的`status`字段判断是否成功,确保操作结果与服务器日志一致。 \ No newline at end of file diff --git a/config.json b/config.json index eb3fd0b..275a0fc 100644 --- a/config.json +++ b/config.json @@ -21,19 +21,19 @@ "name": "AdGuard DNS filter", "url": "https://gitea.amazehome.xyz/AMAZEHOME/hosts-and-filters/raw/branch/main/filter.txt", "enabled": true, - "lastUpdateTime": "2025-11-28T14:43:26.972Z" + "lastUpdateTime": "2025-11-28T15:45:11.073Z" }, { "name": "Adaway Default Blocklist", "url": "https://gitea.amazehome.xyz/AMAZEHOME/hosts-and-Filters/raw/branch/main/hosts/adaway.txt", "enabled": true, - "lastUpdateTime": "2025-11-28T14:41:39.123Z" + "lastUpdateTime": "2025-11-28T15:36:43.086Z" }, { "name": "CHN-anti-AD", "url": "https://gitea.amazehome.xyz/AMAZEHOME/hosts-and-Filters/raw/branch/main/list/easylist.txt", "enabled": true, - "lastUpdateTime": "2025-11-28T14:41:41.318Z" + "lastUpdateTime": "2025-11-28T15:26:24.833Z" }, { "name": "My GitHub Rules", @@ -42,7 +42,7 @@ "lastUpdateTime": "2025-11-28T14:42:09.271Z" }, { - "name": "China List", + "name": "CNList", "url": "https://gitea.amazehome.xyz/AMAZEHOME/hosts-and-filters/raw/branch/main/list/china.list", "enabled": true } diff --git a/http/server.go b/http/server.go index f59f33a..76f6e62 100644 --- a/http/server.go +++ b/http/server.go @@ -752,7 +752,7 @@ func (s *Server) handleShieldBlacklists(w http.ResponseWriter, r *http.Request) } if targetURLOrName == "" { - http.Error(w, "黑名单标识不能为空", http.StatusBadRequest) + json.NewEncoder(w).Encode(map[string]string{"status": "error", "message": "黑名单标识不能为空"}) return } @@ -767,7 +767,7 @@ func (s *Server) handleShieldBlacklists(w http.ResponseWriter, r *http.Request) } if targetIndex == -1 { - http.Error(w, "黑名单不存在", http.StatusNotFound) + json.NewEncoder(w).Encode(map[string]string{"status": "error", "message": "黑名单不存在"}) return } @@ -780,7 +780,7 @@ func (s *Server) handleShieldBlacklists(w http.ResponseWriter, r *http.Request) // 保存配置到文件 if err := saveConfigToFile(s.globalConfig, "config.json"); err != nil { logger.Error("保存配置文件失败", "error", err) - http.Error(w, "保存配置失败", http.StatusInternalServerError) + json.NewEncoder(w).Encode(map[string]string{"status": "error", "message": "保存配置失败"}) return } // 重新加载规则以获取最新的远程规则 @@ -810,7 +810,7 @@ func (s *Server) handleShieldBlacklists(w http.ResponseWriter, r *http.Request) // 保存配置到文件 if err := saveConfigToFile(s.globalConfig, "config.json"); err != nil { logger.Error("保存配置文件失败", "error", err) - http.Error(w, "保存配置失败", http.StatusInternalServerError) + json.NewEncoder(w).Encode(map[string]string{"status": "error", "message": "保存配置失败"}) return } json.NewEncoder(w).Encode(map[string]string{"status": "success"}) @@ -831,12 +831,12 @@ func (s *Server) handleShieldBlacklists(w http.ResponseWriter, r *http.Request) } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - http.Error(w, "Invalid request body", http.StatusBadRequest) + json.NewEncoder(w).Encode(map[string]string{"status": "error", "message": "无效的请求体"}) return } if req.Name == "" || req.URL == "" { - http.Error(w, "Name and URL are required", http.StatusBadRequest) + json.NewEncoder(w).Encode(map[string]string{"status": "error", "message": "名称和URL不能为空"}) return } @@ -846,11 +846,17 @@ func (s *Server) handleShieldBlacklists(w http.ResponseWriter, r *http.Request) // 检查是否已存在 for _, list := range blacklists { if list.URL == req.URL { - http.Error(w, "黑名单URL已存在", http.StatusConflict) + json.NewEncoder(w).Encode(map[string]string{"status": "error", "message": "黑名单URL已存在"}) return } } + // 检查URL是否存在且可访问 + if !checkURLExists(req.URL) { + json.NewEncoder(w).Encode(map[string]string{"status": "error", "message": "URL不存在或无法访问"}) + return + } + // 添加新黑名单 newEntry := config.BlacklistEntry{ Name: req.Name, @@ -865,7 +871,7 @@ func (s *Server) handleShieldBlacklists(w http.ResponseWriter, r *http.Request) // 保存配置到文件 if err := saveConfigToFile(s.globalConfig, "config.json"); err != nil { logger.Error("保存配置文件失败", "error", err) - http.Error(w, "保存配置失败", http.StatusInternalServerError) + json.NewEncoder(w).Encode(map[string]string{"status": "error", "message": "保存配置失败"}) return } @@ -884,7 +890,7 @@ func (s *Server) handleShieldBlacklists(w http.ResponseWriter, r *http.Request) } if err := json.NewDecoder(r.Body).Decode(&updatedBlacklists); err != nil { - http.Error(w, "Invalid request body", http.StatusBadRequest) + json.NewEncoder(w).Encode(map[string]string{"status": "error", "message": "无效的请求体"}) return } @@ -906,7 +912,7 @@ func (s *Server) handleShieldBlacklists(w http.ResponseWriter, r *http.Request) // 保存配置到文件 if err := saveConfigToFile(s.globalConfig, "config.json"); err != nil { logger.Error("保存配置文件失败", "error", err) - http.Error(w, "保存配置失败", http.StatusInternalServerError) + json.NewEncoder(w).Encode(map[string]string{"status": "error", "message": "保存配置失败"}) return } // 重新加载规则 @@ -915,7 +921,7 @@ func (s *Server) handleShieldBlacklists(w http.ResponseWriter, r *http.Request) json.NewEncoder(w).Encode(map[string]string{"status": "success"}) default: - http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + json.NewEncoder(w).Encode(map[string]string{"status": "error", "message": "Method not allowed"}) } } @@ -1193,6 +1199,24 @@ func isValidIP(ip string) bool { return true } +// checkURLExists 检查URL是否存在且可访问 +func checkURLExists(url string) bool { + // 创建一个带有超时的HTTP客户端 + client := &http.Client{ + Timeout: 5 * time.Second, + } + + // 发送HEAD请求来检查URL是否存在 + resp, err := client.Head(url) + if err != nil { + return false + } + defer resp.Body.Close() + + // 检查状态码,2xx和3xx表示成功 + return resp.StatusCode >= 200 && resp.StatusCode < 400 +} + // handleRestart 处理重启服务请求 func (s *Server) handleRestart(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { diff --git a/static/index.html b/static/index.html index 5db56c8..7503240 100644 --- a/static/index.html +++ b/static/index.html @@ -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; + } @@ -739,6 +798,7 @@ +
@@ -748,12 +808,13 @@ 规则 + 状态 操作 - 暂无规则 + 暂无规则 @@ -775,11 +836,13 @@ -
- -
+
+
+ +
+
diff --git a/static/js/shield.js b/static/js/shield.js index 73cfcc3..4ece01e 100644 --- a/static/js/shield.js +++ b/static/js/shield.js @@ -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 = '-'; } + // 强制重排,确保过渡效果生效 + 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 = '-'; + }, 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 = ' 处理中...'; + break; + case 'success': + statusHTML = ` ${message || '成功'}`; + break; + case 'error': + statusHTML = ` ${message || '失败'}`; + break; + default: + statusHTML = '-'; + } + + // 强制重排,确保过渡效果生效 + 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 = '-'; + }, 300); // 与CSS动画持续时间一致 }, 3000); } } @@ -248,7 +329,7 @@ function updateRulesTable(rules) { if (rules.length === 0) { const emptyRow = document.createElement('tr'); - emptyRow.innerHTML = '暂无规则'; + emptyRow.innerHTML = '暂无规则'; 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 = '-'; + 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 = `显示前 ${maxRulesToShow} 条规则,共 ${rules.length} 条`; + infoRow.innerHTML = `显示前 ${maxRulesToShow} 条规则,共 ${rules.length} 条`; 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 = ' 正在添加...'; + + // 强制重排,确保过渡效果生效 + 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 = ' 成功'; + + // 强制重排,确保过渡效果生效 + 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 = ` ${errorMessage}`; + + // 强制重排,确保过渡效果生效 + 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 = ` ${errorMessage}`; + + // 强制重排,确保过渡效果生效 + 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 = ' 正在添加...'; + // 强制重排,确保过渡效果生效 + 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 = ' 成功'; - - 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 = ' 成功'; + + // 强制重排,确保过渡效果生效 + 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 = ` ${errorMessage}`; + + // 强制重排,确保过渡效果生效 + 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 = ` 失败`; - showNotification(error.message || '添加黑名单失败', 'error'); + const errorMessage = error.message || '添加黑名单失败'; + statusElement.innerHTML = ` ${errorMessage}`; + + // 强制重排,确保过渡效果生效 + 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); } }