feat: complete local version to overwrite remote

This commit is contained in:
DelLevin-Home
2026-06-16 03:30:57 +08:00
parent 1735c19f48
commit 3c78293f4d
129 changed files with 22814 additions and 0 deletions

Binary file not shown.

287
www_comments/app.py Normal file
View File

@@ -0,0 +1,287 @@
from flask import Flask, request, jsonify, render_template, session, redirect, url_for
from flask_cors import CORS
import json
import os
from datetime import datetime
import hashlib
from config import *
app = Flask(__name__)
CORS(app) # 允许跨域请求
app.secret_key = SECRET_KEY
# 数据文件路径已在 config.py 中定义
# DATA_FILE = os.path.join(os.path.dirname(__file__), 'data', 'comments.json')
def read_comments():
"""读取评论数据"""
try:
if os.path.exists(DATA_FILE):
with open(DATA_FILE, 'r', encoding='utf-8') as f:
return json.load(f)
return {"comments": []}
except Exception as e:
print(f"读取数据失败: {e}")
return {"comments": []}
def save_comments(data):
"""保存评论数据"""
os.makedirs(os.path.dirname(DATA_FILE), exist_ok=True)
with open(DATA_FILE, 'w', encoding='utf-8') as f:
json.dump(data, f, ensure_ascii=False, indent=2)
def parse_user_agent(ua):
"""解析 User Agent 获取浏览器和系统信息"""
browser = 'Unknown'
os_name = 'Unknown'
# 浏览器检测
if 'Chrome' in ua and 'Edge' not in ua:
browser = 'Chrome'
elif 'Firefox' in ua:
browser = 'Firefox'
elif 'Safari' in ua and 'Chrome' not in ua:
browser = 'Safari'
elif 'Edge' in ua:
browser = 'Edge'
# 系统检测
if 'Windows' in ua:
os_name = 'Windows'
elif 'Mac' in ua:
os_name = 'macOS'
elif 'Linux' in ua:
os_name = 'Linux'
elif 'Android' in ua:
os_name = 'Android'
elif 'iPhone' in ua or 'iPad' in ua:
os_name = 'iOS'
return browser, os_name
@app.route('/api/comment', methods=['GET'])
def get_comments():
"""获取评论列表(前端展示用,仅返回已审核)"""
try:
page = int(request.args.get('page', 1))
page_size = int(request.args.get('pageSize', PAGE_SIZE_FRONTEND))
data = read_comments()
# 过滤已审核的评论
filtered = [c for c in data['comments']
if c.get('status') == 'approved']
# 按时间倒序排序
filtered.sort(key=lambda x: x.get('time', 0), reverse=True)
total_count = len(filtered)
total_pages = (total_count + page_size - 1) // page_size
# 分页
start = (page - 1) * page_size
end = start + page_size
paged_comments = filtered[start:end]
return jsonify({
'errno': 0,
'errmsg': '',
'data': {
'page': page,
'totalPages': total_pages,
'pageSize': page_size,
'count': total_count,
'data': paged_comments
}
})
except Exception as e:
return jsonify({
'errno': 1,
'errmsg': str(e)
}), 500
# 管理员账号配置已在 config.py 中定义
# ADMIN_USERNAME = 'admin'
# ADMIN_PASSWORD = 'zhaoyang0902'
def login_required(f):
"""登录验证装饰器"""
from functools import wraps
@wraps(f)
def decorated_function(*args, **kwargs):
if not session.get('logged_in'):
return redirect(url_for('login_page'))
return f(*args, **kwargs)
return decorated_function
@app.route('/login')
def login_page():
"""登录页面"""
return render_template('login.html')
@app.route('/api/login', methods=['POST'])
def do_login():
"""处理登录请求"""
data = request.get_json()
username = data.get('username')
password = data.get('password')
if username == ADMIN_USERNAME and password == ADMIN_PASSWORD:
session['logged_in'] = True
return jsonify({'errno': 0, 'errmsg': '登录成功'})
else:
return jsonify({'errno': 1, 'errmsg': '用户名或密码错误'}), 401
@app.route('/logout')
def logout():
"""退出登录"""
session.pop('logged_in', None)
return redirect(url_for('login_page'))
@app.route('/admin')
@login_required
def admin_page():
"""后台管理页面"""
return render_template('admin.html')
@app.route('/api/admin/comments', methods=['GET'])
@login_required
def get_admin_comments():
"""获取所有评论(后台管理用)"""
try:
status_filter = request.args.get('status', '')
keyword = request.args.get('keyword', '').lower()
page = int(request.args.get('page', 1))
page_size = int(request.args.get('pageSize', PAGE_SIZE_ADMIN))
data = read_comments()
comments = data['comments']
# 状态过滤
if status_filter:
comments = [c for c in comments if c.get('status') == status_filter]
else:
# 全部状态下默认不显示已删除的评论
comments = [c for c in comments if c.get('status') != 'deleted']
# 关键词搜索(昵称或内容)
if keyword:
comments = [c for c in comments if
keyword in c.get('nick', '').lower() or
keyword in c.get('comment', '').lower()]
# 按时间倒序排序
comments.sort(key=lambda x: x.get('time', 0), reverse=True)
total_count = len(comments)
total_pages = (total_count + page_size - 1) // page_size if total_count > 0 else 0
# 分页
start = (page - 1) * page_size
end = start + page_size
paged_comments = comments[start:end]
return jsonify({
'errno': 0,
'data': {
'list': paged_comments,
'total': total_count,
'page': page,
'pageSize': page_size,
'totalPages': total_pages
}
})
except Exception as e:
return jsonify({
'errno': 1,
'errmsg': str(e)
}), 500
@app.route('/api/admin/comment/<int:comment_id>', methods=['PUT'])
@login_required
def update_comment_status(comment_id):
"""更新评论状态"""
try:
update_data = request.get_json()
new_status = update_data.get('status')
if new_status not in ['approved', 'spam', 'deleted']:
return jsonify({'errno': 1, 'errmsg': '无效的状态'}), 400
data = read_comments()
found = False
for c in data['comments']:
if c['id'] == comment_id:
c['status'] = new_status
found = True
break
if not found:
return jsonify({'errno': 1, 'errmsg': '评论不存在'}), 404
save_comments(data)
return jsonify({'errno': 0, 'errmsg': '更新成功'})
except Exception as e:
return jsonify({
'errno': 1,
'errmsg': str(e)
}), 500
@app.route('/api/comment', methods=['POST'])
def add_comment():
"""添加评论"""
try:
comment_data = request.get_json()
if not comment_data:
return jsonify({
'errno': 1,
'errmsg': '无效的数据'
}), 400
data = read_comments()
# 生成新 ID
new_id = max([c.get('id', 0) for c in data['comments']], default=0) + 1
# 解析 User Agent
ua = comment_data.get('ua', '')
browser, os_name = parse_user_agent(ua)
# 生成头像(基于邮箱 MD5
email = comment_data.get('mail', '')
avatar_hash = hashlib.md5(email.encode('utf-8')).hexdigest()
avatar = f'https://seccdn.libravatar.org/avatar/{avatar_hash}'
# 创建新评论
new_comment = {
'id': new_id,
'nick': comment_data.get('nick', '匿名'),
'mail': comment_data.get('mail', ''),
'link': comment_data.get('link', ''),
'comment': f"<p>{comment_data.get('comment', '')}</p>\n",
'ua': ua,
'browser': browser,
'os': os_name,
'avatar': avatar,
'time': int(datetime.now().timestamp() * 1000),
'status': 'approved' # 直接审核通过
}
# 添加到数据中
data['comments'].append(new_comment)
save_comments(data)
return jsonify({
'errno': 0,
'errmsg': '',
'data': new_comment
})
except Exception as e:
return jsonify({
'errno': 1,
'errmsg': str(e)
}), 500
if __name__ == '__main__':
app.run(host=HOST, port=PORT, debug=DEBUG)

