video_blacklist/index/live-management.html

563 lines
27 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="bg-neutral font-inter text-gray-800">
<!-- 顶部导航栏 - 移动设备 -->
<header class="lg:hidden fixed top-0 left-0 right-0 bg-white shadow-sm z-10">
<div class="container mx-auto px-4 py-3 flex justify-between items-center">
<div class="flex items-center gap-2">
<i class="fa-solid fa-shield text-primary text-lg"></i>
<span class="text-lg font-bold text-primary">智能保镖</span>
</div>
<button id="mobile-menu-button" class="p-2 rounded-lg hover:bg-gray-100">
<i class="fa-solid fa-bars"></i>
</button>
</div>
</header>
<div class="flex min-h-screen pt-14 lg:pt-0">
<!-- 侧边栏导航 - 桌面端 -->
<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 active"><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"><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-white rounded-xl shadow-sm overflow-hidden mb-6">
<div class="border-b p-4 flex justify-between items-center">
<h2 class="font-medium text-lg">直播账号管理</h2>
<div class="flex gap-3">
<!-- 新增按钮调用 login() -->
<button class="btn-primary flex items-center gap-2" onclick="login()">
<i class="fa-solid fa-plus"></i>
<span>新增</span>
</button>
<!-- 刷新按钮调用 refreshTable() -->
<button class="btn-outline flex items-center gap-2" onclick="refreshTable()">
<i class="fa-solid fa-sync"></i>
<span>刷新</span>
</button>
<!-- 全局停止监听按钮 (保留,但逻辑可能依赖后端判断是否有监听任务) -->
</div>
</div>
<div class="overflow-x-auto">
<table class="w-full border-collapse">
<thead>
<tr class="bg-neutral">
<th class="py-3 px-4 text-left text-sm font-medium text-gray-600">序号</th>
<th class="py-3 px-4 text-left text-sm font-medium text-gray-600">抖音号</th>
<th class="py-3 px-4 text-left text-sm font-medium text-gray-600">到期时间</th>
<th class="py-3 px-4 text-left text-sm font-medium text-gray-600">主播昵称</th>
<th class="py-3 px-4 text-left text-sm font-medium text-gray-600">操作</th>
</tr>
</thead>
<tbody id="account-table-body">
<!-- 表格行将由 JavaScript 动态生成 -->
</tbody>
<tfoot class="bg-neutral">
<tr>
<td colspan="5" class="py-3 px-4 text-sm text-gray-600">
<span id="account-count-info">第 0-0 条/总共 0 条</span>
<!-- 简单的分页占位实际分页需复杂JS逻辑 -->
<!-- <a href="#" class="px-2 py-1 rounded hover:bg-primary/10 hover:text-primary transition-all">1</a> -->
</td>
</tr>
</tfoot>
</table>
</div>
</div>
</main>
</div>
<script>
// 移动端菜单切换
document.getElementById('mobile-menu-button').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');
}
});
// --- pywebview API 调用函数 ---
// 调用Python的login_click方法
function login() {
console.log('调用Python登录...');
if (window.pywebview) {
// 禁用新增和刷新按钮,避免重复操作
const addButton = document.querySelector('.btn-primary');
const refreshButton = document.querySelector('.btn-outline');
if (addButton) {
addButton.disabled = true;
addButton.classList.add('opacity-50', 'cursor-not-allowed');
}
if (refreshButton) {
refreshButton.disabled = true;
refreshButton.classList.add('opacity-50', 'cursor-not-allowed');
}
window.pywebview.api.login_click()
.then(success => {
if (success) {
console.log('Python登录流程已启动');
// Note: The actual UI refresh happens when Python calls updateAccountTable
} else {
console.log('Python登录流程启动失败');
// 可以显示一个提示
alert('登录失败,请重试。');
}
})
.catch(error => {
console.error('调用Python login_click出错:', error);
// 可以显示一个错误提示
alert(`登录发生错误: ${error}`);
})
.finally(() => {
// 无论成功或失败,重新启用按钮
if (addButton) {
addButton.disabled = false;
addButton.classList.remove('opacity-50', 'cursor-not-allowed');
}
if (refreshButton) {
refreshButton.disabled = false;
refreshButton.classList.remove('opacity-50', 'cursor-not-allowed');
}
});
} else {
console.error('pywebview API 未就绪.');
alert('应用正在启动中,请稍后再试。');
}
}
// 调用Python的删除账号方法
// 接收 urlPart, nickname, douyinId 作为参数
function deleteAccount(urlPart, nickname, douyinId) {
console.log('前端请求删除账号:', urlPart, '昵称:', nickname, '抖音号:', douyinId);
if (window.pywebview) {
const displayNickname = nickname || '未知昵称';
const displayDouyinId = douyinId || '未知抖音号';
const confirmMessage = `确定要删除账号 "${displayNickname}" (抖音号: ${displayDouyinId}) 吗?`;
const confirmDelete = confirm(confirmMessage);
if (confirmDelete) {
window.pywebview.api.delete_account(urlPart)
.then(success => {
if (success) {
console.log(`Python 成功处理删除请求url_part: ${urlPart}`);
// 删除成功后刷新表格
refreshTable();
} else {
console.warn(`Python 处理删除请求失败或未找到账号url_part: ${urlPart}`);
// Python端在无法删除正在监听的账号时可能已经弹出了提示这里可以不重复弹
}
})
.catch(error => {
console.error(`调用Python delete_account 出错url_part: ${urlPart}:`, error);
alert(`删除账号发生错误: ${error}`);
});
} else {
console.log('用户取消删除。');
}
} else {
console.error('pywebview API 未就绪.');
alert('应用正在启动中,请稍后再试。');
}
}
// 调用Python的监听账号方法
function monitorAccount(urlPart, nickname) {
console.log('前端请求监听账号:', urlPart);
if (window.pywebview) {
// 找到对应的监听按钮并禁用
const monitorButton = document.querySelector(`tr[data-url-part="${urlPart}"] button.text-primary`);
if (monitorButton) {
monitorButton.disabled = true;
monitorButton.classList.add('opacity-50', 'cursor-not-allowed');
}
window.pywebview.api.monitor_account(urlPart, nickname)
.then(success => {
if (success) {
console.log(`Python 成功启动账号 ${urlPart} 的监听流程。`);
// UI更新将通过 updateButtonState 由Python触发
} else {
console.warn(`Python 启动账号 ${urlPart} 监听失败。`);
// Python 在失败时可能已经弹窗提示,这里可以不处理或加一个通用提示
// alert('启动监听失败,请检查日志或稍后再试。');
}
})
.catch(error => {
console.error(`调用Python monitor_account 出错url_part: ${urlPart}:`, error);
alert(`启动监听时发生错误: ${error}`);
})
.finally(() => {
// 无论成功或失败,重新启用按钮
if (monitorButton) {
monitorButton.disabled = false;
monitorButton.classList.remove('opacity-50', 'cursor-not-allowed');
}
});
} else {
console.error('pywebview API 未就绪.');
alert('应用正在启动中,请稍后再试。');
}
}
// 调用Python的停止监听方法
function stopMonitoring(urlPart) {
console.log('前端请求停止监听账号:', urlPart);
if (window.pywebview) {
window.pywebview.api.stop_monitoring(urlPart)
.then(success => {
if (success) {
console.log(`Python 成功停止账号 ${urlPart} 的监听流程。`);
// UI更新将通过 updateButtonState 由Python触发
} else {
console.warn(`Python 停止账号 ${urlPart} 监听失败。`);
// Python 在失败时可能已经弹窗提示,这里可以不处理或加一个通用提示
// alert('停止监听失败,请检查日志或稍后再试。');
}
})
.catch(error => {
console.error(`调用Python stop_monitoring 出错url_part: ${urlPart}:`, error);
alert(`停止监听时发生错误: ${error}`);
});
} else {
console.error('pywebview API 未就绪.');
alert('应用正在启动中,请稍后再试。');
}
}
// 添加 JavaScript 函数来更新按钮状态
function updateButtonState(urlPart, isMonitoring) {
console.log(`更新按钮状态: url_part=${urlPart}, isMonitoring=${isMonitoring}`);
const row = document.querySelector(`tr[data-url-part="${urlPart}"]`);
if (row) {
const actionsCell = row.querySelector('td:last-child');
if (actionsCell) {
let buttonHTML = '';
if (isMonitoring) {
// 如果正在监听,显示停止按钮
buttonHTML = `
<button class="text-warning hover:underline mr-3" onclick="stopMonitoring('${urlPart}')">
<i class="fa-solid fa-stop mr-1"></i>停止
</button>
<button class="text-danger hover:underline mr-3" onclick="deleteAccount('${urlPart}', '${row.dataset.nickname}', '${row.dataset.douyinId}')">
<i class="fa-solid fa-trash-can mr-1"></i>删除
</button>
`;
} else {
// 如果没有在监听,显示监听按钮
buttonHTML = `
<button class="text-primary hover:underline mr-3" onclick="monitorAccount('${urlPart}', '${row.dataset.nickname}')">
<i class="fa-solid fa-play mr-1"></i>监听
</button>
<button class="text-danger hover:underline mr-3" onclick="deleteAccount('${urlPart}', '${row.dataset.nickname}', '${row.dataset.douyinId}')">
<i class="fa-solid fa-trash-can mr-1"></i>删除
</button>
`;
}
actionsCell.innerHTML = buttonHTML;
}
}
}
// 调用Python的refresh_accounts方法
function refreshTable() {
console.log('调用Python刷新账号列表...');
if (window.pywebview) {
// Python 的 refresh_accounts 会读取文件并调用 updateAccountTable 来更新 UI
window.pywebview.api.refresh_accounts()
.catch(error => {
console.error('调用Python refresh_accounts出错:', error);
// 可以显示一个错误提示
alert(`刷新账号列表发生错误: ${error}`);
});
} else {
console.error('pywebview API 未就绪.');
alert('应用正在启动中,请稍后再试。');
}
}
// 这个函数由 Python 调用,用于更新账号列表
function updateAccountTable(accounts) {
console.log('接收到账号数据,正在更新表格:', accounts);
const tbody = document.getElementById('account-table-body');
const countInfo = document.getElementById('account-count-info');
tbody.innerHTML = ''; // 清空当前表格内容
if (!accounts || accounts.length === 0) {
// 如果没有账号,显示提示信息或空状态
const noDataRow = `<tr><td colspan="5" class="py-3 px-4 text-sm text-center text-gray-500">暂无账号,请点击"新增"按钮添加。</td></tr>`;
tbody.innerHTML = noDataRow;
countInfo.textContent = '第 0-0 条/总共 0 条';
return;
}
// 遍历账号数据,创建并添加表格行
accounts.forEach((account, index) => {
const row = document.createElement('tr');
// 添加 data 属性以便通过 url_part 定位行
row.dataset.urlPart = account.url_part;
row.dataset.nickname = account.nickname || ''; // 存储昵称和抖音号,方便 deleteAccount 使用
row.dataset.douyinId = account.douyin_id || '';
row.classList.add('border-b', 'hover:bg-gray-50'); // 添加样式
// 序号
const indexCell = document.createElement('td');
indexCell.classList.add('py-3', 'px-4', 'text-sm');
indexCell.textContent = index + 1; // 从1开始计数
row.appendChild(indexCell);
// 抖音号
const douyinIdCell = document.createElement('td');
douyinIdCell.classList.add('py-3', 'px-4', 'text-sm');
douyinIdCell.textContent = account.douyin_id || '未知抖音号';
row.appendChild(douyinIdCell);
// 到期时间
const expiryCell = document.createElement('td');
expiryCell.classList.add('py-3', 'px-4', 'text-sm');
expiryCell.innerHTML = `${account.expiry_time || '未知到期时间'} <a href="#" class="text-primary hover:underline ml-2">充值</a>`;
row.appendChild(expiryCell);
// 主播昵称
const nicknameCell = document.createElement('td');
nicknameCell.classList.add('py-3', 'px-4', 'text-sm');
nicknameCell.textContent = account.nickname || '未知昵称';
row.appendChild(nicknameCell);
// 操作 (总是显示监听和删除按钮)
const actionsCell = document.createElement('td');
actionsCell.classList.add('py-3', 'px-4', 'text-sm', 'whitespace-nowrap'); // 防止按钮换行
actionsCell.innerHTML = `
<button class="text-primary hover:underline mr-3" onclick="monitorAccount('${account.url_part}', '${account.nickname}')">
<i class="fa-solid fa-play mr-1"></i>监听
</button>
<button class="text-danger hover:underline mr-3" onclick="deleteAccount('${account.url_part}', '${account.nickname}', '${account.douyin_id}')">
<i class="fa-solid fa-trash-can mr-1"></i>删除
</button>
`;
row.appendChild(actionsCell);
tbody.appendChild(row);
});
// 更新总数信息
countInfo.textContent = `第 1-${accounts.length} 条/总共 ${accounts.length}`;
console.log('表格更新完成。');
}
// --- 页面加载完成后执行 ---
// 等待DOM完全加载
window.addEventListener('DOMContentLoaded', (event) => {
console.log('DOM完全加载完成');
// 创建同步的 XMLHttpRequest
const xhr = new XMLHttpRequest();
xhr.open('GET', 'user_Settings_Options.json', false); // 第三个参数设为 false 表示同步请求
xhr.send();
if (xhr.status === 200) {
// 解析 JSON 并写入 localStorage
const data = JSON.parse(xhr.responseText);
localStorage.setItem('smartBodyguardSettings', JSON.stringify(data));
console.log('配置已同步保存到 localStorage');
// 等待pywebview API 就绪
if (window.pywebview) {
console.log('pywebview API 已就绪。');
// 在API就绪后立即调用Python方法获取初始账号列表并渲染
window.pywebview.api.get_initial_accounts()
.then(() => {
console.log('请求初始账号列表已发送到 Python。');
// updateAccountTable will be called by Python when data is ready
})
.catch(error => {
console.error('调用Python get_initial_accounts出错:', error);
// Optionally display an error in the UI
alert(`获取初始账号列表失败: ${error}`);
});
} else {
console.warn('pywebview API 尚未就绪,等待 pywebviewready 事件...');
// 如果API未就绪监听 'pywebviewready' 事件
window.addEventListener('pywebviewready', function () {
console.log('pywebviewready 事件触发API 已就绪。');
window.pywebview.api.get_initial_accounts()
.then(() => {
console.log('请求初始账号列表已发送到 Python (通过事件监听)。');
})
.catch(error => {
console.error('调用Python get_initial_accounts出错 (通过事件监听):', error);
alert(`获取初始账号列表失败 (通过事件监听): ${error}`);
});
});
}
} else {
console.error(`请求配置文件失败,状态码: ${xhr.status}`);
}
// 添加一个事件监听,尝试在窗口关闭前停止监听(不保证总能触发)
window.addEventListener('beforeunload', (event) => {
console.log('窗口即将关闭,尝试停止监听...');
if (window.pywebview && window.pywebview.api && window.pywebview.api.stop_monitoring) {
// 这里只是一个示例性的尝试,可能无效
// window.pywebview.api.stop_monitoring(); // 同步调用,不推荐在 beforeunload
}
// event.preventDefault(); // 取消关闭(用于调试)
// event.returnValue = ''; // 某些浏览器需要这个来显示提示
});
});
</script>
</body>
</html>