diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..0dd2324 --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,222 @@ +# 🚀 微信公众号文章爬虫系统 - Web界面 + +一个现代化的Web界面,用于管理微信公众号文章爬虫功能。 + +## 📋 功能特性 + +### 🍪 Cookie 配置 +- 便捷的Cookie输入和保存 +- Cookie示例和验证 +- 实时状态反馈 + +### 📄 下载单篇文章 +- 支持微信文章链接输入 +- 可选择保存图片和内容 +- 实时下载进度显示 + +### 📋 获取文章列表 +- Access Token URL输入 +- 自定义获取页数 +- 批量文章信息获取 + +### 📦 批量下载文章 +- 公众号名称或链接输入 +- 批量下载文章详情 +- 智能进度跟踪 + +### 📊 数据管理 +- 已下载数据概览 +- 文章统计信息 +- 快速文件夹访问 + +## 🛠 使用方法 + +### 方法1:快速启动(推荐) + +1. **双击启动脚本** + ``` + start_web.bat + ``` + +2. **自动打开浏览器** + - 系统会自动检测Python或使用PowerShell + - 默认地址:`http://localhost:8000` 或 `http://localhost:8080` + +### 方法2:手动启动 + +#### 使用Python(推荐) +```bash +cd frontend +python -m http.server 8000 +``` + +#### 使用Node.js +```bash +cd frontend +npx http-server -p 8000 +``` + +#### 使用其他Web服务器 +- 将frontend文件夹作为Web根目录即可 + +## 🎮 界面使用说明 + +### 主界面 +- **功能卡片**:点击不同卡片进入对应功能 +- **现代UI**:响应式设计,支持桌面和移动端 +- **状态指示**:实时显示操作状态和进度 + +### Cookie配置页面 +1. 点击"Cookie 配置"卡片 +2. 粘贴从Fiddler获取的Cookie内容 +3. 点击"保存Cookie"按钮 +4. 等待保存成功提示 + +### 下载单篇文章 +1. 进入"下载单篇文章"功能 +2. 输入微信文章完整链接 +3. 选择是否保存图片和内容 +4. 点击"开始下载"查看进度 + +### 获取文章列表 +1. 进入"获取文章列表"功能 +2. 粘贴包含认证参数的完整URL +3. 设置获取页数(可选) +4. 点击"开始获取"执行任务 + +### 批量下载 +1. 进入"批量下载文章"功能 +2. 输入公众号名称或任意文章链接 +3. 选择保存选项 +4. 点击"开始批量下载" + +### 数据管理 +1. 进入"数据管理"功能 +2. 点击"刷新列表"查看已下载数据 +3. 可以查看文章详情或打开文件夹 + +## 🎨 界面特性 + +### 响应式设计 +- ✅ 桌面端优化体验 +- ✅ 平板和手机端兼容 +- ✅ 自适应布局 + +### 现代化UI +- 🎨 渐变色彩搭配 +- 💫 平滑动画效果 +- 📱 卡片式设计语言 +- 🌟 悬停交互反馈 + +### 交互体验 +- ⌨️ 快捷键支持(ESC返回,Ctrl+Enter执行) +- 🔄 实时进度条 +- 📊 状态指示器 +- 🔔 操作反馈提示 + +## 🔧 技术架构 + +### 前端技术栈 +- **HTML5**: 现代语义化标记 +- **CSS3**: Flexbox/Grid + 动画 +- **JavaScript**: ES6+ + jQuery +- **响应式**: Mobile-First设计 + +### 文件结构 +``` +frontend/ +├── index.html # 主页面 +├── css/ +│ └── style.css # 样式文件 +├── js/ +│ └── app.js # 应用逻辑 +├── start_web.bat # 启动脚本 +└── README.md # 说明文档 +``` + +### 与后端交互 +- 目前为演示版本,使用前端模拟 +- 预留了完整的API接口结构 +- 支持与命令行程序集成 + +## 🚀 部署选项 + +### 本地开发 +```bash +# 克隆项目 +cd frontend + +# 启动开发服务器 +python -m http.server 8000 +# 或 +npx http-server -p 8000 +``` + +### 生产环境 +- **Nginx**: 部署静态文件 +- **Apache**: 配置虚拟主机 +- **IIS**: Windows服务器部署 +- **Docker**: 容器化部署 + +## 📊 浏览器兼容性 + +| 浏览器 | 版本 | 支持状态 | +|--------|------|---------| +| Chrome | 60+ | ✅ 完全支持 | +| Firefox | 55+ | ✅ 完全支持 | +| Safari | 12+ | ✅ 完全支持 | +| Edge | 79+ | ✅ 完全支持 | +| IE | 11 | ⚠️ 基础支持 | + +## 🐛 常见问题 + +### Q: 页面打不开或样式异常? +A: 确保所有文件在同一目录下,使用HTTP服务器访问(不是file://协议) + +### Q: 功能按钮点击无反应? +A: 检查浏览器控制台是否有JavaScript错误,确保jQuery正常加载 + +### Q: 进度条不显示? +A: 当前为演示版本,进度为模拟效果。实际部署需要连接后端API + +### Q: 如何连接实际的后端? +A: 修改`js/app.js`中的API调用部分,替换模拟逻辑为实际HTTP请求 + +## 🔮 后续计划 + +### v1.1 计划功能 +- [ ] 真实后端API集成 +- [ ] WebSocket实时通信 +- [ ] 文件上传拖拽功能 +- [ ] 任务队列管理 + +### v1.2 计划功能 +- [ ] 用户认证系统 +- [ ] 多公众号管理 +- [ ] 数据可视化图表 +- [ ] 导出功能增强 + +## 📄 开源协议 + +本项目仅供学习和研究使用,请遵守相关法律法规和服务条款。 + +## 🤝 贡献指南 + +1. Fork 本项目 +2. 创建功能分支 +3. 提交更改 +4. 发起 Pull Request + +## 📞 支持联系 + +如有问题或建议,请通过以下方式联系: + +- 📧 邮箱: your-email@example.com +- 💬 Issues: 在GitHub提交Issue +- 📱 QQ群: 123456789 + +--- + +**⚠️ 免责声明**: 本工具仅供学习交流使用,请遵守相关法律法规和平台服务条款。使用者需自行承担使用风险。 + +**🌟 如果这个项目对您有帮助,请给个Star支持一下!** \ No newline at end of file diff --git a/frontend/css/style.css b/frontend/css/style.css new file mode 100644 index 0000000..574121b --- /dev/null +++ b/frontend/css/style.css @@ -0,0 +1,482 @@ +/* 全局样式 */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + line-height: 1.6; + color: #333; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + min-height: 100vh; +} + +.container { + max-width: 1200px; + margin: 0 auto; + padding: 20px; +} + +/* 头部样式 */ +.header { + text-align: center; + color: white; + margin-bottom: 30px; +} + +.header h1 { + font-size: 2.5em; + margin-bottom: 10px; + text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3); +} + +.subtitle { + font-size: 1.1em; + opacity: 0.9; +} + +/* 主内容区域 */ +.main-content { + background: white; + border-radius: 15px; + padding: 30px; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2); + min-height: 500px; +} + +/* 功能卡片网格 */ +.feature-cards { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: 25px; + margin-bottom: 30px; +} + +.card { + background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%); + border-radius: 12px; + padding: 25px; + text-align: center; + transition: all 0.3s ease; + border: 2px solid transparent; + cursor: pointer; +} + +.card:hover { + transform: translateY(-5px); + box-shadow: 0 15px 35px rgba(0, 0, 0, 0.1); + border-color: #667eea; +} + +.card-icon { + font-size: 3em; + margin-bottom: 15px; +} + +.card h3 { + font-size: 1.3em; + margin-bottom: 10px; + color: #2c3e50; +} + +.card p { + color: #7f8c8d; + margin-bottom: 20px; +} + +/* 按钮样式 */ +.btn { + padding: 12px 24px; + border: none; + border-radius: 8px; + cursor: pointer; + font-size: 1em; + font-weight: 600; + transition: all 0.3s ease; + text-decoration: none; + display: inline-block; + min-width: 100px; +} + +.btn-primary { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; +} + +.btn-primary:hover { + transform: translateY(-2px); + box-shadow: 0 8px 25px rgba(102, 126, 234, 0.4); +} + +.btn-success { + background: linear-gradient(135deg, #56ab2f 0%, #a8e6cf 100%); + color: white; +} + +.btn-success:hover { + transform: translateY(-2px); + box-shadow: 0 8px 25px rgba(86, 171, 47, 0.4); +} + +.btn-info { + background: linear-gradient(135deg, #3498db 0%, #74b9ff 100%); + color: white; +} + +.btn-info:hover { + transform: translateY(-2px); + box-shadow: 0 8px 25px rgba(52, 152, 219, 0.4); +} + +.btn-warning { + background: linear-gradient(135deg, #f39c12 0%, #fdcb6e 100%); + color: white; +} + +.btn-warning:hover { + transform: translateY(-2px); + box-shadow: 0 8px 25px rgba(243, 156, 18, 0.4); +} + +.btn-secondary { + background: linear-gradient(135deg, #95a5a6 0%, #bdc3c7 100%); + color: white; +} + +.btn-secondary:hover { + transform: translateY(-2px); + box-shadow: 0 8px 25px rgba(149, 165, 166, 0.4); +} + +/* 区域样式 */ +.section { + animation: fadeIn 0.5s ease-in; +} + +@keyframes fadeIn { + from { opacity: 0; transform: translateY(20px); } + to { opacity: 1; transform: translateY(0); } +} + +.section-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 30px; + padding-bottom: 15px; + border-bottom: 2px solid #ecf0f1; +} + +.section-header h2 { + color: #2c3e50; + font-size: 1.8em; +} + +/* 表单样式 */ +.form-group { + margin-bottom: 25px; +} + +.form-group label { + display: block; + margin-bottom: 8px; + font-weight: 600; + color: #2c3e50; +} + +.form-group input, +.form-group textarea, +.form-group select { + width: 100%; + padding: 12px 15px; + border: 2px solid #ddd; + border-radius: 8px; + font-size: 1em; + transition: border-color 0.3s ease; + font-family: inherit; +} + +.form-group input:focus, +.form-group textarea:focus, +.form-group select:focus { + outline: none; + border-color: #667eea; + box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); +} + +.form-group small { + display: block; + margin-top: 5px; + color: #7f8c8d; + font-size: 0.9em; +} + +.checkbox-label { + display: inline-flex !important; + align-items: center; + margin-right: 20px; + margin-bottom: 10px; + font-weight: normal !important; +} + +.checkbox-label input[type="checkbox"] { + width: auto !important; + margin-right: 8px; +} + +.form-actions { + display: flex; + gap: 15px; + flex-wrap: wrap; + margin-top: 25px; +} + +/* 结果显示区域 */ +.result { + margin-top: 25px; + padding: 20px; + border-radius: 8px; + display: none; +} + +.result.success { + background-color: #d4edda; + color: #155724; + border: 1px solid #c3e6cb; +} + +.result.error { + background-color: #f8d7da; + color: #721c24; + border: 1px solid #f5c6cb; +} + +.result.info { + background-color: #d1ecf1; + color: #0c5460; + border: 1px solid #bee5eb; +} + +.result.loading { + background-color: #fff3cd; + color: #856404; + border: 1px solid #ffeaa7; +} + +/* 数据列表样式 */ +.data-list { + margin-top: 25px; +} + +.data-item { + background: #f8f9fa; + border: 1px solid #dee2e6; + border-radius: 8px; + padding: 20px; + margin-bottom: 15px; + transition: all 0.3s ease; +} + +.data-item:hover { + transform: translateX(5px); + box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1); +} + +.data-item h4 { + color: #2c3e50; + margin-bottom: 10px; + font-size: 1.2em; +} + +.data-item-info { + display: flex; + justify-content: space-between; + align-items: center; + flex-wrap: wrap; + gap: 10px; +} + +.data-item-stats { + display: flex; + gap: 20px; + align-items: center; +} + +.stat-item { + display: flex; + align-items: center; + gap: 5px; + font-size: 0.9em; + color: #7f8c8d; +} + +.data-item-actions { + display: flex; + gap: 10px; +} + +/* 进度条样式 */ +.progress-container { + margin-top: 20px; +} + +.progress-bar { + width: 100%; + height: 8px; + background-color: #ecf0f1; + border-radius: 4px; + overflow: hidden; +} + +.progress-fill { + height: 100%; + background: linear-gradient(90deg, #667eea, #764ba2); + width: 0%; + transition: width 0.3s ease; + animation: pulse 1.5s infinite; +} + +@keyframes pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.7; } +} + +.progress-text { + margin-top: 10px; + text-align: center; + font-size: 0.9em; + color: #7f8c8d; +} + +/* 状态指示器 */ +.status-indicator { + display: inline-block; + width: 10px; + height: 10px; + border-radius: 50%; + margin-right: 8px; +} + +.status-success { background-color: #27ae60; } +.status-error { background-color: #e74c3c; } +.status-warning { background-color: #f39c12; } +.status-info { background-color: #3498db; } + +/* 底部样式 */ +.footer { + text-align: center; + margin-top: 30px; + padding: 20px; + color: white; + opacity: 0.8; +} + +/* 工具提示 */ +.tooltip { + position: relative; + cursor: help; +} + +.tooltip:hover::after { + content: attr(data-tooltip); + position: absolute; + bottom: 100%; + left: 50%; + transform: translateX(-50%); + background: #2c3e50; + color: white; + padding: 8px 12px; + border-radius: 4px; + font-size: 0.9em; + white-space: nowrap; + z-index: 1000; +} + +/* 响应式设计 */ +@media (max-width: 768px) { + .container { + padding: 10px; + } + + .header h1 { + font-size: 2em; + } + + .main-content { + padding: 20px; + } + + .feature-cards { + grid-template-columns: 1fr; + gap: 15px; + } + + .section-header { + flex-direction: column; + align-items: flex-start; + gap: 15px; + } + + .form-actions { + flex-direction: column; + } + + .data-item-info { + flex-direction: column; + align-items: flex-start; + } + + .data-item-actions { + width: 100%; + justify-content: flex-start; + } +} + +@media (max-width: 480px) { + .header h1 { + font-size: 1.8em; + } + + .card { + padding: 20px; + } + + .btn { + padding: 10px 20px; + font-size: 0.9em; + } +} + +/* 加载动画 */ +.loading-spinner { + display: inline-block; + width: 20px; + height: 20px; + border: 2px solid #f3f3f3; + border-top: 2px solid #667eea; + border-radius: 50%; + animation: spin 1s linear infinite; + margin-right: 10px; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +/* 文本对齐工具类 */ +.text-center { text-align: center; } +.text-left { text-align: left; } +.text-right { text-align: right; } + +/* 间距工具类 */ +.mt-10 { margin-top: 10px; } +.mt-20 { margin-top: 20px; } +.mb-10 { margin-bottom: 10px; } +.mb-20 { margin-bottom: 20px; } + +/* 显示/隐藏工具类 */ +.hidden { display: none !important; } +.visible { display: block !important; } \ No newline at end of file diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..14d6442 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,167 @@ + + + + + + 微信公众号文章爬虫系统 + + + +
+
+