18
www_comments/config.py Normal file
View File

@@ -0,0 +1,18 @@
import os
# 基础配置
SECRET_KEY = 'your_secret_key_here'
HOST = '0.0.0.0'
PORT = 27052
DEBUG = False
# 管理员账号配置
ADMIN_USERNAME = 'admin'
ADMIN_PASSWORD = 'zhaoyang0902'
# 数据文件路径
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
DATA_FILE = os.path.join(BASE_DIR, 'data', 'comments.json')
# 分页配置
PAGE_SIZE_ADMIN = 10
PAGE_SIZE_FRONTEND = 10

View File

@@ -0,0 +1,296 @@
{
"comments": [
{
"id": 1,
"nick": "11",
"mail": "",
"link": "",
"comment": "<p>你好</p>\n",
"path": "/",
"ua": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36",
"browser": "Chrome",
"os": "Windows",
"addr": "",
"avatar": "https://seccdn.libravatar.org/avatar/d41d8cd98f00b204e9800998ecf8427e",
"time": 1779017455207,
"status": "approved",
"children": []
},
{
"id": 2,
"nick": "请求",
"mail": "",
"link": "",
"comment": "<p>我问问</p>\n",
"path": "/",
"ua": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36",
"browser": "Chrome",
"os": "Windows",
"addr": "",
"avatar": "https://seccdn.libravatar.org/avatar/d41d8cd98f00b204e9800998ecf8427e",
"time": 1779017466951,
"status": "approved",
"children": []
},
{
"id": 3,
"nick": "匿名",
"mail": "",
"link": "",
"comment": "<p>问问</p>\n",
"ua": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36",
"browser": "Chrome",
"os": "Windows",
"avatar": "https://seccdn.libravatar.org/avatar/d41d8cd98f00b204e9800998ecf8427e",
"time": 1779017669444,
"status": "approved"
},
{
"id": 4,
"nick": "匿名",
"mail": "",
"link": "",
"comment": "<p>2222</p>\n",
"ua": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36",
"browser": "Chrome",
"os": "Windows",
"avatar": "https://seccdn.libravatar.org/avatar/d41d8cd98f00b204e9800998ecf8427e",
"time": 1779017710808,
"status": "approved"
},
{
"id": 5,
"nick": "匿名",
"mail": "",
"link": "",
"comment": "<p>2222</p>\n",
"ua": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36",
"browser": "Chrome",
"os": "Windows",
"avatar": "https://seccdn.libravatar.org/avatar/d41d8cd98f00b204e9800998ecf8427e",
"time": 1779017713660,
"status": "approved"
},
{
"id": 6,
"nick": "匿名",
"mail": "",
"link": "",
"comment": "<p>4344</p>\n",
"ua": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36",
"browser": "Chrome",
"os": "Windows",
"avatar": "https://seccdn.libravatar.org/avatar/d41d8cd98f00b204e9800998ecf8427e",
"time": 1779017717190,
"status": "approved"
},
{
"id": 7,
"nick": "匿名",
"mail": "",
"link": "",
"comment": "<p>2222</p>\n",
"ua": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36",
"browser": "Chrome",
"os": "Windows",
"avatar": "https://seccdn.libravatar.org/avatar/d41d8cd98f00b204e9800998ecf8427e",
"time": 1779017720134,
"status": "approved"
},
{
"id": 8,
"nick": "匿名",
"mail": "",
"link": "",
"comment": "<p>4654</p>\n",
"ua": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36",
"browser": "Chrome",
"os": "Windows",
"avatar": "https://seccdn.libravatar.org/avatar/d41d8cd98f00b204e9800998ecf8427e",
"time": 1779017724265,
"status": "approved"
},
{
"id": 9,
"nick": "匿名",
"mail": "",
"link": "",
"comment": "<p>654123</p>\n",
"ua": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36",
"browser": "Chrome",
"os": "Windows",
"avatar": "https://seccdn.libravatar.org/avatar/d41d8cd98f00b204e9800998ecf8427e",
"time": 1779017730620,
"status": "approved"
},
{
"id": 10,
"nick": "匿名",
"mail": "",
"link": "",
"comment": "<p>1212121</p>\n",
"ua": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36",
"browser": "Chrome",
"os": "Windows",
"avatar": "https://seccdn.libravatar.org/avatar/d41d8cd98f00b204e9800998ecf8427e",
"time": 1779017733598,
"status": "approved"
},
{
"id": 11,
"nick": "匿名",
"mail": "",
"link": "",
"comment": "<p>232323</p>\n",
"ua": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36",
"browser": "Chrome",
"os": "Windows",
"avatar": "https://seccdn.libravatar.org/avatar/d41d8cd98f00b204e9800998ecf8427e",
"time": 1779017740235,
"status": "approved"
},
{
"id": 12,
"nick": "匿名",
"mail": "",
"link": "",
"comment": "<p>wqwqeqwe</p>\n",
"ua": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36",
"browser": "Chrome",
"os": "Windows",
"avatar": "https://seccdn.libravatar.org/avatar/d41d8cd98f00b204e9800998ecf8427e",
"time": 1779017750720,
"status": "approved"
},
{
"id": 13,
"nick": "18631839859@163.com",
"mail": "18631839859@163.com",
"link": "",
"comment": "<p>111</p>\n",
"ua": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36",
"browser": "Chrome",
"os": "Windows",
"avatar": "https://seccdn.libravatar.org/avatar/04c4cbe3c40de052eddb2cf240b2449e",
"time": 1779020082296,
"status": "approved"
},
{
"id": 14,
"nick": "匿名",
"mail": "",
"link": "",
"comment": "<p>测试数据</p>\n",
"ua": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36",
"browser": "Chrome",
"os": "Windows",
"avatar": "https://seccdn.libravatar.org/avatar/d41d8cd98f00b204e9800998ecf8427e",
"time": 1779020759506,
"status": "approved"
},
{
"id": 15,
"nick": "1111",
"mail": "",
"link": "",
"comment": "<p>www</p>\n",
"ua": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36",
"browser": "Chrome",
"os": "Windows",
"avatar": "https://seccdn.libravatar.org/avatar/d41d8cd98f00b204e9800998ecf8427e",
"time": 1779024269862,
"status": "approved"
},
{
"id": 16,
"nick": "1111",
"mail": "",
"link": "",
"comment": "<p>www</p>\n",
"ua": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36",
"browser": "Chrome",
"os": "Windows",
"avatar": "https://seccdn.libravatar.org/avatar/d41d8cd98f00b204e9800998ecf8427e",
"time": 1779024271274,
"status": "approved"
},
{
"id": 17,
"nick": "222",
"mail": "",
"link": "",
"comment": "<p>12121</p>\n",
"ua": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36",
"browser": "Chrome",
"os": "Windows",
"avatar": "https://seccdn.libravatar.org/avatar/d41d8cd98f00b204e9800998ecf8427e",
"time": 1779024413575,
"status": "approved"
},
{
"id": 18,
"nick": "2222",
"mail": "",
"link": "",
"comment": "<p>12123w</p>\n",
"ua": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36",
"browser": "Chrome",
"os": "Windows",
"avatar": "https://seccdn.libravatar.org/avatar/d41d8cd98f00b204e9800998ecf8427e",
"time": 1779025241541,
"status": "approved"
},
{
"id": 19,
"nick": "sadadadas",
"mail": "",
"link": "",
"comment": "<p>dasdada</p>\n",
"ua": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36",
"browser": "Chrome",
"os": "Windows",
"avatar": "https://seccdn.libravatar.org/avatar/d41d8cd98f00b204e9800998ecf8427e",
"time": 1779025245378,
"status": "approved"
},
{
"id": 20,
"nick": "wqeqewq",
"mail": "",
"link": "",
"comment": "<p>sdadad</p>\n",
"ua": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36",
"browser": "Chrome",
"os": "Windows",
"avatar": "https://seccdn.libravatar.org/avatar/d41d8cd98f00b204e9800998ecf8427e",
"time": 1779025249121,
"status": "approved"
},
{
"id": 21,
"nick": "32424",
"mail": "",
"link": "",
"comment": "<p>2342424</p>\n",
"ua": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36",
"browser": "Chrome",
"os": "Windows",
"avatar": "https://seccdn.libravatar.org/avatar/d41d8cd98f00b204e9800998ecf8427e",
"time": 1779025259840,
"status": "deleted"
},
{
"id": 22,
"nick": "匿名",
"mail": "",
"link": "",
"comment": "<p>eee</p>\n",
"ua": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36",
"browser": "Chrome",
"os": "Windows",
"avatar": "https://seccdn.libravatar.org/avatar/d41d8cd98f00b204e9800998ecf8427e",
"time": 1779033891008,
"status": "approved"
}
]
}

