mobile . themes

This commit is contained in:
2026-03-05 22:03:30 +08:00
parent c325007897
commit 2d3163a1a0
15 changed files with 236 additions and 471 deletions

View File

@@ -28,7 +28,9 @@
"Bash(git push)",
"Bash(git commit:*)",
"Bash(git push:*)",
"Bash(sqlite3:*)"
"Bash(sqlite3:*)",
"Bash(source venv/Scripts/activate)",
"Bash(venv/Scripts/python.exe:*)"
]
}
}

View File

@@ -1,40 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
添加 exam.visibility 字段的数据库迁移脚本
"""
from app import app, db
from models import Exam
def add_visibility_column():
"""为 Exam 表添加 visibility 字段"""
with app.app_context():
try:
# 检查字段是否已存在
inspector = db.inspect(db.engine)
columns = [col['name'] for col in inspector.get_columns('exam')]
if 'visibility' in columns:
print("visibility 字段已存在,无需添加")
return
# 添加字段
with db.engine.connect() as conn:
conn.execute(db.text("ALTER TABLE exam ADD COLUMN visibility VARCHAR(20) DEFAULT 'private'"))
conn.commit()
print("成功添加 visibility 字段")
# 将所有现有考试设置为公开(保持向后兼容)
conn.execute(db.text("UPDATE exam SET visibility = 'public' WHERE visibility IS NULL"))
conn.commit()
print("已将现有考试设置为公开")
except Exception as e:
print(f"添加字段时出错: {e}")
raise
if __name__ == '__main__':
print("开始添加 visibility 字段...")
add_visibility_column()
print("完成!")

38
app.py
View File

@@ -4130,6 +4130,24 @@ def api_remove_chat_member(room_id, uid):
socketio.emit('room_updated', {'room_id': room_id}, room=f'room_{room_id}')
return jsonify({'success': True})
@app.route('/api/chat/rooms/<int:room_id>/dissolve', methods=['POST'])
@login_required
def api_dissolve_room(room_id):
"""解散群聊(仅群主)"""
user_id = session['user']['id']
room = ChatRoom.query.get_or_404(room_id)
if room.type == 'private':
return jsonify({'success': False, 'message': '不能解散私聊'}), 400
if room.creator_id != user_id:
return jsonify({'success': False, 'message': '只有群主才能解散群聊'}), 403
# 删除所有成员、消息、群聊
ChatRoomMember.query.filter_by(room_id=room_id).delete()
Message.query.filter_by(room_id=room_id).delete()
db.session.delete(room)
db.session.commit()
socketio.emit('room_dissolved', {'room_id': room_id}, room=f'room_{room_id}')
return jsonify({'success': True, 'message': '群聊已解散'})
@app.route('/api/chat/rooms/<int:room_id>/read', methods=['POST'])
@login_required
def api_mark_chat_read(room_id):
@@ -4920,10 +4938,10 @@ def admin_export_users():
users = User.query.order_by(User.created_at.desc()).all()
output = export_users_to_excel(users)
filename = f"用户数据_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx"
filename = f"用户数据_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
return send_file(
output,
mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
mimetype='text/csv; charset=utf-8',
as_attachment=True,
download_name=filename
)
@@ -4938,10 +4956,10 @@ def admin_export_exams():
exams = Exam.query.order_by(Exam.created_at.desc()).all()
output = export_exams_to_excel(exams)
filename = f"考试数据_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx"
filename = f"考试数据_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
return send_file(
output,
mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
mimetype='text/csv; charset=utf-8',
as_attachment=True,
download_name=filename
)
@@ -4956,10 +4974,10 @@ def admin_export_submissions():
submissions = Submission.query.order_by(Submission.submitted_at.desc()).all()
output = export_submissions_to_excel(submissions)
filename = f"提交记录_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx"
filename = f"提交记录_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
return send_file(
output,
mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
mimetype='text/csv; charset=utf-8',
as_attachment=True,
download_name=filename
)
@@ -4974,10 +4992,10 @@ def admin_export_contests():
contests = Contest.query.order_by(Contest.created_at.desc()).all()
output = export_contests_to_excel(contests)
filename = f"杯赛数据_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx"
filename = f"杯赛数据_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
return send_file(
output,
mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
mimetype='text/csv; charset=utf-8',
as_attachment=True,
download_name=filename
)
@@ -4992,10 +5010,10 @@ def admin_export_posts():
posts = Post.query.order_by(Post.created_at.desc()).all()
output = export_posts_to_excel(posts)
filename = f"论坛帖子_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx"
filename = f"论坛帖子_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
return send_file(
output,
mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
mimetype='text/csv; charset=utf-8',
as_attachment=True,
download_name=filename
)

View File

@@ -1,50 +0,0 @@
"""
检查教师申请数据的完整性
"""
import sqlite3
db_path = 'instance/database.db'
conn = sqlite3.connect(db_path)
cursor = conn.cursor()
print("检查教师申请数据完整性...\n")
# 获取所有待审批的申请
cursor.execute("""
SELECT id, user_id, contest_id, name, email, status
FROM teacher_application
WHERE status = 'pending'
""")
apps = cursor.fetchall()
if not apps:
print("没有待审批的教师申请")
else:
print(f"找到 {len(apps)} 个待审批的申请\n")
for app in apps:
app_id, user_id, contest_id, name, email, status = app
print(f"申请 ID: {app_id}")
print(f" 用户ID: {user_id}, 杯赛ID: {contest_id}")
print(f" 姓名: {name}, 邮箱: {email}")
# 检查用户是否存在
cursor.execute("SELECT id, name FROM user WHERE id = ?", (user_id,))
user = cursor.fetchone()
if user:
print(f" [OK] 用户存在: {user[1]}")
else:
print(f" [ERROR] 用户不存在!")
# 检查杯赛是否存在
cursor.execute("SELECT id, name FROM contest WHERE id = ?", (contest_id,))
contest = cursor.fetchone()
if contest:
print(f" [OK] 杯赛存在: {contest[1]}")
else:
print(f" [ERROR] 杯赛不存在!")
print()
conn.close()
print("检查完成")

View File

@@ -1,55 +0,0 @@
# -*- coding: utf-8 -*-
import sqlite3
import sys
# 设置输出编码为UTF-8
sys.stdout.reconfigure(encoding='utf-8')
db_path = 'instance/database.db'
try:
conn = sqlite3.connect(db_path)
cursor = conn.cursor()
# 检查数据库编码
cursor.execute('PRAGMA encoding')
encoding = cursor.fetchone()[0]
print(f"数据库编码: {encoding}")
print("-" * 50)
# 获取所有表
cursor.execute("SELECT name FROM sqlite_master WHERE type='table'")
tables = [row[0] for row in cursor.fetchall()]
print(f"\n数据表列表 (共{len(tables)}个):")
for table in tables:
print(f" - {table}")
print("\n" + "=" * 50)
# 检查每个表的数据
for table in tables:
cursor.execute(f"SELECT COUNT(*) FROM {table}")
count = cursor.fetchone()[0]
print(f"\n'{table}': {count} 条记录")
# 显示表结构
cursor.execute(f"PRAGMA table_info({table})")
columns = cursor.fetchall()
print(f" 字段: {', '.join([col[1] for col in columns])}")
# 显示前3条数据示例
if count > 0:
cursor.execute(f"SELECT * FROM {table} LIMIT 3")
rows = cursor.fetchall()
print(f" 示例数据 (前{len(rows)}条):")
for i, row in enumerate(rows, 1):
print(f" 记录{i}: {row[:5]}...") # 只显示前5个字段
conn.close()
print("\n" + "=" * 50)
print("数据库检查完成!")
except Exception as e:
print(f"错误: {e}")
import traceback
traceback.print_exc()

View File

@@ -1,49 +0,0 @@
import os
from app import app, db
from models import (
Contest, ContestMembership, ContestApplication, ContestRegistration,
Exam, Submission, Draft, ExamBookmark,
Post, Reply, Poll, Reaction, Bookmark, Report,
QuestionBankItem, Notification, SystemNotification,
TeacherApplication, InviteCode, ChatRoom, ChatRoomMember, Message
)
def clean_test_data():
with app.app_context():
# 删除所有跟杯赛相关的记录
ContestMembership.query.delete()
ContestApplication.query.delete()
ContestRegistration.query.delete()
QuestionBankItem.query.delete()
TeacherApplication.query.delete()
InviteCode.query.delete()
Contest.query.delete()
# 删除所有跟考试相关的记录
Submission.query.delete()
Draft.query.delete()
ExamBookmark.query.delete()
Exam.query.delete()
# 删除所有跟帖子相关的记录
Reply.query.delete()
Poll.query.delete()
Reaction.query.delete()
Bookmark.query.delete()
Report.query.delete()
Post.query.delete()
# 删除聊天系统相关的记录(因为有自动为杯赛建群的功能,顺便清一下比较干净)
Message.query.delete()
ChatRoomMember.query.delete()
ChatRoom.query.delete()
# 也可以清理系统通知、普通通知
Notification.query.delete()
SystemNotification.query.delete()
db.session.commit()
print("所有测试的杯赛、考试、帖子及相关数据已成功删除!")
if __name__ == '__main__':
clean_test_data()

View File

@@ -1,45 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""创建管理员账号脚本"""
from app import app, db
from models import User
def create_admin():
with app.app_context():
# 检查邮箱是否已存在
existing_user = User.query.filter_by(email='1512344589@qq.com').first()
if existing_user:
print(f'邮箱 1512344589@qq.com 已存在')
print(f'用户名: {existing_user.name}')
print(f'角色: {existing_user.role}')
# 更新为管理员
if existing_user.role != 'admin':
existing_user.role = 'admin'
existing_user.password = '1'
db.session.commit()
print('已将该用户更新为管理员')
else:
print('该用户已经是管理员')
return
# 创建新管理员
admin = User(
name='管理员',
email='1512344589@qq.com',
password='1',
role='admin'
)
db.session.add(admin)
db.session.commit()
print('管理员账号创建成功!')
print(f'邮箱: {admin.email}')
print(f'密码: 1')
print(f'角色: {admin.role}')
print(f'用户ID: {admin.id}')
if __name__ == '__main__':
create_admin()