🚀 微信公众号文章爬虫系统

+

Wechat Official Account Article Crawler

+
+ +
+ +
+
+
🏠
+

提取公众号主页

+

输入文章链接获取公众号主页链接

+ +
+ +
+
📄
+

下载单篇文章

+

根据链接下载单篇文章

+ +
+ +
+
📋
+

获取文章列表

+

获取公众号所有文章列表

+ +
+ +
+
📦
+

批量下载文章

+

批量下载文章详细内容

+ +
+ +
+
📊
+

数据管理

+

查看已下载的文章数据

+ +
+
+ + + + + + + + + + + + + + + +
+ + +
+ + + + + diff --git a/frontend/js/app.js b/frontend/js/app.js new file mode 100644 index 0000000..d8bc5ed --- /dev/null +++ b/frontend/js/app.js @@ -0,0 +1,509 @@ +// 全局变量 +let isTaskRunning = false; +let currentSection = 'home'; +let taskCheckInterval; +const API_BASE_URL = 'http://localhost:8080/api'; // API基础地址 + +// DOM加载完成后初始化 +$(document).ready(function() { + showSection('home'); + console.log('✅ 微信公众号文章爬虫系统已加载'); +}); + +// 显示指定区域 +function showSection(sectionName) { + // 隐藏所有区域 + $('.section').hide(); + $('.feature-cards').hide(); + + if (sectionName === 'home') { + $('.feature-cards').show(); + currentSection = 'home'; + } else { + $('#section-' + sectionName).show(); + currentSection = sectionName; + } +} + +// 提取公众号主页相关函数 +function extractHomepage() { + const articleUrl = $('#homepage-url').val().trim(); + + if (!articleUrl) { + showResult('homepage', 'error', '请输入文章链接'); + return; + } + + if (!articleUrl.includes('mp.weixin.qq.com')) { + showResult('homepage', 'error', '请输入有效的微信公众号文章链接'); + return; + } + + if (isTaskRunning) { + showResult('homepage', 'error', '有任务正在执行,请稍候...'); + return; + } + + isTaskRunning = true; + showResult('homepage', 'loading', '正在提取公众号主页链接...'); + + // 调用后端API + $.ajax({ + url: `${API_BASE_URL}/homepage/extract`, + method: 'POST', + contentType: 'application/json', + data: JSON.stringify({ url: articleUrl }), + success: function(response) { + isTaskRunning = false; + if (response.success && response.data && response.data.homepage) { + const homepageUrl = response.data.homepage; + const safeUrl = homepageUrl.replace(/'/g, "\\'"); + const resultHtml = ` +
+

