feat: v1.1.0 安全重构
- 后台添加登录验证(Session + JWT双重验证) - JSON存储改为SQLite数据库,解决并发问题 - API密钥移至config.py,支持环境变量覆盖 - SECRET_KEY改为随机生成 - 新增管理员登录页面 - 修复README.md乱码 - 更新.gitignore忽略敏感配置
This commit is contained in:
343
admin/app.py
343
admin/app.py
@@ -1,90 +1,156 @@
|
||||
"""
|
||||
技术论坛 - 后台管理系统
|
||||
技术论坛 - 后台管理系统 (重构版 + 登录验证)
|
||||
"""
|
||||
|
||||
from flask import Flask, render_template, jsonify, request
|
||||
from flask import Flask, render_template, jsonify, request, redirect, url_for, session, make_response
|
||||
from flask_cors import CORS
|
||||
import json
|
||||
import jwt
|
||||
import datetime
|
||||
import os
|
||||
from functools import wraps
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
|
||||
# 导入配置和模型
|
||||
import sys
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
from config import SECRET_KEY, ADMIN_USERNAME, ADMIN_PASSWORD, DATABASE_PATH, ADMIN_PORT
|
||||
from models import Database, UserModel, PostModel, ReplyModel, TopicModel
|
||||
|
||||
app = Flask(__name__)
|
||||
CORS(app)
|
||||
app.secret_key = SECRET_KEY
|
||||
|
||||
# 数据目录
|
||||
DATA_DIR = Path(__file__).parent.parent / 'data'
|
||||
USERS_FILE = DATA_DIR / 'users.json'
|
||||
POSTS_FILE = DATA_DIR / 'posts.json'
|
||||
TOPICS_FILE = DATA_DIR / 'topics.json'
|
||||
# 初始化数据库
|
||||
db = Database(DATABASE_PATH)
|
||||
user_model = UserModel(db)
|
||||
post_model = PostModel(db)
|
||||
reply_model = ReplyModel(db)
|
||||
topic_model = TopicModel(db)
|
||||
|
||||
def load_users():
|
||||
if USERS_FILE.exists():
|
||||
return json.loads(USERS_FILE.read_text(encoding='utf-8'))
|
||||
return {}
|
||||
# ============ 登录验证装饰器 ============
|
||||
|
||||
def load_posts():
|
||||
if POSTS_FILE.exists():
|
||||
return json.loads(POSTS_FILE.read_text(encoding='utf-8'))
|
||||
return {}
|
||||
def admin_required(f):
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
# 检查 session
|
||||
if not session.get('admin_logged_in'):
|
||||
# 检查 Authorization header
|
||||
token = request.headers.get('Authorization', '').replace('Bearer ', '')
|
||||
if token:
|
||||
try:
|
||||
data = jwt.decode(token, SECRET_KEY, algorithms=['HS256'])
|
||||
if data.get('admin'):
|
||||
return f(*args, **kwargs)
|
||||
except:
|
||||
pass
|
||||
|
||||
# API请求返回401,页面请求跳转登录
|
||||
if request.path.startswith('/api/'):
|
||||
return jsonify({'error': '请先登录', 'code': 401}), 401
|
||||
return redirect('/login')
|
||||
return f(*args, **kwargs)
|
||||
return decorated_function
|
||||
|
||||
def load_topics():
|
||||
if TOPICS_FILE.exists():
|
||||
return json.loads(TOPICS_FILE.read_text(encoding='utf-8'))
|
||||
return {}
|
||||
# ============ 登录相关 ============
|
||||
|
||||
def save_users(users):
|
||||
USERS_FILE.write_text(json.dumps(users, ensure_ascii=False, indent=2), encoding='utf-8')
|
||||
@app.route('/login')
|
||||
def login_page():
|
||||
return render_template('login.html')
|
||||
|
||||
def save_posts(posts):
|
||||
POSTS_FILE.write_text(json.dumps(posts, ensure_ascii=False, indent=2), encoding='utf-8')
|
||||
@app.route('/api/login', methods=['POST'])
|
||||
def api_login():
|
||||
data = request.json
|
||||
|
||||
username = data.get('username', '').strip()
|
||||
password = data.get('password', '')
|
||||
|
||||
if not username or not password:
|
||||
return jsonify({'error': '请输入用户名和密码'}), 400
|
||||
|
||||
if username != ADMIN_USERNAME or password != ADMIN_PASSWORD:
|
||||
return jsonify({'error': '用户名或密码错误'}), 400
|
||||
|
||||
# 设置session
|
||||
session['admin_logged_in'] = True
|
||||
session['admin_username'] = username
|
||||
|
||||
# 生成token(可选,用于API调用)
|
||||
token = jwt.encode({
|
||||
'admin': True,
|
||||
'username': username,
|
||||
'exp': datetime.datetime.utcnow() + datetime.timedelta(hours=24)
|
||||
}, SECRET_KEY, algorithm='HS256')
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'token': token,
|
||||
'message': '登录成功'
|
||||
})
|
||||
|
||||
def save_topics(topics):
|
||||
TOPICS_FILE.write_text(json.dumps(topics, ensure_ascii=False, indent=2), encoding='utf-8')
|
||||
@app.route('/api/logout', methods=['POST'])
|
||||
def api_logout():
|
||||
session.pop('admin_logged_in', None)
|
||||
session.pop('admin_username', None)
|
||||
return jsonify({'success': True, 'message': '已退出登录'})
|
||||
|
||||
@app.route('/api/check-auth')
|
||||
def api_check_auth():
|
||||
if session.get('admin_logged_in'):
|
||||
return jsonify({
|
||||
'logged_in': True,
|
||||
'username': session.get('admin_username')
|
||||
})
|
||||
return jsonify({'logged_in': False})
|
||||
|
||||
# ============ 页面路由 ============
|
||||
|
||||
@app.route('/')
|
||||
@admin_required
|
||||
def index():
|
||||
return render_template('index.html')
|
||||
|
||||
@app.route('/users')
|
||||
@admin_required
|
||||
def users_page():
|
||||
return render_template('users.html')
|
||||
|
||||
@app.route('/posts')
|
||||
@admin_required
|
||||
def posts_page():
|
||||
return render_template('posts.html')
|
||||
|
||||
@app.route('/topics')
|
||||
@admin_required
|
||||
def topics_page():
|
||||
return render_template('topics.html')
|
||||
|
||||
# ============ API路由 ============
|
||||
|
||||
@app.route('/api/stats')
|
||||
@admin_required
|
||||
def api_stats():
|
||||
users = load_users()
|
||||
posts = load_posts()
|
||||
topics = load_topics()
|
||||
users = user_model.get_all()
|
||||
posts, posts_total = post_model.get_all()
|
||||
topics = topic_model.get_all()
|
||||
|
||||
# 统计
|
||||
total_messages = 0
|
||||
for post in posts.values():
|
||||
total_messages += len(post.get('replies', []))
|
||||
# 统计回复数
|
||||
total_replies = 0
|
||||
for post in posts:
|
||||
total_replies += len(reply_model.get_by_post(post['id']))
|
||||
|
||||
today = datetime.now().strftime('%Y-%m-%d')
|
||||
today_posts = sum(1 for p in posts.values() if p.get('created_at', '').startswith(today))
|
||||
today_users = sum(1 for u in users.values() if u.get('created_at', '').startswith(today))
|
||||
today = datetime.datetime.now().strftime('%Y-%m-%d')
|
||||
today_posts = sum(1 for p in posts if p.get('created_at', '').startswith(today))
|
||||
today_users = sum(1 for u in users if u.get('created_at', '').startswith(today))
|
||||
|
||||
# 帖子类型统计
|
||||
discussion_count = sum(1 for p in posts.values() if p.get('type') == 'discussion')
|
||||
share_count = sum(1 for p in posts.values() if p.get('type') == 'share')
|
||||
discussion_count = sum(1 for p in posts if p.get('type') == 'discussion')
|
||||
share_count = sum(1 for p in posts if p.get('type') == 'share')
|
||||
|
||||
return jsonify({
|
||||
'users_count': len(users),
|
||||
'posts_count': len(posts),
|
||||
'posts_count': posts_total,
|
||||
'topics_count': len(topics),
|
||||
'messages_count': total_messages,
|
||||
'messages_count': total_replies,
|
||||
'today_posts': today_posts,
|
||||
'today_users': today_users,
|
||||
'discussion_count': discussion_count,
|
||||
@@ -92,110 +158,74 @@ def api_stats():
|
||||
})
|
||||
|
||||
@app.route('/api/users')
|
||||
@admin_required
|
||||
def api_users():
|
||||
users = load_users()
|
||||
posts = load_posts()
|
||||
users = user_model.get_all()
|
||||
|
||||
user_list = []
|
||||
for uid, user in users.items():
|
||||
# 统计用户帖子和回复数
|
||||
posts_count = len(user.get('posts', []))
|
||||
replies_count = 0
|
||||
for post in posts.values():
|
||||
for reply in post.get('replies', []):
|
||||
if reply.get('author_id') == uid:
|
||||
replies_count += 1
|
||||
|
||||
for user in users:
|
||||
user_list.append({
|
||||
'id': uid,
|
||||
'id': user['id'],
|
||||
'username': user.get('username', ''),
|
||||
'email': user.get('email', ''),
|
||||
'phone': user.get('phone', ''),
|
||||
'posts_count': posts_count,
|
||||
'replies_count': replies_count,
|
||||
'posts_count': user_model.get_posts_count(user['id']),
|
||||
'replies_count': user_model.get_replies_count(user['id']),
|
||||
'created_at': user.get('created_at', ''),
|
||||
})
|
||||
|
||||
user_list.sort(key=lambda x: x['created_at'], reverse=True)
|
||||
return jsonify(user_list)
|
||||
|
||||
@app.route('/api/users/<user_id>', methods=['DELETE'])
|
||||
@admin_required
|
||||
def api_delete_user(user_id):
|
||||
users = load_users()
|
||||
posts = load_posts()
|
||||
topics = load_topics()
|
||||
|
||||
if user_id not in users:
|
||||
return jsonify({'error': '用户不存在'}), 404
|
||||
|
||||
# 删除用户的帖子
|
||||
for post_id in users[user_id].get('posts', []):
|
||||
if post_id in posts:
|
||||
del posts[post_id]
|
||||
|
||||
# 从主题关注中移除
|
||||
for topic in topics.values():
|
||||
if user_id in topic.get('followers', []):
|
||||
topic['followers'].remove(user_id)
|
||||
|
||||
# 删除用户
|
||||
del users[user_id]
|
||||
|
||||
save_users(users)
|
||||
save_posts(posts)
|
||||
save_topics(topics)
|
||||
|
||||
user_model.delete(user_id)
|
||||
return jsonify({'success': True})
|
||||
|
||||
@app.route('/api/posts')
|
||||
@admin_required
|
||||
def api_posts():
|
||||
posts = load_posts()
|
||||
users = load_users()
|
||||
|
||||
post_type = request.args.get('type')
|
||||
|
||||
posts, total = post_model.get_all(post_type)
|
||||
|
||||
post_list = []
|
||||
for pid, post in posts.items():
|
||||
if post_type and post['type'] != post_type:
|
||||
continue
|
||||
|
||||
author = users.get(post['author_id'], {})
|
||||
for post in posts:
|
||||
author = user_model.get_by_id(post['author_id']) or {}
|
||||
post_list.append({
|
||||
'id': pid,
|
||||
'id': post['id'],
|
||||
'title': post['title'],
|
||||
'type': post['type'],
|
||||
'author': author.get('username', '未知'),
|
||||
'author_id': post['author_id'],
|
||||
'likes': len(post.get('likes', [])),
|
||||
'replies': len(post.get('replies', [])),
|
||||
'views': post.get('views', 0),
|
||||
'is_pinned': post.get('is_pinned', False),
|
||||
'likes': len(post['likes']),
|
||||
'replies': len(reply_model.get_by_post(post['id'])),
|
||||
'views': post['views'],
|
||||
'is_pinned': post['is_pinned'],
|
||||
'created_at': post['created_at'],
|
||||
})
|
||||
|
||||
post_list.sort(key=lambda x: x['created_at'], reverse=True)
|
||||
return jsonify(post_list)
|
||||
|
||||
@app.route('/api/posts/<post_id>')
|
||||
@admin_required
|
||||
def api_post_detail(post_id):
|
||||
posts = load_posts()
|
||||
users = load_users()
|
||||
|
||||
post = posts.get(post_id)
|
||||
post = post_model.get_by_id(post_id)
|
||||
if not post:
|
||||
return jsonify({'error': '帖子不存在'}), 404
|
||||
|
||||
author = users.get(post['author_id'], {})
|
||||
author = user_model.get_by_id(post['author_id']) or {}
|
||||
|
||||
# 获取回复
|
||||
replies = []
|
||||
for reply in post.get('replies', []):
|
||||
reply_author = users.get(reply['author_id'], {})
|
||||
replies.append({
|
||||
replies = reply_model.get_by_post(post_id)
|
||||
reply_list = []
|
||||
for reply in replies:
|
||||
reply_author = user_model.get_by_id(reply['author_id']) or {}
|
||||
reply_list.append({
|
||||
'id': reply['id'],
|
||||
'content': reply['content'][:100] + '...' if len(reply['content']) > 100 else reply['content'],
|
||||
'author': reply_author.get('username', '未知'),
|
||||
'likes': len(reply.get('likes', [])),
|
||||
'likes': len(reply['likes']),
|
||||
'created_at': reply['created_at'],
|
||||
})
|
||||
|
||||
@@ -205,83 +235,58 @@ def api_post_detail(post_id):
|
||||
'content': post['content'],
|
||||
'type': post['type'],
|
||||
'author': author.get('username', '未知'),
|
||||
'tags': post.get('tags', []),
|
||||
'likes': len(post.get('likes', [])),
|
||||
'replies': replies,
|
||||
'views': post.get('views', 0),
|
||||
'is_pinned': post.get('is_pinned', False),
|
||||
'tags': post['tags'],
|
||||
'likes': len(post['likes']),
|
||||
'replies': reply_list,
|
||||
'views': post['views'],
|
||||
'is_pinned': post['is_pinned'],
|
||||
'created_at': post['created_at'],
|
||||
})
|
||||
|
||||
@app.route('/api/posts/<post_id>', methods=['DELETE'])
|
||||
@admin_required
|
||||
def api_delete_post(post_id):
|
||||
posts = load_posts()
|
||||
users = load_users()
|
||||
|
||||
if post_id not in posts:
|
||||
return jsonify({'error': '帖子不存在'}), 404
|
||||
|
||||
author_id = posts[post_id].get('author_id')
|
||||
|
||||
del posts[post_id]
|
||||
|
||||
# 从用户列表中移除
|
||||
if author_id and author_id in users:
|
||||
if post_id in users[author_id].get('posts', []):
|
||||
users[author_id]['posts'].remove(post_id)
|
||||
|
||||
save_posts(posts)
|
||||
save_users(users)
|
||||
|
||||
post_model.delete(post_id)
|
||||
return jsonify({'success': True})
|
||||
|
||||
@app.route('/api/posts/<post_id>/pin', methods=['POST'])
|
||||
@admin_required
|
||||
def api_pin_post(post_id):
|
||||
posts = load_posts()
|
||||
|
||||
if post_id not in posts:
|
||||
return jsonify({'error': '帖子不存在'}), 404
|
||||
|
||||
posts[post_id]['is_pinned'] = not posts[post_id].get('is_pinned', False)
|
||||
save_posts(posts)
|
||||
|
||||
new_pin = post_model.toggle_pin(post_id)
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'is_pinned': posts[post_id]['is_pinned']
|
||||
'is_pinned': new_pin
|
||||
})
|
||||
|
||||
@app.route('/api/topics')
|
||||
@admin_required
|
||||
def api_topics():
|
||||
topics = load_topics()
|
||||
users = load_users()
|
||||
topics = topic_model.get_all()
|
||||
|
||||
topic_list = []
|
||||
for tid, topic in topics.items():
|
||||
author = users.get(topic['author_id'], {})
|
||||
for topic in topics:
|
||||
author = user_model.get_by_id(topic['author_id']) or {}
|
||||
topic_list.append({
|
||||
'id': tid,
|
||||
'id': topic['id'],
|
||||
'name': topic['name'],
|
||||
'icon': topic.get('icon', '🔧'),
|
||||
'author': author.get('username', '未知'),
|
||||
'sub_topics_count': len(topic.get('sub_topics', [])),
|
||||
'questions_count': len(topic.get('questions', [])),
|
||||
'followers_count': len(topic.get('followers', [])),
|
||||
'sub_topics_count': len(topic_model.get_sub_topics(topic['id'])),
|
||||
'questions_count': len(topic_model.get_questions(topic['id'])),
|
||||
'followers_count': len(topic['followers']),
|
||||
'created_at': topic['created_at'],
|
||||
})
|
||||
|
||||
topic_list.sort(key=lambda x: x['followers_count'], reverse=True)
|
||||
return jsonify(topic_list)
|
||||
|
||||
@app.route('/api/topics/<topic_id>')
|
||||
@admin_required
|
||||
def api_topic_detail(topic_id):
|
||||
topics = load_topics()
|
||||
users = load_users()
|
||||
|
||||
topic = topics.get(topic_id)
|
||||
topic = topic_model.get_by_id(topic_id)
|
||||
if not topic:
|
||||
return jsonify({'error': '主题不存在'}), 404
|
||||
|
||||
author = users.get(topic['author_id'], {})
|
||||
author = user_model.get_by_id(topic['author_id']) or {}
|
||||
|
||||
return jsonify({
|
||||
'id': topic_id,
|
||||
@@ -289,41 +294,37 @@ def api_topic_detail(topic_id):
|
||||
'description': topic.get('description', ''),
|
||||
'icon': topic.get('icon', '🔧'),
|
||||
'author': author.get('username', '未知'),
|
||||
'sub_topics': topic.get('sub_topics', []),
|
||||
'questions': topic.get('questions', []),
|
||||
'followers_count': len(topic.get('followers', [])),
|
||||
'sub_topics': topic_model.get_sub_topics(topic_id),
|
||||
'questions': topic_model.get_questions(topic_id),
|
||||
'followers_count': len(topic['followers']),
|
||||
'created_at': topic['created_at'],
|
||||
})
|
||||
|
||||
@app.route('/api/topics/<topic_id>', methods=['DELETE'])
|
||||
@admin_required
|
||||
def api_delete_topic(topic_id):
|
||||
topics = load_topics()
|
||||
|
||||
if topic_id not in topics:
|
||||
return jsonify({'error': '主题不存在'}), 404
|
||||
|
||||
del topics[topic_id]
|
||||
save_topics(topics)
|
||||
|
||||
topic_model.delete(topic_id)
|
||||
return jsonify({'success': True})
|
||||
|
||||
@app.route('/api/tags')
|
||||
@admin_required
|
||||
def api_tags():
|
||||
posts = load_posts()
|
||||
|
||||
tag_counts = {}
|
||||
for post in posts.values():
|
||||
for tag in post.get('tags', []):
|
||||
tag_counts[tag] = tag_counts.get(tag, 0) + 1
|
||||
|
||||
tags = sorted(tag_counts.items(), key=lambda x: x[1], reverse=True)
|
||||
tags = post_model.get_tags_stats()
|
||||
return jsonify([{'name': t[0], 'count': t[1]} for t in tags[:20]])
|
||||
|
||||
# ============ 健康检查(无需登录) ============
|
||||
|
||||
@app.route('/api/health')
|
||||
def api_health():
|
||||
return jsonify({'status': 'ok', 'service': 'tech-forum-admin', 'port': ADMIN_PORT})
|
||||
|
||||
if __name__ == '__main__':
|
||||
print("=" * 50)
|
||||
print("技术论坛 - 后台管理系统")
|
||||
print("=" * 50)
|
||||
print(f"访问地址: http://localhost:19005")
|
||||
print(f"访问地址: http://localhost:{ADMIN_PORT}")
|
||||
print(f"默认账号: {ADMIN_USERNAME}")
|
||||
print(f"默认密码: {ADMIN_PASSWORD}")
|
||||
print("=" * 50)
|
||||
|
||||
app.run(host='0.0.0.0', port=19005, debug=True)
|
||||
app.run(host='0.0.0.0', port=ADMIN_PORT, debug=True)
|
||||
116
admin/templates/login.html
Normal file
116
admin/templates/login.html
Normal file
@@ -0,0 +1,116 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>技术论坛 - 后台登录</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link href="https://cdn.jsdelivr.net/npm/remixicon@3.5.0/fonts/remixicon.css" rel="stylesheet">
|
||||
<style>
|
||||
.gradient-bg { background: linear-gradient(135deg, #3b82f6 0%, #8b5cf6 100%); }
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-gray-100 min-h-screen flex items-center justify-center">
|
||||
<div class="w-full max-w-md">
|
||||
<div class="bg-white rounded-2xl shadow-lg p-8">
|
||||
<div class="text-center mb-8">
|
||||
<div class="w-16 h-16 gradient-bg rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<i class="ri-shield-keyhole-line text-3xl text-white"></i>
|
||||
</div>
|
||||
<h1 class="text-2xl font-bold text-gray-800">后台管理系统</h1>
|
||||
<p class="text-gray-500 text-sm mt-2">请输入管理员账号登录</p>
|
||||
</div>
|
||||
|
||||
<form id="loginForm" class="space-y-6">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">用户名</label>
|
||||
<div class="relative">
|
||||
<i class="ri-user-line absolute left-3 top-3 text-gray-400"></i>
|
||||
<input type="text" id="username" name="username" required
|
||||
class="w-full pl-10 pr-4 py-2.5 border border-gray-200 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
placeholder="请输入用户名">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">密码</label>
|
||||
<div class="relative">
|
||||
<i class="ri-lock-line absolute left-3 top-3 text-gray-400"></i>
|
||||
<input type="password" id="password" name="password" required
|
||||
class="w-full pl-10 pr-4 py-2.5 border border-gray-200 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
placeholder="请输入密码">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="errorMsg" class="hidden text-red-500 text-sm text-center"></div>
|
||||
|
||||
<button type="submit" id="submitBtn"
|
||||
class="w-full py-3 gradient-bg text-white rounded-lg font-medium hover:opacity-90 transition flex items-center justify-center gap-2">
|
||||
<i class="ri-login-circle-line"></i>
|
||||
<span>登录</span>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="mt-6 text-center">
|
||||
<a href="http://localhost:19004" target="_blank" class="text-sm text-gray-500 hover:text-blue-600">
|
||||
<i class="ri-external-link-line"></i> 访问前台网站
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 检查是否已登录
|
||||
async function checkAuth() {
|
||||
try {
|
||||
const res = await fetch('/api/check-auth');
|
||||
const data = await res.json();
|
||||
if (data.logged_in) {
|
||||
window.location.href = '/';
|
||||
}
|
||||
} catch (e) {
|
||||
// 未登录,继续显示登录页面
|
||||
}
|
||||
}
|
||||
|
||||
checkAuth();
|
||||
|
||||
// 登录表单提交
|
||||
document.getElementById('loginForm').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const username = document.getElementById('username').value;
|
||||
const password = document.getElementById('password').value;
|
||||
const errorEl = document.getElementById('errorMsg');
|
||||
const btn = document.getElementById('submitBtn');
|
||||
|
||||
errorEl.classList.add('hidden');
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<i class="ri-loader-4-line animate-spin"></i> 登录中...';
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/login', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username, password })
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (data.success) {
|
||||
window.location.href = '/';
|
||||
} else {
|
||||
errorEl.textContent = data.error || '登录失败';
|
||||
errorEl.classList.remove('hidden');
|
||||
}
|
||||
} catch (e) {
|
||||
errorEl.textContent = '网络错误,请稍后重试';
|
||||
errorEl.classList.remove('hidden');
|
||||
}
|
||||
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<i class="ri-login-circle-line"></i> <span>登录</span>';
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user