View File

@@ -1,217 +1,78 @@
# excel_export.py
from openpyxl import Workbook
from openpyxl.styles import Font, Alignment, PatternFill, Border, Side
from datetime import datetime
from io import BytesIO
import csv
from io import StringIO, BytesIO
def _to_csv(headers, rows):
"""生成 UTF-8 BOM 编码的 CSV兼容 VSCode 和 Excel"""
buf = StringIO()
writer = csv.writer(buf)
writer.writerow(headers)
writer.writerows(rows)
output = BytesIO()
output.write(b'\xef\xbb\xbf') # UTF-8 BOM
output.write(buf.getvalue().encode('utf-8'))
output.seek(0)
return output
def export_users_to_excel(users):
"""导出用户数据到Excel"""
wb = Workbook()
ws = wb.active
ws.title = "用户数据"
# 设置表头样式
header_fill = PatternFill(start_color="4472C4", end_color="4472C4", fill_type="solid")
header_font = Font(bold=True, color="FFFFFF", size=12)
header_alignment = Alignment(horizontal="center", vertical="center")
# 表头
headers = ["ID", "用户名", "邮箱", "手机号", "角色", "状态", "完成考试数", "注册时间"]
for col, header in enumerate(headers, 1):
cell = ws.cell(row=1, column=col, value=header)
cell.fill = header_fill
cell.font = header_font
cell.alignment = header_alignment
rows = []
for u in users:
rows.append([
u.id, u.name, u.email or "", u.phone or "", u.role,
"已封禁" if u.is_banned else "正常", u.completed_exams,
u.created_at.strftime("%Y-%m-%d %H:%M:%S") if u.created_at else ""
])
return _to_csv(headers, rows)
# 数据行
for row, user in enumerate(users, 2):
ws.cell(row=row, column=1, value=user.id)
ws.cell(row=row, column=2, value=user.name)
ws.cell(row=row, column=3, value=user.email or "")
ws.cell(row=row, column=4, value=user.phone or "")
ws.cell(row=row, column=5, value=user.role)
ws.cell(row=row, column=6, value="已封禁" if user.is_banned else "正常")
ws.cell(row=row, column=7, value=user.completed_exams)
ws.cell(row=row, column=8, value=user.created_at.strftime("%Y-%m-%d %H:%M:%S") if user.created_at else "")
# 调整列宽
ws.column_dimensions['A'].width = 8
ws.column_dimensions['B'].width = 20
ws.column_dimensions['C'].width = 30
ws.column_dimensions['D'].width = 15
ws.column_dimensions['E'].width = 12
ws.column_dimensions['F'].width = 10
ws.column_dimensions['G'].width = 12
ws.column_dimensions['H'].width = 20
# 保存到内存
output = BytesIO()
wb.save(output)
output.seek(0)
return output
def export_exams_to_excel(exams):
"""导出考试数据到Excel"""
wb = Workbook()
ws = wb.active
ws.title = "考试数据"
header_fill = PatternFill(start_color="4472C4", end_color="4472C4", fill_type="solid")
header_font = Font(bold=True, color="FFFFFF", size=12)
header_alignment = Alignment(horizontal="center", vertical="center")
headers = ["ID", "考试名称", "创建者ID", "总分", "时长(分钟)", "状态", "提交数", "创建时间"]
for col, header in enumerate(headers, 1):
cell = ws.cell(row=1, column=col, value=header)
cell.fill = header_fill
cell.font = header_font
cell.alignment = header_alignment
rows = []
for e in exams:
rows.append([
e.id, e.title, e.creator_id, e.total_score, e.duration, e.status,
len(e.submissions) if e.submissions else 0,
e.created_at.strftime("%Y-%m-%d %H:%M:%S") if e.created_at else ""
])
return _to_csv(headers, rows)
for row, exam in enumerate(exams, 2):
ws.cell(row=row, column=1, value=exam.id)
ws.cell(row=row, column=2, value=exam.title)
ws.cell(row=row, column=3, value=exam.creator_id)
ws.cell(row=row, column=4, value=exam.total_score)
ws.cell(row=row, column=5, value=exam.duration)
ws.cell(row=row, column=6, value=exam.status)
ws.cell(row=row, column=7, value=len(exam.submissions) if exam.submissions else 0)
ws.cell(row=row, column=8, value=exam.created_at.strftime("%Y-%m-%d %H:%M:%S") if exam.created_at else "")
ws.column_dimensions['A'].width = 8
ws.column_dimensions['B'].width = 30
ws.column_dimensions['C'].width = 12
ws.column_dimensions['D'].width = 10
ws.column_dimensions['E'].width = 15
ws.column_dimensions['F'].width = 12
ws.column_dimensions['G'].width = 10
ws.column_dimensions['H'].width = 20
output = BytesIO()
wb.save(output)
output.seek(0)
return output
def export_submissions_to_excel(submissions):
"""导出提交记录到Excel"""
wb = Workbook()
ws = wb.active
ws.title = "提交记录"
header_fill = PatternFill(start_color="4472C4", end_color="4472C4", fill_type="solid")
header_font = Font(bold=True, color="FFFFFF", size=12)
header_alignment = Alignment(horizontal="center", vertical="center")
headers = ["ID", "考试ID", "考试名称", "用户ID", "用户名", "得分", "总分", "状态", "提交时间"]
for col, header in enumerate(headers, 1):
cell = ws.cell(row=1, column=col, value=header)
cell.fill = header_fill
cell.font = header_font
cell.alignment = header_alignment
rows = []
for s in submissions:
rows.append([
s.id, s.exam_id, s.exam.title if s.exam else "", s.user_id,
s.user.name if s.user else "", s.score if s.score is not None else "",
s.total_score, s.status,
s.submitted_at.strftime("%Y-%m-%d %H:%M:%S") if s.submitted_at else ""
])
return _to_csv(headers, rows)
for row, sub in enumerate(submissions, 2):
ws.cell(row=row, column=1, value=sub.id)
ws.cell(row=row, column=2, value=sub.exam_id)
ws.cell(row=row, column=3, value=sub.exam.title if sub.exam else "")
ws.cell(row=row, column=4, value=sub.user_id)
ws.cell(row=row, column=5, value=sub.user.name if sub.user else "")
ws.cell(row=row, column=6, value=sub.score if sub.score is not None else "")
ws.cell(row=row, column=7, value=sub.total_score)
ws.cell(row=row, column=8, value=sub.status)
ws.cell(row=row, column=9, value=sub.submitted_at.strftime("%Y-%m-%d %H:%M:%S") if sub.submitted_at else "")
ws.column_dimensions['A'].width = 8
ws.column_dimensions['B'].width = 10
ws.column_dimensions['C'].width = 30
ws.column_dimensions['D'].width = 10
ws.column_dimensions['E'].width = 20
ws.column_dimensions['F'].width = 10
ws.column_dimensions['G'].width = 10
ws.column_dimensions['H'].width = 12
ws.column_dimensions['I'].width = 20
output = BytesIO()
wb.save(output)
output.seek(0)
return output
def export_contests_to_excel(contests):
"""导出杯赛数据到Excel"""
wb = Workbook()
ws = wb.active
ws.title = "杯赛数据"
header_fill = PatternFill(start_color="4472C4", end_color="4472C4", fill_type="solid")
header_font = Font(bold=True, color="FFFFFF", size=12)
header_alignment = Alignment(horizontal="center", vertical="center")
headers = ["ID", "杯赛名称", "主办方", "开始日期", "结束日期", "状态", "参与人数", "创建时间"]
for col, header in enumerate(headers, 1):
cell = ws.cell(row=1, column=col, value=header)
cell.fill = header_fill
cell.font = header_font
cell.alignment = header_alignment
rows = []
for c in contests:
rows.append([
c.id, c.name, c.organizer or "", c.start_date or "", c.end_date or "",
c.status, c.participants,
c.created_at.strftime("%Y-%m-%d %H:%M:%S") if c.created_at else ""
])
return _to_csv(headers, rows)
for row, contest in enumerate(contests, 2):
ws.cell(row=row, column=1, value=contest.id)
ws.cell(row=row, column=2, value=contest.name)
ws.cell(row=row, column=3, value=contest.organizer or "")
ws.cell(row=row, column=4, value=contest.start_date or "")
ws.cell(row=row, column=5, value=contest.end_date or "")
ws.cell(row=row, column=6, value=contest.status)
ws.cell(row=row, column=7, value=contest.participants)
ws.cell(row=row, column=8, value=contest.created_at.strftime("%Y-%m-%d %H:%M:%S") if contest.created_at else "")
ws.column_dimensions['A'].width = 8
ws.column_dimensions['B'].width = 30
ws.column_dimensions['C'].width = 20
ws.column_dimensions['D'].width = 15
ws.column_dimensions['E'].width = 15
ws.column_dimensions['F'].width = 12
ws.column_dimensions['G'].width = 12
ws.column_dimensions['H'].width = 20
output = BytesIO()
wb.save(output)
output.seek(0)
return output
def export_posts_to_excel(posts):
"""导出论坛帖子到Excel"""
wb = Workbook()
ws = wb.active
ws.title = "论坛帖子"
header_fill = PatternFill(start_color="4472C4", end_color="4472C4", fill_type="solid")
header_font = Font(bold=True, color="FFFFFF", size=12)
header_alignment = Alignment(horizontal="center", vertical="center")
headers = ["ID", "标题", "作者", "分类", "浏览数", "回复数", "点赞数", "创建时间"]
for col, header in enumerate(headers, 1):
cell = ws.cell(row=1, column=col, value=header)
cell.fill = header_fill
cell.font = header_font
cell.alignment = header_alignment
for row, post in enumerate(posts, 2):
ws.cell(row=row, column=1, value=post.id)
ws.cell(row=row, column=2, value=post.title)
ws.cell(row=row, column=3, value=post.author.name if post.author else "")
ws.cell(row=row, column=4, value=post.category or "")
ws.cell(row=row, column=5, value=post.views)
ws.cell(row=row, column=6, value=post.reply_count)
ws.cell(row=row, column=7, value=post.like_count)
ws.cell(row=row, column=8, value=post.created_at.strftime("%Y-%m-%d %H:%M:%S") if post.created_at else "")
ws.column_dimensions['A'].width = 8
ws.column_dimensions['B'].width = 40
ws.column_dimensions['C'].width = 20
ws.column_dimensions['D'].width = 15
ws.column_dimensions['E'].width = 10
ws.column_dimensions['F'].width = 10
ws.column_dimensions['G'].width = 10
ws.column_dimensions['H'].width = 20
output = BytesIO()
wb.save(output)
output.seek(0)
return output
rows = []
for p in posts:
rows.append([
p.id, p.title, p.author.name if p.author else "", p.category or "",
p.views, p.reply_count, p.like_count,
p.created_at.strftime("%Y-%m-%d %H:%M:%S") if p.created_at else ""
])
return _to_csv(headers, rows)

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