✅ 提取成功

+

公众号主页链接:

+
+ ${homepageUrl} +
+
+ + +
+
+ `; + showResult('homepage', 'success', resultHtml); + } else { + showResult('homepage', 'error', response.message || '提取失败'); + } + }, + error: function(xhr, status, error) { + isTaskRunning = false; + let errorMsg = '请求失败:' + error; + if (xhr.status === 0) { + errorMsg = '无法连接到后端服务器,请确保 API 服务器已启动(运行 api_server.exe)'; + } + showResult('homepage', 'error', errorMsg); + } + }); +} + +// 生成模拟的公众号主页链接 +function generateMockHomepageUrl(articleUrl) { + // 从文章链接中提取__biz参数来模拟真实的主页链接 + const bizMatch = articleUrl.match(/__biz=([^&]+)/); + if (bizMatch) { + const biz = bizMatch[1]; + return `https://mp.weixin.qq.com/mp/profile_ext?action=home&__biz=${biz}&scene=124`; + } + // 如果无法提取,返回示例链接 + return 'https://mp.weixin.qq.com/mp/profile_ext?action=home&__biz=MzI1NjEwMTM4OA==&scene=124'; +} + +function loadExampleUrl() { + const exampleUrl = 'https://mp.weixin.qq.com/s?__biz=MzI1NjEwMTM4OA==&mid=2651232405&idx=1&sn=7c8f5b2e3d4a1b9c8e7f6a5b4c3d2e1f&chksm=f1d7e8c4c6a061d2b9e8f7a6b5c4d3e2f1a0b9c8d7e6f5a4b3c2d1e0f9a8b7c6d5e4f3a2b1c0&scene=27'; + + $('#homepage-url').val(exampleUrl); + showResult('homepage', 'info', '已加载文章链接示例,点击"提取主页链接"开始处理'); +} + +// 打开链接的辅助函数 +function openInNewTab(url) { + window.open(url, '_blank'); +} + +// 下载单篇文章 +function downloadSingleArticle() { + alert('此功能需要后端命令行支持。\n\n请使用命令行程序:\n1. 运行 wechat-crawler.exe\n2. 选择对应功能进行下载'); + showResult('single', 'info', '请使用命令行程序执行下载功能'); +} + +// 获取文章列表 +function getArticleList() { + const accessToken = $('#access-token').val().trim(); + const pages = parseInt($('#pages').val()) || 0; + + if (!accessToken) { + showResult('list', 'error', '请输入Access Token URL'); + return; + } + + if (isTaskRunning) { + showResult('list', 'error', '有任务正在执行,请稍候...'); + return; + } + + isTaskRunning = true; + showResult('list', 'loading', '正在获取文章列表,请稍候...'); + + // 调用后端API(同步等待) + $.ajax({ + url: `${API_BASE_URL}/article/list`, + method: 'POST', + contentType: 'application/json', + data: JSON.stringify({ access_token: accessToken, pages: pages }), + success: function(response) { + isTaskRunning = false; + if (response.success && response.data) { + const data = response.data; + const fileExt = data.filename.endsWith('.txt') ? 'TXT文件' : 'Excel文件'; + const resultHtml = ` +
+

✅ 获取成功

+

公众号:${data.account}

+

文件:${data.filename}

+
+ 📥 下载${fileExt} +
+
+ `; + showResult('list', 'success', resultHtml); + + // 自动触发下载 + window.location.href = `${API_BASE_URL}${data.download}`; + } else { + showResult('list', 'error', response.message || '获取失败'); + } + }, + error: function(xhr, status, error) { + isTaskRunning = false; + let errorMsg = '请求失败:' + error; + if (xhr.status === 0) { + errorMsg = '无法连接到后端服务器,请确保 API 服务器已启动'; + } else if (xhr.responseJSON && xhr.responseJSON.message) { + errorMsg = xhr.responseJSON.message; + } + showResult('list', 'error', errorMsg); + }, + timeout: 120000 // 2分钟超时 + }); +} + +// 批量下载文章 +function batchDownload() { + const officialAccount = $('#official-account').val().trim(); + const saveImage = $('#batch-save-image').is(':checked'); + const saveContent = $('#batch-save-content').is(':checked'); + + if (!officialAccount) { + showResult('batch', 'error', '请输入公众号名称或文章链接'); + return; + } + + if (isTaskRunning) { + showResult('batch', 'error', '有任务正在执行,请稍候...'); + return; + } + + isTaskRunning = true; + showResult('batch', 'loading', '正在批量下载文章,请稍候...'); + + // 调用后端API(同步等待) + $.ajax({ + url: `${API_BASE_URL}/article/batch`, + method: 'POST', + contentType: 'application/json', + data: JSON.stringify({ + official_account: officialAccount, + save_image: saveImage, + save_content: saveContent + }), + success: function(response) { + isTaskRunning = false; + if (response.success && response.data) { + const data = response.data; + const resultHtml = ` +
+

✅ ${response.message}

+

公众号:${data.account}

+

文章数量:${data.articleCount} 篇

+

保存路径:${data.path}

+
+ +
+
+ `; + showResult('batch', 'success', resultHtml); + } else { + showResult('batch', 'error', response.message || '批量下载失败'); + } + }, + error: function(xhr, status, error) { + isTaskRunning = false; + let errorMsg = '请求失败:' + error; + if (xhr.status === 0) { + errorMsg = '无法连接到后端服务器,请确保 API 服务器已启动'; + } else if (xhr.responseJSON && xhr.responseJSON.message) { + errorMsg = xhr.responseJSON.message; + } + showResult('batch', 'error', errorMsg); + }, + timeout: 300000 // 5分钟超时 + }); +} + +// 加载数据列表 +function loadDataList() { + showResult('data', 'loading', '正在加载数据列表...'); + + // 调用后端API + $.ajax({ + url: `${API_BASE_URL}/data/list`, + method: 'GET', + success: function(response) { + if (response.success && response.data) { + displayDataList(response.data); + hideResult('data'); + } else { + showResult('data', 'error', '加载失败'); + } + }, + error: function(xhr, status, error) { + let errorMsg = '请求失败:' + error; + if (xhr.status === 0) { + errorMsg = '无法连接到后端服务器,请确保 API 服务器已启动'; + } + showResult('data', 'error', errorMsg); + } + }); +} + +// 显示数据列表 +function displayDataList(dataList) { + let html = ''; + + if (!dataList || dataList.length === 0) { + html = '

暂无数据,请先使用其他功能爬取文章

'; + } else { + dataList.forEach(item => { + const safeItemName = (item.name || '').replace(/'/g, "\\'"); + const safeItemPath = (item.path || '').replace(/'/g, "\\'").replace(/\\/g, '\\\\'); + html += ` +
+

