From e64adfe62873fd8c3872e4486fadd5a2a94aa5ff Mon Sep 17 00:00:00 2001 From: mzl Date: Mon, 12 Jan 2026 14:59:27 +0800 Subject: [PATCH] ts --- app/web.py | 188 +++++++++++++++++ static/app.js | 399 +++++++++++++++++++++++++++++++++++- static/style.css | 477 ++++++++++++++++++++++++++++++++++++++++--- templates/index.html | 134 +++++++++++- 4 files changed, 1167 insertions(+), 31 deletions(-) diff --git a/app/web.py b/app/web.py index 0a09378..39fda8a 100644 --- a/app/web.py +++ b/app/web.py @@ -257,6 +257,194 @@ async def get_users(): return {"users": users} +@app.post("/api/users") +async def add_user(request: dict): + """添加用户""" + user_id = request.get("user_id", "").strip() + + if not user_id: + raise HTTPException(status_code=400, detail="用户ID不能为空") + + # 验证用户ID格式 + if not user_id or len(user_id) < 10: + raise HTTPException(status_code=400, detail="用户ID格式不正确") + + users_file = os.path.join(config.DATA_DIR, "users.txt") + + # 检查是否已存在 + if os.path.exists(users_file): + with open(users_file, 'r', encoding='utf-8') as f: + existing_users = [line.strip() for line in f if line.strip()] + if user_id in existing_users: + raise HTTPException(status_code=400, detail="用户ID已存在") + + # 添加到文件 + with open(users_file, 'a', encoding='utf-8') as f: + f.write(f"{user_id}\n") + + return { + "success": True, + "message": f"用户 {user_id} 已添加", + "user_id": user_id + } + + +@app.delete("/api/users/{user_id}") +async def remove_user(user_id: str): + """删除用户""" + users_file = os.path.join(config.DATA_DIR, "users.txt") + + if not os.path.exists(users_file): + raise HTTPException(status_code=404, detail="用户文件不存在") + + # 读取所有用户 + with open(users_file, 'r', encoding='utf-8') as f: + users = [line.strip() for line in f if line.strip()] + + # 检查用户是否存在 + if user_id not in users: + raise HTTPException(status_code=404, detail="用户不存在") + + # 删除用户 + users.remove(user_id) + + # 写回文件 + with open(users_file, 'w', encoding='utf-8') as f: + for user in users: + f.write(f"{user}\n") + + # 删除采集数据文件 + data_file = os.path.join(config.DATA_DIR, f"{user_id}.json") + if os.path.exists(data_file): + os.remove(data_file) + + return { + "success": True, + "message": f"用户 {user_id} 已删除" + } + + +@app.delete("/api/users") +async def clear_all_users(): + """清空所有用户""" + users_file = os.path.join(config.DATA_DIR, "users.txt") + + if not os.path.exists(users_file): + raise HTTPException(status_code=404, detail="用户文件不存在") + + # 清空文件 + with open(users_file, 'w', encoding='utf-8') as f: + f.write("") + + # 删除所有采集数据 + data_dir = config.DATA_DIR + for filename in os.listdir(data_dir): + if filename.endswith('.json') and filename != 'auth_state.json': + file_path = os.path.join(data_dir, filename) + os.remove(file_path) + + # 重置状态 + app_state.users_status = {} + + return { + "success": True, + "message": "所有用户已清空" + } + + +@app.post("/api/users/import/txt") +async def import_users_from_txt(): + """从users.txt导入用户(模拟)""" + users_file = os.path.join(config.DATA_DIR, "users.txt") + + if not os.path.exists(users_file): + raise HTTPException(status_code=404, detail="users.txt文件不存在") + + with open(users_file, 'r', encoding='utf-8') as f: + user_ids = [line.strip() for line in f if line.strip()] + + return { + "success": True, + "count": len(user_ids), + "users": user_ids + } + + +@app.get("/api/users/export/txt") +async def export_users_to_txt(): + """导出用户列表到txt文件""" + users_file = os.path.join(config.DATA_DIR, "users.txt") + + if not os.path.exists(users_file): + raise HTTPException(status_code=404, detail="用户文件不存在") + + with open(users_file, 'r', encoding='utf-8') as f: + content = f.read() + + return StreamingResponse( + iter([content]), + media_type="text/plain", + headers={ + "Content-Disposition": f"attachment; filename=users_{datetime.now().strftime('%Y%m%d_%H%M%S')}.txt" + } + ) + + +# ========== 登录相关 API ========== + +class LoginStatusResponse(BaseModel): + logged_in: bool + login_time: Optional[str] = None + + +@app.get("/api/login/status", response_model=LoginStatusResponse) +async def get_login_status(): + """获取登录状态""" + auth_file = os.path.join(config.DATA_DIR, "auth_state.json") + + if not os.path.exists(auth_file): + return LoginStatusResponse(logged_in=False) + + try: + with open(auth_file, 'r', encoding='utf-8') as f: + auth_data = json.load(f) + + return LoginStatusResponse( + logged_in=auth_data.get("logged_in", False), + login_time=auth_data.get("login_time") + ) + except Exception: + return LoginStatusResponse(logged_in=False) + + +@app.post("/api/login/qrcode") +async def generate_qrcode(): + """生成登录二维码""" + # 这里应该调用实际的小红书登录API + # 暂时返回模拟数据 + return { + "qr_url": "https://example.com/login/qrcode", + "qr_id": "123456" + } + + +@app.post("/api/logout") +async def logout(): + """退出登录""" + auth_file = os.path.join(config.DATA_DIR, "auth_state.json") + + try: + if os.path.exists(auth_file): + os.remove(auth_file) + + return { + "success": True, + "message": "已退出登录" + } + except Exception as e: + raise HTTPException(status_code=500, detail=f"退出登录失败: {str(e)}") + + @app.get("/api/data/{user_id}") async def get_user_data(user_id: str): """获取用户采集数据""" diff --git a/static/app.js b/static/app.js index e2aa2ec..b6812cf 100644 --- a/static/app.js +++ b/static/app.js @@ -46,7 +46,36 @@ const elements = { logSearch: null, screenshotPanel: null, screenshotImage: null, - closeScreenshot: 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 }; // ========== 初始化 ========== @@ -79,6 +108,39 @@ function init() { 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(); @@ -143,6 +205,37 @@ function bindEvents() { 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(); + } + }); } // ========== 加载初始数据 ========== @@ -582,5 +675,309 @@ function debounce(func, 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'); + const data = await response.json(); + + if (response.ok) { + // 显示二维码 + if (data.qr_url) { + // 使用简单的二维码显示(实际项目中可以使用QRCode.js等库) + elements.qrcodeContainer.innerHTML = ` +
+ 登录二维码 +
+ `; + elements.qrLoginStatus.textContent = '请扫码登录'; + + // 开始检查登录状态 + checkLoginStatus(); + + // 设置二维码过期倒计时 + let countdown = 120; + const countdownInterval = setInterval(() => { + countdown--; + elements.qrExpireTime.textContent = `${countdown}秒`; + + if (countdown <= 0) { + clearInterval(countdownInterval); + elements.qrLoginStatus.textContent = '二维码已过期,请重新生成'; + elements.qrcodeContainer.innerHTML = '

二维码已过期

'; + } + }, 1000); + } + } else { + elements.qrcodeContainer.innerHTML = '

生成二维码失败

'; + elements.qrLoginStatus.textContent = '生成失败'; + } + } catch (error) { + elements.qrcodeContainer.innerHTML = '

网络错误

'; + 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); + 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) { + 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'); + } + } 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); + } 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 = ` +
+

