1058 lines
53 KiB
HTML
1058 lines
53 KiB
HTML
|
<!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>
|