View File

@@ -0,0 +1,2 @@
flask==3.0.0
flask-cors==4.0.0

View File

@@ -0,0 +1,725 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>评论后台管理</title>
<style>
:root {
--el-color-primary: #409eff;
--el-color-success: #67c23a;
--el-color-danger: #f56c6c;
--el-color-info: #909399;
--el-text-primary: #303133;
--el-text-regular: #606266;
--el-text-secondary: #909399;
--el-border-color: #dcdfe6;
--el-border-light: #e4e7ed;
--el-fill-color: #f5f7fa;
--el-header-height: 60px;
--el-aside-width: 200px;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", Arial, sans-serif; background-color: #f0f2f5; color: var(--el-text-regular); }
/* ========== 顶部 Header ========== */
.header {
position: fixed; top: 0; left: 0; right: 0; height: var(--el-header-height);
background: #fff; border-bottom: 1px solid var(--el-border-light);
display: flex; align-items: center; justify-content: space-between; padding: 0 20px;
z-index: 1000; box-shadow: 0 1px 4px rgba(0,0,0,.08);
}
.header-brand { font-size: 18px; font-weight: 600; color: var(--el-text-primary); letter-spacing: 1px; }
.header-right { display: flex; align-items: center; gap: 20px; font-size: 14px; }
.header-right a { color: var(--el-text-secondary); text-decoration: none; transition: color .3s; }
.header-right a:hover { color: var(--el-color-primary); }
/* ========== 左侧 Aside ========== */
.aside {
position: fixed; top: var(--el-header-height); left: 0; bottom: 0;
width: var(--el-aside-width); background: #304156;
z-index: 1000; overflow-y: auto;
}
.menu-item {
display: block; padding: 0 20px; height: 56px; line-height: 56px;
color: #bfcbd9; font-size: 14px; cursor: pointer; transition: all .3s;
text-decoration: none; border-left: 3px solid transparent;
}
.menu-item:hover, .menu-item.active {
background: #263445; color: var(--el-color-primary); border-left-color: var(--el-color-primary);
}
.menu-icon { display: inline-block; width: 20px; margin-right: 10px; text-align: center; }
/* 左侧状态筛选 */
.aside-status-group {
padding: 10px 0;
border-top: 1px solid rgba(255,255,255,0.1);
margin-top: 10px;
}
.aside-menu-item {
display: block; padding: 0 20px; height: 48px; line-height: 48px;
color: #bfcbd9; font-size: 14px; cursor: pointer; transition: all .3s;
text-decoration: none; border-left: 3px solid transparent;
}
.aside-menu-item:hover, .aside-menu-item.active {
background: #263445; color: var(--el-color-primary); border-left-color: var(--el-color-primary);
}
/* ========== 右侧 Main ========== */
.main {
margin-left: var(--el-aside-width); margin-top: var(--el-header-height);
padding: 20px; min-height: calc(100vh - var(--el-header-height));
}
.main-card {
background: #fff; border-radius: 4px; padding: 20px;
box-shadow: 0 1px 4px rgba(0,0,0,.08);
}
.page-title { font-size: 20px; font-weight: 500; color: var(--el-text-primary); margin-bottom: 20px; }
/* 筛选区 */
.filters { display: flex; gap: 10px; margin-bottom: 20px; align-items: center; flex-wrap: wrap; padding: 15px; background: var(--el-fill-color); border-radius: 4px; }
input[type="text"] {
appearance: none; background-color: #fff; border-radius: 4px; border: 1px solid var(--el-border-color);
color: var(--el-text-regular); height: 36px; line-height: 36px; outline: none; padding: 0 15px;
transition: border-color .2s; width: 220px;
}
input[type="text"]:focus { border-color: var(--el-color-primary); }
/* 状态单选按钮组 */
.status-group { display: inline-flex; border: 1px solid var(--el-border-color); border-radius: 4px; overflow: hidden; background: #fff; }
.radio-label {
cursor: pointer; padding: 0 16px; height: 34px; line-height: 34px; font-size: 14px;
transition: all .3s; user-select: none; background: #fff; color: var(--el-text-regular); border-right: 1px solid var(--el-border-color);
}
.radio-label:last-child { border-right: none; }
.radio-label:hover { color: var(--el-color-primary); }
.radio-label.active { background-color: var(--el-color-primary); border-color: var(--el-color-primary); color: #fff; }
input[type="radio"] { display: none; }
button.el-button {
display: inline-block; line-height: 1; white-space: nowrap; cursor: pointer; background: #fff;
border: 1px solid var(--el-border-color); color: var(--el-text-regular); text-align: center;
outline: none; transition: .1s; font-weight: 500; padding: 9px 20px; font-size: 14px; border-radius: 4px;
}
button.el-button:hover { color: var(--el-color-primary); border-color: #c6e2ff; background-color: #ecf5ff; }
button.el-button--primary { color: #fff; background-color: var(--el-color-primary); border-color: var(--el-color-primary); }
button.el-button--primary:hover { background: #66b1ff; border-color: #66b1ff; }
/* 表格 */
table { width: 100%; border-collapse: collapse; font-size: 14px; }
th { background-color: var(--el-fill-color); color: var(--el-text-secondary); font-weight: 500; text-align: left; padding: 12px 10px; border-bottom: 1px solid var(--el-border-light); }
td { padding: 12px 10px; border-bottom: 1px solid var(--el-border-light); color: var(--el-text-regular); }
tr:hover td { background-color: var(--el-fill-color); }
.status-badge { padding: 0 10px; height: 22px; line-height: 22px; border-radius: 4px; font-size: 12px; display: inline-block; }
.status-approved { background-color: #f0f9eb; color: var(--el-color-success); border: 1px solid #e1f3d8; }
.status-spam { background-color: #fef0f0; color: var(--el-color-danger); border: 1px solid #fde2e2; }
.status-deleted { background-color: #f4f4f5; color: var(--el-color-info); border: 1px solid #e9e9eb; }
.actions button {
margin-right: 8px; padding: 0; background: none; border: none; cursor: pointer;
color: var(--el-color-primary); font-size: 14px; transition: .3s;
}
.actions button:hover { color: #66b1ff; }
.actions button.text-danger { color: var(--el-color-danger); }
.actions button.text-danger:hover { color: #f78989; }
/* 弹窗 */
.modal-overlay {
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
background: rgba(0,0,0,0.5); display: none; justify-content: center; align-items: center;
z-index: 2000; backdrop-filter: blur(2px);
}
.modal-box {
background: white; border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
width: 420px; animation: dialog-fade-in .3s;
}
@keyframes dialog-fade-in {
from { transform: translateY(-20px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
.modal-header {
padding: 20px; font-size: 16px; color: var(--el-text-primary);
font-weight: 500; border-bottom: 1px solid var(--el-border-light);
}
.modal-body { padding: 20px; color: var(--el-text-regular); font-size: 14px; line-height: 1.6; }
.modal-footer {
padding: 15px 20px; text-align: right;
border-top: 1px solid var(--el-border-light);
}
.modal-btn {
border-radius: 4px; border: 1px solid var(--el-border-color); background: #fff;
color: var(--el-text-regular); min-width: 80px; height: 36px; line-height: 34px;
padding: 0 20px; cursor: pointer; font-size: 14px; transition: all .3s;
}
.modal-btn:hover { color: var(--el-color-primary); border-color: #c6e2ff; background-color: #ecf5ff; }
.modal-btn.primary { color: #fff; background-color: var(--el-color-primary); border-color: var(--el-color-primary); }
.modal-btn.primary:hover { background: #66b1ff; border-color: #66b1ff; }
/* 详情弹窗样式优化 */
.detail-modal-content { width: 700px; max-width: 90%; }
.detail-grid {
display: grid; grid-template-columns: 100px 1fr;
gap: 12px 20px; align-items: start;
}
.detail-label {
color: var(--el-text-secondary); font-size: 14px;
text-align: right; padding-top: 2px;
}
.detail-value {
word-break: break-all; line-height: 1.8;
color: var(--el-text-primary); font-size: 14px;
}
.comment-html {
background: linear-gradient(to bottom, #fafafa, #f5f7fa);
padding: 20px; border-radius: 6px;
border: 1px solid var(--el-border-light);
box-shadow: inset 0 1px 2px rgba(0,0,0,0.05);
}
.detail-divider {
grid-column: 1 / -1; height: 1px;
background: var(--el-border-light); margin: 8px 0;
}
/* 分页 - Element Pagination 风格 */
.pagination {
display: flex; justify-content: flex-end; align-items: center; gap: 8px;
margin-top: 20px; padding-top: 20px; border-top: 1px solid var(--el-border-light);
flex-wrap: wrap;
}
.page-info { color: var(--el-text-secondary); font-size: 14px; margin-right: 10px; }
.page-btn {
min-width: 32px; height: 32px; line-height: 30px; padding: 0 6px;
border: 1px solid var(--el-border-color); background: #fff; border-radius: 4px;
cursor: pointer; font-size: 14px; color: var(--el-text-regular); transition: all .3s;
}
.page-btn:hover:not(.disabled):not(.active) { color: var(--el-color-primary); border-color: var(--el-color-primary); }
.page-btn.active { background-color: var(--el-color-primary); border-color: var(--el-color-primary); color: #fff; }
.page-btn.disabled { cursor: not-allowed; color: var(--el-text-secondary); background: #f5f7fa; }
/* 暂无数据样式 */
.empty-state {
text-align: center !important;
padding: 40px 0 !important;
color: var(--el-text-secondary) !important;
font-size: 14px !important;
background-color: #fff !important;
}
/* ========== 移动端适配 ========== */
@media screen and (max-width: 768px) {
:root {
--el-aside-width: 0px;
}
/* 顶部 Header */
.header { padding: 0 15px; }
.header-brand { font-size: 16px; }
.header-right span { display: none; }
/* 左侧侧边栏 - 隐藏 */
.aside { display: none; }
/* 右侧 Main */
.main { margin-left: 0; padding: 10px; }
.main-card { padding: 15px; }
.page-title { font-size: 18px; margin-bottom: 15px; }
/* 移动端底部状态筛选 */
.mobile-status-bar {
display: flex !important;
gap: 8px; margin-bottom: 15px; overflow-x: auto;
padding-bottom: 5px;
}
.mobile-status-item {
flex-shrink: 0; padding: 6px 14px; border-radius: 20px;
border: 1px solid var(--el-border-color); background: #fff;
color: var(--el-text-regular); font-size: 13px; cursor: pointer;
transition: all .3s;
}
.mobile-status-item.active {
background: var(--el-color-primary); color: #fff; border-color: var(--el-color-primary);
}
/* 筛选区 */
.filters {
flex-direction: column; align-items: stretch; gap: 10px;
padding: 12px;
}
input[type="text"] { width: 100%; }
button.el-button { width: 100%; }
/* 表格横向滚动 */
.table-wrapper { overflow-x: auto; -webkit-overflow-scrolling: touch; }
table { min-width: 600px; }
th, td { padding: 10px 8px; font-size: 13px; }
th:first-child, td:first-child { padding-left: 8px; }
th:last-child, td:last-child { padding-right: 8px; }
/* 操作按钮调整 */
.actions button { font-size: 13px; margin-right: 6px; }
/* 分页 */
.pagination { justify-content: center; }
.page-info { width: 100%; text-align: center; margin-bottom: 8px; }
/* 弹窗 */
.modal-box { width: 90% !important; max-width: 400px; }
.detail-modal-content { width: 92% !important; }
.detail-grid { grid-template-columns: 70px 1fr; gap: 8px 12px; }
.detail-label { font-size: 13px; }
.detail-value { font-size: 13px; }
}
/* 小于 480px 的屏幕 */
@media screen and (max-width: 480px) {
.main { padding: 8px; }
.main-card { padding: 12px; border-radius: 8px; }
.page-title { font-size: 16px; }
th, td { padding: 8px 6px; font-size: 12px; }
.status-badge { padding: 0 6px; height: 20px; line-height: 20px; font-size: 11px; }
.actions button { font-size: 12px; }
.modal-header { padding: 15px; font-size: 15px; }
.modal-body { padding: 15px; }
.modal-footer { padding: 12px 15px; }
.modal-btn { min-width: 70px; height: 34px; line-height: 32px; padding: 0 15px; font-size: 13px; }
.detail-grid { grid-template-columns: 60px 1fr; gap: 6px 10px; }
}
</style>
</head>
<body>
<!-- 顶部 Header -->
<header class="header">
<div class="header-brand">评论后台管理系统</div>
<div class="header-right">
<span>欢迎admin</span>
<a href="/logout">退出登录</a>
</div>
</header>
<!-- 左侧 Aside -->
<aside class="aside">
<div class="aside-status-group">
<div class="aside-menu-item active" onclick="selectStatus(this, '')">全部</div>
<div class="aside-menu-item" onclick="selectStatus(this, 'approved')">通过</div>
<div class="aside-menu-item" onclick="selectStatus(this, 'spam')">垃圾</div>
<div class="aside-menu-item" onclick="selectStatus(this, 'deleted')">删除</div>
</div>
</aside>
<!-- 右侧 Main -->
<main class="main">
<div class="main-card">
<div class="page-title">评论列表</div>
<!-- 确认弹窗 -->
<div id="confirmModal" class="modal-overlay">
<div class="modal-box">
<div class="modal-header">提示</div>
<div class="modal-body" id="modalTitle"></div>
<div class="modal-footer">
<button class="modal-btn" onclick="closeModal(false)">取消</button>
<button class="modal-btn primary" onclick="closeModal(true)">确定</button>
</div>
</div>
</div>
<!-- 详情弹窗 -->
<div id="detailModal" class="modal-overlay" onclick="if(event.target === this) closeDetailModal()">
<div class="modal-box detail-modal-content">
<div class="modal-header">评论详情</div>
<div class="modal-body" id="detailContent"></div>
<div class="modal-footer">
<button class="modal-btn primary" onclick="closeDetailModal()">关闭</button>
</div>
</div>
</div>
<!-- 新增留言弹窗 -->
<div id="addCommentModal" class="modal-overlay" onclick="if(event.target === this) closeAddCommentModal()">
<div class="modal-box" style="width: 500px;">
<div class="modal-header">新增留言</div>
<div class="modal-body">
<div style="margin-bottom: 15px;">
<label style="display: block; margin-bottom: 5px; color: var(--el-text-secondary); font-size: 14px;">昵称:</label>
<input type="text" id="addNick" placeholder="请输入昵称" style="width: 100%; height: 36px; padding: 0 12px; border: 1px solid var(--el-border-color); border-radius: 4px; outline: none; box-sizing: border-box;">
</div>
<div style="margin-bottom: 15px;">
<label style="display: block; margin-bottom: 5px; color: var(--el-text-secondary); font-size: 14px;">邮箱:</label>
<input type="email" id="addMail" placeholder="请输入邮箱(选填)" style="width: 100%; height: 36px; padding: 0 12px; border: 1px solid var(--el-border-color); border-radius: 4px; outline: none; box-sizing: border-box;">
</div>
<div style="margin-bottom: 15px;">
<label style="display: block; margin-bottom: 5px; color: var(--el-text-secondary); font-size: 14px;">网站:</label>
<input type="url" id="addLink" placeholder="请输入网站链接(选填)" style="width: 100%; height: 36px; padding: 0 12px; border: 1px solid var(--el-border-color); border-radius: 4px; outline: none; box-sizing: border-box;">
</div>
<div style="margin-bottom: 15px;">
<label style="display: block; margin-bottom: 5px; color: var(--el-text-secondary); font-size: 14px;">评论内容:</label>
<textarea id="addComment" placeholder="请输入评论内容" rows="5" style="width: 100%; padding: 10px 12px; border: 1px solid var(--el-border-color); border-radius: 4px; outline: none; resize: vertical; font-family: inherit; font-size: 14px; box-sizing: border-box;"></textarea>
</div>
<div style="margin-bottom: 15px;">
<label style="display: block; margin-bottom: 5px; color: var(--el-text-secondary); font-size: 14px;">状态:</label>
<select id="addStatus" style="width: 100%; height: 36px; padding: 0 12px; border: 1px solid var(--el-border-color); border-radius: 4px; outline: none; background: #fff; box-sizing: border-box;">
<option value="approved">通过</option>
<option value="spam">垃圾</option>
</select>
</div>
</div>
<div class="modal-footer">
<button class="modal-btn" onclick="closeAddCommentModal()">取消</button>
<button class="modal-btn primary" onclick="submitNewComment()">提交</button>
</div>
</div>
</div>
<div class="filters">
<!-- 移动端状态筛选(仅在手机显示) -->
<div class="mobile-status-bar" style="display: none;">
<div class="mobile-status-item active" onclick="selectStatusMobile(this, '')">全部</div>
<div class="mobile-status-item" onclick="selectStatusMobile(this, 'approved')">通过</div>
<div class="mobile-status-item" onclick="selectStatusMobile(this, 'spam')">垃圾</div>
<div class="mobile-status-item" onclick="selectStatusMobile(this, 'deleted')">删除</div>
</div>
<input type="text" id="searchKeyword" placeholder="搜索昵称或评论内容...">
<button class="el-button el-button--primary" onclick="loadComments()">搜索</button>
<button class="el-button" id="addCommentBtn" onclick="showAddCommentModal()" style="margin-left: auto;">+ 新增留言</button>
</div>
<div class="table-wrapper">
<table>
<thead>
<tr>
<th>昵称</th>
<th>评论内容</th>
<th>浏览器/系统</th>
<th>时间</th>
<th>状态</th>
<th>操作</th>
</tr>
</thead>
<tbody id="commentTableBody">
<tr><td colspan="6" class="empty-state">加载中...</td></tr>
</tbody>
</table>
</div>
<!-- 分页 -->
<div class="pagination" id="pagination">
<span class="page-info"><strong id="totalCount">0</strong></span>
<button class="page-btn" id="prevPage" onclick="changePage(-1)">&lt;</button>
<div id="pageNumbers" style="display: inline-flex; gap: 4px;"></div>
<button class="page-btn" id="nextPage" onclick="changePage(1)">&gt;</button>
</div>
</div>
</main>
<script>
let modalResolve = null;
let currentStatusFilter = '';
let currentPage = 1;
let totalPages = 0;
let totalCount = 0;
function selectStatus(element, status) {
// 更新左侧菜单高亮
document.querySelectorAll('.aside-menu-item').forEach(el => el.classList.remove('active'));
if (element.classList.contains('aside-menu-item')) {
element.classList.add('active');
}
currentStatusFilter = status;
// 在“删除”状态下隐藏新增按钮
const addBtn = document.getElementById('addCommentBtn');
if (addBtn) {
addBtn.style.display = status === 'deleted' ? 'none' : 'inline-block';
}
loadComments(); // 点击状态后自动搜索
}
function selectStatusMobile(element, status) {
document.querySelectorAll('.mobile-status-item').forEach(el => el.classList.remove('active'));
element.classList.add('active');
currentStatusFilter = status;
loadComments();
}
function showConfirm(message) {
return new Promise((resolve) => {
document.getElementById('modalTitle').textContent = message;
document.getElementById('confirmModal').style.display = 'flex';
modalResolve = resolve;
});
}
function closeModal(result) {
document.getElementById('confirmModal').style.display = 'none';
if (modalResolve) modalResolve(result);
}
function showDetail(comment) {
const date = new Date(comment.time).toLocaleString('zh-CN', {
year: 'numeric', month: '2-digit', day: '2-digit',
hour: '2-digit', minute: '2-digit', second: '2-digit'
});
const content = `
<div class="detail-grid">
<div class="detail-label">ID</div>
<div class="detail-value">${comment.id}</div>
<div class="detail-label">昵称:</div>
<div class="detail-value">${comment.nick || '匿名'}</div>
<div class="detail-label">邮箱:</div>
<div class="detail-value">${comment.mail || '-'}</div>
<div class="detail-label">网站:</div>
<div class="detail-value">${comment.link ? `<a href="${comment.link}" target="_blank" style="color: var(--el-color-primary); text-decoration: none;">${comment.link}</a>` : '-'}</div>
<div class="detail-label">浏览器:</div>
<div class="detail-value">${comment.browser || '-'} / ${comment.os || '-'}</div>
<div class="detail-divider"></div>
<div class="detail-label">评论内容:</div>
<div class="detail-value comment-html">${comment.comment}</div>
<div class="detail-divider"></div>
<div class="detail-label">发布时间:</div>
<div class="detail-value">${date}</div>
<div class="detail-label">User Agent</div>
<div class="detail-value" style="font-size: 12px; color: var(--el-text-secondary); font-family: monospace;">${comment.ua || '-'}</div>
</div>
`;
document.getElementById('detailContent').innerHTML = content;
document.getElementById('detailModal').style.display = 'flex';
}
function closeDetailModal() {
document.getElementById('detailModal').style.display = 'none';
}
function showAddCommentModal() {
// 清空表单
document.getElementById('addNick').value = '';
document.getElementById('addMail').value = '';
document.getElementById('addLink').value = '';
document.getElementById('addComment').value = '';
document.getElementById('addStatus').value = 'approved';
document.getElementById('addCommentModal').style.display = 'flex';
}
function closeAddCommentModal() {
document.getElementById('addCommentModal').style.display = 'none';
}
function submitNewComment() {
const nick = document.getElementById('addNick').value.trim();
const mail = document.getElementById('addMail').value.trim();
const link = document.getElementById('addLink').value.trim();
const comment = document.getElementById('addComment').value.trim();
const status = document.getElementById('addStatus').value;
if (!comment) {
showConfirm('请输入评论内容').then(() => {});
return;
}
const data = {
nick: nick || '匿名',
mail: mail,
link: link,
comment: comment,
ua: navigator.userAgent,
status: status
};
fetch('/api/comment', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
})
.then(res => res.json())
.then(result => {
if (result.errno === 0) {
closeAddCommentModal();
loadComments();
} else {
showConfirm('添加失败: ' + result.errmsg).then(() => {});
}
})
.catch(err => {
showConfirm('网络错误,请重试').then(() => {});
});
}
function selectStatusMobile(element, status) {
document.querySelectorAll('.mobile-status-item').forEach(el => el.classList.remove('active'));
element.classList.add('active');
currentStatusFilter = status;
loadComments();
}
function changePage(delta) {
const newPage = currentPage + delta;
if (newPage >= 1 && newPage <= totalPages) {
loadComments(newPage);
}
}
function goToPage(page) {
if (page >= 1 && page <= totalPages && page !== currentPage) {
loadComments(page);
}
}
function renderPagination(total, page, pages) {
totalCount = total;
totalPages = pages;
document.getElementById('totalCount').textContent = total;
const paginationDiv = document.getElementById('pagination');
const prevBtn = document.getElementById('prevPage');
const nextBtn = document.getElementById('nextPage');
const pageNumbers = document.getElementById('pageNumbers');
// 更新上一页/下一页按钮状态
prevBtn.classList.toggle('disabled', page <= 1);
nextBtn.classList.toggle('disabled', page >= pages);
// 生成页码按钮
pageNumbers.innerHTML = '';
if (pages <= 1 && total === 0) {
// 如果没有数据,完全隐藏分页器
paginationDiv.style.display = 'none';
} else if (pages <= 1) {
// 如果只有一页但有数据,只显示总数,隐藏翻页按钮
prevBtn.style.display = 'none';
nextBtn.style.display = 'none';
pageNumbers.style.display = 'none';
paginationDiv.style.display = 'flex';
} else {
// 多页时显示完整分页器
prevBtn.style.display = 'inline-block';
nextBtn.style.display = 'inline-block';
pageNumbers.style.display = 'inline-flex';
paginationDiv.style.display = 'flex';
// 显示最多 5 个页码
let start = Math.max(1, page - 2);
let end = Math.min(pages, start + 4);
if (end - start < 4) {
start = Math.max(1, end - 4);
}
for (let i = start; i <= end; i++) {
const btn = document.createElement('button');
btn.className = `page-btn ${i === page ? 'active' : ''}`;
btn.textContent = i;
btn.onclick = () => goToPage(i);
pageNumbers.appendChild(btn);
}
}
}
function loadComments(page = 1) {
const keyword = document.getElementById('searchKeyword').value;
currentPage = page;
fetch(`/api/admin/comments?keyword=${encodeURIComponent(keyword)}&status=${currentStatusFilter}&page=${page}&pageSize=10`)
.then(res => res.json())
.then(result => {
if (result.errno === 0) {
renderTable(result.data.list);
renderPagination(result.data.total, result.data.page, result.data.totalPages);
} else {
showConfirm('加载失败: ' + result.errmsg).then(() => {});
}
})
.catch(err => console.error('Error:', err));
}
function renderTable(comments) {
const tbody = document.getElementById('commentTableBody');
if (!comments || comments.length === 0) {
tbody.innerHTML = '<tr><td colspan="6" class="empty-state">暂无数据</td></tr>';
return;
}
let html = '';
comments.forEach(c => {
const date = new Date(c.time).toLocaleString();
let statusClass = 'status-approved';
let statusText = '通过';
if (c.status === 'spam') { statusClass = 'status-spam'; statusText = '垃圾'; }
if (c.status === 'deleted') { statusClass = 'status-deleted'; statusText = '删除'; }
let actionButtons = '';
if (currentStatusFilter === 'deleted') {
actionButtons = `
<button onclick="updateStatus(${c.id}, 'approved')">通过</button>
<button onclick="showDetail(${JSON.stringify(c).replace(/"/g, '&quot;')})">详情</button>
`;
} else if (currentStatusFilter === 'spam') {
actionButtons = `
<button onclick="updateStatus(${c.id}, 'approved')">通过</button>
<button class="text-danger" onclick="updateStatus(${c.id}, 'deleted')">删除</button>
<button onclick="showDetail(${JSON.stringify(c).replace(/"/g, '&quot;')})">详情</button>
`;
} else {
actionButtons = `
<button onclick="updateStatus(${c.id}, 'approved')">通过</button>
<button class="text-danger" onclick="updateStatus(${c.id}, 'spam')">垃圾</button>
<button class="text-danger" onclick="updateStatus(${c.id}, 'deleted')">删除</button>
<button onclick="showDetail(${JSON.stringify(c).replace(/"/g, '&quot;')})">详情</button>
`;
}
html += `<tr>
<td>${c.nick || '匿名'}</td>
<td style="max-width: 300px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">${c.comment.replace(/<[^>]*>/g, '')}</td>
<td>${c.browser} / ${c.os}</td>
<td>${date}</td>
<td><span class="status-badge ${statusClass}">${statusText}</span></td>
<td class="actions">
${actionButtons}
</td>
</tr>`;
});
tbody.innerHTML = html;
}
async function updateStatus(id, status) {
const confirmed = await showConfirm(`确定要将此评论标记为 "${status}" 吗?`);
if (!confirmed) return;
fetch(`/api/admin/comment/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ status: status })
})
.then(res => res.json())
.then(result => {
if (result.errno === 0) {
loadComments(); // 重新加载列表
} else {
showConfirm('更新失败: ' + result.errmsg).then(() => {});
}
});
}
// 初始加载
loadComments();
</script>
</body>
</html>

View File

@@ -0,0 +1,58 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>管理员登录</title>
<style>
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif; background-color: #f0f2f5; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; }
.login-box { background: white; padding: 40px; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.1); width: 320px; text-align: center; }
h2 { margin-top: 0; color: #333; }
input { width: 100%; padding: 10px; margin: 10px 0; border: 1px solid #ddd; border-radius: 4px; box-sizing: border-box; }
button { width: 100%; padding: 10px; background-color: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 16px; margin-top: 10px; }
button:hover { background-color: #0056b3; }
.error { color: red; font-size: 14px; margin-top: 10px; display: none; }
</style>
</head>
<body>
<div class="login-box">
<h2>后台管理登录</h2>
<input type="text" id="username" placeholder="用户名">
<input type="password" id="password" placeholder="密码">
<button onclick="handleLogin()">登录</button>
<div id="errorMsg" class="error"></div>
</div>
<script>
function handleLogin() {
const username = document.getElementById('username').value;
const password = document.getElementById('password').value;
const errorDiv = document.getElementById('errorMsg');
fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
})
.then(res => res.json())
.then(result => {
if (result.errno === 0) {
window.location.href = '/admin';
} else {
errorDiv.textContent = result.errmsg;
errorDiv.style.display = 'block';
}
})
.catch(err => {
errorDiv.textContent = '网络错误,请重试';
errorDiv.style.display = 'block';
});
}
// 允许按回车键登录
document.addEventListener('keypress', function(e) {
if (e.key === 'Enter') handleLogin();
});
</script>
</body>
</html>