${item.name || '未知'}

+
+
+
+ 📊 + ${item.articleCount || 0} 篇文章 +
+
+ 📅 + ${item.lastUpdate || '未知'} +
+
+ 📁 + ${item.path || '未知'} +
+
+
+ + +
+
+
+ `; + }); + } + + $('#data-list').html(html); +} + +// 查看文章列表 +function viewArticles(accountName) { + alert(`查看 ${accountName} 的文章列表 + +这里将展示该公众号的所有文章,包括: +- 文章标题 +- 发布时间 +- 文件大小 +- 下载状态等`); +} + +// 打开文件夹 +function openFolder(path) { + alert(`打开文件夹: ${path}\n\n在实际环境中,这里会调用系统命令打开文件资源管理器。`); +} + +// 打开数据文件夹 +function openDataFolder() { + alert('打开数据文件夹\n\n在实际环境中,这里会打开data目录。'); +} + +// 任务管理函数 +function startTask(section, message) { + isTaskRunning = true; + showResult(section, 'loading', message); + + // 显示进度条 + const resultDiv = $(`#${section}-result`); + resultDiv.append(` +
+
+
+
+
0%
+
+ `); + + // 禁用相关按钮 + disableButtons(); +} + +function updateTaskProgress(percent, message) { + const progressFill = $('.progress-fill'); + const progressText = $('.progress-text'); + + progressFill.css('width', percent + '%'); + progressText.text(Math.floor(percent) + '% - ' + message); +} + +function endTask(section, type, message) { + isTaskRunning = false; + + // 移除进度条 + $('.progress-container').remove(); + + showResult(section, type, message); + enableButtons(); +} + +function disableButtons() { + $('.btn').prop('disabled', true).addClass('disabled'); +} + +function enableButtons() { + $('.btn').prop('disabled', false).removeClass('disabled'); +} + +// 结果显示函数 +function showResult(section, type, message) { + const resultDiv = $(`#${section}-result`); + resultDiv.removeClass('success error info loading') + .addClass(type) + .html(getResultIcon(type) + message) + .show(); + + // 自动滚动到结果区域 + resultDiv[0].scrollIntoView({ behavior: 'smooth' }); +} + +function hideResult(section) { + $(`#${section}-result`).hide(); +} + +function getResultIcon(type) { + switch (type) { + case 'success': return '✅ '; + case 'error': return '❌ '; + case 'info': return 'ℹ️ '; + case 'loading': return ''; + default: return ''; + } +} + +// 表单验证函数 +function validateUrl(url) { + try { + new URL(url); + return true; + } catch { + return false; + } +} + +function validateInput(value, type) { + switch (type) { + case 'url': + return validateUrl(value); + case 'notEmpty': + return value.trim().length > 0; + case 'number': + return !isNaN(value) && parseInt(value) > 0; + default: + return true; + } +} + +// 工具函数 +function formatFileSize(bytes) { + if (bytes === 0) return '0 Bytes'; + const k = 1024; + const sizes = ['Bytes', 'KB', 'MB', 'GB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; +} + +function formatDate(dateString) { + const date = new Date(dateString); + return date.getFullYear() + '-' + + String(date.getMonth() + 1).padStart(2, '0') + '-' + + String(date.getDate()).padStart(2, '0'); +} + +function copyToClipboard(text) { + navigator.clipboard.writeText(text).then(() => { + alert('已复制到剪贴板'); + }).catch(() => { + alert('复制失败,请手动复制'); + }); +} + +// 快捷键支持 +$(document).keydown(function(e) { + // ESC键返回首页 + if (e.keyCode === 27 && currentSection !== 'home') { + showSection('home'); + } + + // Ctrl+Enter 执行当前页面的主要操作 + if (e.ctrlKey && e.keyCode === 13) { + switch (currentSection) { + case 'homepage': + extractHomepage(); + break; + case 'single': + downloadSingleArticle(); + break; + case 'list': + getArticleList(); + break; + case 'batch': + batchDownload(); + break; + case 'data': + loadDataList(); + break; + } + } +}); + +// 页面可见性变化时的处理 +document.addEventListener('visibilitychange', function() { + if (document.hidden) { + console.log('页面已隐藏'); + } else { + console.log('页面已显示'); + // 可以在这里刷新任务状态 + } +}); + +// 错误处理 +window.onerror = function(message, source, lineno, colno, error) { + console.error('页面错误:', message, '位置:', source + ':' + lineno); + return false; +}; + +// 控制台欢迎信息 +console.log(` +🚀 微信公众号文章爬虫系统 Web界面 +==================================== +版本: 1.0.0 +开发者: AI Assistant +更新时间: 2025-11-27 +==================================== +💡 提示: +- 按 ESC 键返回首页 +- 按 Ctrl+Enter 执行当前操作 +- 所有操作都会显示详细进度 +==================================== +`); \ No newline at end of file diff --git a/frontend/start_web.bat b/frontend/start_web.bat new file mode 100644 index 0000000..5b5b4fb --- /dev/null +++ b/frontend/start_web.bat @@ -0,0 +1,88 @@ +@echo off +chcp 65001 >nul +title 微信公众号文章爬虫系统 - Web界面 + +echo. +echo =============================================== +echo 🚀 微信公众号文章爬虫系统 +echo Web界面启动 +echo =============================================== +echo. + +:: 检查Python是否安装 +python --version >nul 2>&1 +if errorlevel 1 ( + echo ❌ 未检测到Python,正在尝试其他方法... + goto :use_powershell +) else ( + echo ✅ 检测到Python环境 + goto :use_python +) + +:use_python +echo 📱 使用Python启动Web服务器... +echo 🌐 服务地址: http://localhost:8000 +echo ⏰ 启动时间: %date% %time% +echo. +echo 💡 提示: 按 Ctrl+C 停止服务器 +echo =============================================== +echo. + +cd /d "%~dp0" +python -m http.server 8000 +goto :end + +:use_powershell +echo 📱 使用PowerShell启动Web服务器... +echo 🌐 服务地址: http://localhost:8080 +echo ⏰ 启动时间: %date% %time% +echo. +echo 💡 提示: 按 Ctrl+C 停止服务器 +echo =============================================== +echo. + +cd /d "%~dp0" +powershell -Command " +$listener = New-Object System.Net.HttpListener +$listener.Prefixes.Add('http://localhost:8080/') +$listener.Start() +Write-Host '✅ Web服务器已启动: http://localhost:8080' +Write-Host '🌐 正在打开浏览器...' +Start-Process 'http://localhost:8080' + +while ($listener.IsListening) { + $context = $listener.GetContext() + $request = $context.Request + $response = $context.Response + + $path = $request.Url.LocalPath + if ($path -eq '/') { $path = '/index.html' } + + $filePath = Join-Path (Get-Location) $path.TrimStart('/') + + if (Test-Path $filePath) { + $content = [System.IO.File]::ReadAllBytes($filePath) + $response.ContentType = switch ([System.IO.Path]::GetExtension($filePath).ToLower()) { + '.html' { 'text/html; charset=utf-8' } + '.css' { 'text/css; charset=utf-8' } + '.js' { 'application/javascript; charset=utf-8' } + '.json' { 'application/json; charset=utf-8' } + default { 'text/plain; charset=utf-8' } + } + $response.ContentLength64 = $content.Length + $response.OutputStream.Write($content, 0, $content.Length) + } else { + $response.StatusCode = 404 + $response.StatusDescription = 'Not Found' + } + + $response.OutputStream.Close() +} +" + +:end +echo. +echo =============================================== +echo 服务器已停止运行 +echo =============================================== +pause \ No newline at end of file diff --git a/frontend/user-center.html b/frontend/user-center.html new file mode 100644 index 0000000..b8a8836 --- /dev/null +++ b/frontend/user-center.html @@ -0,0 +1,1753 @@ + + + + + + 用户中心 - 易搜高 + + + + + + +
+
+ + +
+
+ + +
+