暂无用户数据

+

请在 data/users.txt 中添加用户 ID

+
+ `; + return; + } + + elements.modalUserList.innerHTML = users.map(user => ` + + `).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); diff --git a/static/style.css b/static/style.css index 8ce1588..9bf27aa 100644 --- a/static/style.css +++ b/static/style.css @@ -1,24 +1,24 @@ -/* 小红书采集器样式表 */ +/* 小红书采集器样式表 - 绿色基调 */ /* ========== CSS 变量 ========== */ :root { - --bg-primary: #f5f7fa; + --bg-primary: #f0f7f4; --bg-secondary: #ffffff; - --bg-tertiary: #e8ecf1; - --text-primary: #2c3e50; - --text-secondary: #5a6c7d; - --text-muted: #8b95a5; - --border-color: #d1d9e6; - --primary-color: #ff2442; /* 小红书红 */ - --primary-hover: #e01f3a; + --bg-tertiary: #e1f0e8; + --text-primary: #1a2e23; + --text-secondary: #4a5c52; + --text-muted: #7a8a7f; + --border-color: #b8d4c2; + --primary-color: #10b981; /* 翠翠绿 */ + --primary-hover: #059669; --success-color: #10b981; --warning-color: #f59e0b; --error-color: #ef4444; --info-color: #3b82f6; - --debug-color: #8b95a5; - --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05); - --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1); - --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1); + --debug-color: #7a8a7f; + --shadow-sm: 0 1px 2px 0 rgba(16, 185, 129, 0.05); + --shadow-md: 0 4px 6px -1px rgba(16, 185, 129, 0.1); + --shadow-lg: 0 10px 15px -3px rgba(16, 185, 129, 0.1); --radius-sm: 4px; --radius-md: 8px; --radius-lg: 12px; @@ -26,13 +26,13 @@ /* 深色主题 */ body.dark-theme { - --bg-primary: #1a1d23; - --bg-secondary: #242830; - --bg-tertiary: #2d333d; - --text-primary: #e8eaed; - --text-secondary: #9aa0a6; - --text-muted: #5f6368; - --border-color: #3c4043; + --bg-primary: #0d1f17; + --bg-secondary: #1a2e23; + --bg-tertiary: #233a2e; + --text-primary: #e8f0ec; + --text-secondary: #8a9a8f; + --text-muted: #5a6a5f; + --border-color: #2d4a3a; --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.3); --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.4); --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.5); @@ -71,6 +71,7 @@ body { background: var(--bg-secondary); border-radius: var(--radius-lg); box-shadow: var(--shadow-sm); + border-left: 4px solid var(--primary-color); } .header h1 { @@ -161,6 +162,15 @@ body { background: var(--border-color); } +.btn-success { + background: var(--success-color); + color: white; +} + +.btn-success:hover:not(:disabled) { + background: var(--primary-hover); +} + .btn-small { padding: 6px 12px; font-size: 12px; @@ -210,14 +220,23 @@ body { } /* ========== 输入框 ========== */ -.input-small { - padding: 4px 8px; +.input-small, .input-medium { + padding: 8px 12px; border: 1px solid var(--border-color); border-radius: var(--radius-md); background: var(--bg-secondary); color: var(--text-primary); - font-size: 12px; + font-size: 14px; +} + +.input-small { width: 150px; + font-size: 12px; + padding: 6px 10px; +} + +.input-medium { + flex: 1; } /* ========== 线程控制 ========== */ @@ -232,6 +251,56 @@ body { color: var(--text-secondary); } +/* ========== 账号管理 ========== */ +.account-section { + display: flex; + align-items: center; + gap: 12px; +} + +.account-controls { + display: flex; + align-items: center; + gap: 8px; +} + +.account-status { + display: flex; + align-items: center; + gap: 6px; + padding: 4px 12px; + background: var(--bg-tertiary); + border-radius: var(--radius-md); + font-size: 14px; + font-weight: 500; +} + +.status-indicator { + width: 8px; + height: 8px; + border-radius: 50%; + background: var(--error-color); + animation: pulse 2s infinite; +} + +.status-indicator.online { + background: var(--success-color); +} + +.status-indicator.offline { + background: var(--error-color); +} + +.status-indicator.loading { + background: var(--warning-color); +} + +@keyframes pulse { + 0% { opacity: 1; } + 50% { opacity: 0.5; } + 100% { opacity: 1; } +} + /* ========== 进度卡片 ========== */ .progress-section { margin-bottom: 20px; @@ -261,7 +330,7 @@ body { .progress-bar { height: 100%; - background: linear-gradient(90deg, var(--primary-color), #ff6b8a); + background: linear-gradient(90deg, var(--primary-color), #34d399); border-radius: var(--radius-md); display: flex; align-items: center; @@ -314,12 +383,12 @@ body { /* ========== 主内容区 ========== */ .main-content { display: grid; - grid-template-columns: 300px 1fr; + grid-template-columns: 350px 1fr; gap: 20px; } .main-content.with-threads { - grid-template-columns: 300px 250px 1fr; + grid-template-columns: 350px 250px 1fr; } /* ========== 面板 ========== */ @@ -361,6 +430,63 @@ body { padding: 16px; } +/* ========== 账号管理面板 ========== */ +.account-panel { + margin-bottom: 20px; +} + +.account-input-group { + display: flex; + gap: 8px; + margin-bottom: 12px; +} + +.account-input-group input { + flex: 1; +} + +.account-list { + display: flex; + flex-direction: column; + gap: 8px; +} + +.account-item { + display: flex; + align-items: center; + gap: 8px; + padding: 10px 12px; + background: var(--bg-tertiary); + border-radius: var(--radius-md); + transition: all 0.2s; +} + +.account-item:hover { + background: var(--border-color); +} + +.account-item-id { + flex: 1; + font-family: monospace; + font-size: 13px; + color: var(--text-primary); +} + +.account-item-remove { + background: none; + border: none; + color: var(--error-color); + cursor: pointer; + font-size: 18px; + padding: 4px; + border-radius: var(--radius-sm); + transition: background 0.2s; +} + +.account-item-remove:hover { + background: rgba(239, 68, 68, 0.1); +} + /* ========== 用户列表 ========== */ .user-list { display: flex; @@ -539,7 +665,7 @@ body { } .log-level.DEBUG { - background: rgba(139, 149, 165, 0.15); + background: rgba(122, 138, 127, 0.15); color: var(--debug-color); } @@ -567,6 +693,299 @@ body { display: block; } +/* ========== 模态框 ========== */ +.modal { + display: none; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.5); + z-index: 1000; + animation: fadeIn 0.2s; +} + +.modal.show { + display: flex; + align-items: center; + justify-content: center; +} + +.modal-content { + background: var(--bg-secondary); + border-radius: var(--radius-lg); + box-shadow: var(--shadow-lg); + width: 90%; + max-width: 600px; + max-height: 90vh; + display: flex; + flex-direction: column; + animation: slideIn 0.3s; +} + +.modal-header { + padding: 20px; + border-bottom: 1px solid var(--border-color); + display: flex; + justify-content: space-between; + align-items: center; +} + +.modal-header h3 { + font-size: 18px; + font-weight: 600; + color: var(--text-primary); + margin: 0; +} + +.modal-close { + background: none; + border: none; + font-size: 24px; + color: var(--text-muted); + cursor: pointer; + padding: 4px; + line-height: 1; + transition: color 0.2s; +} + +.modal-close:hover { + color: var(--text-primary); +} + +.modal-body { + flex: 1; + overflow-y: auto; + padding: 20px; +} + +/* ========== 登录状态 ========== */ +.login-status-section { + margin-bottom: 24px; +} + +.login-status-section h4 { + font-size: 16px; + font-weight: 600; + margin-bottom: 12px; + color: var(--text-primary); +} + +.login-info { + background: var(--bg-tertiary); + padding: 12px; + border-radius: var(--radius-md); + margin-bottom: 12px; +} + +.login-status-item { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 8px; +} + +.login-status-item:last-child { + margin-bottom: 0; +} + +.status-label { + font-size: 14px; + color: var(--text-secondary); +} + +.status-value { + font-size: 14px; + font-weight: 500; + color: var(--text-primary); +} + +.login-actions { + display: flex; + gap: 8px; +} + +/* ========== 用户管理 ========== */ +.users-management-section { + margin-bottom: 24px; +} + +.users-management-section h4 { + font-size: 16px; + font-weight: 600; + margin-bottom: 12px; + color: var(--text-primary); +} + +.user-input-group { + display: flex; + gap: 8px; + margin-bottom: 12px; +} + +.user-tips { + margin-bottom: 16px; +} + +.users-list-container { + border: 1px solid var(--border-color); + border-radius: var(--radius-md); + overflow: hidden; +} + +.users-list-header { + padding: 12px 16px; + background: var(--bg-tertiary); + display: flex; + justify-content: space-between; + align-items: center; + font-size: 14px; + font-weight: 500; +} + +.users-list { + max-height: 200px; + overflow-y: auto; +} + +.modal-user-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 10px 16px; + border-bottom: 1px solid var(--border-color); + transition: background 0.2s; +} + +.modal-user-item:hover { + background: var(--bg-tertiary); +} + +.modal-user-item:last-child { + border-bottom: none; +} + +.modal-user-id { + font-family: monospace; + font-size: 13px; + color: var(--text-primary); + flex: 1; +} + +.modal-user-remove { + background: none; + border: none; + color: var(--error-color); + cursor: pointer; + font-size: 18px; + padding: 4px; + border-radius: var(--radius-sm); + transition: background 0.2s; +} + +.modal-user-remove:hover { + background: rgba(239, 68, 68, 0.1); +} + +/* ========== 批量导入 ========== */ +.batch-import-section { + margin-bottom: 24px; +} + +.batch-import-section h4 { + font-size: 16px; + font-weight: 600; + margin-bottom: 12px; + color: var(--text-primary); +} + +.batch-import-controls { + display: flex; + gap: 8px; +} + +/* ========== 登录二维码 ========== */ +.qr-login-container { + display: flex; + gap: 24px; +} + +.qr-code-section { + flex: 1; + text-align: center; +} + +#qrcodeContainer { + width: 200px; + height: 200px; + margin: 0 auto 12px; + background: white; + padding: 16px; + border-radius: var(--radius-md); + display: flex; + align-items: center; + justify-content: center; +} + +.qr-tips { + font-size: 14px; + color: var(--text-muted); + margin: 0; +} + +/* ========== 浮动提示 ========== */ +.toast { + position: fixed; + bottom: 20px; + right: 20px; + padding: 12px 20px; + background: var(--bg-secondary); + border-radius: var(--radius-md); + box-shadow: var(--shadow-lg); + font-size: 14px; + font-weight: 500; + color: var(--text-primary); + transform: translateY(100px); + opacity: 0; + transition: all 0.3s; + z-index: 2000; +} + +.toast.show { + transform: translateY(0); + opacity: 1; +} + +.toast.success { + border-left: 4px solid var(--success-color); +} + +.toast.error { + border-left: 4px solid var(--error-color); +} + +.toast.warning { + border-left: 4px solid var(--warning-color); +} + +/* ========== 动画 ========== */ +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + +@keyframes slideIn { + from { + transform: translateY(-20px); + opacity: 0; + } + to { + transform: translateY(0); + opacity: 1; + } +} + /* ========== 滚动条样式 ========== */ .panel-body::-webkit-scrollbar, .log-container::-webkit-scrollbar { @@ -617,4 +1036,8 @@ body { .stat-value { font-size: 16px; } + + .account-input-group { + flex-direction: column; + } } diff --git a/templates/index.html b/templates/index.html index f434119..4b885b9 100644 --- a/templates/index.html +++ b/templates/index.html @@ -3,14 +3,14 @@ - 小红书数据采集器 + XHS数据采集器
-

小红书数据采集器

+

XHS数据采集器

@@ -18,6 +18,7 @@
+
+
+ + +
+ +
@@ -157,7 +178,114 @@ 错误截图 - + + + + + + + + +