generated from dellevin/template
feat: complete local version to overwrite remote
This commit is contained in:
BIN
www_comments/__pycache__/config.cpython-312.pyc
Normal file
BIN
www_comments/__pycache__/config.cpython-312.pyc
Normal file
Binary file not shown.
287
www_comments/app.py
Normal file
287
www_comments/app.py
Normal 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
18
www_comments/config.py
Normal 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
|
||||
296
www_comments/data/comments.json
Normal file
296
www_comments/data/comments.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
2
www_comments/requirements.txt
Normal file
2
www_comments/requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
flask==3.0.0
|
||||
flask-cors==4.0.0
|
||||
725
www_comments/templates/admin.html
Normal file
725
www_comments/templates/admin.html
Normal 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)"><</button>
|
||||
<div id="pageNumbers" style="display: inline-flex; gap: 4px;"></div>
|
||||
<button class="page-btn" id="nextPage" onclick="changePage(1)">></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, '"')})">详情</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, '"')})">详情</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, '"')})">详情</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>
|
||||
58
www_comments/templates/login.html
Normal file
58
www_comments/templates/login.html
Normal 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>
|
||||
Reference in New Issue
Block a user