View File

@@ -187,7 +187,7 @@
<div id="friendListForGroup" class="max-h-48 overflow-y-auto space-y-1"></div>
</div>
<div class="px-4 py-3 border-t border-white/10">
<button onclick="createGroup()" class="btn-futuristic w-full px-4 py-2 text-sm">创建</button>
<button id="groupActionBtn" onclick="createGroup()" class="btn-futuristic w-full px-4 py-2 text-sm">创建</button>
</div>
</div>
</div>
@@ -203,8 +203,9 @@
</div>
</div>
<div id="membersList" class="p-4 overflow-y-auto max-h-96 space-y-2"></div>
<div id="inviteSection" class="px-4 py-3 border-t border-white/10 hidden">
<div id="inviteSection" class="px-4 py-3 border-t border-white/10 hidden space-y-2">
<button onclick="showInvite()" class="btn-futuristic w-full px-4 py-2 text-sm">邀请好友</button>
<button id="dissolveBtn" onclick="dissolveGroup()" class="w-full px-4 py-2 text-sm rounded-lg bg-red-500/20 text-red-400 hover:bg-red-500/30 border border-red-500/30 transition-colors hidden">解散群聊</button>
</div>
</div>
</div>
@@ -943,7 +944,7 @@ async function showCreateGroup() {
modal.classList.add('flex');
// 重置弹窗标题和按钮(防止被邀请好友功能覆盖)
document.querySelector('#createGroupModal h3').textContent = '创建群聊';
const createBtn = document.querySelector('#createGroupModal .bg-primary');
const createBtn = document.getElementById('groupActionBtn');
createBtn.textContent = '创建';
createBtn.onclick = createGroup;
document.getElementById('groupName').value = '';
@@ -1036,6 +1037,7 @@ async function showMembers() {
</div>`;
}).join('');
document.getElementById('inviteSection').classList.toggle('hidden', !isGroup);
document.getElementById('dissolveBtn').classList.toggle('hidden', !isCreator);
}
function hideMembers() {
@@ -1044,11 +1046,27 @@ function hideMembers() {
modal.classList.remove('flex');
}
async function dissolveGroup() {
if (!confirm('确定要解散该群聊?所有聊天记录将被删除,此操作不可撤销。')) return;
const res = await fetch(`/api/chat/rooms/${currentRoomId}/dissolve`, {method:'POST'});
const data = await res.json();
if (data.success) {
hideMembers();
currentRoomId = null;
document.getElementById('chat-header-info').innerHTML = '';
document.getElementById('messages').innerHTML = '';
document.getElementById('chat-input-area').classList.add('hidden');
await loadRooms();
} else {
alert(data.message || '操作失败');
}
}
async function showInvite() {
hideMembers();
showCreateGroup();
document.querySelector('#createGroupModal h3').textContent = '邀请好友';
const btn = document.querySelector('#createGroupModal .bg-primary');
const btn = document.getElementById('groupActionBtn');
btn.textContent = '邀请';
btn.onclick = async () => {
const ids = [...document.querySelectorAll('.friend-check:checked')].map(c => parseInt(c.value));

View File

@@ -491,7 +491,7 @@ function renderPosts(posts) {
<div class="mt-auto flex flex-col sm:flex-row sm:items-center justify-between gap-3 pt-4 border-t border-slate-100/60">
<div class="flex items-center gap-3">
<span class="text-sm font-bold text-slate-800 hover:text-indigo-600 transition-colors cursor-pointer" onclick="event.stopPropagation();showProfile('${p.author_id}')">${esc(p.author)}</span>
${!p.is_official ? `<span class="text-[10px] text-slate-400 flex items-center gap-1 bg-slate-50 px-2 py-0.5 rounded-md border border-slate-100"><svg class="w-3 h-3 text-amber-400" fill="currentColor" viewBox="0 0 20 20"><path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"/></svg>Lv.${randomLv}</span>` : ''}
${!p.is_official ? `<span class="text-[10px] text-slate-400 flex items-center gap-1 bg-slate-50 px-2 py-0.5 rounded-md border border-slate-100"><svg class="w-3 h-3 text-amber-400" fill="currentColor" viewBox="0 0 20 20"><path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"/></svg>Lv.${p.author_level || 1}</span>` : ''}
<span class="text-xs text-slate-400 bg-slate-50 px-2 py-1 rounded-md">${p.created_at}</span>
</div>

View File

@@ -2,15 +2,16 @@
{% block title %}登录 - 智联青云{% endblock %}
{% block navbar %}{% endblock %}
{% block content %}
<div class="min-h-screen bg-slate-50 flex flex-col justify-center py-12 sm:px-6 lg:px-8">
<canvas id="meteor-canvas" style="position:fixed;top:0;left:0;width:100%;height:100%;z-index:0;pointer-events:none;"></canvas>
<div class="min-h-screen flex flex-col justify-center py-12 sm:px-6 lg:px-8 relative" style="z-index:1;">
<div class="sm:mx-auto sm:w-full sm:max-w-md">
<h2 class="mt-6 text-center text-3xl font-extrabold bg-gradient-to-r from-blue-600 via-purple-600 to-pink-600 bg-clip-text text-transparent animate-pulse">登录您的账户</h2>
<p class="mt-2 text-center text-sm text-slate-600">
<p class="mt-2 text-center text-sm text-slate-300">
或者 <a href="/register" class="font-medium text-primary hover:text-blue-500 transition-all duration-300 hover:scale-110 inline-block">注册新账户</a>
</p>
</div>
<div class="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
<div class="futuristic-card py-8 px-4 sm:px-10">
<div class="futuristic-card py-8 px-4 sm:px-10" style="background:rgba(255,255,255,0.92);backdrop-filter:blur(12px);">
<div class="flex border-b border-slate-200 mb-6">
<button id="tab-phone" onclick="switchTab('phone')" class="flex-1 pb-4 text-sm font-medium text-center text-primary border-b-2 border-primary transition-all duration-300 hover:scale-105">手机验证码登录</button>
<button id="tab-email" onclick="switchTab('email')" class="flex-1 pb-4 text-sm font-medium text-center text-slate-500 hover:text-slate-700 transition-all duration-300 hover:scale-105">邮箱密码登录</button>
@@ -143,5 +144,109 @@ async function handleEmailLogin(e) {
}
fetchCaptcha('phone');
// 流星粒子背景
(function() {
const canvas = document.getElementById('meteor-canvas');
const ctx = canvas.getContext('2d');
let W, H;
window._mouseX = -9999;
window._mouseY = -9999;
document.addEventListener('mousemove', function(e) { window._mouseX = e.clientX; window._mouseY = e.clientY; });
document.addEventListener('mouseleave', function() { window._mouseX = -9999; window._mouseY = -9999; });
function resize() {
W = canvas.width = window.innerWidth;
H = canvas.height = window.innerHeight;
}
resize();
window.addEventListener('resize', resize);
const COUNT = 120;
const MOUSE_R = 150;
const REPEL = 8;
const particles = [];
function rand(a, b) { return Math.random() * (b - a) + a; }
function spawn(warm) {
const angle = rand(Math.PI * 0.6, Math.PI * 0.8);
const speed = rand(2, 5);
const p = {
x: rand(0, W * 1.2),
y: warm ? rand(-H * 0.3, H) : rand(-H * 0.3, 0),
vx: Math.cos(angle) * speed,
vy: Math.sin(angle) * speed,
len: rand(15, 40),
size: rand(0.8, 2),
alpha: rand(0.3, 0.9),
hue: rand(190, 260),
life: 0,
maxLife: rand(120, 300)
};
if (warm) p.life = rand(0, p.maxLife);
return p;
}
for (let i = 0; i < COUNT; i++) particles.push(spawn(true));
function loop() {
ctx.clearRect(0, 0, W, H);
const bg = ctx.createRadialGradient(W/2, H/2, 0, W/2, H/2, Math.max(W, H) * 0.7);
bg.addColorStop(0, '#0f172a');
bg.addColorStop(1, '#020617');
ctx.fillStyle = bg;
ctx.fillRect(0, 0, W, H);
const mx = window._mouseX, my = window._mouseY;
for (let i = 0; i < particles.length; i++) {
const p = particles[i];
p.life++;
const dx = p.x - mx, dy = p.y - my;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist < MOUSE_R && dist > 0) {
const f = (1 - dist / MOUSE_R) * REPEL;
p.x += (dx / dist) * f;
p.y += (dy / dist) * f;
}
p.x += p.vx;
p.y += p.vy;
let a = p.alpha;
if (p.life < 20) a *= p.life / 20;
if (p.life > p.maxLife - 30) a *= (p.maxLife - p.life) / 30;
const spd = Math.sqrt(p.vx * p.vx + p.vy * p.vy);
const tx = p.x - (p.vx / spd) * p.len * 0.6;
const ty = p.y - (p.vy / spd) * p.len * 0.6;
const g = ctx.createLinearGradient(tx, ty, p.x, p.y);
g.addColorStop(0, `hsla(${p.hue},80%,70%,0)`);
g.addColorStop(1, `hsla(${p.hue},80%,70%,${a})`);
ctx.beginPath();
ctx.moveTo(tx, ty);
ctx.lineTo(p.x, p.y);
ctx.strokeStyle = g;
ctx.lineWidth = p.size;
ctx.lineCap = 'round';
ctx.stroke();
ctx.beginPath();
ctx.arc(p.x, p.y, p.size * 0.8, 0, Math.PI * 2);
ctx.fillStyle = `hsla(${p.hue},90%,85%,${a})`;
ctx.fill();
if (p.life >= p.maxLife || p.y > H + 50 || p.x < -100) {
particles[i] = spawn(false);
}
}
requestAnimationFrame(loop);
}
loop();
})();
</script>
{% endblock %}

View File

@@ -277,23 +277,23 @@ async function loadFriends() {
if (!data.success) throw new Error(data.message);
if (data.friends.length === 0) {
container.innerHTML = '<div class="col-span-2 text-center py-4 text-slate-400">暂无好友</div>';
return;
} else {
let html = '';
data.friends.forEach(f => {
html += `
<div class="flex items-center space-x-3 p-3 border border-slate-100 rounded-xl bg-white">
<div class="w-10 h-10 bg-slate-200 rounded-full flex items-center justify-center text-slate-600 text-sm font-bold overflow-hidden">
${f.avatar ? `<img src="${f.avatar}" class="w-full h-full object-cover">` : f.name.charAt(0).toUpperCase()}
</div>
<div class="flex-1 min-w-0">
<div class="text-sm font-medium text-slate-900 truncate">${f.name}</div>
<div class="text-xs text-slate-400">好友 · ${f.created_at}</div>
</div>
<a href="/chat?dm=${f.id}" class="btn-futuristic px-3 py-1.5 text-xs rounded-lg transition-colors font-medium">私聊</a>
</div>`;
});
container.innerHTML = html;
}
let html = '';
data.friends.forEach(f => {
html += `
<div class="flex items-center space-x-3 p-3 border border-slate-100 rounded-xl bg-white">
<div class="w-10 h-10 bg-slate-200 rounded-full flex items-center justify-center text-slate-600 text-sm font-bold overflow-hidden">
${f.avatar ? `<img src="${f.avatar}" class="w-full h-full object-cover">` : f.name.charAt(0).toUpperCase()}
</div>
<div class="flex-1 min-w-0">
<div class="text-sm font-medium text-slate-900 truncate">${f.name}</div>
<div class="text-xs text-slate-400">好友 · ${f.created_at}</div>
</div>
<a href="/chat?dm=${f.id}" class="btn-futuristic px-3 py-1.5 text-xs rounded-lg transition-colors font-medium">私聊</a>
</div>`;
});
container.innerHTML = html;
} catch (e) {
container.innerHTML = '<div class="col-span-2 text-center py-4 text-red-500">加载失败</div>';
}