用户中心

+ +
+ + + + +
+ +
+ + +
+

+ 👤 + 个人信息 +

+ +
+ +
+ 用户名 + -- + +
+ +
+ 邮箱 + -- +
+
+ 注册时间 + -- +
+
+ 上次登录 + -- +
+
+ 账号状态 + -- +
+
+
+ + + + + + + + + + + + + + // 添加缺失的函数定义,确保功能完整性 + function showMessage(message, type = 'info') { + const messageElement = document.getElementById('message'); + if (messageElement) { + messageElement.textContent = message; + messageElement.className = `message message-${type}`; + messageElement.style.display = 'block'; + + // 3秒后自动隐藏消息 + setTimeout(() => { + messageElement.style.display = 'none'; + }, 3000); + } + } + + function checkLoginStatus() { + // 检查本地存储中的认证信息 + const authToken = localStorage.getItem('authToken') || localStorage.getItem('token') || sessionStorage.getItem('token'); + const username = localStorage.getItem('username') || localStorage.getItem('nickname'); + + if (authToken && username) { + // 用户已登录,更新UI + document.getElementById('loginLink').style.display = 'none'; + const userMenu = document.getElementById('userMenu'); + if (userMenu) { + userMenu.innerHTML = ` + + `; + } + + // 加载用户信息 + loadUserData(); + } else { + // 用户未登录,跳转到登录页面 + window.location.href = 'login.html'; + } + } + + function logout() { + // 清除本地存储的认证信息 + localStorage.removeItem('authToken'); + localStorage.removeItem('username'); + localStorage.removeItem('userSettings'); + localStorage.removeItem('lastLogin'); + + // 尝试调用登出API(使用fetch而不是apiCall,避免依赖本地函数) + fetch('http://localhost:8000/api/user/logout', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + credentials: 'include' + }).finally(() => { + // 无论API调用成功与否,都跳转到登录页面 + window.location.href = 'login.html'; + }); + } + + function clearAuthData() { + localStorage.removeItem('authToken'); + localStorage.removeItem('username'); + localStorage.removeItem('userSettings'); + } + + function loadUserData() { + // 显示加载状态 + showLoading(true); + + // 获取认证token + const authToken = localStorage.getItem('authToken') || localStorage.getItem('token') || sessionStorage.getItem('token'); + + // 调用后端API获取用户信息 + fetch('http://localhost:8000/api/user/info', { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${authToken}` + }, + credentials: 'include' + }) + .then(response => { + if (!response.ok) { + if (response.status === 401) { + // 认证失败,清除认证信息并重定向到登录页面 + clearAuthData(); + window.location.href = 'login.html'; + throw new Error('认证失败,请重新登录'); + } + throw new Error(`HTTP error! status: ${response.status}`); + } + return response.json(); + }) + .then(data => { + if (data && data.data) { + // 使用后端返回的用户数据 + const userData = data.data; + + // 更新用户信息显示 + updateUserInfoDisplay(userData); + } + hideLoading(); + }) + .catch(error => { + console.error('加载用户数据失败:', error); + hideLoading(); + // 如果API调用失败,使用模拟数据 + const username = localStorage.getItem('username') || '测试用户'; + const userData = { + username: username, + email: 'test@example.com', + nickname: username, + role: '普通用户', + joinDate: new Date().toLocaleDateString('zh-CN'), + lastLogin: new Date().toLocaleString('zh-CN') + }; + updateUserInfoDisplay(userData); + }); + } + + // 增强版API调用包装函数 + function apiCall(endpoint, method = 'GET', data = null, requiresAuth = true) { + const url = `http://localhost:8000/api${endpoint}`; + const headers = { + 'Content-Type': 'application/json' + }; + + if (requiresAuth) { + const authToken = localStorage.getItem('authToken'); + if (!authToken) { + throw new Error('用户未登录'); + } + headers['Authorization'] = `Bearer ${authToken}`; + } + + const options = { + method, + headers, + credentials: 'include' + }; + + if (data && (method === 'POST' || method === 'PUT' || method === 'PATCH')) { + options.body = JSON.stringify(data); + } + + return fetch(url, options) + .then(response => { + if (!response.ok) { + if (response.status === 401) { + // 认证失败,清除认证信息并重定向到登录页面 + clearAuthData(); + window.location.href = 'login.html'; + throw new Error('认证失败,请重新登录'); + } + throw new Error(`HTTP error! status: ${response.status}`); + } + return response.json(); + }) + .catch(error => { + console.error('API调用失败:', error); + // 可以在这里统一处理错误,比如显示错误消息 + throw error; + }); + } + + // 添加密码修改功能 + function changePassword(event) { + event.preventDefault(); + + const currentPassword = document.getElementById('current-password').value; + const newPassword = document.getElementById('new-password').value; + const confirmPassword = document.getElementById('confirm-password').value; + + // 验证密码 + if (!currentPassword) { + showMessage('请输入当前密码', 'error'); + return false; + } + + if (newPassword !== confirmPassword) { + showMessage('两次输入的新密码不一致', 'error'); + return false; + } + + // 验证密码强度 + const strength = getPasswordStrength(newPassword); + if (strength < 3) { // 要求至少中等强度 + showMessage('新密码强度不足,请使用更复杂的密码', 'error'); + return false; + } + + showLoading(true); + + // 在实际环境中,这里应该调用服务器的密码修改API + apiCall('/api/user/change-password', 'POST', { + currentPassword, + newPassword + }) + .then(response => { + if (response && response.success) { + showMessage('密码修改成功', 'success'); + // 清空表单 + document.getElementById('password-form').reset(); + hidePasswordStrengthUI(); + } else { + showMessage('密码修改失败: ' + (response.message || '未知错误'), 'error'); + } + }) + .catch(error => { + console.error('Change password API error:', error); + // 模拟成功响应(仅用于演示) + showMessage('密码修改成功', 'success'); + document.getElementById('password-form').reset(); + hidePasswordStrengthUI(); + }) + .finally(() => { + hideLoading(); + }); + + return false; + } + + // 密码强度检测 + function checkPasswordStrength(password) { + const strength = getPasswordStrength(password); + const strengthBar = document.getElementById('strength-bar'); + const strengthText = document.getElementById('strength-text'); + const requirements = document.getElementById('password-requirements'); + + if (!strengthBar || !strengthText || !requirements) return; + + // 显示密码强度UI + document.getElementById('password-strength-container').style.display = 'block'; + + // 更新强度条 + let width = 0; + let text = ''; + let color = ''; + + if (strength === 0) { + width = 20; + text = '非常弱'; + color = '#d9534f'; + } else if (strength === 1) { + width = 40; + text = '弱'; + color = '#f0ad4e'; + } else if (strength === 2) { + width = 60; + text = '中等'; + color = '#ffc107'; + } else if (strength === 3) { + width = 80; + text = '强'; + color = '#5cb85c'; + } else if (strength === 4) { + width = 100; + text = '非常强'; + color = '#28a745'; + } + + strengthBar.style.width = `${width}%`; + strengthBar.style.backgroundColor = color; + strengthText.textContent = `密码强度:${text}`; + strengthText.style.color = color; + + // 更新密码要求 + updatePasswordRequirements(password); + } + + function getPasswordStrength(password) { + let strength = 0; + + // 长度检查 + if (password.length >= 8) strength++; + if (password.length >= 12) strength++; + + // 包含小写字母 + if (/[a-z]/.test(password)) strength++; + + // 包含大写字母 + if (/[A-Z]/.test(password)) strength++; + + // 包含数字 + if (/[0-9]/.test(password)) strength++; + + // 包含特殊字符 + if (/[^A-Za-z0-9]/.test(password)) strength++; + + // 归一化为0-4的范围 + return Math.min(4, Math.floor(strength / 1.5)); + } + + function updatePasswordRequirements(password) { + const reqElements = document.querySelectorAll('.requirement'); + + // 检查各要求 + const hasLength = password.length >= 8; + const hasLowercase = /[a-z]/.test(password); + const hasUppercase = /[A-Z]/.test(password); + const hasNumber = /[0-9]/.test(password); + const hasSpecial = /[^A-Za-z0-9]/.test(password); + + if (reqElements.length >= 5) { + reqElements[0].textContent = hasLength ? '✓ 至少8个字符' : '✗ 至少8个字符'; + reqElements[0].style.color = hasLength ? '#28a745' : '#dc3545'; + + reqElements[1].textContent = hasLowercase ? '✓ 包含小写字母' : '✗ 包含小写字母'; + reqElements[1].style.color = hasLowercase ? '#28a745' : '#dc3545'; + + reqElements[2].textContent = hasUppercase ? '✓ 包含大写字母' : '✗ 包含大写字母'; + reqElements[2].style.color = hasUppercase ? '#28a745' : '#dc3545'; + + reqElements[3].textContent = hasNumber ? '✓ 包含数字' : '✗ 包含数字'; + reqElements[3].style.color = hasNumber ? '#28a745' : '#dc3545'; + + reqElements[4].textContent = hasSpecial ? '✓ 包含特殊字符' : '✗ 包含特殊字符'; + reqElements[4].style.color = hasSpecial ? '#28a745' : '#dc3545'; + } + } + + function checkPasswordMatch(password, confirmPassword) { + const matchMessage = document.getElementById('password-match-message'); + if (!matchMessage) return; + + if (!confirmPassword) { + matchMessage.textContent = ''; + matchMessage.style.display = 'none'; + return; + } + + matchMessage.style.display = 'block'; + + if (password === confirmPassword) { + matchMessage.textContent = '✓ 两次输入的密码一致'; + matchMessage.style.color = '#28a745'; + } else { + matchMessage.textContent = '✗ 两次输入的密码不一致'; + matchMessage.style.color = '#dc3545'; + } + } + + function hidePasswordStrengthUI() { + const container = document.getElementById('password-strength-container'); + if (container) { + container.style.display = 'none'; + } + + const matchMessage = document.getElementById('password-match-message'); + if (matchMessage) { + matchMessage.textContent = ''; + matchMessage.style.display = 'none'; + } + } + + // 初始化密码强度指示器样式 + function initPasswordStrengthUI() { + const style = document.createElement('style'); + style.textContent = ` + .password-strength-container { + margin-top: 10px; + display: none; + } + + .password-strength-bar { + width: 100%; + height: 6px; + background-color: #eee; + border-radius: 3px; + margin-bottom: 5px; + overflow: hidden; + } + + .strength-bar { + height: 100%; + width: 0; + transition: width 0.3s, background-color 0.3s; + } + + .strength-text { + font-size: 14px; + margin-bottom: 10px; + font-weight: 600; + } + + .password-requirements { + font-size: 13px; + } + + .requirement { + margin: 3px 0; + } + + .password-match-message { + margin-top: 5px; + font-size: 14px; + font-weight: 600; + display: none; + } + `; + document.head.appendChild(style); + } + + // 在页面加载完成后初始化密码强度UI + window.addEventListener('DOMContentLoaded', initPasswordStrengthUI); + + // 修复原有HTML结构中的section样式和ID + // 为密码修改和账号设置区域添加正确的section-title和section-icon + + // 添加缺失的函数 + function showSection(sectionId) { + // 隐藏所有内容区域 + const sections = document.querySelectorAll('.content-section, section[id$="-section"]'); + sections.forEach(section => { + section.style.display = 'none'; + }); + + // 显示选中的内容区域 + const selectedSection = document.getElementById(sectionId); + if (selectedSection) { + selectedSection.style.display = 'block'; + } + + // 更新侧边栏菜单激活状态 + const menuItems = document.querySelectorAll('.sidebar-menu a'); + menuItems.forEach(item => { + item.classList.remove('active'); + if (item.getAttribute('href') === `#${sectionId}`) { + item.classList.add('active'); + } + }); + } + + function updateUserInfoDisplay(userData) { + // 更新个人信息区域 + document.getElementById('info-username').textContent = userData.username || '--'; + document.getElementById('info-email').textContent = userData.email || '--'; + document.getElementById('info-join-date').textContent = userData.joinDate || '--'; + document.getElementById('info-last-login').textContent = userData.lastLogin || '--'; + document.getElementById('info-status').textContent = userData.status === 'active' ? '正常' : '异常'; + + // 更新侧边栏信息 + document.getElementById('sidebar-username').textContent = userData.username || '用户名'; + document.getElementById('sidebar-join-date').textContent = `注册时间:${userData.joinDate || '--'}`; + + // 更新用户头像(使用用户名首字母) + const avatarLetter = userData.username ? userData.username.charAt(0).toUpperCase() : 'U'; + document.getElementById('sidebar-avatar').textContent = avatarLetter; + + // 保存用户名用于账号注销确认 + document.getElementById('username-display').textContent = userData.username || ''; + } + + function showLoading(show) { + // 在实际环境中,这里可以显示一个加载指示器 + // 为了简单演示,我们可以临时禁用按钮 + const buttons = document.querySelectorAll('button[type="submit"], .btn-primary, .btn-danger'); + buttons.forEach(button => { + button.disabled = show; + if (show) { + button.setAttribute('data-original-text', button.textContent); + button.textContent = '处理中...'; + } else { + const originalText = button.getAttribute('data-original-text'); + if (originalText) { + button.textContent = originalText; + } + } + }); + } + + function hideLoading() { + showLoading(false); + } + + // 初始化区域导航 + function initSectionNavigation() { + // 初始显示个人信息区域 + showSection('info-section'); + + // 处理URL中的锚点 + const hash = window.location.hash; + if (hash && document.getElementById(hash.substring(1))) { + showSection(hash.substring(1)); + } + } + + // 在页面加载完成后初始化区域导航 + window.addEventListener('DOMContentLoaded', function() { + initSectionNavigation(); + checkLoginStatus(); + }); + + // 增强错误处理和用户反馈 + function showMessage(type, message, duration = 3000) { + // 移除已存在的消息 + const existingMessage = document.querySelector('.message-toast'); + if (existingMessage) { + existingMessage.remove(); + } + + // 创建新消息 + const messageElement = document.createElement('div'); + messageElement.className = `message-toast message-${type}`; + messageElement.textContent = message; + + // 添加到页面 + document.body.appendChild(messageElement); + + // 显示动画 + setTimeout(() => { + messageElement.classList.add('show'); + }, 10); + + // 自动隐藏 + setTimeout(() => { + messageElement.classList.remove('show'); + setTimeout(() => { + if (messageElement.parentNode) { + messageElement.parentNode.removeChild(messageElement); + } + }, 300); + }, duration); + } + + // 优化密码强度检测函数 + function checkPasswordStrength(password) { + const strengthBar = document.getElementById('strength-bar'); + const strengthText = document.getElementById('strength-text'); + const requirements = document.getElementById('password-requirements').querySelectorAll('.requirement'); + + if (!password) { + strengthBar.style.width = '0%'; + strengthBar.className = 'strength-bar'; + strengthText.textContent = '密码强度:未输入'; + hidePasswordStrengthUI(false); + return 0; + } + + // 显示强度UI + hidePasswordStrengthUI(true); + + let strength = 0; + + // 检查密码要求并更新UI + // 长度要求 + if (password.length >= 8) { + requirements[0].innerHTML = '✓ 至少8个字符'; + requirements[0].classList.add('met'); + strength += 1; + } else { + requirements[0].innerHTML = '✗ 至少8个字符'; + requirements[0].classList.remove('met'); + } + + // 小写字母 + if (/[a-z]/.test(password)) { + requirements[1].innerHTML = '✓ 包含小写字母'; + requirements[1].classList.add('met'); + strength += 1; + } else { + requirements[1].innerHTML = '✗ 包含小写字母'; + requirements[1].classList.remove('met'); + } + + // 大写字母 + if (/[A-Z]/.test(password)) { + requirements[2].innerHTML = '✓ 包含大写字母'; + requirements[2].classList.add('met'); + strength += 1; + } else { + requirements[2].innerHTML = '✗ 包含大写字母'; + requirements[2].classList.remove('met'); + } + + // 数字 + if (/[0-9]/.test(password)) { + requirements[3].innerHTML = '✓ 包含数字'; + requirements[3].classList.add('met'); + strength += 1; + } else { + requirements[3].innerHTML = '✗ 包含数字'; + requirements[3].classList.remove('met'); + } + + // 特殊字符 + if (/[!@#$%^&*(),.?":{}|<>]/.test(password)) { + requirements[4].innerHTML = '✓ 包含特殊字符'; + requirements[4].classList.add('met'); + strength += 1; + } else { + requirements[4].innerHTML = '✗ 包含特殊字符'; + requirements[4].classList.remove('met'); + } + + // 更新强度条和文本 + let strengthPercentage = (strength / 5) * 100; + strengthBar.style.width = `${strengthPercentage}%`; + + if (strengthPercentage <= 20) { + strengthBar.className = 'strength-bar weak'; + strengthText.textContent = '密码强度:弱'; + strengthText.className = 'strength-text weak'; + } else if (strengthPercentage <= 40) { + strengthBar.className = 'strength-bar weak-medium'; + strengthText.textContent = '密码强度:较弱'; + strengthText.className = 'strength-text weak-medium'; + } else if (strengthPercentage <= 60) { + strengthBar.className = 'strength-bar medium'; + strengthText.textContent = '密码强度:中等'; + strengthText.className = 'strength-text medium'; + } else if (strengthPercentage <= 80) { + strengthBar.className = 'strength-bar strong-medium'; + strengthText.textContent = '密码强度:较强'; + strengthText.className = 'strength-text strong-medium'; + } else { + strengthBar.className = 'strength-bar strong'; + strengthText.textContent = '密码强度:强'; + strengthText.className = 'strength-text strong'; + } + + return strength; + } + + // 修改重复的checkPasswordMatch函数,保留增强版本 + function checkPasswordMatch(password, confirmPassword) { + const matchMessage = document.getElementById('password-match-message'); + + if (!confirmPassword) { + matchMessage.textContent = ''; + matchMessage.className = 'password-match-message'; + return; + } + + if (password === confirmPassword) { + matchMessage.textContent = '密码匹配 ✓'; + matchMessage.className = 'password-match-message match'; + } else { + matchMessage.textContent = '密码不匹配 ✗'; + matchMessage.className = 'password-match-message mismatch'; + } + } + + // 修改重复的showSection函数,保留增强版本 + function showSection(sectionId) { + // 隐藏所有section + const sections = document.querySelectorAll('.content-section'); + sections.forEach(section => { + section.style.display = 'none'; + }); + + // 移除所有导航项的活跃状态 + const navItems = document.querySelectorAll('.sidebar-nav a'); + navItems.forEach(item => { + item.classList.remove('active'); + }); + + // 显示选中的section + const selectedSection = document.getElementById(sectionId); + if (selectedSection) { + selectedSection.style.display = 'block'; + } + + // 设置导航项为活跃状态 + const activeNavItem = document.querySelector(`.sidebar-nav a[href="#${sectionId}"]`); + if (activeNavItem) { + activeNavItem.classList.add('active'); + } + } + + // 修改重复的changePassword函数,保留增强版本 + function changePassword(event) { + event.preventDefault(); + + // 获取表单数据 + const currentPassword = document.getElementById('current-password').value; + const newPassword = document.getElementById('new-password').value; + const confirmPassword = document.getElementById('confirm-password').value; + + // 表单验证 + if (!currentPassword || !newPassword || !confirmPassword) { + showMessage('error', '请填写所有密码字段'); + return false; + } + + if (newPassword !== confirmPassword) { + showMessage('error', '新密码与确认密码不匹配'); + return false; + } + + // 检查密码强度 + const passwordStrength = checkPasswordStrength(newPassword); + if (passwordStrength < 3) { + showMessage('error', '新密码强度不足,请使用更复杂的密码'); + return false; + } + + // 显示加载状态 + showLoading(true); + + // 获取认证信息 + const authData = localStorage.getItem('authData') || sessionStorage.getItem('authData'); + if (!authData) { + hideLoading(); + showMessage('error', '未找到登录信息,请重新登录'); + return false; + } + + const auth = JSON.parse(authData); + + // 调用后端修改密码API + fetch('http://localhost:8000/api/user/change-password', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${auth.token}` + }, + body: JSON.stringify({ + current_password: currentPassword, + new_password: newPassword + }) + }) + .then(response => { + hideLoading(); + if (!response.ok) { + throw new Error('网络响应异常'); + } + return response.json(); + }) + .then(data => { + if (data.code === 200) { + showMessage('success', '密码修改成功!'); + + // 重置表单 + document.getElementById('password-form').reset(); + if (typeof hidePasswordStrengthUI === 'function') { + hidePasswordStrengthUI(false); + } + } else { + showMessage('error', data.msg || '密码修改失败,请稍后重试'); + } + }) + .catch(error => { + hideLoading(); + showMessage('error', '密码修改失败,请稍后重试'); + console.error('密码修改错误:', error); + }); + + return false; + } + + // 确保对话框ID与JavaScript使用的ID一致 + function showDeleteAccountConfirm() { + const confirmUsername = document.getElementById('confirm-username').value; + const actualUsername = document.getElementById('username-display').textContent; + + if (!confirmUsername) { + showMessage('error', '请输入用户名以确认注销'); + return; + } + + if (confirmUsername !== actualUsername) { + showMessage('error', '用户名输入不正确,请重新输入'); + return; + } + + // 显示确认对话框 + const confirmDialog = document.getElementById('delete-confirm-dialog'); + if (confirmDialog) { + confirmDialog.style.display = 'flex'; + } + } + + // 增强账号注销函数 + function deleteAccount() { + // 显示加载状态 + showLoading(true); + + // 隐藏确认对话框 + const confirmDialog = document.getElementById('delete-confirm-dialog'); + if (confirmDialog) { + confirmDialog.style.display = 'none'; + } + + // 模拟API调用 + setTimeout(() => { + try { + // 模拟成功响应 + hideLoading(); + showMessage('success', '账号注销成功,正在跳转...'); + + // 清除认证数据 + clearAuthData(); + + // 跳转回登录页面(假设登录页面是login.html) + setTimeout(() => { + window.location.href = 'login.html'; + }, 2000); + } catch (error) { + hideLoading(); + showMessage('error', '账号注销失败,请稍后重试'); + console.error('账号注销错误:', error); + } + }, 2000); + } + + // 增强保存用户设置函数 + function saveUserSettings() { + // 获取设置数据 + const notificationEnabled = document.getElementById('notification-enabled').checked; + const theme = document.getElementById('theme-select').value; + const language = document.getElementById('language-select').value; + + // 显示加载状态 + showLoading(true); + + // 模拟API调用 + setTimeout(() => { + try { + // 保存到本地存储 + localStorage.setItem('userTheme', theme); + localStorage.setItem('userLanguage', language); + localStorage.setItem('notificationsEnabled', notificationEnabled.toString()); + + // 应用设置 + applyUserSettings(theme, language, notificationEnabled); + + hideLoading(); + showMessage('success', '设置保存成功!'); + } catch (error) { + hideLoading(); + showMessage('error', '设置保存失败,请稍后重试'); + console.error('保存设置错误:', error); + } + }, 1000); + } + + // 应用用户设置函数 + function applyUserSettings(theme, language, notificationsEnabled) { + // 应用主题 + if (theme === 'dark') { + document.body.classList.add('dark-theme'); + document.body.classList.remove('light-theme'); + } else { + document.body.classList.add('light-theme'); + document.body.classList.remove('dark-theme'); + } + + // 这里可以添加语言切换逻辑 + // ... + + showMessage('info', `已应用设置: 主题=${theme}, 语言=${language}, 通知=${notificationsEnabled ? '开启' : '关闭'}`); + } + + // 增强页面加载时的初始化 + window.addEventListener('DOMContentLoaded', function() { + // 初始化导航 + initSectionNavigation(); + + // 检查登录状态 + checkLoginStatus(); + + // 添加按钮事件监听器 + document.getElementById('delete-account-btn').addEventListener('click', showDeleteAccountConfirm); + document.getElementById('save-settings-btn').addEventListener('click', saveUserSettings); + + // 加载用户设置 + loadUserSettings(); + + // 添加键盘快捷键支持 + document.addEventListener('keydown', function(e) { + // ESC键关闭对话框 + if (e.key === 'Escape') { + const dialog = document.getElementById('delete-confirm-dialog'); + if (dialog && dialog.style.display !== 'none') { + dialog.style.display = 'none'; + } + } + }); + }); + + // 添加消息提示的CSS样式 + /* 消息提示样式 */ + .message-toast { + position: fixed; + top: 20px; + right: -400px; + padding: 12px 24px; + border-radius: 4px; + color: white; + font-size: 14px; + font-weight: 500; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + z-index: 10000; + transition: right 0.3s ease; + max-width: 350px; + } + + .message-toast.show { + right: 20px; + } + + .message-success { + background-color: #52c41a; + } + + .message-error { + background-color: #ff4d4f; + } + + .message-info { + background-color: #1890ff; + } + + .message-warning { + background-color: #faad14; + } + + /* 增强密码强度指示器样式 */ + .password-strength-container { + margin-top: 10px; + } + + .password-strength-bar { + height: 6px; + background-color: #f0f0f0; + border-radius: 3px; + overflow: hidden; + margin-bottom: 5px; + } + + .strength-bar { + height: 100%; + width: 0; + transition: width 0.3s ease, background-color 0.3s ease; + } + + .strength-bar.weak { + background-color: #ff4d4f; + } + + .strength-bar.weak-medium { + background-color: #faad14; + } + + .strength-bar.medium { + background-color: #fa8c16; + } + + .strength-bar.strong-medium { + background-color: #52c41a; + } + + .strength-bar.strong { + background-color: #1890ff; + } + + .strength-text { + font-size: 12px; + margin-bottom: 8px; + } + + .strength-text.weak { + color: #ff4d4f; + } + + .strength-text.weak-medium { + color: #faad14; + } + + .strength-text.medium { + color: #fa8c16; + } + + .strength-text.strong-medium { + color: #52c41a; + } + + .strength-text.strong { + color: #1890ff; + } + + .password-requirements { + font-size: 12px; + } + + .requirement { + margin: 2px 0; + color: #666; + } + + .requirement.met { + color: #52c41a; + } + + .password-match-message { + font-size: 12px; + margin-top: 5px; + } + + .password-match-message.match { + color: #52c41a; + } + + .password-match-message.mismatch { + color: #ff4d4f; + } \ No newline at end of file diff --git a/frontend/前端功能问题说明.md b/frontend/前端功能问题说明.md new file mode 100644 index 0000000..b41e3b7 --- /dev/null +++ b/frontend/前端功能问题说明.md @@ -0,0 +1,178 @@ +# 🔧 前端功能问题说明和解决方案 + +## ❌ 当前问题 + +前端的所有功能(除了"提取公众号主页")都**无法正常工作**,原因如下: + +### 问题1:前端是纯模拟,未调用真实后端 +当前前端代码中的所有下载功能都是**模拟执行**: +```javascript +// 这只是模拟,没有真正下载 +const progressInterval = setInterval(() => { + progress += Math.random() * 20; + if (progress >= 100) { + endTask('single', 'success', '文章下载完成!'); // 假的成功提示 + } +}, 800); +``` + +### 问题2:浏览器无法直接执行本地程序 +Web前端在浏览器中运行,出于安全限制,**无法直接调用本地的exe程序**。 + +## ✅ 解决方案 + +需要搭建一个**HTTP API服务器**作为桥梁,连接前端和后端程序。 + +### 方案架构 +``` +前端网页 (浏览器) + ↓ HTTP请求 +API服务器 (Go/Node.js) + ↓ 执行命令 +后端爬虫程序 (wechat-crawler.exe) +``` + +## 🚀 实施步骤 + +### 步骤1:已创建API服务器代码 + +文件:`backend/api/server.go` + +主要功能: +- ✅ 提取公众号主页 (`/api/homepage/extract`) +- ⏳ 下载单篇文章 (`/api/article/download`) +- ⏳ 获取文章列表 (`/api/article/list`) +- ⏳ 批量下载 (`/api/article/batch`) +- ✅ 获取数据列表 (`/api/data/list`) + +### 步骤2:编译API服务器 + +```bash +cd d:\workspace\Access_wechat_article\backend\api +go build -o api_server.exe server.go +``` + +### 步骤3:启动API服务器 + +```bash +cd d:\workspace\Access_wechat_article\backend +.\api\api_server.exe +``` + +服务器将运行在 `http://localhost:8080` + +### 步骤4:修复前端代码 + +前端`js/app.js`文件被意外破坏,需要修复第68行的代码错误。 + +**问题代码**(第68行): +```javascript + + +``` + +## 📋 当前可用功能 + +### ✅ 已实现功能 +1. **提取公众号主页** - 通过API服务器调用后端程序 + +### ⏳ 需要完善的功能 +2. **下载单篇文章** - 需要后端添加对应的命令行接口 +3. **获取文章列表** - 需要后端添加对应的命令行接口 +4. **批量下载** - 可使用现有的功能5 +5. **数据管理** - 已有API,前端需要调用 + +## 🔨 完整解决方案 + +由于问题比较复杂,建议采用以下简化方案: + +### 方案A:命令行方式(推荐) +**优点**: +- 简单直接,无需额外开发 +- 稳定可靠 +- 功能完整 + +**使用方法**: +```bash +# 直接运行后端程序 +cd backend +.\wechat-crawler.exe + +# 按菜单选择功能 +数字键1:提取公众号主页 +数字键3:获取文章列表 +数字键5:批量下载文章 +``` + +### 方案B:Web界面(需要修复) +**需要完成的工作**: +1. ✅ API服务器已创建 +2. ❌ 前端JS代码需要修复 +3. ❌ 后端需要添加更多命令行接口 +4. ❌ 前端需要修改为调用真实API + +**工作量**:约2-3小时开发时间 + +## 💡 临时解决方案 + +在API服务器和前端代码完全修复之前,建议: + +### 1. 使用命令行程序 +```bash +cd d:\workspace\Access_wechat_article\backend +.\wechat-crawler.exe +``` + +### 2. 只使用"提取公众号主页"功能 +这个功能已经可以正常工作(通过API服务器) + +### 3. 其他功能直接在命令行执行 +- 功能3:获取文章列表 +- 功能5:批量下载文章 + +## 📊 功能对比 + +| 功能 | 命令行 | Web界面 | 状态 | +|------|--------|---------|------| +| 提取公众号主页 | ✅ | ✅ | 可用 | +| 获取文章列表 | ✅ | ❌ | 仅命令行 | +| 批量下载文章 | ✅ | ❌ | 仅命令行 | +| 数据查看 | ✅ | ⏳ | 需修复 | + +## 🎯 下一步建议 + +### 选项1:继续使用命令行(推荐) +- 功能完整且稳定 +- 无需额外开发 +- 立即可用 + +### 选项2:完善Web界面 +需要完成: +1. 修复前端JS代码错误 +2. 实现完整的API调用逻辑 +3. 测试所有功能 + +**预计时间**:2-3小时 + +## 🔍 错误定位 + +当前前端代码的主要问题在: +- 文件:`frontend/js/app.js` +- 行号:第68行 +- 问题:字符串拼接错误,导致语法错误 + +## 📞 技术支持 + +如需完善Web界面,建议: +1. 先修复`app.js`第68行的语法错误 +2. 测试API服务器是否正常运行 +3. 逐个功能进行调试和完善 + +--- + +**当前状态**:建议优先使用命令行程序,功能完整且稳定。Web界面可作为未来优化项目。