video_blacklist/index/id-blacklist.html

1058 lines
53 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>智能保镖 | 直播间管理工具</title>
<script src="./js/tailwindcss.js"></script>
<link href="./fontawesome-free-6.4.0-web/css/all.css" rel="stylesheet">
<script>
tailwind.config = {
theme: {
extend: {
colors: {
primary: '#165DFF',
secondary: '#36CFC9',
neutral: '#F5F7FA',
'neutral-dark': '#4E5969',
success: '#00B42A',
warning: '#FF7D00',
danger: '#F53F3F',
},
fontFamily: {
inter: ['Inter', 'system-ui', 'sans-serif'],
},
spacing: { // 添加或修改 gap 的定义,确保 gap-4 存在
'4': '1rem', // 16px - 列表项和表头的主要列间距
'3': '1rem', // 12px - 保留原有的gap-3 (侧边栏等使用)
'8': '3rem', // 32px - 预留给按钮占位 (操作按钮区域宽度)
'10': '3rem', // 40px - 预留给头像占位 (头像宽度)
// 根据需要添加更多间距值
}
}
}
}
</script>
<style type="text/tailwindcss">
@layer utilities {
.content-auto {
content-visibility: auto;
}
.sidebar-item {
@apply flex items-center gap-3 px-4 py-3 rounded-lg transition-all duration-200 hover:bg-primary/10 hover:text-primary;
}
.sidebar-item.active {
@apply bg-primary/10 text-primary font-medium;
}
.rule-card {
@apply bg-white rounded-xl shadow-sm border border-gray-100 transition-all duration-300 hover:shadow-md;
}
.btn-primary {
@apply bg-primary hover:bg-primary/90 text-white px-4 py-2 rounded-lg transition-all duration-200 shadow-sm hover:shadow flex items-center justify-center gap-2;
}
.btn-outline {
@apply border border-gray-200 hover:border-primary/50 text-neutral-dark hover:text-primary px-4 py-2 rounded-lg transition-all duration-200 flex items-center justify-center gap-2;
}
.fade-in {
animation: fadeIn 0.5s ease-in-out;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.pulse {
animation: pulse 2s infinite;
}
@keyframes pulse {
0% {
transform: scale(1);
}
50% {
transform: scale(1.05);
}
100% {
transform: scale(1);
}
}
.scrollbar-thin {
scrollbar-width: thin;
}
.scrollbar-thin::-webkit-scrollbar {
width: 4px;
}
.scrollbar-thin::-webkit-scrollbar-thumb {
background-color: rgba(156, 163, 175, 0.5);
border-radius: 4px;
}
.sticky-top {
position: sticky;
top: 20px;
z-index: 10;
}
}
</style>
</head>
<body class="font-inter bg-neutral min-h-screen flex flex-col">
<!-- 顶部导航栏 (移动设备) -->
<header class="lg:hidden bg-white shadow-sm px-4 py-3 flex items-center justify-between">
<div class="flex items-center gap-2">
<i class="fa-solid fa-shield text-primary text-xl"></i>
<h1 class="text-lg font-bold text-primary">智能保镖</h1>
</div>
<button id="mobile-menu-btn" class="p-2 rounded-lg hover:bg-gray-100">
<i class="fa-solid fa-bars text-neutral-dark"></i>
</button>
</header>
<div class="flex flex-1 overflow-hidden">
<!-- 侧边栏导航 -->
<aside id="sidebar"
class="lg:w-64 bg-white shadow-sm flex-shrink-0 hidden lg:block transition-all duration-300 z-20">
<div class="p-4 border-b border-gray-100">
<div class="flex items-center gap-2">
<i class="fa-solid fa-shield text-primary text-xl"></i>
<h1 class="text-lg font-bold text-primary">智能保镖</h1>
</div>
<p class="text-xs text-gray-400 mt-1">直播间智能管理助手</p>
</div>
<nav class="p-4">
<ul class="space-y-1">
<li><a href="live-management.html" class="sidebar-item"><i class="fa-solid fa-video"></i> 直播管理</a>
</li>
<li><a href="smart-bodyguard.html" class="sidebar-item"><i class="fa-solid fa-cogs"></i> 保镖设置</a>
</li>
<li><a href="id-whitelist-blacklist.html" class="sidebar-item"><i class="fa-solid fa-user-check"></i> ID
黑白名单</a></li>
<li><a href="id-blacklist.html" class="sidebar-item active"><i class="fas fa-ban"></i> 拉黑记录</a></li>
<li><a href="Logging.html" class="sidebar-item"><i class="fa-solid fa-file-lines"></i>
运行日志</a></li>
<li><a href="#" class="sidebar-item"><i class="fa-solid fa-book"></i> 使用教程</a></li>
<li><a href="#" class="sidebar-item"><i class="fa-solid fa-comment-dots"></i> 意见反馈</a></li>
</ul>
</nav>
</aside>
<!-- 主内容区 -->
<main class="flex-1 overflow-y-auto bg-neutral p-4 lg:p-6">
<!-- 主播信息 -->
<div class="bg-primary rounded-lg p-4 text-white mb-6 fade-in flex items-center">
<div class="flex items-center gap-3 flex-1">
<img src="https://picsum.photos/id/64/40/40" alt="用户头像"
class="w-10 h-10 rounded-full object-cover border-2 border-white">
<div>
<p id="anchor-name" class="text-base font-medium">主播名称</p>
<button id="change-account"
class="text-xs bg-white/20 hover:bg-white/30 px-2 py-1 rounded transition-all mt-1">
点击切换账号
</button>
</div>
</div>
<!-- 新增清除缓存按钮(移至最右边) -->
<button id="clear-cache-btn"
class="flex items-center justify-center w-10 h-10 rounded-full bg-white/10 hover:bg-white/20 transition-all text-white ml-2">
<i class="fa-solid fa-trash"></i>
</button>
</div>
<!-- 欢迎信息 -->
<div class="mb-6 fade-in">
<h2 class="text-[clamp(1.5rem,3vw,2rem)] font-bold text-gray-800">欢迎使用智能保镖</h2>
<p class="text-gray-500 mt-1">为您的直播间提供全方位保护,有效过滤不良用户</p>
</div>
<!-- 拉黑记录区域 -->
<div class="w-full bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
<div class="p-5 border-b border-gray-100 sticky top-0 bg-white z-10 flex justify-between items-center">
<div>
<h3 class="font-medium flex items-center gap-2">
<i class="fa-solid fa-user-ban text-primary"></i> 拉黑记录
</h3>
<p class="text-xs text-gray-400 mt-1">最近7天共拉黑 <span id="total-blacklist-count">--</span>
</p>
</div>
<button id="refresh-blacklist" class="text-gray-400 hover:text-primary transition-colors">
<i class="fa-solid fa-refresh"></i>
</button>
</div>
<div class="p-4">
<!-- 选择控制区域 -->
<div class="flex items-center justify-between mb-4 flex-wrap gap-2">
<div class="flex items-center gap-3">
<button id="select-all"
class="text-sm text-primary hover:text-primary/80 flex items-center gap-1">
<i class="fa-solid fa-check-square"></i> 全选
</button>
<button id="invert-selection"
class="text-sm text-primary hover:text-primary/80 flex items-center gap-1">
<i class="fa-solid fa-exchange-alt"></i> 反选
</button>
<button id="batch-unblock"
class="text-sm text-danger hover:text-danger/80 flex items-center gap-1">
<i class="fa-solid fa-user-plus"></i> 批量解除
</button>
</div>
<div class="relative w-full sm:w-auto flex-grow max-w-sm">
<input type="text" id="search-blacklist" placeholder="搜索昵称、抖音号"
class="w-full pl-10 pr-8 py-2 border border-gray-200 rounded-lg focus:ring-2 focus:ring-primary/20 focus:border-primary outline-none text-sm">
<i class="fa-solid fa-search absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400"></i>
<button id="clear-search"
class="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-primary hidden">
<i class="fa-solid fa-xmark"></i>
</button>
</div>
</div>
<!-- 拉黑记录表头 -->
<!-- 使用 Flexbox 布局,并添加占位元素 (含文本) 以对齐 -->
<!-- gap-4 用于增大列之间的间距 -->
<div class="blacklist-header bg-neutral/50 p-3 flex items-center gap-4 font-medium text-sm text-gray-600 rounded-t-lg lg:rounded-t-xl">
<!-- Checkbox Placeholder -->
<div class="w-[16px] flex-shrink-0 text-center"></div> <!-- 宽度匹配复选框,留空 -->
<!-- Avatar Placeholder - 宽度与实际头像匹配 -->
<div class="w-10 flex-shrink-0 text-center">
头像
</div>
<!-- Content Columns (uses grid) - 占据剩余空间 -->
<div class="flex-1 min-w-0 grid grid-cols-4 gap-4">
<div>昵称</div>
<div>抖音号</div>
<div>拉黑原因</div>
<div>拉黑时间</div>
</div>
<!-- Options Button Placeholder - 宽度与实际按钮区域匹配 -->
<div class="w-8 flex-shrink-0 text-center">
操作
</div>
</div>
<!-- 拉黑记录列表容器 -->
<div id="blacklist-container"
class="space-y-0 max-h-[calc(100vh-350px)] lg:max-h-[calc(100vh-300px)] overflow-y-auto scrollbar-thin">
<!-- space-y-0 因为列表项自身提供了间距 -->
<!-- 拉黑记录项将在这里动态生成 -->
</div>
<div class="mt-4 text-center">
<button id="load-more" class="text-primary text-sm hover:underline hidden">
查看更多 <i class="fa-solid fa-chevron-right text-xs ml-1"></i>
</button>
</div>
</div>
</div>
</main>
</div>
<!-- 规则名称编辑弹窗 (未使用,但保留在代码中) -->
<div id="edit-modal"
class="fixed inset-0 bg-black/50 flex items-center justify-center z-50 opacity-0 pointer-events-none transition-opacity duration-300">
<div class="bg-white rounded-xl shadow-lg p-6 w-full max-w-md transform scale-95 transition-transform duration-300">
<div class="flex justify-between items-center mb-4">
<h3 class="font-medium text-lg">编辑规则名称</h3>
<button id="close-edit-modal" class="text-gray-400 hover:text-gray-600">
<i class="fa-solid fa-times"></i>
</button>
</div>
<div class="mb-4">
<input type="text" id="rule-name-input"
class="w-full px-4 py-2 border border-gray-200 rounded-lg focus:ring-2 focus:ring-primary/20 focus:border-primary outline-none">
</div>
<div class="flex justify-end gap-3">
<button id="cancel-edit" class="btn-outline">取消</button>
<button id="save-edit" class="btn-primary">保存</button>
</div>
</div>
</div>
<!-- 操作成功弹窗 -->
<div id="success-modal"
class="fixed inset-0 bg-black/50 flex items-center justify-center z-50 opacity-0 pointer-events-none transition-opacity duration-300">
<div class="bg-white rounded-xl shadow-lg p-6 w-full max-w-sm transform scale-95 transition-transform duration-300 text-center">
<div class="w-16 h-16 bg-success/10 rounded-full flex items-center justify-center mx-auto mb-4">
<i class="fa-solid fa-check text-success text-xl"></i>
</div>
<h3 class="font-medium text-lg mb-2" id="success-message">操作成功</h3>
<p class="text-gray-500 text-sm mb-4">该弹窗将在5秒后自动关闭</p>
<button id="close-success-modal" class="btn-primary w-full">
关闭
</button>
</div>
</div>
<!-- 切换账号弹窗 -->
<div id="change-account-modal"
class="fixed inset-0 bg-black/50 flex items-center justify-center z-50 opacity-0 pointer-events-none transition-opacity duration-300">
<div class="bg-white rounded-xl shadow-lg p-6 w-full max-w-md transform scale-95 transition-transform duration-300">
<div class="flex justify-between items-center mb-4">
<h3 class="font-medium text-lg">切换账号</h3>
<button id="close-change-account-modal" class="text-gray-400 hover:text-gray-600">
<i class="fa-solid fa-times"></i>
</button>
</div>
<div class="mb-4">
<select id="account-select"
class="w-full px-4 py-2 border border-gray-200 rounded-lg focus:ring-2 focus:ring-primary/20 focus:border-primary outline-none">
<option value="anchor1">主播名称1</option>
<option value="anchor2">主播名称2</option>
<option value="anchor3">主播名称3</option>
</select>
</div>
<div class="flex justify-end gap-3">
<button id="cancel-change-account" class="btn-outline">取消</button>
<button id="confirm-change-account" class="btn-primary">确认切换</button>
</div>
</div>
</div>
<!-- 用户操作选项菜单 -->
<div id="user-options-menu"
class="fixed bg-white rounded-lg shadow-lg z-50 opacity-0 pointer-events-none transition-all duration-200 transform scale-95">
<ul class="py-2 min-w-[120px]">
<li class="px-4 py-2 hover:bg-gray-100 cursor-pointer text-sm" id="unblock-user">
<i class="fa-solid fa-user-plus mr-2"></i> 解除拉黑
</li>
<li class="px-4 py-2 hover:bg-gray-100 cursor-pointer text-sm" id="view-profile">
<i class="fa-solid fa-user mr-2"></i> 查看用户主页
</li>
</ul>
</div>
<script>
// 页面加载完成后执行
document.addEventListener('DOMContentLoaded', function () {
window.addEventListener('pywebviewready', function () {
console.log('pywebviewready event fired');
currentAccountUrlPart = 'MS4wLjABAAAAmqCtESSNlEubCwi95GKLd77fsja9MyGBzjb11zLojW4'
pywebview.api.get_blacklist_records(currentAccountUrlPart)
.then(data => {
// 后端返回的是 JSON 字符串,需要解析
try {
testData = JSON.parse(data);
console.log(`Successfully loaded ${testData.length} records from backend.`); // 调试打印
// 数据加载成功后,更新总数并渲染列表
updateBlacklistCount(); // 更新总人数显示
applyFilterAndRender(); // 应用过滤(初始没有搜索词)并渲染第一页
} catch (e) {
console.error("Failed to parse blacklist data from backend:", e); // 错误打印
testData = []; // 解析失败则清空数据
updateBlacklistCount(); // 更新总人数显示
applyFilterAndRender(); // 渲染空列表
}
})
.catch(error => {
console.error("Error fetching blacklist records from backend:", error); // 错误打印
testData = []; // 获取数据失败则清空数据
updateBlacklistCount(); // 更新总人数显示
applyFilterAndRender(); // 渲染空列表
});
});
// 初始化提示框关闭功能 (未使用,但保留)
// document.getElementById('close-tip').addEventListener('click', function () {
// hideInitTip();
// });
// 移动端菜单切换
// document.getElementById('mobile-menu-btn').addEventListener('click', function () {
// const sidebar = document.getElementById('sidebar');
// sidebar.classList.toggle('hidden');
// // 其他类切换,保持原有逻辑
// if (!sidebar.classList.contains('hidden')) {
// sidebar.classList.add('fixed', 'inset-0');
// sidebar.classList.remove('lg:block', 'lg:w-64');
// } else {
// sidebar.classList.remove('fixed', 'inset-0');
// sidebar.classList.add('lg:block', 'lg:w-64'); // Corrected typo: lg-w-64 -> lg:w-64
// }
// });
// --- 新增/修改状态变量 ---
let testData = []; // 所有拉黑数据的原始来源
let currentSearchTerm = ''; // 当前搜索词
let filteredData = []; // 根据搜索词过滤后的数据
let currentFilteredIndex = 0; // 当前在filteredData中已加载的索引
const itemsPerLoad = 10; // 每页加载数量
const blacklistContainer = document.getElementById('blacklist-container');
const loadMoreButton = document.getElementById('load-more');
const searchInput = document.getElementById('search-blacklist');
const clearSearchBtn = document.getElementById('clear-search');
const userOptionsMenu = document.getElementById('user-options-menu');
let currentUserItem = null; // 用于单条解除拉黑时保存当前操作项
const totalBlacklistCountElement = document.getElementById('total-blacklist-count');
// Debugging: Log the loadMoreButton element immediately after getting it
console.log('Load More button element:', loadMoreButton);
// --- 工具函数 ---
// 搜索框清空按钮显示/隐藏
searchInput.addEventListener('input', function () {
clearSearchBtn.classList.toggle('hidden', this.value.trim() === '');
});
document.getElementById('refresh-blacklist').addEventListener('click', function () {
console.log('Refresh button clicked. Forcing refresh from database via backend.');
if (typeof currentAccountUrlPart !== 'undefined' && currentAccountUrlPart) {
// 调用后端新增的强制刷新方法
pywebview.api.refresh_blacklist_records(currentAccountUrlPart)
.then(data => {
try {
testData = JSON.parse(data); // 更新 testData 变量
console.log(`Successfully refreshed and loaded ${testData.length} records from database.`);
updateBlacklistCount();
applyFilterAndRender();
showSuccessModal('拉黑记录已刷新'); // 显示成功消息
} catch (e) {
console.error("Failed to parse refreshed blacklist data:", e);
showSuccessModal('刷新失败:数据格式错误'); // 显示错误消息
}
})
.catch(error => {
console.error("Error fetching blacklist records during refresh:", error);
showSuccessModal('刷新失败:获取数据出错'); // 显示错误消息
});
} else {
console.error("currentAccountUrlPart is not defined. Cannot refresh blacklist.");
showSuccessModal('刷新失败:未选择账号'); // 如果账号未选择,显示错误
}
});
// 点击清空搜索按钮
clearSearchBtn.addEventListener('click', function () {
searchInput.value = '';
this.classList.add('hidden');
// 重置搜索词并重新加载数据
currentSearchTerm = '';
applyFilterAndRender(); // 调用应用过滤并重新渲染函数
});
// 初始检查搜索框内容,决定是否显示清空按钮
searchInput.dispatchEvent(new Event('input'));
// 创建拉黑记录项的HTML使用Grid布局保持与表头一致
function createBlacklistItemHTML(item) {
// 使用 Flexbox 在主级别布局 [checkbox] [avatar] [content grid] [options button]
// gap-4 用于增大列之间的间距
return `
<div class="blacklist-item bg-white p-3 flex items-center gap-4 hover:bg-primary/5 transition-all border-b border-gray-100 last:border-b-0"
data-id="${item.id}"
data-user-id="${item.userId}"> <!-- 隐藏的主页ID -->
<input type="checkbox" class="select-item flex-shrink-0" data-id="${item.id}">
<img src="${item.avatar}" alt="用户头像" class="w-10 h-10 rounded-full object-cover flex-shrink-0">
<!-- 内容区使用Grid布局列宽与表头匹配 -->
<div class="flex-1 min-w-0 grid grid-cols-4 gap-4 items-center text-sm"> <!-- 添加text-sm -->
<div class="truncate">${item.nickname}</div>
<div class="truncate text-gray-600">${item.douyinId}</div>
<div class="truncate text-gray-600">${item.reason}</div>
<div class="truncate text-gray-500 text-xs">${item.time}</div> <!-- 使用精确时间 -->
</div>
<!-- 操作按钮 -->
<button class="text-gray-400 hover:text-primary options-btn flex-shrink-0 w-8 flex items-center justify-center"> <!-- w-8 确保宽度,并居中 -->
<i class="fa-solid fa-ellipsis-v"></i>
</button>
</div>
`;
}
// 过滤数据 (添加抖音号搜索)
function filterData(data, searchTerm) {
if (!searchTerm) {
return [...data]; // 没有搜索词时返回所有数据的副本
}
const lowerTerm = searchTerm.toLowerCase().trim();
return data.filter(item => {
// 确保属性存在且是字符串避免调用toLowerCase()出错
const nickname = (item.nickname || '').toString().toLowerCase();
const douyinId = (item.douyinId || '').toString().toLowerCase(); // 添加抖音号字段
// const reason = (item.reason || '').toString().toLowerCase();
// 时间格式化后进行搜索(如果需要搜时间)
// const time = (item.time || '').toString().toLowerCase(); // 通常不搜索时间
return nickname.includes(lowerTerm) ||
douyinId.includes(lowerTerm)
// time.includes(lowerTerm); // 如果需要搜索时间可以加上这行
});
}
// 显示过滤后的数据(分页加载)
function displayFilteredData(count) {
const startIndex = currentFilteredIndex;
const endIndex = Math.min(currentFilteredIndex + count, filteredData.length);
const fragment = document.createDocumentFragment(); // 使用 DocumentFragment 提高性能
console.log(`displayFilteredData called. currentFilteredIndex: ${currentFilteredIndex}, count: ${count}, filteredData.length: ${filteredData.length}`);
console.log(`Loading items from index ${startIndex} to ${endIndex - 1}`);
for (let i = startIndex; i < endIndex; i++) {
const item = filteredData[i];
const itemHTML = createBlacklistItemHTML(item); // 仍然生成 HTML 字符串
// 创建一个临时 div 来解析 HTML 字符串并获取 DOM节点
const tempDiv = document.createElement('div');
tempDiv.innerHTML = itemHTML.trim(); // 使用trim()确保没有空白字符影响解析
if (tempDiv.firstElementChild) {
fragment.appendChild(tempDiv.firstElementChild); // 添加实际的元素节点
//console.log(`Added item ${i}: ${item.nickname || item.username} (ID: ${item.id})`);
} else {
console.error(`Failed to create DOM element for item ${i}`, item);
}
}
blacklistContainer.appendChild(fragment); // 将所有节点一次性添加到容器中
currentFilteredIndex = endIndex; // 更新已加载的索引
console.log(`displayFilteredData finished. New currentFilteredIndex: ${currentFilteredIndex}`);
updateLoadMoreButton(); // 更新加载更多按钮状态
}
// 更新“加载更多”按钮状态
function updateLoadMoreButton() {
console.log(`updateLoadMoreButton called. currentFilteredIndex: ${currentFilteredIndex}, filteredData.length: ${filteredData.length}`);
if (loadMoreButton) { // Add null check for safety
if (currentFilteredIndex < filteredData.length) {
loadMoreButton.classList.remove('hidden'); // 移除 hidden 类,使按钮可见
console.log("Load More button visible");
} else {
loadMoreButton.classList.add('hidden'); // 添加 hidden 类,隐藏按钮
console.log("Load More button hidden");
}
} else {
console.error("Load More button element is null in updateLoadMoreButton!");
}
}
// 应用当前搜索过滤并重新渲染整个列表
function applyFilterAndRender() {
console.log(`Applying filter "${currentSearchTerm}" and re-rendering.`);
filteredData = filterData(testData, currentSearchTerm); // 重新过滤数据
console.log(`Filtered data length: ${filteredData.length}`);
blacklistContainer.innerHTML = ''; // 清空DOM列表项
currentFilteredIndex = 0; // 重置加载索引
displayFilteredData(itemsPerLoad); // 显示第一页过滤后的数据
}
// 更新拉黑总人数显示
function updateBlacklistCount() {
if (totalBlacklistCountElement) {
// 显示总数,但过滤后可能只显示一部分
// 可以选择显示总数,或者显示当前过滤结果的数量
// 这里显示原始总数更符合“最近7天共拉黑 X 人”的描述
totalBlacklistCountElement.textContent = testData.length;
console.log(`Total blacklist count updated to: ${testData.length}`);
}
}
// --- 事件绑定 ---
// 搜索拉黑记录功能 (调用 applyFilterAndRender)
searchInput.addEventListener('input', function () {
currentSearchTerm = this.value; // 更新搜索词状态
applyFilterAndRender(); // 应用过滤并重新渲染
});
// 监听规则下拉框变化并保存 (不变, 仅用于其他页面)
document.querySelectorAll('.rule-select').forEach(select => {
select.addEventListener('change', function () {
// 立即保存设置
saveAllSettings();
});
});
document.querySelectorAll('.rule-input').forEach(input => {
input.addEventListener('input', function () {
saveAllSettings();
});
});
// 用户选项菜单显示逻辑
document.getElementById('blacklist-container').addEventListener('click', function (e) {
// 确保只在点击 .options-btn 时触发
const optionsBtn = e.target.closest('.options-btn');
if (optionsBtn) {
e.stopPropagation(); // 阻止事件冒泡
currentUserItem = optionsBtn.closest('.blacklist-item'); // 保存当前操作的列表项元素
// 获取按钮位置并定位菜单
const rect = optionsBtn.getBoundingClientRect();
// 确保菜单元素已经渲染,获取实际宽度和高度
// 短暂显示以获取尺寸
userOptionsMenu.style.display = 'block';
userOptionsMenu.classList.remove('opacity-0', 'pointer-events-none', 'scale-95');
userOptionsMenu.classList.add('opacity-100', 'scale-100');
const menuWidth = userOptionsMenu.offsetWidth;
const menuHeight = userOptionsMenu.offsetHeight;
// 重置初始状态以便定位 (可以延迟这个重置,或在定位后立即重置)
// userOptionsMenu.classList.add('opacity-0', 'pointer-events-none', 'scale-95');
// userOptionsMenu.classList.remove('opacity-100', 'scale-100');
const windowWidth = window.innerWidth;
const windowHeight = window.innerHeight;
let menuLeft = rect.right + 5; // 距离按钮右侧5px
let menuTop = rect.top;
// 确保菜单不会超出右边界
if (menuLeft + menuWidth > windowWidth - 10) {
menuLeft = rect.left - menuWidth - 5; // 显示在按钮左侧
}
// 确保菜单不会超出左边界 (防止在最左边显示时跑到屏幕外)
if (menuLeft < 5) {
menuLeft = 5;
}
// 确保菜单不会超出下边界
if (menuTop + menuHeight > windowHeight - 10) {
menuTop = rect.bottom - menuHeight; // 显示在按钮上方
if (menuTop < 5) menuTop = 5; // 防止超出上边界
}
userOptionsMenu.style.top = `${menuTop}px`;
userOptionsMenu.style.left = `${menuLeft}px`;
// 最终显示菜单
// setTimeout(() => { // 不再需要 setTimeout 如果尺寸获取已经通过 display = 'block' 解决
userOptionsMenu.classList.remove('opacity-0', 'pointer-events-none', 'scale-95');
userOptionsMenu.classList.add('opacity-100', 'scale-100');
// }, 10);
}
});
// 点击其他地方关闭菜单
document.addEventListener('click', function (event) {
// 如果点击不在菜单内部,且不是点击触发菜单的按钮,则关闭菜单
if (!userOptionsMenu.contains(event.target) && !event.target.closest('.options-btn')) {
userOptionsMenu.classList.add('opacity-0', 'pointer-events-none', 'scale-95');
userOptionsMenu.classList.remove('opacity-100', 'scale-100');
userOptionsMenu.style.display = 'none'; // Hide it completely when closed
currentUserItem = null; // 点击外部后重置当前操作项
}
});
// 初始化时隐藏菜单 (确保菜单初始状态是隐藏且不占空间的)
userOptionsMenu.style.display = 'none';
// 解除拉黑功能 (修改:调用后端方法,并在成功后刷新列表)
document.getElementById('unblock-user').addEventListener('click', function (e) {
e.stopPropagation(); // 阻止事件冒泡
if (currentUserItem && typeof currentAccountUrlPart !== 'undefined' && currentAccountUrlPart) {
const blacklistRecordIdToRemove = currentUserItem.dataset.id; // 获取要移除的拉黑记录的数据库ID
const userSidToUnblock = currentUserItem.dataset.userId; // 获取要解除拉黑用户的 user_sid
console.log(`Attempting to unblock user (record ID: ${blacklistRecordIdToRemove}, user_sid: ${userSidToUnblock}) via backend.`);
// 调用后端解除拉黑方法
pywebview.api.perform_unblock(currentAccountUrlPart, userSidToUnblock)
.then(result_message => {
console.log("Backend unblock result:", result_message);
// 根据后端返回的结果消息判断是否成功
if (result_message && result_message.includes("成功")) {
// 如果后端解除成功,进一步调用后端方法从数据库删除记录
pywebview.api.delete_blacklist_record_by_id(blacklistRecordIdToRemove)
.then(delete_success => {
if (delete_success) {
console.log(`Successfully deleted record ${blacklistRecordIdToRemove} from database.`);
// *** 核心修改在这里:在数据库删除成功后更新前端 ***
// 从 testData 中移除该拉黑记录
const initialLength = testData.length;
testData = testData.filter(item => item.id !== parseInt(blacklistRecordIdToRemove)); // 确保ID类型匹配数据库ID通常是数字
const removedCount = initialLength - testData.length;
console.log(`Removed ${removedCount} item(s) with record ID: ${blacklistRecordIdToRemove} from testData.`);
updateBlacklistCount(); // 更新总人数显示
applyFilterAndRender(); // 应用当前过滤并重新渲染整个列表
showSuccessModal('已解除拉黑'); // 显示成功消息
} else {
console.error(`Failed to delete record ${blacklistRecordIdToRemove} from database.`);
// 即使数据库删除失败,抖音接口可能解除了,这里提示用户并考虑是否刷新整个列表
showSuccessModal('解除拉黑成功,但从列表中移除失败,请刷新');
// 可以选择强制刷新来同步数据
// location.reload();
}
})
.catch(delete_error => {
console.error(`Error deleting record ${blacklistRecordIdToRemove} from database:`, delete_error);
showSuccessModal('解除拉黑成功,但从列表中移除出错');
});
} else {
// 如果后端解除失败(抖音接口未成功),显示后端返回的失败消息
showSuccessModal(result_message || '解除拉黑失败');
}
})
.catch(error => {
console.error("Error calling backend perform_unblock:", error);
showSuccessModal('解除拉黑操作出错'); // 显示通用错误消息
});
// 隐藏用户选项菜单
userOptionsMenu.classList.add('opacity-0', 'pointer-events-none', 'scale-95');
userOptionsMenu.classList.remove('opacity-100', 'scale-100');
userOptionsMenu.style.display = 'none'; // Completely hide
currentUserItem = null; // 重置当前操作项
} else if (!currentAccountUrlPart) {
console.error("currentAccountUrlPart is not defined. Cannot perform unblock.");
showSuccessModal('解除拉黑失败:未选择账号'); // 如果账号未选择,显示错误
}
});
// 查看用户主页 (修改:使用 data-user-id)
document.getElementById('view-profile').addEventListener('click', function (e) {
e.stopPropagation(); // 阻止事件冒泡
if (currentUserItem) {
const userId = currentUserItem.dataset.userId; // 从data属性获取用户主页ID
console.log(`Attempting to view profile for user ID: ${userId}`);
if (userId) {
const douyinUrl = `https://www.douyin.com/user/${userId}`; // 拼接完整URL
window.open(douyinUrl, '_blank'); // 新窗口打开
console.log(`Opening profile URL: ${douyinUrl}`);
} else {
console.warn("User ID not found on the selected item for viewing profile.");
showSuccessModal('未能获取用户主页信息'); // 可选:显示提示
}
}
// 隐藏用户选项菜单
userOptionsMenu.classList.add('opacity-0', 'pointer-events-none', 'scale-95');
userOptionsMenu.classList.remove('opacity-100', 'scale-100');
userOptionsMenu.style.display = 'none'; // Hide it completely
});
// 切换账号功能
const changeAccountModal = document.getElementById('change-account-modal');
document.getElementById('change-account').addEventListener('click', function () {
showChangeAccountModal();
});
document.getElementById('close-change-account-modal').addEventListener('click', hideChangeAccountModal);
document.getElementById('cancel-change-account').addEventListener('click', hideChangeAccountModal);
document.getElementById('confirm-change-account').addEventListener('click', function () {
const selectElement = document.getElementById('account-select');
const selectedAccount = selectElement.value;
const accountName = selectElement.options[selectElement.selectedIndex].text;
// 更新显示的主播名称
document.getElementById('anchor-name').textContent = accountName;
// 保存当前选择的账号
localStorage.setItem('currentAnchor', selectedAccount);
hideChangeAccountModal();
showSuccessModal('账号已切换为 ' + accountName);
// 切换账号后可能需要重新加载数据,这里简单刷新页面模拟
// location.reload(); // 如果需要完全重置状态,可以取消注释
});
// 成功提示框自动关闭
document.getElementById('close-success-modal').addEventListener('click', function () {
hideSuccessModal();
});
// 清除缓存
document.getElementById('clear-cache-btn').addEventListener('click', function () {
// 清除本地存储
localStorage.removeItem('smartBodyguardSettings');
localStorage.removeItem('currentAnchor'); // 清除主播信息缓存
data = {
"id-whitelist": {"name": "id白名单", "enabled": false, "option": "", "inputValue": ""},
"id-blacklist-rule": {"name": "id黑名单", "enabled": false, "inputValue": ""},
"danmaku": {"name": "弹幕关键词拉黑", "enabled": false, "inputValue": ""},
"local-data": {"name": "数据互通", "enabled": false},
"suspected-account": {"name": "疑似账号", "enabled": false},
"follow": {"name": "关注拉黑", "enabled": false},
"share-entry": {"name": "通过分享进入直播间", "enabled": false},
"follow-entry": {"name": "通过关注进入直播间", "enabled": false},
"private-account": {"name": "私密账号", "enabled": false},
"blue-v": {"name": "开通蓝v", "enabled": false},
"share": {"name": "分享拉黑", "enabled": false, "options": []},
"gender": {"name": "性别拉黑", "enabled": false, "options": []},
"age-range": {"name": "年龄区间拉黑", "enabled": false, "inputValue": ""},
"follower-count": {"name": "粉丝数量大于拉黑", "enabled": false, "inputValue": ""},
"following-count": {"name": "关注数量大于拉黑", "enabled": false, "inputValue": ""},
"work-count": {"name": "作品数量大于拉黑", "enabled": false, "inputValue": ""},
"entry-count": {"name": "用户进入次数拉黑", "enabled": false, "inputValue": ""},
"nickname": {"name": "昵称关键词", "enabled": false, "inputValue": ""},
"signature": {"name": "个性签名关键词拉黑", "enabled": false, "inputValue": ""},
"region": {"name": "地区拉黑", "enabled": false, "inputValue": ""},
"ip": {"name": "IP拉黑", "enabled": false, "inputValue": ""}
}
pywebview.api.get_localStorage(JSON.stringify(data))
// 刷新页面或重置UI状态
location.reload();
// 显示成功提示 (刷新后提示会消失,可以考虑其他方式提示)
// showSuccessModal('缓存已清除');
});
// 工具函数 - 显示/隐藏模态框
const editModal = document.getElementById('edit-modal'); // 定义 editModal
const ruleNameInput = document.getElementById('rule-name-input'); // 定义 ruleNameInput
function showEditModal() {
editModal.classList.remove('opacity-0', 'pointer-events-none');
editModal.querySelector('div').classList.remove('scale-95');
editModal.querySelector('div').classList.add('scale-100');
ruleNameInput.focus();
}
function hideEditModal() {
editModal.classList.add('opacity-0', 'pointer-events-none');
editModal.querySelector('div').classList.remove('scale-100');
editModal.querySelector('div').classList.add('scale-95');
}
function showSuccessModal(message) {
const successMessage = document.getElementById('success-message');
successMessage.textContent = message || '操作成功';
const successModal = document.getElementById('success-modal');
successModal.classList.remove('opacity-0', 'pointer-events-none');
successModal.querySelector('div').classList.remove('scale-95');
successModal.querySelector('div').classList.add('scale-100');
// 5秒后自动关闭
setTimeout(hideSuccessModal, 5000);
}
function hideSuccessModal() {
const successModal = document.getElementById('success-modal');
successModal.classList.add('opacity-0', 'pointer-events-none');
successModal.querySelector('div').classList.remove('scale-100');
successModal.querySelector('div').classList.add('scale-95');
}
function showChangeAccountModal() {
changeAccountModal.classList.remove('opacity-0', 'pointer-events-none');
changeAccountModal.querySelector('div').classList.remove('scale-95');
changeAccountModal.querySelector('div').classList.add('scale-100');
}
function hideChangeAccountModal() {
changeAccountModal.classList.add('opacity-0', 'pointer-events-none');
changeAccountModal.querySelector('div').classList.remove('scale-100');
changeAccountModal.querySelector('div').classList.add('scale-95');
}
// function hideInitTip() { (未使用,但保留)
// const initTip = document.getElementById('init-tip');
// initTip.classList.add('opacity-0', 'pointer-events-none');
// }
// 保存所有设置到本地存储 (不变, 仅用于其他页面)
function saveAllSettings() {
// 此函数主要用于其他页面,当前页面(拉黑记录)没有需要保存的规则设置
console.log("Attempted to save settings, but saveAllSettings logic for rules is not present on this page.");
}
// 从本地存储加载设置 (加载主播信息)
function loadSettings() {
// 加载当前选择的账号
const currentAnchor = localStorage.getItem('currentAnchor');
const select = document.getElementById('account-select');
if (currentAnchor && select) {
for (let i = 0; i < select.options.length; i++) {
if (select.options[i].value === currentAnchor) {
select.selectedIndex = i;
document.getElementById('anchor-name').textContent = select.options[i].text;
break;
}
}
} else if (select && select.options.length > 0) {
// If no account saved, default to the first option
document.getElementById('anchor-name').textContent = select.options[0].text;
}
}
// 全选功能 (作用于当前过滤并显示在DOM中的项)
document.getElementById('select-all').addEventListener('click', function () {
document.querySelectorAll('#blacklist-container .blacklist-item input[type="checkbox"]').forEach(checkbox => {
checkbox.checked = true;
});
});
// 反选功能 (作用于当前过滤并显示在DOM中的项)
document.getElementById('invert-selection').addEventListener('click', function () {
document.querySelectorAll('#blacklist-container .blacklist-item input[type="checkbox"]').forEach(checkbox => {
checkbox.checked = !checkbox.checked;
});
});
// 批量解除拉黑
document.getElementById('batch-unblock').addEventListener('click', function () {
const selectedCheckboxes = document.querySelectorAll('#blacklist-container .blacklist-item input[type="checkbox"]:checked');
if (selectedCheckboxes.length === 0) {
showSuccessModal('请先选择要解除拉黑的用户');
console.log("Batch unblock clicked, but no items selected.");
return;
}
// 获取选中的拉黑记录的数据库ID和用户的 user_sid
const selectedRecordIds = [];
const selectedUserSids = [];
selectedCheckboxes.forEach(cb => {
selectedRecordIds.push(cb.dataset.id); // 数据库记录ID
// 从对应的列表项中获取 user_sid
const listItem = cb.closest('.blacklist-item');
if (listItem) {
selectedUserSids.push(listItem.dataset.userId); // 用户主页ID (user_sid)
} else {
console.error("Could not find parent list item for checkbox with id:", cb.dataset.id);
}
});
console.log("Attempting to batch unblock record IDs:", selectedRecordIds);
console.log("Attempting to batch unblock user SIDs:", selectedUserSids);
if (selectedUserSids.length === 0) {
showSuccessModal('未能获取到要解除拉黑的用户信息');
console.error("No user SIDs collected for batch unblock.");
return;
}
// 调用后端批量解除拉黑方法
pywebview.api.perform_batch_unblock(currentAccountUrlPart, selectedUserSids)
.then(result_json => {
console.log("Backend batch unblock result JSON:", result_json);
try {
const result = JSON.parse(result_json);
if (result.success) {
console.log("Backend batch unblock reported success.");
// 如果后端批量解除成功(抖音接口成功),进一步调用后端方法批量删除数据库记录
pywebview.api.batch_delete_blacklist_records_by_ids(selectedRecordIds)
.then(delete_success => {
if (delete_success) { // 批量删除数据库记录成功
console.log(`Successfully deleted ${selectedRecordIds.length} records from database.`);
// *** 核心修改在这里:在数据库删除成功后更新前端 ***
// 从 testData 中移除选中的拉黑记录
const initialLength = testData.length;
// 将 selectedRecordIds 转换为数字数组进行过滤比较
const selectedRecordIdsNum = selectedRecordIds.map(id => parseInt(id));
testData = testData.filter(item => !selectedRecordIdsNum.includes(item.id));
const removedCount = initialLength - testData.length;
console.log(`Removed ${removedCount} item(s) from testData during batch unblock.`);
updateBlacklistCount(); // 更新总人数显示
applyFilterAndRender(); // 应用过滤并重新渲染整个列表
showSuccessModal(result.message); // 显示后端返回的成功消息
} else {
console.error(`Failed to delete some records from database after batch unblock.`);
// 即使数据库删除失败,抖音接口可能解除了,这里提示用户并考虑是否刷新整个列表
showSuccessModal('批量解除拉黑成功,但部分记录从列表中移除失败,请刷新');
// 可以选择强制刷新来同步数据
// location.reload();
}
})
.catch(delete_error => {
console.error(`Error deleting records from database after batch unblock:`, delete_error);
showSuccessModal('批量解除拉黑成功,但从列表中移除出错');
});
} else {
// 如果后端批量解除失败(抖音接口未成功),显示后端返回的失败消息
console.log("Backend batch unblock reported failure.");
showSuccessModal(result.message || '批量解除拉黑失败');
// 可以选择是否在失败时刷新列表,以便用户看到哪些未解除
// pywebview.api.get_blacklist_records(currentAccountUrlPart).then(...)
}
} catch (e) {
console.error("Failed to parse backend batch unblock result:", e);
showSuccessModal('处理批量解除结果时出错'); // 显示错误消息
}
})
.catch(error => {
console.error("Error calling backend perform_batch_unblock:", error);
showSuccessModal('批量解除拉黑操作出错'); // 显示通用错误消息
});
});
// 页面加载时初始化数据并渲染
window.addEventListener('load', () => {
console.log("Page loaded. Initializing data and rendering.");
// testData = generateInitialTestData(); // 生成初始测试数据
updateBlacklistCount(); // 更新总人数显示
applyFilterAndRender(); // 应用过滤(初始没有搜索词)并渲染第一页
loadSettings(); // 加载其他设置(例如主播名称等)
});
// (原有的初始化提示框显示,如果需要可以保留)
// setTimeout(() => {
// const initTip = document.getElementById('init-tip');
// initTip.classList.remove('opacity-0', 'pointer-events-none');
// setTimeout(hideInitTip, 5000);
// }, 1000);
// Attach click listener for load more button AFTER getting the element reference
if (loadMoreButton) {
loadMoreButton.addEventListener('click', () => {
console.log("Load More button clicked!"); // Debug log INSIDE the listener
displayFilteredData(itemsPerLoad);
});
console.log("Load More button click listener attached.");
} else {
console.error("Load More button element was not found when trying to attach listener!");
}
});
</script>
</body>
</html>