Files
xhs_crawler_claude/static/app.js
T
2026-01-12 15:49:09 +08:00

998 lines
31 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 小红书采集器前端逻辑
*/
// ========== 状态管理 ==========
const state = {
isRunning: false,
mode: 'single',
maxThreads: 3,
totalUsers: 0,
completedUsers: 0,
failedUsers: 0,
activeThreads: 0,
usersStatus: {},
threadStatus: {},
eventSource: null,
autoScrollLogs: true,
logFilter: 'all',
logs: []
};
// ========== DOM 元素 ==========
const elements = {
startBtn: null,
stopBtn: null,
clearLogsBtn: null,
exportDataBtn: null,
themeToggle: null,
themeIcon: null,
modeRadios: null,
threadControl: null,
threadCount: null,
progressBar: null,
progressText: null,
totalUsers: null,
completedUsers: null,
failedUsers: null,
activeThreadsStat: null,
activeThreads: null,
userList: null,
userCount: null,
threadMonitorPanel: null,
threadList: null,
logContainer: null,
logFilter: null,
logSearch: null,
screenshotPanel: null,
screenshotImage: null,
closeScreenshot: null,
// 账号管理相关
addAccountBtn: null,
refreshAccountsBtn: null,
accountStatus: null,
loginStatusIndicator: null,
loginStatusText: null,
// 模态框相关
accountModal: null,
closeAccountModal: null,
loginBtn: null,
logoutBtn: null,
loginInfo: null,
modalLoginStatus: null,
loginTime: null,
modalUserList: null,
modalUserCount: null,
clearAllUsersBtn: null,
newUserIdInput: null,
addUserIdBtn: null,
importFromTxtBtn: null,
exportToTxtBtn: null,
// 登录二维码模态框
loginQrModal: null,
closeLoginQrModal: null,
qrcodeContainer: null,
qrLoginStatus: null,
qrExpireTime: null,
// Toast
toast: null
};
// ========== 初始化 ==========
function init() {
// 获取 DOM 元素
elements.startBtn = document.getElementById('startBtn');
elements.stopBtn = document.getElementById('stopBtn');
elements.clearLogsBtn = document.getElementById('clearLogsBtn');
elements.exportDataBtn = document.getElementById('exportDataBtn');
elements.themeToggle = document.getElementById('themeToggle');
elements.themeIcon = document.getElementById('themeIcon');
elements.modeRadios = document.querySelectorAll('input[name="mode"]');
elements.threadControl = document.getElementById('threadControl');
elements.threadCount = document.getElementById('threadCount');
elements.progressBar = document.getElementById('progressBar');
elements.progressText = document.getElementById('progressText');
elements.totalUsers = document.getElementById('totalUsers');
elements.completedUsers = document.getElementById('completedUsers');
elements.failedUsers = document.getElementById('failedUsers');
elements.activeThreadsStat = document.getElementById('activeThreadsStat');
elements.activeThreads = document.getElementById('activeThreads');
elements.userList = document.getElementById('userList');
elements.userCount = document.getElementById('userCount');
elements.threadMonitorPanel = document.getElementById('threadMonitorPanel');
elements.threadList = document.getElementById('threadList');
elements.logContainer = document.getElementById('logContainer');
elements.logFilter = document.getElementById('logFilter');
elements.logSearch = document.getElementById('logSearch');
elements.screenshotPanel = document.getElementById('screenshotPanel');
elements.screenshotImage = document.getElementById('screenshotImage');
elements.closeScreenshot = document.getElementById('closeScreenshot');
// 账号管理相关
elements.addAccountBtn = document.getElementById('addAccountBtn');
elements.refreshAccountsBtn = document.getElementById('refreshAccountsBtn');
elements.accountStatus = document.getElementById('accountStatus');
elements.loginStatusIndicator = document.getElementById('loginStatusIndicator');
elements.loginStatusText = document.getElementById('loginStatusText');
// 模态框相关
elements.accountModal = document.getElementById('accountModal');
elements.closeAccountModal = document.getElementById('closeAccountModal');
elements.loginBtn = document.getElementById('loginBtn');
elements.logoutBtn = document.getElementById('logoutBtn');
elements.loginInfo = document.getElementById('loginInfo');
elements.modalLoginStatus = document.getElementById('modalLoginStatus');
elements.loginTime = document.getElementById('loginTime');
elements.modalUserList = document.getElementById('modalUserList');
elements.modalUserCount = document.getElementById('modalUserCount');
elements.clearAllUsersBtn = document.getElementById('clearAllUsersBtn');
elements.newUserIdInput = document.getElementById('newUserIdInput');
elements.addUserIdBtn = document.getElementById('addUserIdBtn');
elements.importFromTxtBtn = document.getElementById('importFromTxtBtn');
elements.exportToTxtBtn = document.getElementById('exportToTxtBtn');
// 登录二维码模态框
elements.loginQrModal = document.getElementById('loginQrModal');
elements.closeLoginQrModal = document.getElementById('closeLoginQrModal');
elements.qrcodeContainer = document.getElementById('qrcodeContainer');
elements.qrLoginStatus = document.getElementById('qrLoginStatus');
elements.qrExpireTime = document.getElementById('qrExpireTime');
// Toast
elements.toast = document.getElementById('toast');
// 加载主题
loadTheme();
// 加载登录状态
loadAccountStatus();
// 加载初始数据
loadInitialData();
// 绑定事件
bindEvents();
// 连接 SSE
connectEventStream();
}
// ========== 事件绑定 ==========
function bindEvents() {
// 启动按钮
elements.startBtn.addEventListener('click', startScraping);
// 停止按钮
elements.stopBtn.addEventListener('click', stopScraping);
// 清空日志
elements.clearLogsBtn.addEventListener('click', clearLogs);
// 导出数据
elements.exportDataBtn.addEventListener('click', exportData);
// 主题切换
elements.themeToggle.addEventListener('click', toggleTheme);
// 模式切换
elements.modeRadios.forEach(radio => {
radio.addEventListener('change', (e) => {
state.mode = e.target.value;
updateModeUI();
});
});
// 线程数变化
elements.threadCount.addEventListener('change', (e) => {
state.maxThreads = parseInt(e.target.value);
});
// 日志过滤
elements.logFilter.addEventListener('change', (e) => {
state.logFilter = e.target.value;
renderLogs();
});
// 日志搜索
elements.logSearch.addEventListener('input', debounce((e) => {
searchLogs(e.target.value);
}, 300));
// 关闭截图
elements.closeScreenshot.addEventListener('click', () => {
elements.screenshotPanel.style.display = 'none';
});
// 日志容器滚动监听(自动滚动控制)
elements.logContainer.addEventListener('scroll', () => {
const { scrollTop, scrollHeight, clientHeight } = elements.logContainer;
state.autoScrollLogs = scrollTop + clientHeight >= scrollHeight - 10;
});
// 账号管理事件
elements.addAccountBtn.addEventListener('click', openAccountModal);
elements.refreshAccountsBtn.addEventListener('click', refreshAccounts);
elements.closeAccountModal.addEventListener('click', closeAccountModal);
elements.loginBtn.addEventListener('click', openLoginQrModal);
elements.logoutBtn.addEventListener('click', logout);
elements.clearAllUsersBtn.addEventListener('click', clearAllUsers);
elements.addUserIdBtn.addEventListener('click', addUserId);
elements.importFromTxtBtn.addEventListener('click', importFromTxt);
elements.exportToTxtBtn.addEventListener('click', exportToTxt);
// 新用户输入框回车事件
elements.newUserIdInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
addUserId();
}
});
// 登录二维码模态框关闭
elements.closeLoginQrModal.addEventListener('click', closeLoginQrModal);
// 点击模态框外部关闭
window.addEventListener('click', (e) => {
if (e.target === elements.accountModal) {
closeAccountModal();
}
if (e.target === elements.loginQrModal) {
closeLoginQrModal();
}
});
}
// ========== 加载初始数据 ==========
async function loadInitialData() {
try {
// 加载状态
const statusRes = await fetch('/api/status');
const statusData = await statusRes.json();
updateStatus(statusData);
// 加载用户列表
const usersRes = await fetch('/api/users');
const usersData = await usersRes.json();
renderUserList(usersData.users);
// 加载历史日志
const logsRes = await fetch('/api/logs?limit=100');
const logsData = await logsRes.json();
state.logs = logsData.logs;
renderLogs();
} catch (error) {
console.error('加载初始数据失败:', error);
}
}
// ========== 启动采集 ==========
async function startScraping() {
try {
const response = await fetch('/api/start', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
mode: state.mode,
max_threads: state.maxThreads
})
});
const data = await response.json();
if (response.ok) {
state.isRunning = true;
updateButtons();
addLog('SUCCESS', data.message);
} else {
addLog('ERROR', data.detail || '启动失败');
}
} catch (error) {
addLog('ERROR', `启动失败: ${error.message}`);
}
}
// ========== 停止采集 ==========
async function stopScraping() {
try {
const response = await fetch('/api/stop', { method: 'POST' });
const data = await response.json();
if (response.ok) {
state.isRunning = false;
updateButtons();
addLog('WARNING', data.message);
}
} catch (error) {
addLog('ERROR', `停止失败: ${error.message}`);
}
}
// ========== 清空日志 ==========
async function clearLogs() {
try {
const response = await fetch('/api/logs/clear', { method: 'POST' });
const data = await response.json();
if (response.ok) {
state.logs = [];
renderLogs();
addLog('SUCCESS', '日志已清空');
}
} catch (error) {
addLog('ERROR', `清空日志失败: ${error.message}`);
}
}
// ========== 导出数据 ==========
async function exportData() {
try {
// 下载日志
window.location.href = '/api/logs/download';
addLog('SUCCESS', '正在下载日志文件...');
} catch (error) {
addLog('ERROR', `导出失败: ${error.message}`);
}
}
// ========== 搜索日志 ==========
async function searchLogs(keyword) {
if (!keyword) {
// 加载所有日志
const logsRes = await fetch('/api/logs?limit=100');
const logsData = await logsRes.json();
state.logs = logsData.logs;
} else {
// 搜索日志
const searchRes = await fetch(`/api/logs/search?keyword=${encodeURIComponent(keyword)}&limit=100`);
const searchData = await searchRes.json();
state.logs = searchData.logs;
}
renderLogs();
}
// ========== SSE 事件流 ==========
function connectEventStream() {
if (state.eventSource) {
state.eventSource.close();
}
state.eventSource = new EventSource('/api/events');
state.eventSource.onmessage = (event) => {
if (event.data.startsWith(': heartbeat')) {
return; // 忽略心跳
}
try {
const data = JSON.parse(event.data);
handleEvent(data);
} catch (error) {
console.error('解析事件失败:', error);
}
};
state.eventSource.onerror = (error) => {
console.error('SSE 连接错误:', error);
// 5秒后重连
setTimeout(() => {
if (!state.eventSource || state.eventSource.readyState === EventSource.CLOSED) {
connectEventStream();
}
}, 5000);
};
}
// ========== 处理事件 ==========
function handleEvent(event) {
switch (event.type) {
case 'log':
addLog(event.level, event.message, event.thread_id, event.timestamp, false);
break;
case 'progress':
updateProgress(event);
break;
case 'user_status':
updateUserStatus(event);
break;
case 'thread_status':
updateThreadStatus(event);
break;
case 'screenshot':
showScreenshot(event.path);
break;
case 'finished':
state.isRunning = false;
updateButtons();
addLog('SUCCESS', '所有采集任务完成');
break;
case 'error':
addLog('ERROR', event.message);
break;
}
}
// ========== 更新状态 ==========
function updateStatus(data) {
state.isRunning = data.is_running;
state.totalUsers = data.total_users;
state.completedUsers = data.completed_users;
state.failedUsers = data.failed_users;
state.activeThreads = data.active_threads;
state.usersStatus = data.users_status || {};
updateButtons();
updateProgress();
renderUserListFromStatus();
}
// ========== 更新进度 ==========
function updateProgress(data) {
if (data) {
state.completedUsers = data.completed || state.completedUsers;
state.failedUsers = data.failed || state.failedUsers;
state.activeThreads = data.active_threads || state.activeThreads;
}
const total = state.totalUsers;
const completed = state.completedUsers;
const failed = state.failedUsers;
const progress = total > 0 ? Math.round(((completed + failed) / total) * 100) : 0;
elements.progressBar.style.width = `${progress}%`;
elements.progressText.textContent = `${progress}%`;
elements.totalUsers.textContent = total;
elements.completedUsers.textContent = completed;
elements.failedUsers.textContent = failed;
elements.activeThreads.textContent = state.activeThreads;
// 多线程模式显示活跃线程
if (state.mode === 'multi') {
elements.activeThreadsStat.style.display = 'flex';
}
}
// ========== 更新用户状态 ==========
function updateUserStatus(data) {
state.usersStatus[data.user_id] = {
status: data.status,
note_count: data.note_count,
thread_id: data.thread_id
};
renderUserListFromStatus();
}
// ========== 更新线程状态 ==========
function updateThreadStatus(data) {
state.threadStatus[data.thread_id] = {
status: data.status,
current_user: data.current_user
};
renderThreadList();
}
// ========== 渲染用户列表 ==========
function renderUserList(users) {
if (!users || users.length === 0) {
elements.userList.innerHTML = `
<div class="empty-state">
<p>暂无用户数据</p>
<p class="hint">请在 data/users.txt 中添加用户 ID</p>
</div>
`;
elements.userCount.textContent = '0';
return;
}
elements.userCount.textContent = users.length;
elements.userList.innerHTML = users.map(user => {
const status = user.status || 'pending';
const noteCount = user.note_count || 0;
return `
<div class="user-item" data-user-id="${user.id}">
<div class="user-item-header">
<span class="user-id">${user.id}</span>
<div class="user-status">
<span class="status-icon status-${status}"></span>
<span>${getStatusText(status)}</span>
</div>
</div>
<div class="user-meta">
${noteCount > 0 ? `已采集 ${noteCount} 条笔记` : '等待采集'}
</div>
</div>
`;
}).join('');
}
// ========== 从状态渲染用户列表 ==========
function renderUserListFromStatus() {
const users = Object.keys(state.usersStatus).map(userId => ({
id: userId,
...state.usersStatus[userId]
}));
renderUserList(users);
}
// ========== 渲染线程列表 ==========
function renderThreadList() {
const threads = Object.entries(state.threadStatus);
if (threads.length === 0) {
elements.threadList.innerHTML = `
<div class="empty-state">
<p>暂无线程活动</p>
</div>
`;
return;
}
elements.threadList.innerHTML = threads.map(([threadId, status]) => {
const statusClass = status.status;
const statusText = status.status === 'busy' ? '处理中' :
status.status === 'idle' ? '空闲' : '错误';
const currentUser = status.current_user || '-';
return `
<div class="thread-item">
<div class="thread-header">
<span class="thread-name">${threadId}</span>
<span class="thread-status ${statusClass}">${statusText}</span>
</div>
<div class="thread-current">${currentUser}</div>
</div>
`;
}).join('');
}
// ========== 添加日志 ==========
function addLog(level, message, threadId = null, timestamp = null, save = true) {
const logEntry = {
timestamp: timestamp || new Date().toISOString(),
level,
message,
thread_id: threadId
};
state.logs.push(logEntry);
// 限制日志数量
if (state.logs.length > 1000) {
state.logs = state.logs.slice(-1000);
}
renderLogs();
}
// ========== 渲染日志 ==========
function renderLogs() {
let logs = state.logs;
// 过滤
if (state.logFilter !== 'all') {
logs = logs.filter(log => log.level === state.logFilter);
}
elements.logContainer.innerHTML = logs.map(log => {
const time = formatTime(log.timestamp);
const thread = log.thread_id ? `<span class="log-thread">[${log.thread_id}]</span>` : '';
return `
<div class="log-entry">
<span class="log-time">${time}</span>
<span class="log-level ${log.level}">${log.level}</span>
<span class="log-message">${thread}${escapeHtml(log.message)}</span>
</div>
`;
}).join('');
// 自动滚动到底部
if (state.autoScrollLogs) {
elements.logContainer.scrollTop = elements.logContainer.scrollHeight;
}
}
// ========== 显示截图 ==========
function showScreenshot(path) {
elements.screenshotImage.src = path;
elements.screenshotPanel.style.display = 'block';
addLog('WARNING', `错误截图已生成: ${path}`);
}
// ========== 更新按钮状态 ==========
function updateButtons() {
elements.startBtn.disabled = state.isRunning;
elements.stopBtn.disabled = !state.isRunning;
}
// ========== 更新模式 UI ==========
function updateModeUI() {
if (state.mode === 'multi') {
elements.threadControl.style.display = 'flex';
elements.threadMonitorPanel.style.display = 'block';
document.querySelector('.main-content').classList.add('with-threads');
} else {
elements.threadControl.style.display = 'none';
elements.threadMonitorPanel.style.display = 'none';
document.querySelector('.main-content').classList.remove('with-threads');
}
}
// ========== 主题相关 ==========
function loadTheme() {
const savedTheme = localStorage.getItem('theme') || 'light';
if (savedTheme === 'dark') {
document.body.classList.add('dark-theme');
elements.themeIcon.textContent = '☀️';
}
}
function toggleTheme() {
document.body.classList.toggle('dark-theme');
const isDark = document.body.classList.contains('dark-theme');
elements.themeIcon.textContent = isDark ? '☀️' : '🌙';
localStorage.setItem('theme', isDark ? 'dark' : 'light');
}
// ========== 工具函数 ==========
function formatTime(isoString) {
const date = new Date(isoString);
return date.toLocaleTimeString('zh-CN', { hour12: false });
}
function getStatusText(status) {
const statusMap = {
'pending': '待处理',
'processing': '进行中',
'completed': '已完成',
'failed': '失败'
};
return statusMap[status] || status;
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
// ========== 账号管理功能 ==========
// 打开账号管理模态框
function openAccountModal() {
elements.accountModal.classList.add('show');
loadAccountStatus();
renderModalUserList();
}
// 关闭账号管理模态框
function closeAccountModal() {
elements.accountModal.classList.remove('show');
}
// 打开登录二维码模态框
async function openLoginQrModal() {
elements.loginQrModal.classList.add('show');
elements.qrLoginStatus.textContent = '正在模拟登录...';
try {
const response = await fetch('/api/login/qrcode', {
method: 'POST'
});
const data = await response.json();
if (response.ok) {
// 显示登录成功信息
elements.qrcodeContainer.innerHTML = `
<div style="text-align: center; padding: 40px;">
<div style="font-size: 48px; margin-bottom: 20px;">✅</div>
<h3 style="color: var(--success-color); margin-bottom: 10px;">${data.message}</h3>
<p style="color: var(--text-secondary);">登录状态已保存到本地</p>
${data.expire_in_hours ? `<p style="color: var(--text-muted); font-size: 14px;">有效期:${data.expire_in_hours}小时</p>` : ''}
</div>
`;
elements.qrLoginStatus.textContent = '登录成功!';
// 开始检查登录状态
checkLoginStatus();
// 3秒后自动关闭
setTimeout(() => {
closeLoginQrModal();
closeAccountModal();
showToast('登录成功', 'success');
}, 3000);
} else {
elements.qrcodeContainer.innerHTML = '<p style="color: var(--error-color);">登录失败</p>';
elements.qrLoginStatus.textContent = '登录失败';
}
} catch (error) {
elements.qrcodeContainer.innerHTML = '<p style="color: var(--error-color);">网络错误</p>';
elements.qrLoginStatus.textContent = '网络错误';
}
}
// 关闭登录二维码模态框
function closeLoginQrModal() {
elements.loginQrModal.classList.remove('show');
}
// 检查登录状态
async function checkLoginStatus() {
try {
const response = await fetch('/api/login/status');
const data = await response.json();
if (data.logged_in) {
elements.qrLoginStatus.textContent = '登录成功!';
setTimeout(() => {
closeLoginQrModal();
closeAccountModal();
updateLoginStatus(true, data.login_time, data.expire_time);
showToast('登录成功', 'success');
}, 1000);
} else {
// 继续检查
setTimeout(checkLoginStatus, 2000);
}
} catch (error) {
// 网络错误,继续检查
setTimeout(checkLoginStatus, 2000);
}
}
// 退出登录
async function logout() {
try {
const response = await fetch('/api/logout', { method: 'POST' });
const data = await response.json();
if (response.ok) {
updateLoginStatus(false);
showToast('已退出登录', 'success');
closeAccountModal();
} else {
showToast('退出失败', 'error');
}
} catch (error) {
showToast('退出失败', 'error');
}
}
// 更新登录状态显示
function updateLoginStatus(isLoggedIn, loginTime = null, expireTime = null) {
const indicator = elements.loginStatusIndicator;
const statusText = elements.loginStatusText;
const modalStatus = elements.modalLoginStatus;
if (isLoggedIn) {
indicator.className = 'status-indicator online';
statusText.textContent = '已登录';
modalStatus.textContent = '已登录';
modalStatus.style.color = 'var(--success-color)';
if (loginTime) {
elements.loginTime.textContent = new Date(loginTime).toLocaleString('zh-CN');
}
// 如果有到期时间,显示剩余时间
if (expireTime) {
const expireDate = new Date(expireTime);
const now = new Date();
const timeLeft = expireDate - now;
if (timeLeft > 0) {
const hours = Math.floor(timeLeft / (1000 * 60 * 60));
const minutes = Math.floor((timeLeft % (1000 * 60 * 60)) / (1000 * 60));
elements.loginTime.textContent = new Date(loginTime).toLocaleString('zh-CN');
elements.loginTime.innerHTML += `<br><span style="color: var(--warning-color); font-size: 12px;">(${hours}小时${minutes}分钟后过期)</span>`;
}
}
} else {
indicator.className = 'status-indicator offline';
statusText.textContent = '未登录';
modalStatus.textContent = '未登录';
modalStatus.style.color = 'var(--error-color)';
elements.loginTime.textContent = '-';
}
}
// 加载账号状态
async function loadAccountStatus() {
try {
const response = await fetch('/api/login/status');
const data = await response.json();
updateLoginStatus(data.logged_in, data.login_time, data.expire_time);
} catch (error) {
updateLoginStatus(false);
}
}
// 刷新账号列表
async function refreshAccounts() {
try {
const response = await fetch('/api/users');
const data = await response.json();
renderUserList(data.users);
renderModalUserList();
showToast('账号列表已刷新', 'success');
} catch (error) {
showToast('刷新失败', 'error');
}
}
// 渲染模态框中的用户列表
function renderModalUserList() {
// 先从主用户列表获取数据
const userItems = elements.userList.querySelectorAll('.user-item');
const users = Array.from(userItems).map(item => ({
id: item.dataset.userId,
note_count: item.querySelector('.user-meta')?.textContent.match(/已采集 (\d+) 条笔记/)?.[1] || 0
}));
elements.modalUserCount.textContent = users.length;
if (users.length === 0) {
elements.modalUserList.innerHTML = `
<div class="empty-state" style="padding: 20px;">
<p>暂无用户数据</p>
<p class="hint">请在 data/users.txt 中添加用户 ID</p>
</div>
`;
return;
}
elements.modalUserList.innerHTML = users.map(user => `
<div class="modal-user-item">
<span class="modal-user-id">${user.id}</span>
<button class="modal-user-remove" onclick="removeUser('${user.id}')">
<span>×</span>
</button>
</div>
`).join('');
}
// 添加用户ID
async function addUserId() {
const input = elements.newUserIdInput;
const userId = input.value.trim();
if (!userId) {
showToast('请输入用户ID', 'warning');
return;
}
// 验证用户ID格式(简单验证)
if (!/^[a-f0-9]{24}$/i.test(userId)) {
showToast('用户ID格式不正确', 'warning');
return;
}
try {
const response = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ user_id: userId })
});
const data = await response.json();
if (response.ok) {
input.value = '';
renderModalUserList();
showToast('用户添加成功', 'success');
} else {
showToast(data.detail || '添加失败', 'error');
}
} catch (error) {
showToast('添加失败', 'error');
}
}
// 删除单个用户
async function removeUser(userId) {
try {
const response = await fetch(`/api/users/${userId}`, { method: 'DELETE' });
const data = await response.json();
if (response.ok) {
renderModalUserList();
showToast('用户已删除', 'success');
} else {
showToast(data.detail || '删除失败', 'error');
}
} catch (error) {
showToast('删除失败', 'error');
}
}
// 清空所有用户
async function clearAllUsers() {
if (!confirm('确定要清空所有用户吗?此操作不可恢复!')) {
return;
}
try {
const response = await fetch('/api/users', { method: 'DELETE' });
const data = await response.json();
if (response.ok) {
renderUserList([]);
renderModalUserList();
showToast('所有用户已清空', 'success');
} else {
showToast(data.detail || '清空失败', 'error');
}
} catch (error) {
showToast('清空失败', 'error');
}
}
// 从txt文件导入用户
async function importFromTxt() {
try {
// 在实际应用中,这里可以添加文件上传功能
// 这里先模拟从users.txt读取
const response = await fetch('/api/users/import/txt');
const data = await response.json();
if (response.ok) {
renderUserList(data.users);
renderModalUserList();
showToast(`成功导入 ${data.count} 个用户`, 'success');
} else {
showToast(data.detail || '导入失败', 'error');
}
} catch (error) {
showToast('导入失败', 'error');
}
}
// 导出到txt文件
async function exportToTxt() {
try {
window.location.href = '/api/users/export/txt';
showToast('正在导出用户列表...', 'success');
} catch (error) {
showToast('导出失败', 'error');
}
}
// 显示Toast提示
function showToast(message, type = 'info') {
const toast = elements.toast;
toast.textContent = message;
toast.className = `toast ${type}`;
toast.classList.add('show');
setTimeout(() => {
toast.classList.remove('show');
}, 3000);
}
// ========== 启动 ==========
document.addEventListener('DOMContentLoaded', init);