扫码登录,获取cookies
This commit is contained in:
6
frontend/.env.example
Normal file
6
frontend/.env.example
Normal file
@@ -0,0 +1,6 @@
|
||||
FLASK_ENV=development
|
||||
FLASK_DEBUG=True
|
||||
SECRET_KEY=your-secret-key-here
|
||||
API_BASE_URL=http://localhost:8000
|
||||
AUTH_BASE_URL=http://localhost:8001
|
||||
SESSION_TYPE=filesystem
|
||||
36
frontend/.gitignore
vendored
Normal file
36
frontend/.gitignore
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
.DS_Store
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
.flask_session/
|
||||
instance/
|
||||
153
frontend/README.md
Normal file
153
frontend/README.md
Normal file
@@ -0,0 +1,153 @@
|
||||
# Weibo-HotSign Frontend
|
||||
|
||||
Flask-based web frontend for the Weibo-HotSign multi-user signin system.
|
||||
|
||||
## Features
|
||||
|
||||
- User registration and login with JWT authentication
|
||||
- Weibo account management (add, edit, delete)
|
||||
- Signin task configuration with Cron expressions
|
||||
- Signin log viewing with pagination and filtering
|
||||
- Responsive design with clean UI
|
||||
|
||||
## Setup
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Python 3.8+
|
||||
- pip
|
||||
|
||||
### Installation
|
||||
|
||||
1. Create a virtual environment:
|
||||
```bash
|
||||
python -m venv venv
|
||||
source venv/bin/activate # On Windows: venv\Scripts\activate
|
||||
```
|
||||
|
||||
2. Install dependencies:
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
3. Create `.env` file from `.env.example`:
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
4. Update `.env` with your configuration:
|
||||
```
|
||||
FLASK_ENV=development
|
||||
FLASK_DEBUG=True
|
||||
SECRET_KEY=your-secret-key-here
|
||||
API_BASE_URL=http://localhost:8000
|
||||
AUTH_BASE_URL=http://localhost:8001
|
||||
```
|
||||
|
||||
### Running
|
||||
|
||||
```bash
|
||||
python app.py
|
||||
```
|
||||
|
||||
The application will be available at `http://localhost:5000`
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
frontend/
|
||||
├── app.py # Main Flask application
|
||||
├── requirements.txt # Python dependencies
|
||||
├── .env.example # Environment variables template
|
||||
├── templates/ # HTML templates
|
||||
│ ├── base.html # Base template with navigation
|
||||
│ ├── login.html # Login page
|
||||
│ ├── register.html # Registration page
|
||||
│ ├── dashboard.html # Account list
|
||||
│ ├── add_account.html # Add account form
|
||||
│ ├── edit_account.html # Edit account form
|
||||
│ ├── account_detail.html # Account details with tasks and logs
|
||||
│ ├── add_task.html # Add task form
|
||||
│ ├── 404.html # 404 error page
|
||||
│ └── 500.html # 500 error page
|
||||
└── README.md # This file
|
||||
```
|
||||
|
||||
## API Integration
|
||||
|
||||
The frontend communicates with two backend services:
|
||||
|
||||
- **Auth Service** (default: http://localhost:8001)
|
||||
- `/auth/register` - User registration
|
||||
- `/auth/login` - User login
|
||||
- `/auth/refresh` - Token refresh
|
||||
- `/auth/me` - Get current user
|
||||
|
||||
- **API Service** (default: http://localhost:8000)
|
||||
- `/api/v1/accounts` - Account CRUD operations
|
||||
- `/api/v1/accounts/{id}/tasks` - Task management
|
||||
- `/api/v1/accounts/{id}/signin-logs` - Signin logs
|
||||
|
||||
## Features
|
||||
|
||||
### Authentication
|
||||
- User registration with password strength validation
|
||||
- Login with email and password
|
||||
- JWT token-based authentication
|
||||
- Automatic token refresh on expiration
|
||||
|
||||
### Account Management
|
||||
- Add multiple Weibo accounts with encrypted cookies
|
||||
- View account list with status indicators
|
||||
- Edit account details and cookies
|
||||
- Delete accounts with cascade deletion
|
||||
|
||||
### Task Configuration
|
||||
- Create signin tasks with Cron expressions
|
||||
- Enable/disable tasks
|
||||
- Delete tasks
|
||||
- View task status
|
||||
|
||||
### Signin Logs
|
||||
- View signin history for each account
|
||||
- Pagination support
|
||||
- Status filtering
|
||||
- Reward information display
|
||||
|
||||
## Security
|
||||
|
||||
- Passwords are validated for strength (uppercase, lowercase, number, special char, 8+ chars)
|
||||
- Cookies are encrypted on the backend
|
||||
- JWT tokens are used for authentication
|
||||
- Session-based state management
|
||||
- CSRF protection via Flask-Session
|
||||
|
||||
## Styling
|
||||
|
||||
The frontend uses a custom CSS framework with:
|
||||
- Responsive grid layout
|
||||
- Card-based design
|
||||
- Color-coded status badges
|
||||
- Mobile-friendly navigation
|
||||
- Smooth transitions and hover effects
|
||||
|
||||
## Error Handling
|
||||
|
||||
- Graceful error messages for API failures
|
||||
- Connection error handling
|
||||
- Form validation
|
||||
- 404 and 500 error pages
|
||||
|
||||
## Development
|
||||
|
||||
To enable debug mode and auto-reload:
|
||||
|
||||
```bash
|
||||
export FLASK_ENV=development
|
||||
export FLASK_DEBUG=True
|
||||
python app.py
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
708
frontend/app.py
Normal file
708
frontend/app.py
Normal file
@@ -0,0 +1,708 @@
|
||||
import os
|
||||
from flask import Flask, render_template, request, redirect, url_for, session, flash, jsonify
|
||||
from flask_session import Session
|
||||
import requests
|
||||
from functools import wraps
|
||||
from dotenv import load_dotenv
|
||||
from datetime import datetime
|
||||
|
||||
load_dotenv()
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config['SECRET_KEY'] = os.getenv('SECRET_KEY', 'dev-secret-key')
|
||||
app.config['SESSION_TYPE'] = 'filesystem'
|
||||
Session(app)
|
||||
|
||||
API_BASE_URL = os.getenv('API_BASE_URL', 'http://localhost:8000')
|
||||
AUTH_BASE_URL = os.getenv('AUTH_BASE_URL', 'http://localhost:8001')
|
||||
|
||||
def get_headers():
|
||||
"""获取请求头,包含认证令牌"""
|
||||
headers = {'Content-Type': 'application/json'}
|
||||
if 'access_token' in session:
|
||||
headers['Authorization'] = f"Bearer {session['access_token']}"
|
||||
return headers
|
||||
|
||||
def login_required(f):
|
||||
"""登录验证装饰器"""
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
if 'user' not in session:
|
||||
flash('Please login first', 'warning')
|
||||
return redirect(url_for('login'))
|
||||
return f(*args, **kwargs)
|
||||
return decorated_function
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
if 'user' in session:
|
||||
return redirect(url_for('dashboard'))
|
||||
return redirect(url_for('login'))
|
||||
|
||||
@app.route('/register', methods=['GET', 'POST'])
|
||||
def register():
|
||||
if request.method == 'POST':
|
||||
username = request.form.get('username')
|
||||
email = request.form.get('email')
|
||||
password = request.form.get('password')
|
||||
confirm_password = request.form.get('confirm_password')
|
||||
|
||||
if password != confirm_password:
|
||||
flash('Passwords do not match', 'danger')
|
||||
return redirect(url_for('register'))
|
||||
|
||||
try:
|
||||
response = requests.post(
|
||||
f'{AUTH_BASE_URL}/auth/register',
|
||||
json={'username': username, 'email': email, 'password': password},
|
||||
timeout=10
|
||||
)
|
||||
|
||||
if response.status_code == 201:
|
||||
data = response.json()
|
||||
session['user'] = data['user']
|
||||
session['access_token'] = data['access_token']
|
||||
session['refresh_token'] = data['refresh_token']
|
||||
flash('Registration successful!', 'success')
|
||||
return redirect(url_for('dashboard'))
|
||||
else:
|
||||
error_data = response.json()
|
||||
flash(error_data.get('detail', 'Registration failed'), 'danger')
|
||||
except requests.RequestException as e:
|
||||
flash(f'Connection error: {str(e)}', 'danger')
|
||||
|
||||
return render_template('register.html')
|
||||
|
||||
@app.route('/login', methods=['GET', 'POST'])
|
||||
def login():
|
||||
if request.method == 'POST':
|
||||
email = request.form.get('email')
|
||||
password = request.form.get('password')
|
||||
|
||||
try:
|
||||
response = requests.post(
|
||||
f'{AUTH_BASE_URL}/auth/login',
|
||||
json={'email': email, 'password': password},
|
||||
timeout=10
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
session['user'] = data['user']
|
||||
session['access_token'] = data['access_token']
|
||||
session['refresh_token'] = data['refresh_token']
|
||||
flash('Login successful!', 'success')
|
||||
return redirect(url_for('dashboard'))
|
||||
else:
|
||||
error_data = response.json()
|
||||
flash(error_data.get('detail', 'Login failed'), 'danger')
|
||||
except requests.RequestException as e:
|
||||
flash(f'Connection error: {str(e)}', 'danger')
|
||||
|
||||
return render_template('login.html')
|
||||
|
||||
@app.route('/logout')
|
||||
def logout():
|
||||
session.clear()
|
||||
flash('Logged out successfully', 'success')
|
||||
return redirect(url_for('login'))
|
||||
|
||||
@app.route('/dashboard')
|
||||
@login_required
|
||||
def dashboard():
|
||||
try:
|
||||
response = requests.get(
|
||||
f'{API_BASE_URL}/api/v1/accounts',
|
||||
headers=get_headers(),
|
||||
timeout=10
|
||||
)
|
||||
data = response.json()
|
||||
accounts = data.get('data', []) if data.get('success') else []
|
||||
except requests.RequestException:
|
||||
accounts = []
|
||||
flash('Failed to load accounts', 'warning')
|
||||
|
||||
return render_template('dashboard.html', accounts=accounts, user=session.get('user'))
|
||||
|
||||
@app.route('/accounts/new', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def add_account():
|
||||
if request.method == 'POST':
|
||||
login_method = request.form.get('login_method', 'manual')
|
||||
|
||||
if login_method == 'manual':
|
||||
weibo_user_id = request.form.get('weibo_user_id')
|
||||
cookie = request.form.get('cookie')
|
||||
remark = request.form.get('remark')
|
||||
|
||||
try:
|
||||
response = requests.post(
|
||||
f'{API_BASE_URL}/api/v1/accounts',
|
||||
json={
|
||||
'weibo_user_id': weibo_user_id,
|
||||
'cookie': cookie,
|
||||
'remark': remark
|
||||
},
|
||||
headers=get_headers(),
|
||||
timeout=10
|
||||
)
|
||||
data = response.json()
|
||||
|
||||
if response.status_code == 200 and data.get('success'):
|
||||
flash('Account added successfully!', 'success')
|
||||
return redirect(url_for('dashboard'))
|
||||
else:
|
||||
flash(data.get('message', 'Failed to add account'), 'danger')
|
||||
except requests.RequestException as e:
|
||||
flash(f'Connection error: {str(e)}', 'danger')
|
||||
|
||||
# 扫码授权功能总是启用(使用微博网页版接口)
|
||||
weibo_qrcode_enabled = True
|
||||
|
||||
return render_template('add_account.html',
|
||||
weibo_qrcode_enabled=weibo_qrcode_enabled)
|
||||
|
||||
|
||||
@app.route('/api/weibo/qrcode/generate', methods=['POST'])
|
||||
@login_required
|
||||
def generate_weibo_qrcode():
|
||||
"""生成微博扫码登录二维码(模拟网页版)"""
|
||||
import uuid
|
||||
import time
|
||||
import traceback
|
||||
|
||||
try:
|
||||
# 模拟微博网页版的二维码生成接口
|
||||
# 实际接口:https://login.sina.com.cn/sso/qrcode/image
|
||||
|
||||
# 生成唯一的 qrcode_id
|
||||
qrcode_id = str(uuid.uuid4())
|
||||
|
||||
# 调用微博的二维码生成接口
|
||||
qr_api_url = 'https://login.sina.com.cn/sso/qrcode/image'
|
||||
params = {
|
||||
'entry': 'weibo',
|
||||
'size': '180',
|
||||
'callback': f'STK_{int(time.time() * 1000)}'
|
||||
}
|
||||
|
||||
# 添加浏览器请求头,模拟真实浏览器
|
||||
headers = {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
||||
'Referer': 'https://weibo.com/',
|
||||
'Accept': '*/*',
|
||||
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
|
||||
'Accept-Encoding': 'gzip, deflate, br',
|
||||
'Connection': 'keep-alive'
|
||||
}
|
||||
|
||||
print(f"[DEBUG] 请求微博 API: {qr_api_url}")
|
||||
response = requests.get(qr_api_url, params=params, headers=headers, timeout=10)
|
||||
print(f"[DEBUG] 响应状态码: {response.status_code}")
|
||||
print(f"[DEBUG] 响应内容: {response.text[:200]}")
|
||||
|
||||
# 微博返回的是 JSONP 格式,需要解析
|
||||
# 格式:STK_xxx({"retcode":20000000,"qrid":"xxx","image":"data:image/png;base64,xxx"})
|
||||
import re
|
||||
import json
|
||||
|
||||
match = re.search(r'\((.*)\)', response.text)
|
||||
if match:
|
||||
data = json.loads(match.group(1))
|
||||
print(f"[DEBUG] 解析的数据: retcode={data.get('retcode')}, data={data.get('data')}")
|
||||
|
||||
if data.get('retcode') == 20000000:
|
||||
# 微博返回的数据结构:{"retcode":20000000,"data":{"qrid":"...","image":"..."}}
|
||||
qr_data = data.get('data', {})
|
||||
qrid = qr_data.get('qrid')
|
||||
qr_image_url = qr_data.get('image')
|
||||
|
||||
if not qrid or not qr_image_url:
|
||||
print(f"[ERROR] 缺少 qrid 或 image: qrid={qrid}, image={qr_image_url}")
|
||||
return jsonify({'success': False, 'error': '二维码数据不完整'}), 500
|
||||
|
||||
# 如果 image 是相对 URL,补全为完整 URL
|
||||
if qr_image_url.startswith('//'):
|
||||
qr_image_url = 'https:' + qr_image_url
|
||||
|
||||
print(f"[DEBUG] 二维码 URL: {qr_image_url}")
|
||||
|
||||
# 存储二维码状态
|
||||
if 'weibo_qrcodes' not in session:
|
||||
session['weibo_qrcodes'] = {}
|
||||
session['weibo_qrcodes'][qrid] = {
|
||||
'status': 'waiting',
|
||||
'created_at': str(datetime.now()),
|
||||
'qrcode_id': qrcode_id
|
||||
}
|
||||
session.modified = True
|
||||
|
||||
print(f"[DEBUG] 二维码生成成功: qrid={qrid}")
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'qrid': qrid,
|
||||
'qr_image': qr_image_url, # 返回二维码图片 URL
|
||||
'expires_in': 180
|
||||
})
|
||||
|
||||
print("[DEBUG] 未能解析响应或 retcode 不正确")
|
||||
return jsonify({'success': False, 'error': '生成二维码失败'}), 500
|
||||
|
||||
except Exception as e:
|
||||
print(f"[ERROR] 生成二维码异常: {str(e)}")
|
||||
print(f"[ERROR] 堆栈跟踪:\n{traceback.format_exc()}")
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
|
||||
@app.route('/api/weibo/qrcode/check/<qrid>', methods=['GET'])
|
||||
@login_required
|
||||
def check_weibo_qrcode(qrid):
|
||||
"""检查微博扫码状态(模拟网页版)"""
|
||||
import time
|
||||
|
||||
try:
|
||||
# 检查二维码是否存在
|
||||
qrcodes = session.get('weibo_qrcodes', {})
|
||||
if qrid not in qrcodes:
|
||||
return jsonify({'status': 'expired'})
|
||||
|
||||
# 调用微博的轮询接口
|
||||
# 实际接口:https://login.sina.com.cn/sso/qrcode/check
|
||||
check_api_url = 'https://login.sina.com.cn/sso/qrcode/check'
|
||||
params = {
|
||||
'entry': 'weibo',
|
||||
'qrid': qrid,
|
||||
'callback': f'STK_{int(time.time() * 1000)}'
|
||||
}
|
||||
|
||||
# 添加浏览器请求头
|
||||
headers = {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
||||
'Referer': 'https://weibo.com/',
|
||||
'Accept': '*/*',
|
||||
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
|
||||
'Connection': 'keep-alive'
|
||||
}
|
||||
|
||||
response = requests.get(check_api_url, params=params, headers=headers, timeout=10)
|
||||
|
||||
print(f"[DEBUG] 检查状态 - qrid: {qrid}")
|
||||
print(f"[DEBUG] 检查状态 - 响应状态码: {response.status_code}")
|
||||
print(f"[DEBUG] 检查状态 - 响应内容: {response.text[:500]}")
|
||||
|
||||
# 解析 JSONP 响应
|
||||
import re
|
||||
match = re.search(r'\((.*)\)', response.text)
|
||||
if match:
|
||||
import json
|
||||
data = json.loads(match.group(1))
|
||||
|
||||
retcode = data.get('retcode')
|
||||
print(f"[DEBUG] 检查状态 - retcode: {retcode}, data: {data}")
|
||||
|
||||
# 微博扫码状态码:
|
||||
# 20000000: 等待扫码
|
||||
# 50050001: 已扫码,等待确认
|
||||
# 20000001: 确认成功
|
||||
# 50050002: 二维码过期
|
||||
# 50050004: 取消授权
|
||||
# 50114001: 未使用(等待扫码)
|
||||
# 50114004: 该二维码已登录(可能是成功状态)
|
||||
|
||||
if retcode == 20000000 or retcode == 50114001:
|
||||
# 等待扫码
|
||||
return jsonify({'status': 'waiting'})
|
||||
elif retcode == 50050001:
|
||||
# 已扫码,等待确认
|
||||
return jsonify({'status': 'scanned'})
|
||||
elif retcode == 20000001 or retcode == 50114004:
|
||||
# 登录成功,获取跳转 URL
|
||||
# 50114004 也表示已登录成功
|
||||
alt_url = data.get('alt')
|
||||
|
||||
# 如果没有 alt 字段,尝试从 data 中获取
|
||||
if not alt_url and data.get('data'):
|
||||
alt_url = data.get('data', {}).get('alt')
|
||||
|
||||
print(f"[DEBUG] 登录成功 - retcode: {retcode}, alt_url: {alt_url}, full_data: {data}")
|
||||
|
||||
# 如果没有 alt_url,尝试构造登录 URL
|
||||
if not alt_url:
|
||||
# 尝试使用 qrid 构造登录 URL
|
||||
# 微博可能使用不同的 URL 格式
|
||||
possible_urls = [
|
||||
f"https://login.sina.com.cn/sso/login.php?entry=weibo&qrid={qrid}",
|
||||
f"https://passport.weibo.com/sso/login?qrid={qrid}",
|
||||
f"https://login.sina.com.cn/sso/qrcode/login?qrid={qrid}"
|
||||
]
|
||||
|
||||
print(f"[DEBUG] 尝试构造登录 URL")
|
||||
for url in possible_urls:
|
||||
try:
|
||||
print(f"[DEBUG] 尝试 URL: {url}")
|
||||
test_response = requests.get(url, headers=headers, allow_redirects=False, timeout=5)
|
||||
print(f"[DEBUG] 响应状态码: {test_response.status_code}")
|
||||
if test_response.status_code in [200, 302, 301]:
|
||||
alt_url = url
|
||||
print(f"[DEBUG] 找到有效 URL: {alt_url}")
|
||||
break
|
||||
except Exception as e:
|
||||
print(f"[DEBUG] URL 失败: {str(e)}")
|
||||
continue
|
||||
|
||||
if alt_url:
|
||||
# 添加浏览器请求头
|
||||
headers = {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
||||
'Referer': 'https://weibo.com/',
|
||||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
|
||||
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
|
||||
'Connection': 'keep-alive'
|
||||
}
|
||||
|
||||
print(f"[DEBUG] 访问跳转 URL: {alt_url}")
|
||||
# 访问跳转 URL 获取 Cookie
|
||||
cookie_response = requests.get(alt_url, headers=headers, allow_redirects=True, timeout=10)
|
||||
cookies = cookie_response.cookies
|
||||
|
||||
print(f"[DEBUG] 获取到的 Cookies: {dict(cookies)}")
|
||||
|
||||
# 构建 Cookie 字符串
|
||||
cookie_str = '; '.join([f'{k}={v}' for k, v in cookies.items()])
|
||||
|
||||
if not cookie_str:
|
||||
print("[ERROR] 未获取到任何 Cookie")
|
||||
return jsonify({'status': 'error', 'error': '未获取到 Cookie'})
|
||||
|
||||
print(f"[DEBUG] Cookie 字符串长度: {len(cookie_str)}")
|
||||
|
||||
# 获取用户信息
|
||||
# 可以通过 Cookie 访问微博 API 获取 uid
|
||||
user_info_url = 'https://weibo.com/ajax/profile/info'
|
||||
user_headers = {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
||||
'Referer': 'https://weibo.com/',
|
||||
'Accept': 'application/json, text/plain, */*',
|
||||
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8'
|
||||
}
|
||||
|
||||
print(f"[DEBUG] 请求用户信息: {user_info_url}")
|
||||
user_response = requests.get(user_info_url, cookies=cookies, headers=user_headers, timeout=10)
|
||||
|
||||
print(f"[DEBUG] 用户信息响应状态码: {user_response.status_code}")
|
||||
print(f"[DEBUG] 用户信息响应内容: {user_response.text[:500]}")
|
||||
|
||||
user_data = user_response.json()
|
||||
|
||||
if user_data.get('ok') == 1:
|
||||
user_info = user_data.get('data', {}).get('user', {})
|
||||
weibo_uid = user_info.get('idstr', '')
|
||||
screen_name = user_info.get('screen_name', 'Weibo User')
|
||||
|
||||
print(f"[DEBUG] 获取用户信息成功: uid={weibo_uid}, name={screen_name}")
|
||||
|
||||
# 更新状态
|
||||
session['weibo_qrcodes'][qrid]['status'] = 'success'
|
||||
session['weibo_qrcodes'][qrid]['cookie'] = cookie_str
|
||||
session['weibo_qrcodes'][qrid]['weibo_uid'] = weibo_uid
|
||||
session['weibo_qrcodes'][qrid]['screen_name'] = screen_name
|
||||
session.modified = True
|
||||
|
||||
return jsonify({
|
||||
'status': 'success',
|
||||
'weibo_uid': weibo_uid,
|
||||
'screen_name': screen_name
|
||||
})
|
||||
else:
|
||||
print(f"[ERROR] 获取用户信息失败: {user_data}")
|
||||
return jsonify({'status': 'error', 'error': '获取用户信息失败'})
|
||||
else:
|
||||
print("[ERROR] 未获取到跳转 URL")
|
||||
|
||||
return jsonify({'status': 'error', 'error': '获取登录信息失败'})
|
||||
elif retcode == 50050002:
|
||||
return jsonify({'status': 'expired'})
|
||||
elif retcode == 50050004:
|
||||
return jsonify({'status': 'cancelled'})
|
||||
else:
|
||||
# 未知状态码,记录日志
|
||||
print(f"[WARN] 未知的 retcode: {retcode}, msg: {data.get('msg')}")
|
||||
return jsonify({'status': 'waiting'}) # 默认继续等待
|
||||
|
||||
print("[DEBUG] 未能解析响应")
|
||||
return jsonify({'status': 'error', 'error': '检查状态失败'})
|
||||
|
||||
except Exception as e:
|
||||
print(f"[ERROR] 检查二维码状态异常: {str(e)}")
|
||||
import traceback
|
||||
print(f"[ERROR] 堆栈跟踪:\n{traceback.format_exc()}")
|
||||
return jsonify({'status': 'error', 'error': str(e)})
|
||||
|
||||
|
||||
@app.route('/api/weibo/qrcode/add-account', methods=['POST'])
|
||||
@login_required
|
||||
def add_account_from_qrcode():
|
||||
"""从扫码结果添加账号"""
|
||||
try:
|
||||
data = request.json
|
||||
qrid = data.get('qrid')
|
||||
remark = data.get('remark', '')
|
||||
|
||||
print(f"[DEBUG] 添加账号 - qrid: {qrid}")
|
||||
|
||||
# 获取扫码结果
|
||||
qrcodes = session.get('weibo_qrcodes', {})
|
||||
qr_info = qrcodes.get(qrid)
|
||||
|
||||
print(f"[DEBUG] 添加账号 - qr_info: {qr_info}")
|
||||
|
||||
if not qr_info or qr_info.get('status') != 'success':
|
||||
print(f"[ERROR] 添加账号失败 - 二维码状态不正确: {qr_info.get('status') if qr_info else 'None'}")
|
||||
return jsonify({'success': False, 'message': '二维码未完成授权'}), 400
|
||||
|
||||
cookie = qr_info.get('cookie')
|
||||
weibo_uid = qr_info.get('weibo_uid')
|
||||
screen_name = qr_info.get('screen_name', 'Weibo User')
|
||||
|
||||
print(f"[DEBUG] 添加账号 - uid: {weibo_uid}, name: {screen_name}, cookie_len: {len(cookie) if cookie else 0}")
|
||||
|
||||
if not remark:
|
||||
remark = f"{screen_name} (扫码添加)"
|
||||
|
||||
# 添加账号到系统
|
||||
print(f"[DEBUG] 调用后端 API 添加账号: {API_BASE_URL}/api/v1/accounts")
|
||||
response = requests.post(
|
||||
f'{API_BASE_URL}/api/v1/accounts',
|
||||
json={
|
||||
'weibo_user_id': weibo_uid,
|
||||
'cookie': cookie,
|
||||
'remark': remark
|
||||
},
|
||||
headers=get_headers(),
|
||||
timeout=10
|
||||
)
|
||||
|
||||
print(f"[DEBUG] 后端响应状态码: {response.status_code}")
|
||||
print(f"[DEBUG] 后端响应内容: {response.text[:500]}")
|
||||
|
||||
result = response.json()
|
||||
|
||||
if response.status_code == 200 and result.get('success'):
|
||||
# 清除已使用的二维码
|
||||
session['weibo_qrcodes'].pop(qrid, None)
|
||||
session.modified = True
|
||||
|
||||
print(f"[DEBUG] 账号添加成功")
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': 'Account added successfully',
|
||||
'account': result.get('data', {})
|
||||
})
|
||||
else:
|
||||
print(f"[ERROR] 后端返回失败: {result}")
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': result.get('message', 'Failed to add account')
|
||||
}), 400
|
||||
|
||||
except Exception as e:
|
||||
print(f"[ERROR] 添加账号异常: {str(e)}")
|
||||
import traceback
|
||||
print(f"[ERROR] 堆栈跟踪:\n{traceback.format_exc()}")
|
||||
return jsonify({'success': False, 'message': str(e)}), 500
|
||||
|
||||
@app.route('/accounts/<account_id>')
|
||||
@login_required
|
||||
def account_detail(account_id):
|
||||
try:
|
||||
# 获取账号详情
|
||||
response = requests.get(
|
||||
f'{API_BASE_URL}/api/v1/accounts/{account_id}',
|
||||
headers=get_headers(),
|
||||
timeout=10
|
||||
)
|
||||
account_data = response.json()
|
||||
account = account_data.get('data') if account_data.get('success') else None
|
||||
|
||||
# 获取任务列表
|
||||
tasks_response = requests.get(
|
||||
f'{API_BASE_URL}/api/v1/accounts/{account_id}/tasks',
|
||||
headers=get_headers(),
|
||||
timeout=10
|
||||
)
|
||||
tasks_data = tasks_response.json()
|
||||
tasks = tasks_data.get('data', []) if tasks_data.get('success') else []
|
||||
|
||||
# 获取签到日志
|
||||
page = request.args.get('page', 1, type=int)
|
||||
logs_response = requests.get(
|
||||
f'{API_BASE_URL}/api/v1/accounts/{account_id}/signin-logs',
|
||||
params={'page': page, 'size': 20},
|
||||
headers=get_headers(),
|
||||
timeout=10
|
||||
)
|
||||
logs_data = logs_response.json()
|
||||
logs = logs_data.get('data', {}) if logs_data.get('success') else {}
|
||||
|
||||
if not account:
|
||||
flash('Account not found', 'danger')
|
||||
return redirect(url_for('dashboard'))
|
||||
|
||||
return render_template(
|
||||
'account_detail.html',
|
||||
account=account,
|
||||
tasks=tasks,
|
||||
logs=logs,
|
||||
user=session.get('user')
|
||||
)
|
||||
except requests.RequestException as e:
|
||||
flash(f'Connection error: {str(e)}', 'danger')
|
||||
return redirect(url_for('dashboard'))
|
||||
|
||||
@app.route('/accounts/<account_id>/edit', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def edit_account(account_id):
|
||||
if request.method == 'POST':
|
||||
remark = request.form.get('remark')
|
||||
cookie = request.form.get('cookie')
|
||||
|
||||
try:
|
||||
data = {'remark': remark}
|
||||
if cookie:
|
||||
data['cookie'] = cookie
|
||||
|
||||
response = requests.put(
|
||||
f'{API_BASE_URL}/api/v1/accounts/{account_id}',
|
||||
json=data,
|
||||
headers=get_headers(),
|
||||
timeout=10
|
||||
)
|
||||
result = response.json()
|
||||
|
||||
if response.status_code == 200 and result.get('success'):
|
||||
flash('Account updated successfully!', 'success')
|
||||
return redirect(url_for('account_detail', account_id=account_id))
|
||||
else:
|
||||
flash(result.get('message', 'Failed to update account'), 'danger')
|
||||
except requests.RequestException as e:
|
||||
flash(f'Connection error: {str(e)}', 'danger')
|
||||
|
||||
try:
|
||||
response = requests.get(
|
||||
f'{API_BASE_URL}/api/v1/accounts/{account_id}',
|
||||
headers=get_headers(),
|
||||
timeout=10
|
||||
)
|
||||
data = response.json()
|
||||
account = data.get('data') if data.get('success') else None
|
||||
|
||||
if not account:
|
||||
flash('Account not found', 'danger')
|
||||
return redirect(url_for('dashboard'))
|
||||
|
||||
return render_template('edit_account.html', account=account)
|
||||
except requests.RequestException as e:
|
||||
flash(f'Connection error: {str(e)}', 'danger')
|
||||
return redirect(url_for('dashboard'))
|
||||
|
||||
@app.route('/accounts/<account_id>/delete', methods=['POST'])
|
||||
@login_required
|
||||
def delete_account(account_id):
|
||||
try:
|
||||
response = requests.delete(
|
||||
f'{API_BASE_URL}/api/v1/accounts/{account_id}',
|
||||
headers=get_headers(),
|
||||
timeout=10
|
||||
)
|
||||
data = response.json()
|
||||
|
||||
if response.status_code == 200 and data.get('success'):
|
||||
flash('Account deleted successfully!', 'success')
|
||||
else:
|
||||
flash(data.get('message', 'Failed to delete account'), 'danger')
|
||||
except requests.RequestException as e:
|
||||
flash(f'Connection error: {str(e)}', 'danger')
|
||||
|
||||
return redirect(url_for('dashboard'))
|
||||
|
||||
@app.route('/accounts/<account_id>/tasks/new', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def add_task(account_id):
|
||||
if request.method == 'POST':
|
||||
cron_expression = request.form.get('cron_expression')
|
||||
|
||||
try:
|
||||
response = requests.post(
|
||||
f'{API_BASE_URL}/api/v1/accounts/{account_id}/tasks',
|
||||
json={'cron_expression': cron_expression},
|
||||
headers=get_headers(),
|
||||
timeout=10
|
||||
)
|
||||
data = response.json()
|
||||
|
||||
if response.status_code == 200 and data.get('success'):
|
||||
flash('Task created successfully!', 'success')
|
||||
return redirect(url_for('account_detail', account_id=account_id))
|
||||
else:
|
||||
flash(data.get('message', 'Failed to create task'), 'danger')
|
||||
except requests.RequestException as e:
|
||||
flash(f'Connection error: {str(e)}', 'danger')
|
||||
|
||||
return render_template('add_task.html', account_id=account_id)
|
||||
|
||||
@app.route('/tasks/<task_id>/toggle', methods=['POST'])
|
||||
@login_required
|
||||
def toggle_task(task_id):
|
||||
is_enabled = request.form.get('is_enabled') == 'true'
|
||||
|
||||
try:
|
||||
response = requests.put(
|
||||
f'{API_BASE_URL}/api/v1/tasks/{task_id}',
|
||||
json={'is_enabled': not is_enabled},
|
||||
headers=get_headers(),
|
||||
timeout=10
|
||||
)
|
||||
data = response.json()
|
||||
|
||||
if response.status_code == 200 and data.get('success'):
|
||||
flash('Task updated successfully!', 'success')
|
||||
else:
|
||||
flash(data.get('message', 'Failed to update task'), 'danger')
|
||||
except requests.RequestException as e:
|
||||
flash(f'Connection error: {str(e)}', 'danger')
|
||||
|
||||
account_id = request.form.get('account_id')
|
||||
return redirect(url_for('account_detail', account_id=account_id))
|
||||
|
||||
@app.route('/tasks/<task_id>/delete', methods=['POST'])
|
||||
@login_required
|
||||
def delete_task(task_id):
|
||||
account_id = request.form.get('account_id')
|
||||
|
||||
try:
|
||||
response = requests.delete(
|
||||
f'{API_BASE_URL}/api/v1/tasks/{task_id}',
|
||||
headers=get_headers(),
|
||||
timeout=10
|
||||
)
|
||||
data = response.json()
|
||||
|
||||
if response.status_code == 200 and data.get('success'):
|
||||
flash('Task deleted successfully!', 'success')
|
||||
else:
|
||||
flash(data.get('message', 'Failed to delete task'), 'danger')
|
||||
except requests.RequestException as e:
|
||||
flash(f'Connection error: {str(e)}', 'danger')
|
||||
|
||||
return redirect(url_for('account_detail', account_id=account_id))
|
||||
|
||||
@app.errorhandler(404)
|
||||
def not_found(error):
|
||||
return render_template('404.html'), 404
|
||||
|
||||
@app.errorhandler(500)
|
||||
def server_error(error):
|
||||
return render_template('500.html'), 500
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(debug=True, port=5000)
|
||||
BIN
frontend/flask_session/2029240f6d1128be89ddc32729463129
Normal file
BIN
frontend/flask_session/2029240f6d1128be89ddc32729463129
Normal file
Binary file not shown.
BIN
frontend/flask_session/841ecc86f9b7cf9085a6ad3204aa3a43
Normal file
BIN
frontend/flask_session/841ecc86f9b7cf9085a6ad3204aa3a43
Normal file
Binary file not shown.
BIN
frontend/flask_session/9954d94905e0926ef31c3ae1f3a81f9f
Normal file
BIN
frontend/flask_session/9954d94905e0926ef31c3ae1f3a81f9f
Normal file
Binary file not shown.
6
frontend/requirements.txt
Normal file
6
frontend/requirements.txt
Normal file
@@ -0,0 +1,6 @@
|
||||
Flask==3.0.0
|
||||
Flask-Session==0.5.0
|
||||
requests==2.31.0
|
||||
python-dotenv==1.0.0
|
||||
Werkzeug==3.0.1
|
||||
qrcode[pil]==7.4.2
|
||||
12
frontend/templates/404.html
Normal file
12
frontend/templates/404.html
Normal file
@@ -0,0 +1,12 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Page Not Found - Weibo-HotSign{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div style="text-align: center; padding: 60px 20px;">
|
||||
<h1 style="font-size: 48px; color: #6366f1; margin-bottom: 20px;">404</h1>
|
||||
<p style="font-size: 24px; color: #333; margin-bottom: 20px;">Page Not Found</p>
|
||||
<p style="color: #999; margin-bottom: 30px;">The page you're looking for doesn't exist.</p>
|
||||
<a href="{{ url_for('dashboard') }}" class="btn btn-primary">Go to Dashboard</a>
|
||||
</div>
|
||||
{% endblock %}
|
||||
12
frontend/templates/500.html
Normal file
12
frontend/templates/500.html
Normal file
@@ -0,0 +1,12 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Server Error - Weibo-HotSign{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div style="text-align: center; padding: 60px 20px;">
|
||||
<h1 style="font-size: 48px; color: #dc3545; margin-bottom: 20px;">500</h1>
|
||||
<p style="font-size: 24px; color: #333; margin-bottom: 20px;">Server Error</p>
|
||||
<p style="color: #999; margin-bottom: 30px;">Something went wrong on our end.</p>
|
||||
<a href="{{ url_for('dashboard') }}" class="btn btn-primary">Go to Dashboard</a>
|
||||
</div>
|
||||
{% endblock %}
|
||||
171
frontend/templates/account_detail.html
Normal file
171
frontend/templates/account_detail.html
Normal file
@@ -0,0 +1,171 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Account Detail - Weibo-HotSign{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div style="max-width: 1000px; margin: 0 auto;">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 30px;">
|
||||
<h1>{{ account.weibo_user_id }}</h1>
|
||||
<div style="display: flex; gap: 10px;">
|
||||
<a href="{{ url_for('edit_account', account_id=account.id) }}" class="btn btn-secondary">Edit</a>
|
||||
<form method="POST" action="{{ url_for('delete_account', account_id=account.id) }}" style="display: inline;" onsubmit="return confirm('Are you sure?');">
|
||||
<button type="submit" class="btn btn-danger">Delete</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-bottom: 30px;">
|
||||
<div class="card">
|
||||
<div class="card-header">Account Info</div>
|
||||
<table>
|
||||
<tr>
|
||||
<td style="font-weight: 500; width: 30%;">Status</td>
|
||||
<td>
|
||||
{% if account.status == 'active' %}
|
||||
<span class="badge badge-success">Active</span>
|
||||
{% elif account.status == 'pending' %}
|
||||
<span class="badge badge-warning">Pending</span>
|
||||
{% elif account.status == 'invalid_cookie' %}
|
||||
<span class="badge badge-danger">Invalid Cookie</span>
|
||||
{% elif account.status == 'banned' %}
|
||||
<span class="badge badge-danger">Banned</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="font-weight: 500;">Remark</td>
|
||||
<td>{{ account.remark or '-' }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="font-weight: 500;">Created</td>
|
||||
<td>{{ account.created_at[:10] }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="font-weight: 500;">Last Checked</td>
|
||||
<td>{{ account.last_checked_at[:10] if account.last_checked_at else '-' }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">Quick Actions</div>
|
||||
<div style="display: flex; flex-direction: column; gap: 10px;">
|
||||
<a href="{{ url_for('add_task', account_id=account.id) }}" class="btn btn-primary" style="text-align: center;">+ Add Task</a>
|
||||
<a href="{{ url_for('edit_account', account_id=account.id) }}" class="btn btn-secondary" style="text-align: center;">Update Cookie</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">Tasks</div>
|
||||
{% if tasks %}
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Cron Expression</th>
|
||||
<th>Status</th>
|
||||
<th>Created</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for task in tasks %}
|
||||
<tr>
|
||||
<td>{{ task.cron_expression }}</td>
|
||||
<td>
|
||||
{% if task.is_enabled %}
|
||||
<span class="badge badge-success">Enabled</span>
|
||||
{% else %}
|
||||
<span class="badge badge-warning">Disabled</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ task.created_at[:10] }}</td>
|
||||
<td>
|
||||
<form method="POST" action="{{ url_for('toggle_task', task_id=task.id) }}" style="display: inline;">
|
||||
<input type="hidden" name="account_id" value="{{ account.id }}">
|
||||
<input type="hidden" name="is_enabled" value="{{ task.is_enabled|lower }}">
|
||||
<button type="submit" class="btn btn-secondary" style="padding: 6px 12px; font-size: 12px;">
|
||||
{% if task.is_enabled %}Disable{% else %}Enable{% endif %}
|
||||
</button>
|
||||
</form>
|
||||
<form method="POST" action="{{ url_for('delete_task', task_id=task.id) }}" style="display: inline;" onsubmit="return confirm('Are you sure?');">
|
||||
<input type="hidden" name="account_id" value="{{ account.id }}">
|
||||
<button type="submit" class="btn btn-danger" style="padding: 6px 12px; font-size: 12px;">Delete</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p style="color: #999; text-align: center; padding: 20px;">No tasks yet</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">Signin Logs</div>
|
||||
{% if logs.items %}
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Topic</th>
|
||||
<th>Status</th>
|
||||
<th>Reward</th>
|
||||
<th>Time</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for log in logs.items %}
|
||||
<tr>
|
||||
<td>{{ log.topic_title or '-' }}</td>
|
||||
<td>
|
||||
{% if log.status == 'success' %}
|
||||
<span class="badge badge-success">Success</span>
|
||||
{% elif log.status == 'failed_already_signed' %}
|
||||
<span class="badge badge-info">Already Signed</span>
|
||||
{% elif log.status == 'failed_network' %}
|
||||
<span class="badge badge-warning">Network Error</span>
|
||||
{% elif log.status == 'failed_banned' %}
|
||||
<span class="badge badge-danger">Banned</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if log.reward_info %}
|
||||
{{ log.reward_info.get('points', '-') }} pts
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ log.signed_at[:10] }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{% if logs.total > logs.size %}
|
||||
<div class="pagination">
|
||||
{% if logs.page > 1 %}
|
||||
<a href="?page=1">First</a>
|
||||
<a href="?page={{ logs.page - 1 }}">Previous</a>
|
||||
{% endif %}
|
||||
|
||||
{% for p in range(max(1, logs.page - 2), min(logs.total // logs.size + 2, logs.page + 3)) %}
|
||||
{% if p == logs.page %}
|
||||
<span class="active">{{ p }}</span>
|
||||
{% else %}
|
||||
<a href="?page={{ p }}">{{ p }}</a>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% if logs.page < logs.total // logs.size + 1 %}
|
||||
<a href="?page={{ logs.page + 1 }}">Next</a>
|
||||
<a href="?page={{ logs.total // logs.size + 1 }}">Last</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<p style="color: #999; text-align: center; padding: 20px;">No signin logs yet</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
444
frontend/templates/add_account.html
Normal file
444
frontend/templates/add_account.html
Normal file
@@ -0,0 +1,444 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Add Account - Weibo-HotSign{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
.tab-container {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 20px;
|
||||
border-bottom: 2px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.tab {
|
||||
padding: 10px 20px;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
background: none;
|
||||
font-size: 16px;
|
||||
color: #666;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.tab.active {
|
||||
color: #6366f1;
|
||||
border-bottom: 2px solid #6366f1;
|
||||
margin-bottom: -2px;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tab-content.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.help-text {
|
||||
background: #f8f9fa;
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
margin-top: 20px;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.help-text h4 {
|
||||
margin-top: 0;
|
||||
color: #333;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.help-text ol, .help-text ul {
|
||||
margin: 10px 0;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.help-text li {
|
||||
margin: 8px 0;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.help-text code {
|
||||
background: #e9ecef;
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
font-family: monospace;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.method-card {
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin-bottom: 15px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.method-card:hover {
|
||||
border-color: #6366f1;
|
||||
box-shadow: 0 2px 8px rgba(99, 102, 241, 0.1);
|
||||
}
|
||||
|
||||
.method-title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.method-badge {
|
||||
display: inline-block;
|
||||
padding: 4px 12px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.badge-recommended {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.warning-box {
|
||||
background: #fff3cd;
|
||||
border-left: 4px solid #ffc107;
|
||||
padding: 15px;
|
||||
margin: 20px 0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.warning-box h4 {
|
||||
color: #856404;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
pre {
|
||||
background: #f8f9fa;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
overflow-x: auto;
|
||||
font-size: 13px;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div style="max-width: 800px; margin: 0 auto;">
|
||||
<h1 style="margin-bottom: 30px;">Add Weibo Account</h1>
|
||||
|
||||
<div class="card">
|
||||
<div class="tab-container">
|
||||
<button class="tab active" onclick="switchTab('guide')">获取教程</button>
|
||||
<button class="tab" onclick="switchTab('qrcode')">扫码添加</button>
|
||||
<button class="tab" onclick="switchTab('manual')">手动添加</button>
|
||||
</div>
|
||||
|
||||
<!-- 获取教程 Tab -->
|
||||
<div id="guide-tab" class="tab-content active">
|
||||
<h3 style="margin-bottom: 20px;">如何获取微博 Cookie</h3>
|
||||
|
||||
<div class="method-card">
|
||||
<div class="method-title">
|
||||
方法一:使用浏览器开发者工具
|
||||
<span class="method-badge badge-recommended">推荐</span>
|
||||
</div>
|
||||
<div class="help-text">
|
||||
<ol>
|
||||
<li>在浏览器中打开 <a href="https://weibo.com" target="_blank">https://weibo.com</a> 并登录</li>
|
||||
<li>按 <code>F12</code> 打开开发者工具</li>
|
||||
<li>切换到 <code>Network</code> (网络) 标签</li>
|
||||
<li>刷新页面 (<code>F5</code>)</li>
|
||||
<li>在请求列表中点击任意一个请求(通常是第一个)</li>
|
||||
<li>在右侧找到 <code>Request Headers</code> (请求头)</li>
|
||||
<li>找到 <code>Cookie:</code> 字段,复制整行内容</li>
|
||||
<li>切换到"添加账号"标签页,粘贴 Cookie</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="warning-box">
|
||||
<h4>⚠️ 重要提示</h4>
|
||||
<ul>
|
||||
<li>Cookie 包含你的登录凭证,请妥善保管</li>
|
||||
<li>不要在公共场合或不信任的网站输入 Cookie</li>
|
||||
<li>Cookie 会被加密存储在数据库中</li>
|
||||
<li>如果 Cookie 失效,系统会提示你更新</li>
|
||||
<li>建议使用小号或测试账号,避免主账号风险</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div style="text-align: center; margin-top: 30px;">
|
||||
<button class="btn btn-primary" onclick="switchTab('qrcode')" style="margin-right: 10px;">
|
||||
使用扫码添加 →
|
||||
</button>
|
||||
<button class="btn btn-secondary" onclick="switchTab('manual')">
|
||||
手动添加账号 →
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 扫码添加 Tab -->
|
||||
<div id="qrcode-tab" class="tab-content">
|
||||
<h3 style="margin-bottom: 20px;">微博扫码登录</h3>
|
||||
|
||||
<div class="method-card">
|
||||
<div class="method-title">
|
||||
扫码快速添加账号
|
||||
<span class="method-badge badge-recommended">推荐</span>
|
||||
</div>
|
||||
<div class="help-text">
|
||||
<p style="font-size: 16px; margin-bottom: 15px;">
|
||||
使用微博网页版扫码登录,安全便捷地添加账号。
|
||||
</p>
|
||||
<h4>使用步骤:</h4>
|
||||
<ol>
|
||||
<li>点击下方"生成二维码"按钮</li>
|
||||
<li>使用手机微博 APP 扫描二维码</li>
|
||||
<li>在手机上点击"确认登录"</li>
|
||||
<li>等待页面自动完成账号添加</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="qr-container" style="text-align: center; margin: 40px 0;">
|
||||
<button id="generate-qr-btn" class="btn btn-primary" style="font-size: 18px; padding: 15px 40px;">
|
||||
📱 生成二维码
|
||||
</button>
|
||||
|
||||
<div id="qr-display" style="display: none; margin-top: 30px;">
|
||||
<div style="background: white; padding: 20px; border-radius: 8px; display: inline-block; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
|
||||
<img id="qr-image" src="" alt="QR Code" style="width: 256px; height: 256px; margin-bottom: 15px;">
|
||||
<p style="color: #666; margin: 0;">请使用微博 APP 扫描</p>
|
||||
<p id="qr-timer" style="color: #999; font-size: 14px; margin-top: 10px;">有效期: 3:00</p>
|
||||
</div>
|
||||
<div id="qr-status" style="margin-top: 20px; font-size: 16px; color: #666;">
|
||||
等待扫码...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="help-text">
|
||||
<h4>💡 说明</h4>
|
||||
<ul>
|
||||
<li>使用微博网页版扫码登录接口,无需注册开放平台应用</li>
|
||||
<li>扫码后自动获取登录 Cookie</li>
|
||||
<li>Cookie 会被加密存储在数据库中</li>
|
||||
<li>建议使用小号或测试账号,避免主账号风险</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="warning-box">
|
||||
<h4>⚠️ 注意事项</h4>
|
||||
<ul>
|
||||
<li>二维码有效期 3 分钟,过期后需重新生成</li>
|
||||
<li>扫码后请在手机上点击"确认登录"</li>
|
||||
<li>如果长时间未响应,请刷新页面重试</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 添加账号 Tab -->
|
||||
<div id="manual-tab" class="tab-content">
|
||||
<h3 style="margin-bottom: 20px;">手动添加账号</h3>
|
||||
<form method="POST">
|
||||
<input type="hidden" name="login_method" value="manual">
|
||||
|
||||
<div class="form-group">
|
||||
<label for="weibo_user_id">Weibo User ID</label>
|
||||
<input type="text" id="weibo_user_id" name="weibo_user_id" required placeholder="e.g., 123456789">
|
||||
<small style="color: #999; display: block; margin-top: 8px;">
|
||||
你的微博数字 ID,可以在个人主页 URL 中找到
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="cookie">Cookie</label>
|
||||
<textarea id="cookie" name="cookie" required placeholder="Paste your Weibo cookie here" style="min-height: 150px;"></textarea>
|
||||
<small style="color: #999; display: block; margin-top: 8px;">
|
||||
粘贴从浏览器获取的完整 Cookie 字符串。Cookie 将被加密存储。
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="remark">Remark (Optional)</label>
|
||||
<input type="text" id="remark" name="remark" placeholder="e.g., My main account">
|
||||
<small style="color: #999; display: block; margin-top: 8px;">
|
||||
给这个账号添加备注,方便识别
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="btn-group">
|
||||
<button type="submit" class="btn btn-primary">Add Account</button>
|
||||
<a href="{{ url_for('dashboard') }}" class="btn btn-secondary">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="help-text" style="margin-top: 30px;">
|
||||
<h4>💡 快速提示</h4>
|
||||
<p><strong>Weibo User ID 在哪里找?</strong></p>
|
||||
<ol>
|
||||
<li>登录微博后,点击右上角头像进入个人主页</li>
|
||||
<li>查看浏览器地址栏,格式类似:<code>https://weibo.com/u/1234567890</code></li>
|
||||
<li>最后的数字 <code>1234567890</code> 就是你的 User ID</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function switchTab(tabName) {
|
||||
// 更新 tab 按钮状态
|
||||
document.querySelectorAll('.tab').forEach(tab => {
|
||||
tab.classList.remove('active');
|
||||
});
|
||||
event.target.classList.add('active');
|
||||
|
||||
// 更新 tab 内容显示
|
||||
document.querySelectorAll('.tab-content').forEach(content => {
|
||||
content.classList.remove('active');
|
||||
});
|
||||
document.getElementById(tabName + '-tab').classList.add('active');
|
||||
}
|
||||
|
||||
// 微博扫码登录相关代码
|
||||
let pollInterval = null;
|
||||
let timerInterval = null;
|
||||
let currentQrid = null;
|
||||
let timeLeft = 180; // 3分钟
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const generateBtn = document.getElementById('generate-qr-btn');
|
||||
if (generateBtn) {
|
||||
generateBtn.addEventListener('click', async function() {
|
||||
try {
|
||||
// 生成二维码
|
||||
const response = await fetch('/api/weibo/qrcode/generate', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
if (!data.success) {
|
||||
alert('生成二维码失败: ' + (data.error || '未知错误'));
|
||||
return;
|
||||
}
|
||||
|
||||
currentQrid = data.qrid;
|
||||
const qrImage = data.qr_image;
|
||||
|
||||
// 显示二维码
|
||||
document.getElementById('qr-image').src = qrImage;
|
||||
document.getElementById('generate-qr-btn').style.display = 'none';
|
||||
document.getElementById('qr-display').style.display = 'block';
|
||||
document.getElementById('qr-status').textContent = '等待扫码...';
|
||||
|
||||
// 开始倒计时
|
||||
timeLeft = 180;
|
||||
updateTimer();
|
||||
timerInterval = setInterval(updateTimer, 1000);
|
||||
|
||||
// 开始轮询状态
|
||||
pollInterval = setInterval(checkQRStatus, 2000);
|
||||
|
||||
} catch (error) {
|
||||
alert('生成二维码失败: ' + error.message);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
function updateTimer() {
|
||||
if (timeLeft <= 0) {
|
||||
clearInterval(timerInterval);
|
||||
clearInterval(pollInterval);
|
||||
document.getElementById('qr-status').innerHTML = '<span style="color: #dc3545;">二维码已过期,请重新生成</span>';
|
||||
document.getElementById('generate-qr-btn').style.display = 'inline-block';
|
||||
document.getElementById('qr-display').style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
const minutes = Math.floor(timeLeft / 60);
|
||||
const seconds = timeLeft % 60;
|
||||
document.getElementById('qr-timer').textContent =
|
||||
`有效期: ${minutes}:${seconds.toString().padStart(2, '0')}`;
|
||||
timeLeft--;
|
||||
}
|
||||
|
||||
async function checkQRStatus() {
|
||||
try {
|
||||
const response = await fetch(`/api/weibo/qrcode/check/${currentQrid}`);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.status === 'waiting') {
|
||||
document.getElementById('qr-status').textContent = '等待扫码...';
|
||||
} else if (data.status === 'scanned') {
|
||||
document.getElementById('qr-status').innerHTML =
|
||||
'<span style="color: #ffc107;">✓ 已扫码,请在手机上确认登录</span>';
|
||||
} else if (data.status === 'success') {
|
||||
// 扫码成功
|
||||
clearInterval(pollInterval);
|
||||
clearInterval(timerInterval);
|
||||
|
||||
document.getElementById('qr-status').innerHTML =
|
||||
'<span style="color: #28a745;">✓ 登录成功!正在添加账号...</span>';
|
||||
|
||||
// 添加账号
|
||||
const addResponse = await fetch('/api/weibo/qrcode/add-account', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ qrid: currentQrid })
|
||||
});
|
||||
|
||||
const addResult = await addResponse.json();
|
||||
|
||||
if (addResult.success) {
|
||||
document.getElementById('qr-status').innerHTML =
|
||||
'<span style="color: #28a745;">✓ 账号添加成功!正在跳转...</span>';
|
||||
setTimeout(() => {
|
||||
window.location.href = '/dashboard';
|
||||
}, 1500);
|
||||
} else {
|
||||
document.getElementById('qr-status').innerHTML =
|
||||
'<span style="color: #dc3545;">添加账号失败: ' + addResult.message + '</span>';
|
||||
}
|
||||
|
||||
} else if (data.status === 'expired') {
|
||||
clearInterval(pollInterval);
|
||||
clearInterval(timerInterval);
|
||||
document.getElementById('qr-status').innerHTML =
|
||||
'<span style="color: #dc3545;">二维码已过期,请重新生成</span>';
|
||||
document.getElementById('generate-qr-btn').style.display = 'inline-block';
|
||||
document.getElementById('qr-display').style.display = 'none';
|
||||
} else if (data.status === 'cancelled') {
|
||||
clearInterval(pollInterval);
|
||||
clearInterval(timerInterval);
|
||||
document.getElementById('qr-status').innerHTML =
|
||||
'<span style="color: #dc3545;">已取消登录</span>';
|
||||
document.getElementById('generate-qr-btn').style.display = 'inline-block';
|
||||
document.getElementById('qr-display').style.display = 'none';
|
||||
} else if (data.status === 'error') {
|
||||
clearInterval(pollInterval);
|
||||
clearInterval(timerInterval);
|
||||
document.getElementById('qr-status').innerHTML =
|
||||
'<span style="color: #dc3545;">错误: ' + (data.error || '未知错误') + '</span>';
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('检查二维码状态失败:', error);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
30
frontend/templates/add_task.html
Normal file
30
frontend/templates/add_task.html
Normal file
@@ -0,0 +1,30 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Add Task - Weibo-HotSign{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div style="max-width: 600px; margin: 0 auto;">
|
||||
<h1 style="margin-bottom: 30px;">Add Signin Task</h1>
|
||||
|
||||
<div class="card">
|
||||
<form method="POST">
|
||||
<div class="form-group">
|
||||
<label for="cron_expression">Cron Expression</label>
|
||||
<input type="text" id="cron_expression" name="cron_expression" required placeholder="e.g., 0 9 * * *">
|
||||
<small style="color: #999; display: block; margin-top: 8px;">
|
||||
Standard cron format (minute hour day month weekday)<br>
|
||||
Examples:<br>
|
||||
• <code>0 9 * * *</code> - Every day at 9:00 AM<br>
|
||||
• <code>0 9 * * 1-5</code> - Weekdays at 9:00 AM<br>
|
||||
• <code>0 9,21 * * *</code> - Every day at 9:00 AM and 9:00 PM
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="btn-group">
|
||||
<button type="submit" class="btn btn-primary">Create Task</button>
|
||||
<a href="{{ url_for('account_detail', account_id=account_id) }}" class="btn btn-secondary">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
398
frontend/templates/base.html
Normal file
398
frontend/templates/base.html
Normal file
@@ -0,0 +1,398 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}Weibo-HotSign{% endblock %}</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
|
||||
background-color: #f5f5f5;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.navbar {
|
||||
background-color: #fff;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
padding: 0 20px;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.navbar-content {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
height: 60px;
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #6366f1;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.navbar-menu {
|
||||
display: flex;
|
||||
gap: 30px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.navbar-menu a {
|
||||
color: #666;
|
||||
text-decoration: none;
|
||||
transition: color 0.3s;
|
||||
}
|
||||
|
||||
.navbar-menu a:hover {
|
||||
color: #6366f1;
|
||||
}
|
||||
|
||||
.navbar-user {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.btn-logout {
|
||||
background-color: #dc3545;
|
||||
color: white;
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
|
||||
.btn-logout:hover {
|
||||
background-color: #c82333;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 30px auto;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.alert {
|
||||
padding: 12px 20px;
|
||||
margin-bottom: 20px;
|
||||
border-radius: 4px;
|
||||
border-left: 4px solid;
|
||||
}
|
||||
|
||||
.alert-success {
|
||||
background-color: #d4edda;
|
||||
color: #155724;
|
||||
border-color: #28a745;
|
||||
}
|
||||
|
||||
.alert-danger {
|
||||
background-color: #f8d7da;
|
||||
color: #721c24;
|
||||
border-color: #f5c6cb;
|
||||
}
|
||||
|
||||
.alert-warning {
|
||||
background-color: #fff3cd;
|
||||
color: #856404;
|
||||
border-color: #ffeaa7;
|
||||
}
|
||||
|
||||
.alert-info {
|
||||
background-color: #d1ecf1;
|
||||
color: #0c5460;
|
||||
border-color: #bee5eb;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
input[type="text"],
|
||||
input[type="email"],
|
||||
input[type="password"],
|
||||
textarea,
|
||||
select {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
input[type="text"]:focus,
|
||||
input[type="email"]:focus,
|
||||
input[type="password"]:focus,
|
||||
textarea:focus,
|
||||
select:focus {
|
||||
outline: none;
|
||||
border-color: #6366f1;
|
||||
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
|
||||
}
|
||||
|
||||
textarea {
|
||||
resize: vertical;
|
||||
min-height: 100px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: #6366f1;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: #4f46e5;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background-color: #6c757d;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background-color: #5a6268;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background-color: #dc3545;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background-color: #c82333;
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background-color: #28a745;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-success:hover {
|
||||
background-color: #218838;
|
||||
}
|
||||
|
||||
.btn-group {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.card {
|
||||
background-color: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 15px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 2px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.badge {
|
||||
display: inline-block;
|
||||
padding: 4px 12px;
|
||||
border-radius: 20px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.badge-success {
|
||||
background-color: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.badge-warning {
|
||||
background-color: #fff3cd;
|
||||
color: #856404;
|
||||
}
|
||||
|
||||
.badge-danger {
|
||||
background-color: #f8d7da;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
.badge-info {
|
||||
background-color: #d1ecf1;
|
||||
color: #0c5460;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
th {
|
||||
background-color: #f8f9fa;
|
||||
padding: 12px;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
border-bottom: 2px solid #dee2e6;
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 12px;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
tr:hover {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 20px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.grid-item {
|
||||
background-color: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
padding: 20px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.grid-item:hover {
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.grid-item-title {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 10px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.grid-item-subtitle {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.grid-item-status {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
justify-content: center;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.pagination a,
|
||||
.pagination span {
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
text-decoration: none;
|
||||
color: #6366f1;
|
||||
}
|
||||
|
||||
.pagination a:hover {
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
||||
.pagination .active {
|
||||
background-color: #6366f1;
|
||||
color: white;
|
||||
border-color: #6366f1;
|
||||
}
|
||||
|
||||
.pagination .disabled {
|
||||
color: #ccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.navbar-menu {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.btn-group {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
{% block extra_css %}{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
{% if session.get('user') %}
|
||||
<nav class="navbar">
|
||||
<div class="navbar-content">
|
||||
<a href="{{ url_for('dashboard') }}" class="navbar-brand">Weibo-HotSign</a>
|
||||
<div class="navbar-menu">
|
||||
<a href="{{ url_for('dashboard') }}">Dashboard</a>
|
||||
<div class="navbar-user">
|
||||
<span>{{ session.get('user').get('username') }}</span>
|
||||
<a href="{{ url_for('logout') }}" class="btn-logout">Logout</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
{% endif %}
|
||||
|
||||
<div class="container">
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
<div class="alert alert-{{ category }}">{{ message }}</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
40
frontend/templates/dashboard.html
Normal file
40
frontend/templates/dashboard.html
Normal file
@@ -0,0 +1,40 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Dashboard - Weibo-HotSign{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 30px;">
|
||||
<h1>Weibo Accounts</h1>
|
||||
<a href="{{ url_for('add_account') }}" class="btn btn-primary">+ Add Account</a>
|
||||
</div>
|
||||
|
||||
{% if accounts %}
|
||||
<div class="grid">
|
||||
{% for account in accounts %}
|
||||
<div class="grid-item" onclick="window.location.href='{{ url_for('account_detail', account_id=account.id) }}'">
|
||||
<div class="grid-item-title">{{ account.weibo_user_id }}</div>
|
||||
<div class="grid-item-subtitle">{{ account.remark or 'No remark' }}</div>
|
||||
<div class="grid-item-status">
|
||||
{% if account.status == 'active' %}
|
||||
<span class="badge badge-success">Active</span>
|
||||
{% elif account.status == 'pending' %}
|
||||
<span class="badge badge-warning">Pending</span>
|
||||
{% elif account.status == 'invalid_cookie' %}
|
||||
<span class="badge badge-danger">Invalid Cookie</span>
|
||||
{% elif account.status == 'banned' %}
|
||||
<span class="badge badge-danger">Banned</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div style="font-size: 12px; color: #999; margin-top: 10px;">
|
||||
Created: {{ account.created_at[:10] }}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="card" style="text-align: center; padding: 60px 20px;">
|
||||
<p style="color: #999; margin-bottom: 20px;">No accounts yet</p>
|
||||
<a href="{{ url_for('add_account') }}" class="btn btn-primary">Add your first account</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
31
frontend/templates/edit_account.html
Normal file
31
frontend/templates/edit_account.html
Normal file
@@ -0,0 +1,31 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Edit Account - Weibo-HotSign{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div style="max-width: 600px; margin: 0 auto;">
|
||||
<h1 style="margin-bottom: 30px;">Edit Account</h1>
|
||||
|
||||
<div class="card">
|
||||
<form method="POST">
|
||||
<div class="form-group">
|
||||
<label for="remark">Remark</label>
|
||||
<input type="text" id="remark" name="remark" value="{{ account.remark or '' }}" placeholder="e.g., My main account">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="cookie">Cookie (Leave empty to keep current)</label>
|
||||
<textarea id="cookie" name="cookie" placeholder="Paste new cookie here if you want to update"></textarea>
|
||||
<small style="color: #999; display: block; margin-top: 8px;">
|
||||
Your cookie will be encrypted and stored securely.
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="btn-group">
|
||||
<button type="submit" class="btn btn-primary">Save Changes</button>
|
||||
<a href="{{ url_for('account_detail', account_id=account.id) }}" class="btn btn-secondary">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
69
frontend/templates/login.html
Normal file
69
frontend/templates/login.html
Normal file
@@ -0,0 +1,69 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Login - Weibo-HotSign{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
.auth-container {
|
||||
max-width: 400px;
|
||||
margin: 60px auto;
|
||||
}
|
||||
|
||||
.auth-card {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
.auth-title {
|
||||
text-align: center;
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 30px;
|
||||
color: #6366f1;
|
||||
}
|
||||
|
||||
.auth-link {
|
||||
text-align: center;
|
||||
margin-top: 20px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.auth-link a {
|
||||
color: #6366f1;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.auth-link a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="auth-container">
|
||||
<div class="auth-card">
|
||||
<h1 class="auth-title">Login</h1>
|
||||
|
||||
<form method="POST">
|
||||
<div class="form-group">
|
||||
<label for="email">Email</label>
|
||||
<input type="email" id="email" name="email" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="password">Password</label>
|
||||
<input type="password" id="password" name="password" required>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary" style="width: 100%;">Login</button>
|
||||
</form>
|
||||
|
||||
<div class="auth-link">
|
||||
Don't have an account? <a href="{{ url_for('register') }}">Register</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
134
frontend/templates/register.html
Normal file
134
frontend/templates/register.html
Normal file
@@ -0,0 +1,134 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Register - Weibo-HotSign{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
.auth-container {
|
||||
max-width: 400px;
|
||||
margin: 60px auto;
|
||||
}
|
||||
|
||||
.auth-card {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
.auth-title {
|
||||
text-align: center;
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 30px;
|
||||
color: #6366f1;
|
||||
}
|
||||
|
||||
.password-strength {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.strength-bar {
|
||||
flex: 1;
|
||||
height: 4px;
|
||||
background-color: #ddd;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.strength-bar.active {
|
||||
background-color: #28a745;
|
||||
}
|
||||
|
||||
.strength-text {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.auth-link {
|
||||
text-align: center;
|
||||
margin-top: 20px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.auth-link a {
|
||||
color: #6366f1;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.auth-link a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="auth-container">
|
||||
<div class="auth-card">
|
||||
<h1 class="auth-title">Create Account</h1>
|
||||
|
||||
<form method="POST" id="registerForm">
|
||||
<div class="form-group">
|
||||
<label for="username">Username</label>
|
||||
<input type="text" id="username" name="username" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="email">Email</label>
|
||||
<input type="email" id="email" name="email" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="password">Password</label>
|
||||
<input type="password" id="password" name="password" required onchange="checkPasswordStrength()">
|
||||
<div class="password-strength" id="strengthBars">
|
||||
<div class="strength-bar"></div>
|
||||
<div class="strength-bar"></div>
|
||||
<div class="strength-bar"></div>
|
||||
<div class="strength-bar"></div>
|
||||
<div class="strength-bar"></div>
|
||||
</div>
|
||||
<div class="strength-text">
|
||||
Must contain: uppercase, lowercase, number, special character, 8+ chars
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="confirm_password">Confirm Password</label>
|
||||
<input type="password" id="confirm_password" name="confirm_password" required>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary" style="width: 100%;">Register</button>
|
||||
</form>
|
||||
|
||||
<div class="auth-link">
|
||||
Already have an account? <a href="{{ url_for('login') }}">Login</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function checkPasswordStrength() {
|
||||
const password = document.getElementById('password').value;
|
||||
let strength = 0;
|
||||
|
||||
if (password.length >= 8) strength++;
|
||||
if (/[a-z]/.test(password)) strength++;
|
||||
if (/[A-Z]/.test(password)) strength++;
|
||||
if (/\d/.test(password)) strength++;
|
||||
if (/[!@#$%^&*]/.test(password)) strength++;
|
||||
|
||||
const bars = document.querySelectorAll('.strength-bar');
|
||||
bars.forEach((bar, index) => {
|
||||
if (index < strength) {
|
||||
bar.classList.add('active');
|
||||
} else {
|
||||
bar.classList.remove('active');
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
82
frontend/templates/weibo_callback.html
Normal file
82
frontend/templates/weibo_callback.html
Normal file
@@ -0,0 +1,82 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>微博授权 - Weibo-HotSign</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
}
|
||||
.container {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 40px;
|
||||
max-width: 500px;
|
||||
width: 100%;
|
||||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
|
||||
text-align: center;
|
||||
}
|
||||
.icon {
|
||||
font-size: 64px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.success { color: #28a745; }
|
||||
.error { color: #dc3545; }
|
||||
h1 {
|
||||
color: #333;
|
||||
margin-bottom: 15px;
|
||||
font-size: 24px;
|
||||
}
|
||||
p {
|
||||
color: #666;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.message {
|
||||
background: #f8f9fa;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
margin: 20px 0;
|
||||
color: #333;
|
||||
}
|
||||
.close-btn {
|
||||
background: #6366f1;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px 30px;
|
||||
border-radius: 6px;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
transition: background 0.3s;
|
||||
}
|
||||
.close-btn:hover {
|
||||
background: #5558dd;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
{% if status == 'success' %}
|
||||
<div class="icon success">✓</div>
|
||||
<h1>授权成功!</h1>
|
||||
<p>你的微博账号 <strong>{{ screen_name }}</strong> 已成功授权。</p>
|
||||
<p>请返回电脑端页面完成账号添加。</p>
|
||||
{% else %}
|
||||
<div class="icon error">✗</div>
|
||||
<h1>授权失败</h1>
|
||||
<div class="message">{{ message }}</div>
|
||||
<p>请返回电脑端页面重试。</p>
|
||||
{% endif %}
|
||||
|
||||
<button class="close-btn" onclick="window.close()">关闭此页面</button>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user