video_blacklist/index/id-blacklist.html

1058 lines
53 KiB
HTML
Raw Normal View History

2025-06-03 18:23:22 +08:00
<!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>