diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000..26d3352
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,3 @@
+# Default ignored files
+/shelf/
+/workspace.xml
diff --git a/.idea/DaFuWeng.iml b/.idea/DaFuWeng.iml
new file mode 100644
index 0000000..d0876a7
--- /dev/null
+++ b/.idea/DaFuWeng.iml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml
new file mode 100644
index 0000000..105ce2d
--- /dev/null
+++ b/.idea/inspectionProfiles/profiles_settings.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 0000000..2a2c1b9
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 0000000..a31e05c
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..35eb1dd
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/__pycache__/draw.cpython-312.pyc b/__pycache__/draw.cpython-312.pyc
new file mode 100644
index 0000000..f317f4a
Binary files /dev/null and b/__pycache__/draw.cpython-312.pyc differ
diff --git a/__pycache__/game.cpython-312.pyc b/__pycache__/game.cpython-312.pyc
new file mode 100644
index 0000000..d6d1f27
Binary files /dev/null and b/__pycache__/game.cpython-312.pyc differ
diff --git a/__pycache__/player.cpython-312.pyc b/__pycache__/player.cpython-312.pyc
new file mode 100644
index 0000000..8509f18
Binary files /dev/null and b/__pycache__/player.cpython-312.pyc differ
diff --git a/app.py b/app.py
new file mode 100644
index 0000000..3ffd94f
--- /dev/null
+++ b/app.py
@@ -0,0 +1,11 @@
+from backend import create_app, db
+from backend.models import User, Log, Resource, LogEntry
+
+app = create_app()
+
+@app.shell_context_processor
+def make_shell_context():
+ return {'db': db, 'User': User, 'Log': Log, 'Resource': Resource, 'LogEntry': LogEntry}
+
+if __name__ == '__main__':
+ app.run(debug=True)
diff --git a/backend/__init__.py b/backend/__init__.py
new file mode 100644
index 0000000..7c75ad5
--- /dev/null
+++ b/backend/__init__.py
@@ -0,0 +1,79 @@
+import os
+from flask import Flask
+from flask_sqlalchemy import SQLAlchemy
+from flask_login import LoginManager
+from flask_mail import Mail
+from flask_jwt_extended import JWTManager
+from .config import Config
+from logstash_async.handler import AsynchronousLogstashHandler
+from logstash_async.formatter import FlaskLogstashFormatter
+import logging
+from logging.handlers import RotatingFileHandler
+
+# 创建扩展实例
+db = SQLAlchemy()
+login_manager = LoginManager()
+login_manager.login_view = 'auth.login'
+login_manager.login_message_category = 'info'
+mail = Mail()
+jwt = JWTManager()
+
+def create_app(config_class=Config):
+ app = Flask(__name__)
+ app.config.from_object(config_class)
+
+ # 确保上传文件夹存在
+ if not os.path.exists(app.config['UPLOAD_FOLDER']):
+ os.makedirs(app.config['UPLOAD_FOLDER'])
+
+ # 初始化扩展
+ db.init_app(app)
+ login_manager.init_app(app)
+ mail.init_app(app)
+ jwt.init_app(app)
+
+ # 配置日志
+ configure_logging(app)
+
+ # 注册蓝图
+ from backend.main import bp as main_bp
+ app.register_blueprint(main_bp)
+
+ from backend.auth import bp as auth_bp
+ app.register_blueprint(auth_bp, url_prefix='/auth')
+
+ from backend.logs import bp as logs_bp
+ app.register_blueprint(logs_bp, url_prefix='/logs')
+
+ from backend.resources import bp as resources_bp
+ app.register_blueprint(resources_bp, url_prefix='/resources')
+
+ return app
+
+def configure_logging(app):
+ # 设置日志级别
+ app.logger.setLevel(logging.INFO)
+
+ # 创建文件日志处理器
+ if not os.path.exists('logs'):
+ os.mkdir('logs')
+ file_handler = RotatingFileHandler('logs/dafuweng.log', maxBytes=10240, backupCount=10)
+ file_handler.setFormatter(logging.Formatter(
+ '%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]'
+ ))
+ file_handler.setLevel(logging.INFO)
+ app.logger.addHandler(file_handler)
+
+ # 添加Logstash日志处理器
+ if app.config['LOGSTASH_HOST'] and app.config['LOGSTASH_PORT']:
+ logstash_handler = AsynchronousLogstashHandler(
+ host=app.config['LOGSTASH_HOST'],
+ port=app.config['LOGSTASH_PORT'],
+ database_path=None,
+ )
+ logstash_formatter = FlaskLogstashFormatter()
+ logstash_handler.setFormatter(logstash_formatter)
+ logstash_handler.setLevel(logging.INFO)
+ app.logger.addHandler(logstash_handler)
+
+ app.logger.info('DaFuWeng application startup')
diff --git a/backend/auth.py b/backend/auth.py
new file mode 100644
index 0000000..dc9af0c
--- /dev/null
+++ b/backend/auth.py
@@ -0,0 +1,98 @@
+from flask import Blueprint, render_template, redirect, url_for, flash, request, jsonify
+from flask_login import login_user, logout_user, current_user, login_required
+from werkzeug.urls import url_parse
+from flask_mail import Message
+from flask_jwt_extended import create_access_token, create_refresh_token, jwt_required, get_jwt_identity
+from backend import db, mail
+from backend.models import User
+from backend.forms import LoginForm, RegistrationForm, ResetPasswordRequestForm, ResetPasswordForm
+import secrets
+import string
+
+# 创建认证蓝图
+bp = Blueprint('auth', __name__)
+
+def generate_secure_token(length=32):
+ """生成安全令牌"""
+ alphabet = string.ascii_letters + string.digits
+ return ''.join(secrets.choice(alphabet) for i in range(length))
+
+@bp.route('/login', methods=['GET', 'POST'])
+def login():
+ if current_user.is_authenticated:
+ return redirect(url_for('main.index'))
+ form = LoginForm()
+ if form.validate_on_submit():
+ user = User.query.filter_by(email=form.email.data).first()
+ if user is None or not user.check_password(form.password.data):
+ flash('无效的邮箱或密码', 'danger')
+ return redirect(url_for('auth.login'))
+ login_user(user, remember=form.remember_me.data)
+ next_page = request.args.get('next')
+ if not next_page or url_parse(next_page).netloc != '':
+ next_page = url_for('main.index')
+ return redirect(next_page)
+ return render_template('auth/login.html', title='登录', form=form)
+
+@bp.route('/logout')
+def logout():
+ logout_user()
+ return redirect(url_for('main.index'))
+
+@bp.route('/register', methods=['GET', 'POST'])
+def register():
+ if current_user.is_authenticated:
+ return redirect(url_for('main.index'))
+ form = RegistrationForm()
+ if form.validate_on_submit():
+ user = User(username=form.username.data, email=form.email.data)
+ user.set_password(form.password.data)
+ db.session.add(user)
+ db.session.commit()
+ flash('恭喜!您已成功注册', 'success')
+ return redirect(url_for('auth.login'))
+ return render_template('auth/register.html', title='注册', form=form)
+
+def send_reset_email(user):
+ """发送密码重置邮件"""
+ token = user.get_reset_token()
+ msg = Message('密码重置请求',
+ sender='noreply@example.com',
+ recipients=[user.email])
+ msg.body = f'''要重置您的密码,请访问以下链接:
+
+{url_for('auth.reset_password', token=token, _external=True)}
+
+如果您没有请求此重置,请忽略此邮件,您的密码将保持不变。
+'''
+ mail.send(msg)
+
+@bp.route('/reset_password_request', methods=['GET', 'POST'])
+def reset_password_request():
+ if current_user.is_authenticated:
+ return redirect(url_for('main.index'))
+ form = ResetPasswordRequestForm()
+ if form.validate_on_submit():
+ user = User.query.filter_by(email=form.email.data).first()
+ if user:
+ send_reset_email(user)
+ flash('检查您的邮箱以获取密码重置说明', 'info')
+ return redirect(url_for('auth.login'))
+ return render_template('auth/reset_password_request.html',
+ title='重置密码', form=form)
+
+@bp.route('/reset_password/', methods=['GET', 'POST'])
+def reset_password(token):
+ if current_user.is_authenticated:
+ return redirect(url_for('main.index'))
+ user = User.verify_reset_token(token)
+ if not user:
+ flash('无效或过期的令牌', 'warning')
+ return redirect(url_for('auth.reset_password_request'))
+ form = ResetPasswordForm()
+ if form.validate_on_submit():
+ user.set_password(form.password.data)
+ db.session.commit()
+ flash('您的密码已成功重置', 'success')
+ return redirect(url_for('auth.login'))
+ return render_template('auth/reset_password.html', form=form)
diff --git a/backend/config.py b/backend/config.py
new file mode 100644
index 0000000..0c6dab7
--- /dev/null
+++ b/backend/config.py
@@ -0,0 +1,20 @@
+import os
+from dotenv import load_dotenv
+
+load_dotenv()
+
+class Config:
+ SECRET_KEY = os.environ.get('SECRET_KEY') or 'you-will-never-guess'
+ SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or 'sqlite:///' + os.path.join(os.path.abspath(os.path.dirname(__file__)), '../app.db')
+ SQLALCHEMY_TRACK_MODIFICATIONS = False
+ UPLOAD_FOLDER = os.path.join(os.path.abspath(os.path.dirname(__file__)), '../uploads')
+ ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'obj', 'fbx', 'mtl'}
+ MAX_CONTENT_LENGTH = 16 * 1024 * 1024 # 16MB
+ MAIL_SERVER = os.environ.get('MAIL_SERVER')
+ MAIL_PORT = int(os.environ.get('MAIL_PORT') or 587)
+ MAIL_USE_TLS = os.environ.get('MAIL_USE_TLS') is not None
+ MAIL_USERNAME = os.environ.get('MAIL_USERNAME')
+ MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD')
+ ADMINS = ['your-email@example.com']
+ LOGSTASH_HOST = os.environ.get('LOGSTASH_HOST') or 'localhost'
+ LOGSTASH_PORT = int(os.environ.get('LOGSTASH_PORT') or 5000)
diff --git a/backend/forms.py b/backend/forms.py
new file mode 100644
index 0000000..8004ef0
--- /dev/null
+++ b/backend/forms.py
@@ -0,0 +1,51 @@
+from flask_wtf import FlaskForm
+from wtforms import StringField, PasswordField, BooleanField, SubmitField, TextAreaField, SelectField
+from wtforms.validators import DataRequired, ValidationError, Email, EqualTo, Length
+from backend.models import User
+
+class LoginForm(FlaskForm):
+ email = StringField('邮箱', validators=[DataRequired(), Email()])
+ password = PasswordField('密码', validators=[DataRequired()])
+ remember_me = BooleanField('记住我')
+ submit = SubmitField('登录')
+
+class RegistrationForm(FlaskForm):
+ username = StringField('用户名', validators=[DataRequired(), Length(min=2, max=64)])
+ email = StringField('邮箱', validators=[DataRequired(), Email()])
+ password = PasswordField('密码', validators=[DataRequired(), Length(min=6)])
+ password2 = PasswordField(
+ '确认密码', validators=[DataRequired(), EqualTo('password')])
+ submit = SubmitField('注册')
+
+ def validate_username(self, username):
+ user = User.query.filter_by(username=username.data).first()
+ if user is not None:
+ raise ValidationError('请使用其他用户名')
+
+ def validate_email(self, email):
+ user = User.query.filter_by(email=email.data).first()
+ if user is not None:
+ raise ValidationError('请使用其他邮箱地址')
+
+class ResetPasswordRequestForm(FlaskForm):
+ email = StringField('邮箱', validators=[DataRequired(), Email()])
+ submit = SubmitField('请求重置密码')
+
+class ResetPasswordForm(FlaskForm):
+ password = PasswordField('新密码', validators=[DataRequired(), Length(min=6)])
+ password2 = PasswordField(
+ '确认新密码', validators=[DataRequired(), EqualTo('password')])
+ submit = SubmitField('重置密码')
+
+class LogForm(FlaskForm):
+ title = StringField('标题', validators=[DataRequired(), Length(max=120)])
+ content = TextAreaField('内容', validators=[DataRequired()])
+ category = StringField('分类', validators=[Length(max=64)])
+ tags = StringField('标签', validators=[Length(max=128)])
+ submit = SubmitField('保存')
+
+class ResourceForm(FlaskForm):
+ file = StringField('文件', validators=[DataRequired()])
+ file_type = SelectField('类型', choices=[('skin', '皮肤'), ('model', '人物模型')], validators=[DataRequired()])
+ description = StringField('描述', validators=[Length(max=256)])
+ submit = SubmitField('上传')
diff --git a/backend/logs.py b/backend/logs.py
new file mode 100644
index 0000000..bc79aa1
--- /dev/null
+++ b/backend/logs.py
@@ -0,0 +1,124 @@
+from flask import Blueprint, render_template, redirect, url_for, flash, request, jsonify
+from flask_login import current_user, login_required
+from backend import db
+from backend.models import Log
+from backend.forms import LogForm
+from datetime import datetime
+
+# 创建日志蓝图
+bp = Blueprint('logs', __name__)
+
+@bp.route('/')
+@bp.route('/index')
+@login_required
+def index():
+ page = request.args.get('page', 1, type=int)
+ logs = Log.query.filter_by(user_id=current_user.id).order_by(Log.updated_at.desc()).paginate(
+ page=page, per_page=10, error_out=False)
+ next_url = url_for('logs.index', page=logs.next_num) if logs.has_next else None
+ prev_url = url_for('logs.index', page=logs.prev_num) if logs.has_prev else None
+ return render_template('logs/index.html', title='我的日志', logs=logs.items, next_url=next_url, prev_url=prev_url)
+
+@bp.route('/create', methods=['GET', 'POST'])
+@login_required
+def create():
+ form = LogForm()
+ if form.validate_on_submit():
+ log = Log(
+ title=form.title.data,
+ content=form.content.data,
+ category=form.category.data,
+ tags=form.tags.data,
+ author=current_user
+ )
+ db.session.add(log)
+ db.session.commit()
+ flash('日志已成功创建', 'success')
+ return redirect(url_for('logs.index'))
+ return render_template('logs/create.html', title='创建日志', form=form)
+
+@bp.route('/')
+@login_required
+def detail(log_id):
+ log = Log.query.get_or_404(log_id)
+ if log.author != current_user:
+ flash('您没有权限访问此日志', 'danger')
+ return redirect(url_for('logs.index'))
+ return render_template('logs/detail.html', title=log.title, log=log)
+
+@bp.route('//edit', methods=['GET', 'POST'])
+@login_required
+def edit(log_id):
+ log = Log.query.get_or_404(log_id)
+ if log.author != current_user:
+ flash('您没有权限编辑此日志', 'danger')
+ return redirect(url_for('logs.index'))
+ form = LogForm(obj=log)
+ if form.validate_on_submit():
+ log.title = form.title.data
+ log.content = form.content.data
+ log.category = form.category.data
+ log.tags = form.tags.data
+ db.session.commit()
+ flash('日志已成功更新', 'success')
+ return redirect(url_for('logs.detail', log_id=log.id))
+ return render_template('logs/edit.html', title='编辑日志', form=form, log=log)
+
+@bp.route('//delete', methods=['POST'])
+@login_required
+def delete(log_id):
+ log = Log.query.get_or_404(log_id)
+ if log.author != current_user:
+ flash('您没有权限删除此日志', 'danger')
+ return redirect(url_for('logs.index'))
+ db.session.delete(log)
+ db.session.commit()
+ flash('日志已成功删除', 'success')
+ return redirect(url_for('logs.index'))
+
+@bp.route('/search')
+@login_required
+def search():
+ query = request.args.get('q', '')
+ category = request.args.get('category', '')
+ start_date = request.args.get('start_date', '')
+ end_date = request.args.get('end_date', '')
+
+ logs_query = Log.query.filter_by(user_id=current_user.id)
+
+ if query:
+ logs_query = logs_query.filter(Log.title.ilike(f'%{query}%') | Log.content.ilike(f'%{query}%'))
+
+ if category:
+ logs_query = logs_query.filter(Log.category == category)
+
+ if start_date:
+ try:
+ start = datetime.strptime(start_date, '%Y-%m-%d')
+ logs_query = logs_query.filter(Log.created_at >= start)
+ except ValueError:
+ flash('无效的开始日期格式', 'warning')
+
+ if end_date:
+ try:
+ end = datetime.strptime(end_date, '%Y-%m-%d')
+ logs_query = logs_query.filter(Log.created_at <= end)
+ except ValueError:
+ flash('无效的结束日期格式', 'warning')
+
+ page = request.args.get('page', 1, type=int)
+ logs = logs_query.order_by(Log.updated_at.desc()).paginate(
+ page=page, per_page=10, error_out=False)
+
+ next_url = url_for('logs.search', q=query, category=category, start_date=start_date, end_date=end_date, page=logs.next_num) if logs.has_next else None
+ prev_url = url_for('logs.search', q=query, category=category, start_date=start_date, end_date=end_date, page=logs.prev_num) if logs.has_prev else None
+
+ return render_template('logs/search.html', title='搜索日志', logs=logs.items, next_url=next_url, prev_url=prev_url, query=query, category=category, start_date=start_date, end_date=end_date)
+
+@bp.route('/categories')
+@login_required
+def categories():
+ """获取所有分类"""
+ categories = Log.query.filter_by(user_id=current_user.id).with_entities(Log.category).distinct().all()
+ categories_list = [cat[0] for cat in categories if cat[0]] # 过滤掉空分类
+ return jsonify(categories_list)
diff --git a/backend/main.py b/backend/main.py
new file mode 100644
index 0000000..f2a799a
--- /dev/null
+++ b/backend/main.py
@@ -0,0 +1,44 @@
+from flask import Blueprint, render_template, jsonify
+from flask_login import current_user, login_required
+from backend import db
+from backend.models import User, Log, Resource
+
+# 创建主蓝图
+bp = Blueprint('main', __name__)
+
+@bp.route('/')
+def index():
+ if current_user.is_authenticated:
+ return render_template('main/dashboard.html', title='控制台')
+ return render_template('main/index.html', title='首页')
+
+@bp.route('/dashboard')
+@login_required
+def dashboard():
+ # 获取用户统计信息
+ logs_count = Log.query.filter_by(user_id=current_user.id).count()
+ resources_count = Resource.query.filter_by(user_id=current_user.id).count()
+ active_skin = Resource.query.filter_by(user_id=current_user.id, file_type='skin', is_active=True).first()
+ active_model = Resource.query.filter_by(user_id=current_user.id, file_type='model', is_active=True).first()
+
+ # 获取最近的日志
+ recent_logs = Log.query.filter_by(user_id=current_user.id).order_by(Log.created_at.desc()).limit(5).all()
+
+ return render_template('main/dashboard.html', title='控制台',
+ logs_count=logs_count,
+ resources_count=resources_count,
+ active_skin=active_skin,
+ active_model=active_model,
+ recent_logs=recent_logs)
+
+@bp.route('/api/statistics')
+@login_required
+def api_statistics():
+ """API端点:获取用户统计信息"""
+ logs_count = Log.query.filter_by(user_id=current_user.id).count()
+ resources_count = Resource.query.filter_by(user_id=current_user.id).count()
+
+ return jsonify({
+ 'logs_count': logs_count,
+ 'resources_count': resources_count
+ })
diff --git a/backend/models.py b/backend/models.py
new file mode 100644
index 0000000..bca2e87
--- /dev/null
+++ b/backend/models.py
@@ -0,0 +1,94 @@
+from datetime import datetime, timedelta
+from werkzeug.security import generate_password_hash, check_password_hash
+from flask_login import UserMixin
+from backend import db, login_manager
+import jwt
+from time import time
+
+@login_manager.user_loader
+def load_user(user_id):
+ return User.query.get(int(user_id))
+
+class User(UserMixin, db.Model):
+ id = db.Column(db.Integer, primary_key=True)
+ username = db.Column(db.String(64), index=True, unique=True)
+ email = db.Column(db.String(120), index=True, unique=True)
+ password_hash = db.Column(db.String(128))
+ created_at = db.Column(db.DateTime, default=datetime.utcnow)
+
+ # 关系定义
+ logs = db.relationship('Log', backref='author', lazy='dynamic')
+ resources = db.relationship('Resource', backref='owner', lazy='dynamic')
+
+ def __repr__(self):
+ return ''.format(self.username)
+
+ def set_password(self, password):
+ self.password_hash = generate_password_hash(password)
+
+ def check_password(self, password):
+ return check_password_hash(self.password_hash, password)
+
+ def get_reset_token(self, expires_in=600):
+ return jwt.encode(
+ {'reset_password': self.id, 'exp': time() + expires_in},
+ login_manager.app.config['SECRET_KEY'], algorithm='HS256')
+
+ @staticmethod
+ def verify_reset_token(token):
+ try:
+ id = jwt.decode(token, login_manager.app.config['SECRET_KEY'],
+ algorithms=['HS256'])['reset_password']
+ except:
+ return
+ return User.query.get(id)
+
+class Log(db.Model):
+ id = db.Column(db.Integer, primary_key=True)
+ title = db.Column(db.String(120))
+ content = db.Column(db.Text)
+ created_at = db.Column(db.DateTime, default=datetime.utcnow)
+ updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
+ user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
+ category = db.Column(db.String(64))
+ tags = db.Column(db.String(128)) # 以逗号分隔的标签列表
+
+ def __repr__(self):
+ return ''.format(self.title)
+
+class Resource(db.Model):
+ id = db.Column(db.Integer, primary_key=True)
+ filename = db.Column(db.String(128))
+ original_filename = db.Column(db.String(128))
+ file_type = db.Column(db.String(64)) # 'skin' 或 'model'
+ description = db.Column(db.String(256))
+ created_at = db.Column(db.DateTime, default=datetime.utcnow)
+ user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
+ is_active = db.Column(db.Boolean, default=True)
+
+ def __repr__(self):
+ return ''.format(self.filename)
+
+ def to_dict(self):
+ return {
+ 'id': self.id,
+ 'filename': self.filename,
+ 'original_filename': self.original_filename,
+ 'file_type': self.file_type,
+ 'description': self.description,
+ 'created_at': self.created_at.isoformat(),
+ 'is_active': self.is_active
+ }
+
+class LogEntry(db.Model):
+ id = db.Column(db.Integer, primary_key=True)
+ timestamp = db.Column(db.DateTime, default=datetime.utcnow)
+ level = db.Column(db.String(64))
+ message = db.Column(db.Text)
+ module = db.Column(db.String(128))
+ line_number = db.Column(db.Integer)
+ user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=True)
+ ip_address = db.Column(db.String(45))
+
+ def __repr__(self):
+ return ''.format(self.message[:50])
diff --git a/backend/resources.py b/backend/resources.py
new file mode 100644
index 0000000..913977f
--- /dev/null
+++ b/backend/resources.py
@@ -0,0 +1,174 @@
+import os
+import secrets
+from flask import Blueprint, render_template, redirect, url_for, flash, request, jsonify, send_from_directory
+from flask_login import current_user, login_required
+from werkzeug.utils import secure_filename
+from backend import db
+from backend.models import Resource
+from backend.forms import ResourceForm
+from backend.config import Config
+from PIL import Image
+
+# 创建资源管理蓝图
+bp = Blueprint('resources', __name__)
+
+def allowed_file(filename):
+ """检查文件扩展名是否被允许"""
+ return '.' in filename and filename.rsplit('.', 1)[1].lower() in Config.ALLOWED_EXTENSIONS
+
+def generate_unique_filename(filename):
+ """生成唯一文件名"""
+ filename = secure_filename(filename)
+ name, ext = os.path.splitext(filename)
+ unique_filename = f"{name}_{secrets.token_hex(8)}{ext}"
+ return unique_filename
+
+def save_file(file):
+ """保存文件到服务器"""
+ if not file or not allowed_file(file.filename):
+ raise ValueError('不允许的文件类型')
+
+ filename = generate_unique_filename(file.filename)
+ file_path = os.path.join(Config.UPLOAD_FOLDER, filename)
+
+ # 保存文件
+ file.save(file_path)
+
+ return filename
+
+@bp.route('/')
+@login_required
+def index():
+ page = request.args.get('page', 1, type=int)
+ resources = Resource.query.filter_by(user_id=current_user.id).order_by(Resource.created_at.desc()).paginate(
+ page=page, per_page=10, error_out=False)
+ next_url = url_for('resources.index', page=resources.next_num) if resources.has_next else None
+ prev_url = url_for('resources.index', page=resources.prev_num) if resources.has_prev else None
+ return render_template('resources/index.html', title='我的资源', resources=resources.items, next_url=next_url, prev_url=prev_url)
+
+@bp.route('/upload', methods=['GET', 'POST'])
+@login_required
+def upload():
+ form = ResourceForm()
+ if request.method == 'POST':
+ if 'file' not in request.files:
+ flash('没有选择文件', 'danger')
+ return redirect(request.url)
+
+ file = request.files['file']
+ if file.filename == '':
+ flash('没有选择文件', 'danger')
+ return redirect(request.url)
+
+ try:
+ filename = save_file(file)
+
+ # 创建资源记录
+ resource = Resource(
+ filename=filename,
+ original_filename=file.filename,
+ file_type=request.form['file_type'],
+ description=request.form.get('description', ''),
+ owner=current_user
+ )
+
+ db.session.add(resource)
+ db.session.commit()
+
+ flash('文件上传成功', 'success')
+ return redirect(url_for('resources.index'))
+ except Exception as e:
+ flash(f'文件上传失败: {str(e)}', 'danger')
+ return redirect(request.url)
+
+ return render_template('resources/upload.html', title='上传资源', form=form)
+
+@bp.route('/')
+@login_required
+def detail(resource_id):
+ resource = Resource.query.get_or_404(resource_id)
+ if resource.owner != current_user:
+ flash('您没有权限访问此资源', 'danger')
+ return redirect(url_for('resources.index'))
+ return render_template('resources/detail.html', title=resource.original_filename, resource=resource)
+
+@bp.route('//delete', methods=['POST'])
+@login_required
+def delete(resource_id):
+ resource = Resource.query.get_or_404(resource_id)
+ if resource.owner != current_user:
+ flash('您没有权限删除此资源', 'danger')
+ return redirect(url_for('resources.index'))
+
+ try:
+ # 删除文件
+ file_path = os.path.join(Config.UPLOAD_FOLDER, resource.filename)
+ if os.path.exists(file_path):
+ os.remove(file_path)
+
+ # 删除数据库记录
+ db.session.delete(resource)
+ db.session.commit()
+
+ flash('资源已成功删除', 'success')
+ except Exception as e:
+ flash(f'删除资源失败: {str(e)}', 'danger')
+
+ return redirect(url_for('resources.index'))
+
+@bp.route('//activate', methods=['POST'])
+@login_required
+def activate(resource_id):
+ resource = Resource.query.get_or_404(resource_id)
+ if resource.owner != current_user:
+ flash('您没有权限操作此资源', 'danger')
+ return redirect(url_for('resources.index'))
+
+ try:
+ # 取消所有同类型资源的激活状态
+ Resource.query.filter_by(user_id=current_user.id, file_type=resource.file_type, is_active=True).update({'is_active': False})
+
+ # 激活当前资源
+ resource.is_active = True
+ db.session.commit()
+
+ flash(f'{resource.original_filename}已设为当前使用的{"皮肤" if resource.file_type == "skin" else "人物模型"}', 'success')
+ except Exception as e:
+ flash(f'操作失败: {str(e)}', 'danger')
+
+ return redirect(url_for('resources.index'))
+
+@bp.route('/uploads/')
+@login_required
+def uploaded_file(filename):
+ """提供文件下载或预览"""
+ # 检查文件是否属于当前用户
+ resource = Resource.query.filter_by(filename=filename, user_id=current_user.id).first()
+ if not resource:
+ flash('您没有权限访问此文件', 'danger')
+ return redirect(url_for('resources.index'))
+
+ return send_from_directory(Config.UPLOAD_FOLDER, filename)
+
+@bp.route('/active')
+@login_required
+def active_resources():
+ """获取当前激活的资源"""
+ active_skin = Resource.query.filter_by(user_id=current_user.id, file_type='skin', is_active=True).first()
+ active_model = Resource.query.filter_by(user_id=current_user.id, file_type='model', is_active=True).first()
+
+ return jsonify({
+ 'skin': active_skin.to_dict() if active_skin else None,
+ 'model': active_model.to_dict() if active_model else None
+ })
+
+@bp.route('/types/')
+@login_required
+def resources_by_type(file_type):
+ """按类型获取资源列表"""
+ if file_type not in ['skin', 'model']:
+ flash('无效的资源类型', 'danger')
+ return redirect(url_for('resources.index'))
+
+ resources = Resource.query.filter_by(user_id=current_user.id, file_type=file_type).order_by(Resource.created_at.desc()).all()
+ return render_template('resources/type.html', title=f'{"皮肤" if file_type == "skin" else "人物模型"}列表', resources=resources, file_type=file_type)
diff --git a/frontend/login_page.py b/frontend/login_page.py
new file mode 100644
index 0000000..24cb154
--- /dev/null
+++ b/frontend/login_page.py
@@ -0,0 +1,260 @@
+from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel,
+ QLineEdit, QPushButton, QMessageBox, QFormLayout,
+ QSpacerItem, QSizePolicy)
+from PyQt5.QtGui import QFont, QIcon
+from PyQt5.QtCore import Qt, pyqtSignal
+from frontend.main_window import APIClient
+
+class LoginPage(QWidget):
+ # 定义登录成功信号
+ login_success_signal = pyqtSignal()
+
+ def __init__(self, parent=None):
+ super().__init__(parent)
+ self.api_client = APIClient()
+ self.initUI()
+
+ def initUI(self):
+ # 创建布局
+ main_layout = QVBoxLayout()
+ main_layout.setContentsMargins(50, 50, 50, 50)
+ main_layout.setSpacing(20)
+
+ # 添加标题
+ title_label = QLabel('登录')
+ title_label.setFont(QFont('Arial', 24, QFont.Bold))
+ title_label.setAlignment(Qt.AlignCenter)
+ main_layout.addWidget(title_label)
+
+ # 添加表单布局
+ form_layout = QFormLayout()
+ form_layout.setSpacing(15)
+
+ # 邮箱输入框
+ self.email_edit = QLineEdit()
+ self.email_edit.setPlaceholderText('请输入邮箱地址')
+ self.email_edit.setFont(QFont('Arial', 12))
+ self.email_edit.setFixedHeight(40)
+ form_layout.addRow('邮箱:', self.email_edit)
+
+ # 密码输入框
+ self.password_edit = QLineEdit()
+ self.password_edit.setPlaceholderText('请输入密码')
+ self.password_edit.setEchoMode(QLineEdit.Password)
+ self.password_edit.setFont(QFont('Arial', 12))
+ self.password_edit.setFixedHeight(40)
+ form_layout.addRow('密码:', self.password_edit)
+
+ main_layout.addLayout(form_layout)
+
+ # 添加登录按钮
+ self.login_button = QPushButton('登录')
+ self.login_button.setFont(QFont('Arial', 14, QFont.Bold))
+ self.login_button.setFixedHeight(45)
+ self.login_button.setStyleSheet('background-color: #4CAF50; color: white; border: none; border-radius: 5px;')
+ self.login_button.clicked.connect(self.handle_login)
+ main_layout.addWidget(self.login_button)
+
+ # 添加注册链接
+ register_layout = QHBoxLayout()
+ register_layout.setAlignment(Qt.AlignCenter)
+
+ register_label = QLabel('还没有账号?')
+ register_label.setFont(QFont('Arial', 12))
+
+ self.register_link = QPushButton('立即注册')
+ self.register_link.setFont(QFont('Arial', 12, QFont.Bold))
+ self.register_link.setStyleSheet('background-color: transparent; color: #2196F3; border: none;')
+ self.register_link.clicked.connect(self.handle_register_click)
+
+ register_layout.addWidget(register_label)
+ register_layout.addWidget(self.register_link)
+
+ main_layout.addLayout(register_layout)
+
+ # 添加忘记密码链接
+ forgot_password_layout = QHBoxLayout()
+ forgot_password_layout.setAlignment(Qt.AlignRight)
+
+ self.forgot_password_link = QPushButton('忘记密码?')
+ self.forgot_password_link.setFont(QFont('Arial', 12))
+ self.forgot_password_link.setStyleSheet('background-color: transparent; color: #757575; border: none;')
+ self.forgot_password_link.clicked.connect(self.handle_forgot_password)
+
+ forgot_password_layout.addWidget(self.forgot_password_link)
+
+ main_layout.addLayout(forgot_password_layout)
+
+ # 设置布局
+ self.setLayout(main_layout)
+
+ def handle_login(self):
+ email = self.email_edit.text().strip()
+ password = self.password_edit.text().strip()
+
+ # 基本验证
+ if not email or not password:
+ QMessageBox.warning(self, '输入错误', '请输入邮箱和密码')
+ return
+
+ if '@' not in email:
+ QMessageBox.warning(self, '输入错误', '请输入有效的邮箱地址')
+ return
+
+ # 尝试登录
+ if self.api_client.login(email, password):
+ QMessageBox.information(self, '登录成功', f'欢迎回来,{self.api_client.username}')
+ # 发送登录成功信号
+ self.login_success_signal.emit()
+ else:
+ QMessageBox.critical(self, '登录失败', '邮箱或密码错误')
+ self.password_edit.clear()
+
+ def handle_register_click(self):
+ # 通知父窗口显示注册页面
+ self.parent().show_register_page()
+
+ def handle_forgot_password(self):
+ # 实现忘记密码功能
+ email = self.email_edit.text().strip()
+
+ if not email:
+ QMessageBox.warning(self, '输入错误', '请输入您的邮箱地址')
+ return
+
+ if '@' not in email:
+ QMessageBox.warning(self, '输入错误', '请输入有效的邮箱地址')
+ return
+
+ # 这里可以添加发送密码重置邮件的逻辑
+ QMessageBox.information(self, '密码重置', f'密码重置邮件已发送到 {email}')
+
+class RegisterPage(QWidget):
+ def __init__(self, parent=None):
+ super().__init__(parent)
+ self.api_client = APIClient()
+ self.initUI()
+
+ def initUI(self):
+ # 创建布局
+ main_layout = QVBoxLayout()
+ main_layout.setContentsMargins(50, 50, 50, 50)
+ main_layout.setSpacing(20)
+
+ # 添加标题
+ title_label = QLabel('注册')
+ title_label.setFont(QFont('Arial', 24, QFont.Bold))
+ title_label.setAlignment(Qt.AlignCenter)
+ main_layout.addWidget(title_label)
+
+ # 添加表单布局
+ form_layout = QFormLayout()
+ form_layout.setSpacing(15)
+
+ # 用户名输入框
+ self.username_edit = QLineEdit()
+ self.username_edit.setPlaceholderText('请输入用户名')
+ self.username_edit.setFont(QFont('Arial', 12))
+ self.username_edit.setFixedHeight(40)
+ form_layout.addRow('用户名:', self.username_edit)
+
+ # 邮箱输入框
+ self.email_edit = QLineEdit()
+ self.email_edit.setPlaceholderText('请输入邮箱地址')
+ self.email_edit.setFont(QFont('Arial', 12))
+ self.email_edit.setFixedHeight(40)
+ form_layout.addRow('邮箱:', self.email_edit)
+
+ # 密码输入框
+ self.password_edit = QLineEdit()
+ self.password_edit.setPlaceholderText('请输入密码(至少6位)')
+ self.password_edit.setEchoMode(QLineEdit.Password)
+ self.password_edit.setFont(QFont('Arial', 12))
+ self.password_edit.setFixedHeight(40)
+ form_layout.addRow('密码:', self.password_edit)
+
+ # 确认密码输入框
+ self.confirm_password_edit = QLineEdit()
+ self.confirm_password_edit.setPlaceholderText('请确认密码')
+ self.confirm_password_edit.setEchoMode(QLineEdit.Password)
+ self.confirm_password_edit.setFont(QFont('Arial', 12))
+ self.confirm_password_edit.setFixedHeight(40)
+ form_layout.addRow('确认密码:', self.confirm_password_edit)
+
+ main_layout.addLayout(form_layout)
+
+ # 添加注册按钮
+ self.register_button = QPushButton('注册')
+ self.register_button.setFont(QFont('Arial', 14, QFont.Bold))
+ self.register_button.setFixedHeight(45)
+ self.register_button.setStyleSheet('background-color: #4CAF50; color: white; border: none; border-radius: 5px;')
+ self.register_button.clicked.connect(self.handle_register)
+ main_layout.addWidget(self.register_button)
+
+ # 添加登录链接
+ login_layout = QHBoxLayout()
+ login_layout.setAlignment(Qt.AlignCenter)
+
+ login_label = QLabel('已有账号?')
+ login_label.setFont(QFont('Arial', 12))
+
+ self.login_link = QPushButton('立即登录')
+ self.login_link.setFont(QFont('Arial', 12, QFont.Bold))
+ self.login_link.setStyleSheet('background-color: transparent; color: #2196F3; border: none;')
+ self.login_link.clicked.connect(self.handle_login_click)
+
+ login_layout.addWidget(login_label)
+ login_layout.addWidget(self.login_link)
+
+ main_layout.addLayout(login_layout)
+
+ # 设置布局
+ self.setLayout(main_layout)
+
+ def handle_register(self):
+ username = self.username_edit.text().strip()
+ email = self.email_edit.text().strip()
+ password = self.password_edit.text().strip()
+ confirm_password = self.confirm_password_edit.text().strip()
+
+ # 基本验证
+ if not username or not email or not password or not confirm_password:
+ QMessageBox.warning(self, '输入错误', '请填写所有必填字段')
+ return
+
+ if len(username) < 2 or len(username) > 64:
+ QMessageBox.warning(self, '输入错误', '用户名长度必须在2到64个字符之间')
+ return
+
+ if '@' not in email:
+ QMessageBox.warning(self, '输入错误', '请输入有效的邮箱地址')
+ return
+
+ if len(password) < 6:
+ QMessageBox.warning(self, '输入错误', '密码长度不能少于6位')
+ return
+
+ if password != confirm_password:
+ QMessageBox.warning(self, '输入错误', '两次输入的密码不一致')
+ self.confirm_password_edit.clear()
+ return
+
+ # 尝试注册
+ if self.api_client.register(username, email, password):
+ QMessageBox.information(self, '注册成功', '您已成功注册,请登录')
+ # 通知父窗口注册成功,切换到登录页面
+ self.parent().show_login_page()
+ # 清空表单
+ self.clear_form()
+ else:
+ QMessageBox.critical(self, '注册失败', '注册失败,请检查您的输入或网络连接')
+
+ def handle_login_click(self):
+ # 通知父窗口显示登录页面
+ self.parent().show_login_page()
+
+ def clear_form(self):
+ self.username_edit.clear()
+ self.email_edit.clear()
+ self.password_edit.clear()
+ self.confirm_password_edit.clear()
diff --git a/frontend/logs_page.py b/frontend/logs_page.py
new file mode 100644
index 0000000..1e0f959
--- /dev/null
+++ b/frontend/logs_page.py
@@ -0,0 +1,381 @@
+from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel,
+ QLineEdit, QPushButton, QMessageBox, QFormLayout,
+ QSpacerItem, QSizePolicy, QTableWidget, QTableWidgetItem,
+ QTextEdit, QDialog, QComboBox, QCalendarWidget, QSplitter,
+ QHeaderView)
+from PyQt5.QtGui import QFont, QIcon
+from PyQt5.QtCore import Qt, pyqtSignal, QDate
+import requests
+
+class LogsPage(QWidget):
+ def __init__(self, parent=None):
+ super().__init__(parent)
+ self.api_client = parent.login_page.api_client
+ self.initUI()
+ self.load_logs()
+
+ def initUI(self):
+ # 创建主布局
+ main_layout = QVBoxLayout()
+ main_layout.setContentsMargins(20, 20, 20, 20)
+ main_layout.setSpacing(20)
+
+ # 创建顶部布局(标题和操作按钮)
+ top_layout = QHBoxLayout()
+
+ # 标题
+ title_label = QLabel('日志管理')
+ title_label.setFont(QFont('Arial', 20, QFont.Bold))
+ top_layout.addWidget(title_label)
+
+ # 搜索框
+ self.search_edit = QLineEdit()
+ self.search_edit.setPlaceholderText('搜索日志...')
+ self.search_edit.setFont(QFont('Arial', 12))
+ self.search_edit.setFixedWidth(300)
+ self.search_edit.textChanged.connect(self.load_logs)
+ top_layout.addWidget(self.search_edit)
+
+ # 占位符
+ top_layout.addStretch()
+
+ # 创建日志按钮
+ self.create_log_button = QPushButton('创建日志')
+ self.create_log_button.setFont(QFont('Arial', 12))
+ self.create_log_button.setFixedHeight(35)
+ self.create_log_button.setStyleSheet('background-color: #4CAF50; color: white; border: none; border-radius: 5px;')
+ self.create_log_button.clicked.connect(self.show_create_log_dialog)
+ top_layout.addWidget(self.create_log_button)
+
+ main_layout.addLayout(top_layout)
+
+ # 创建日志列表
+ self.logs_table = QTableWidget()
+ self.logs_table.setColumnCount(4)
+ self.logs_table.setHorizontalHeaderLabels(['标题', '分类', '创建时间', '更新时间'])
+ self.logs_table.horizontalHeader().setStretchLastSection(True)
+ self.logs_table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
+ self.logs_table.setSelectionBehavior(QTableWidget.SelectRows)
+ self.logs_table.setEditTriggers(QTableWidget.NoEditTriggers)
+ self.logs_table.doubleClicked.connect(self.show_log_detail)
+ main_layout.addWidget(self.logs_table)
+
+ # 创建底部布局(操作按钮)
+ bottom_layout = QHBoxLayout()
+
+ # 编辑按钮
+ self.edit_button = QPushButton('编辑')
+ self.edit_button.setFont(QFont('Arial', 12))
+ self.edit_button.setFixedHeight(35)
+ self.edit_button.setStyleSheet('background-color: #2196F3; color: white; border: none; border-radius: 5px;')
+ self.edit_button.clicked.connect(self.show_edit_log_dialog)
+ bottom_layout.addWidget(self.edit_button)
+
+ # 删除按钮
+ self.delete_button = QPushButton('删除')
+ self.delete_button.setFont(QFont('Arial', 12))
+ self.delete_button.setFixedHeight(35)
+ self.delete_button.setStyleSheet('background-color: #F44336; color: white; border: none; border-radius: 5px;')
+ self.delete_button.clicked.connect(self.handle_delete_log)
+ bottom_layout.addWidget(self.delete_button)
+
+ # 刷新按钮
+ self.refresh_button = QPushButton('刷新')
+ self.refresh_button.setFont(QFont('Arial', 12))
+ self.refresh_button.setFixedHeight(35)
+ self.refresh_button.setStyleSheet('background-color: #9E9E9E; color: white; border: none; border-radius: 5px;')
+ self.refresh_button.clicked.connect(self.load_logs)
+ bottom_layout.addWidget(self.refresh_button)
+
+ main_layout.addLayout(bottom_layout)
+
+ # 设置布局
+ self.setLayout(main_layout)
+
+ def load_logs(self):
+ """加载日志列表"""
+ if not self.api_client.is_authenticated():
+ return
+
+ try:
+ search_query = self.search_edit.text().strip()
+ url = f'{self.api_client.base_url}/logs'
+
+ if search_query:
+ url = f'{url}?q={search_query}'
+
+ response = self.api_client.session.get(url)
+ response.raise_for_status()
+
+ logs = response.json()
+
+ # 更新表格
+ self.logs_table.setRowCount(len(logs))
+
+ for row, log in enumerate(logs):
+ title_item = QTableWidgetItem(log['title'])
+ category_item = QTableWidgetItem(log['category'] if log['category'] else '')
+ created_at_item = QTableWidgetItem(log['created_at'].split('T')[0])
+ updated_at_item = QTableWidgetItem(log['updated_at'].split('T')[0])
+
+ self.logs_table.setItem(row, 0, title_item)
+ self.logs_table.setItem(row, 1, category_item)
+ self.logs_table.setItem(row, 2, created_at_item)
+ self.logs_table.setItem(row, 3, updated_at_item)
+
+ # 存储日志ID
+ self.logs_table.setVerticalHeaderItem(row, QTableWidgetItem(str(log['id'])))
+
+ except requests.exceptions.RequestException as e:
+ QMessageBox.warning(self, '加载失败', f'日志加载失败: {e}')
+
+ def show_log_detail(self, index):
+ """显示日志详情"""
+ log_id = int(self.logs_table.verticalHeaderItem(index.row()).text())
+
+ try:
+ response = self.api_client.session.get(f'{self.api_client.base_url}/logs/{log_id}')
+ response.raise_for_status()
+
+ log = response.json()
+
+ # 创建详情对话框
+ detail_dialog = QDialog(self)
+ detail_dialog.setWindowTitle(log['title'])
+ detail_dialog.setGeometry(200, 200, 800, 600)
+
+ layout = QVBoxLayout()
+
+ # 标题
+ title_label = QLabel(log['title'])
+ title_label.setFont(QFont('Arial', 18, QFont.Bold))
+ layout.addWidget(title_label)
+
+ # 元数据
+ meta_layout = QHBoxLayout()
+
+ category_label = QLabel(f'分类: {log["category"] if log["category"] else "未分类"}')
+ category_label.setFont(QFont('Arial', 12))
+ meta_layout.addWidget(category_label)
+
+ created_at_label = QLabel(f'创建时间: {log["created_at"].split("T")[0]}')
+ created_at_label.setFont(QFont('Arial', 12))
+ meta_layout.addWidget(created_at_label)
+
+ updated_at_label = QLabel(f'更新时间: {log["updated_at"].split("T")[0]}')
+ updated_at_label.setFont(QFont('Arial', 12))
+ meta_layout.addWidget(updated_at_label)
+
+ meta_layout.addStretch()
+ layout.addLayout(meta_layout)
+
+ # 标签
+ if log['tags']:
+ tags_label = QLabel(f'标签: {log["tags"]}')
+ tags_label.setFont(QFont('Arial', 12))
+ layout.addWidget(tags_label)
+
+ # 内容
+ content_label = QLabel('内容:')
+ content_label.setFont(QFont('Arial', 14, QFont.Bold))
+ layout.addWidget(content_label)
+
+ content_text = QTextEdit()
+ content_text.setPlainText(log['content'])
+ content_text.setReadOnly(True)
+ content_text.setFont(QFont('Arial', 12))
+ layout.addWidget(content_text)
+
+ # 关闭按钮
+ close_button = QPushButton('关闭')
+ close_button.setFont(QFont('Arial', 12))
+ close_button.setFixedHeight(35)
+ close_button.clicked.connect(detail_dialog.close)
+ layout.addWidget(close_button)
+
+ detail_dialog.setLayout(layout)
+ detail_dialog.exec_()
+
+ except requests.exceptions.RequestException as e:
+ QMessageBox.warning(self, '加载失败', f'日志详情加载失败: {e}')
+
+ def show_create_log_dialog(self):
+ """显示创建日志对话框"""
+ dialog = LogFormDialog(self, self.api_client, mode='create')
+ if dialog.exec_():
+ self.load_logs()
+
+ def show_edit_log_dialog(self):
+ """显示编辑日志对话框"""
+ selected_row = self.logs_table.currentRow()
+ if selected_row == -1:
+ QMessageBox.warning(self, '操作提示', '请先选择一条日志')
+ return
+
+ log_id = int(self.logs_table.verticalHeaderItem(selected_row).text())
+
+ dialog = LogFormDialog(self, self.api_client, mode='edit', log_id=log_id)
+ if dialog.exec_():
+ self.load_logs()
+
+ def handle_delete_log(self):
+ """处理删除日志"""
+ selected_row = self.logs_table.currentRow()
+ if selected_row == -1:
+ QMessageBox.warning(self, '操作提示', '请先选择一条日志')
+ return
+
+ log_id = int(self.logs_table.verticalHeaderItem(selected_row).text())
+ log_title = self.logs_table.item(selected_row, 0).text()
+
+ reply = QMessageBox.question(self, '确认删除', f'确定要删除日志"{log_title}"吗?',
+ QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
+
+ if reply == QMessageBox.Yes:
+ try:
+ response = self.api_client.session.delete(f'{self.api_client.base_url}/logs/{log_id}')
+ response.raise_for_status()
+
+ QMessageBox.information(self, '删除成功', '日志已成功删除')
+ self.load_logs()
+
+ except requests.exceptions.RequestException as e:
+ QMessageBox.warning(self, '删除失败', f'日志删除失败: {e}')
+
+class LogFormDialog(QDialog):
+ def __init__(self, parent=None, api_client=None, mode='create', log_id=None):
+ super().__init__(parent)
+ self.api_client = api_client
+ self.mode = mode
+ self.log_id = log_id
+
+ if mode == 'create':
+ self.setWindowTitle('创建日志')
+ else:
+ self.setWindowTitle('编辑日志')
+ self.load_log_data()
+
+ self.initUI()
+
+ def initUI(self):
+ # 创建布局
+ main_layout = QVBoxLayout()
+ main_layout.setContentsMargins(30, 30, 30, 30)
+ main_layout.setSpacing(20)
+
+ # 创建表单布局
+ form_layout = QFormLayout()
+ form_layout.setSpacing(15)
+
+ # 标题输入框
+ self.title_edit = QLineEdit()
+ self.title_edit.setPlaceholderText('请输入日志标题')
+ self.title_edit.setFont(QFont('Arial', 12))
+ self.title_edit.setFixedHeight(40)
+ form_layout.addRow('标题:', self.title_edit)
+
+ # 分类输入框
+ self.category_edit = QLineEdit()
+ self.category_edit.setPlaceholderText('请输入日志分类')
+ self.category_edit.setFont(QFont('Arial', 12))
+ self.category_edit.setFixedHeight(40)
+ form_layout.addRow('分类:', self.category_edit)
+
+ # 标签输入框
+ self.tags_edit = QLineEdit()
+ self.tags_edit.setPlaceholderText('请输入标签,以逗号分隔')
+ self.tags_edit.setFont(QFont('Arial', 12))
+ self.tags_edit.setFixedHeight(40)
+ form_layout.addRow('标签:', self.tags_edit)
+
+ # 内容输入框
+ form_layout.addRow('内容:', self.create_content_editor())
+
+ main_layout.addLayout(form_layout)
+
+ # 创建按钮布局
+ button_layout = QHBoxLayout()
+
+ # 保存按钮
+ self.save_button = QPushButton('保存')
+ self.save_button.setFont(QFont('Arial', 12))
+ self.save_button.setFixedHeight(35)
+ self.save_button.setStyleSheet('background-color: #4CAF50; color: white; border: none; border-radius: 5px;')
+ self.save_button.clicked.connect(self.handle_save)
+ button_layout.addWidget(self.save_button)
+
+ # 取消按钮
+ cancel_button = QPushButton('取消')
+ cancel_button.setFont(QFont('Arial', 12))
+ cancel_button.setFixedHeight(35)
+ cancel_button.setStyleSheet('background-color: #9E9E9E; color: white; border: none; border-radius: 5px;')
+ cancel_button.clicked.connect(self.close)
+ button_layout.addWidget(cancel_button)
+
+ main_layout.addLayout(button_layout)
+
+ # 设置布局
+ self.setLayout(main_layout)
+
+ def create_content_editor(self):
+ """创建内容编辑器"""
+ self.content_edit = QTextEdit()
+ self.content_edit.setPlaceholderText('请输入日志内容')
+ self.content_edit.setFont(QFont('Arial', 12))
+ self.content_edit.setMinimumHeight(300)
+ return self.content_edit
+
+ def load_log_data(self):
+ """加载日志数据(编辑模式下)"""
+ try:
+ response = self.api_client.session.get(f'{self.api_client.base_url}/logs/{self.log_id}')
+ response.raise_for_status()
+
+ log = response.json()
+
+ self.title_edit.setText(log['title'])
+ self.category_edit.setText(log['category'] if log['category'] else '')
+ self.tags_edit.setText(log['tags'] if log['tags'] else '')
+ self.content_edit.setPlainText(log['content'])
+
+ except requests.exceptions.RequestException as e:
+ QMessageBox.warning(self, '加载失败', f'日志数据加载失败: {e}')
+ self.reject()
+
+ def handle_save(self):
+ """处理保存日志"""
+ title = self.title_edit.text().strip()
+ content = self.content_edit.toPlainText().strip()
+ category = self.category_edit.text().strip()
+ tags = self.tags_edit.text().strip()
+
+ # 基本验证
+ if not title or not content:
+ QMessageBox.warning(self, '输入错误', '请填写标题和内容')
+ return
+
+ if len(title) > 120:
+ QMessageBox.warning(self, '输入错误', '标题长度不能超过120个字符')
+ return
+
+ # 准备数据
+ data = {
+ 'title': title,
+ 'content': content,
+ 'category': category,
+ 'tags': tags
+ }
+
+ try:
+ if self.mode == 'create':
+ response = self.api_client.session.post(f'{self.api_client.base_url}/logs/create', json=data)
+ else:
+ response = self.api_client.session.put(f'{self.api_client.base_url}/logs/{self.log_id}/edit', json=data)
+
+ response.raise_for_status()
+
+ QMessageBox.information(self, '保存成功', '日志已成功保存')
+ self.accept()
+
+ except requests.exceptions.RequestException as e:
+ QMessageBox.warning(self, '保存失败', f'日志保存失败: {e}')
diff --git a/frontend/main_window.py b/frontend/main_window.py
new file mode 100644
index 0000000..6213d23
--- /dev/null
+++ b/frontend/main_window.py
@@ -0,0 +1,417 @@
+import sys
+from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
+ QHBoxLayout, QStackedWidget, QMenuBar, QMenu,
+ QAction, QToolBar, QStatusBar, QLabel, QPushButton,
+ QMessageBox)
+from PyQt5.QtGui import QIcon, QFont
+from PyQt5.QtCore import Qt, QThread, pyqtSignal
+import requests
+from frontend.login_page import LoginPage, RegisterPage
+from frontend.logs_page import LogsPage
+from frontend.resources_page import ResourcesPage
+
+class MainWindow(QMainWindow):
+ def __init__(self):
+ super().__init__()
+ self.initUI()
+
+ # 初始化API客户端
+ self.api_client = APIClient()
+
+ # 检查登录状态
+ self.check_login_status()
+
+ def initUI(self):
+ # 设置窗口基本属性
+ self.setWindowTitle('大富翁——清华之旅')
+ self.setGeometry(100, 100, 1080, 680)
+ self.setMinimumSize(800, 500)
+
+ # 创建菜单栏
+ self.create_menu_bar()
+
+ # 创建工具栏
+ self.create_tool_bar()
+
+ # 创建状态栏
+ self.create_status_bar()
+
+ # 创建中央部件和布局
+ self.central_widget = QStackedWidget()
+ self.setCentralWidget(self.central_widget)
+
+ # 创建各种页面
+ self.create_pages()
+
+ # 连接登录成功信号
+ self.login_page.login_success_signal.connect(self.on_login_success)
+
+ # 显示初始页面
+ self.show_login_page()
+
+ def create_menu_bar(self):
+ menubar = self.menuBar()
+
+ # 文件菜单
+ file_menu = menubar.addMenu('文件')
+
+ exit_action = QAction(QIcon('exit.png'), '退出', self)
+ exit_action.setShortcut('Ctrl+Q')
+ exit_action.setStatusTip('退出应用')
+ exit_action.triggered.connect(self.close)
+ file_menu.addAction(exit_action)
+
+ # 编辑菜单
+ edit_menu = menubar.addMenu('编辑')
+
+ # 视图菜单
+ view_menu = menubar.addMenu('视图')
+
+ # 帮助菜单
+ help_menu = menubar.addMenu('帮助')
+
+ def create_tool_bar(self):
+ toolbar = self.addToolBar('工具')
+
+ # 添加工具按钮
+ login_action = QAction(QIcon('login.png'), '登录', self)
+ login_action.setStatusTip('登录')
+ login_action.triggered.connect(self.show_login_page)
+ toolbar.addAction(login_action)
+
+ register_action = QAction(QIcon('register.png'), '注册', self)
+ register_action.setStatusTip('注册')
+ register_action.triggered.connect(self.show_register_page)
+ toolbar.addAction(register_action)
+
+ dashboard_action = QAction(QIcon('dashboard.png'), '控制台', self)
+ dashboard_action.setStatusTip('控制台')
+ dashboard_action.triggered.connect(self.show_dashboard_page)
+ toolbar.addAction(dashboard_action)
+
+ logs_action = QAction(QIcon('logs.png'), '日志', self)
+ logs_action.setStatusTip('日志管理')
+ logs_action.triggered.connect(self.show_logs_page)
+ toolbar.addAction(logs_action)
+
+ resources_action = QAction(QIcon('resources.png'), '资源', self)
+ resources_action.setStatusTip('资源管理')
+ resources_action.triggered.connect(self.show_resources_page)
+ toolbar.addAction(resources_action)
+
+ settings_action = QAction(QIcon('settings.png'), '设置', self)
+ settings_action.setStatusTip('设置')
+ settings_action.triggered.connect(self.show_settings_page)
+ toolbar.addAction(settings_action)
+
+ def create_status_bar(self):
+ self.status_bar = QStatusBar()
+ self.setStatusBar(self.status_bar)
+ self.status_bar.showMessage('欢迎使用大富翁——清华之旅')
+
+ def create_pages(self):
+ # 登录页面
+ self.login_page = LoginPage(self)
+ self.central_widget.addWidget(self.login_page)
+
+ # 注册页面
+ self.register_page = RegisterPage(self)
+ self.central_widget.addWidget(self.register_page)
+
+ # 控制台页面
+ self.dashboard_page = QWidget()
+ self.central_widget.addWidget(self.dashboard_page)
+
+ # 日志页面
+ self.logs_page = LogsPage(self)
+ self.central_widget.addWidget(self.logs_page)
+
+ # 资源页面
+ self.resources_page = ResourcesPage(self)
+ self.central_widget.addWidget(self.resources_page)
+
+ # 设置页面
+ self.settings_page = QWidget()
+ self.central_widget.addWidget(self.settings_page)
+
+ # 初始化页面内容
+ self.init_dashboard_page()
+
+ def show_login_page(self):
+ self.central_widget.setCurrentWidget(self.login_page)
+ self.status_bar.showMessage('登录')
+
+ def show_register_page(self):
+ self.central_widget.setCurrentWidget(self.register_page)
+ self.status_bar.showMessage('注册')
+
+ def on_login_success(self):
+ """处理登录成功事件"""
+ self.status_bar.showMessage(f'欢迎回来,{self.login_page.api_client.username}')
+ self.show_dashboard_page()
+
+ def login_success(self):
+ """供LoginPage调用的登录成功方法"""
+ # 这里可以添加更多登录成功后的处理
+ self.on_login_success()
+
+ def init_dashboard_page(self):
+ """初始化控制台页面"""
+ layout = QVBoxLayout()
+ layout.setContentsMargins(20, 20, 20, 20)
+
+ # 添加标题
+ title_label = QLabel('控制台')
+ title_label.setFont(QFont('Arial', 20, QFont.Bold))
+ title_label.setMargin(10)
+ layout.addWidget(title_label)
+
+ # 添加一些示例信息
+ info_layout = QHBoxLayout()
+ info_layout.setSpacing(20)
+
+ # 日志统计
+ logs_widget = QWidget()
+ logs_layout = QVBoxLayout(logs_widget)
+ logs_widget.setStyleSheet('background-color: #E3F2FD; border-radius: 10px; padding: 15px;')
+
+ logs_title = QLabel('我的日志')
+ logs_title.setFont(QFont('Arial', 14, QFont.Bold))
+ logs_layout.addWidget(logs_title)
+
+ self.logs_count_label = QLabel('0')
+ self.logs_count_label.setFont(QFont('Arial', 24, QFont.Bold))
+ self.logs_count_label.setAlignment(Qt.AlignCenter)
+ logs_layout.addWidget(self.logs_count_label)
+
+ info_layout.addWidget(logs_widget)
+
+ # 资源统计
+ resources_widget = QWidget()
+ resources_layout = QVBoxLayout(resources_widget)
+ resources_widget.setStyleSheet('background-color: #C8E6C9; border-radius: 10px; padding: 15px;')
+
+ resources_title = QLabel('我的资源')
+ resources_title.setFont(QFont('Arial', 14, QFont.Bold))
+ resources_layout.addWidget(resources_title)
+
+ self.resources_count_label = QLabel('0')
+ self.resources_count_label.setFont(QFont('Arial', 24, QFont.Bold))
+ self.resources_count_label.setAlignment(Qt.AlignCenter)
+ resources_layout.addWidget(self.resources_count_label)
+
+ info_layout.addWidget(resources_widget)
+
+ layout.addLayout(info_layout)
+
+ # 添加刷新按钮
+ refresh_button = QPushButton('刷新数据')
+ refresh_button.setFont(QFont('Arial', 12))
+ refresh_button.setFixedHeight(35)
+ refresh_button.clicked.connect(self.refresh_dashboard_data)
+ layout.addWidget(refresh_button)
+
+ # 添加占位符
+ layout.addStretch()
+
+ self.dashboard_page.setLayout(layout)
+
+ def refresh_dashboard_data(self):
+ """刷新控制台数据"""
+ if not self.login_page.api_client.is_authenticated():
+ return
+
+ try:
+ # 从API获取统计数据
+ response = self.login_page.api_client.session.get(f'{self.login_page.api_client.base_url}/api/statistics')
+ response.raise_for_status()
+
+ data = response.json()
+ self.logs_count_label.setText(str(data['logs_count']))
+ self.resources_count_label.setText(str(data['resources_count']))
+
+ self.status_bar.showMessage('数据刷新成功')
+ except requests.exceptions.RequestException as e:
+ QMessageBox.warning(self, '刷新失败', f'数据刷新失败: {e}')
+
+ def show_dashboard_page(self):
+ if not self.api_client.is_authenticated():
+ self.status_bar.showMessage('请先登录')
+ self.show_login_page()
+ return
+ self.central_widget.setCurrentWidget(self.dashboard_page)
+ self.status_bar.showMessage('控制台')
+
+ def show_logs_page(self):
+ if not self.api_client.is_authenticated():
+ self.status_bar.showMessage('请先登录')
+ self.show_login_page()
+ return
+ self.central_widget.setCurrentWidget(self.logs_page)
+ self.status_bar.showMessage('日志管理')
+
+ def show_resources_page(self):
+ if not self.api_client.is_authenticated():
+ self.status_bar.showMessage('请先登录')
+ self.show_login_page()
+ return
+ self.central_widget.setCurrentWidget(self.resources_page)
+ self.status_bar.showMessage('资源管理')
+
+ def show_settings_page(self):
+ self.central_widget.setCurrentWidget(self.settings_page)
+ self.status_bar.showMessage('设置')
+
+ def check_login_status(self):
+ # 检查本地是否有保存的登录信息
+ if self.api_client.load_session():
+ self.status_bar.showMessage(f'欢迎回来,{self.api_client.username}')
+ self.show_dashboard_page()
+ else:
+ self.show_login_page()
+
+class APIClient:
+ def __init__(self):
+ self.base_url = 'http://localhost:5000'
+ self.session = requests.Session()
+ self.username = None
+ self.user_id = None
+ self.access_token = None
+ self.refresh_token = None
+ # 添加用户代理
+ self.session.headers.update({
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
+ })
+
+ def login(self, email, password):
+ try:
+ response = self.session.post(f'{self.base_url}/auth/login', json={
+ 'email': email,
+ 'password': password
+ })
+ response.raise_for_status()
+
+ # 登录成功,保存会话信息
+ data = response.json()
+ self.username = data.get('username')
+ self.user_id = data.get('user_id')
+ self.access_token = data.get('access_token')
+ self.refresh_token = data.get('refresh_token')
+
+ # 设置Authorization头
+ self.session.headers.update({
+ 'Authorization': f'Bearer {self.access_token}'
+ })
+
+ self.save_session()
+ return True
+ except requests.exceptions.RequestException as e:
+ print(f'登录失败: {e}')
+ return False
+
+ def register(self, username, email, password):
+ try:
+ response = self.session.post(f'{self.base_url}/auth/register', json={
+ 'username': username,
+ 'email': email,
+ 'password': password,
+ 'password2': password
+ })
+ response.raise_for_status()
+ return True
+ except requests.exceptions.RequestException as e:
+ print(f'注册失败: {e}')
+ return False
+
+ def logout(self):
+ try:
+ response = self.session.get(f'{self.base_url}/auth/logout')
+ response.raise_for_status()
+ except requests.exceptions.RequestException as e:
+ print(f'登出失败: {e}')
+ finally:
+ self.clear_session()
+
+ def refresh_access_token(self):
+ """刷新访问令牌"""
+ if not self.refresh_token:
+ return False
+
+ try:
+ response = self.session.post(f'{self.base_url}/auth/refresh', json={
+ 'refresh_token': self.refresh_token
+ })
+ response.raise_for_status()
+
+ data = response.json()
+ self.access_token = data.get('access_token')
+
+ # 更新Authorization头
+ self.session.headers.update({
+ 'Authorization': f'Bearer {self.access_token}'
+ })
+
+ self.save_session()
+ return True
+ except requests.exceptions.RequestException as e:
+ print(f'刷新令牌失败: {e}')
+ self.clear_session()
+ return False
+
+ def is_authenticated(self):
+ return self.access_token is not None
+
+ def save_session(self):
+ # 保存会话信息到本地文件
+ with open('session.json', 'w') as f:
+ import json
+ session_data = {
+ 'username': self.username,
+ 'user_id': self.user_id,
+ 'access_token': self.access_token,
+ 'refresh_token': self.refresh_token
+ }
+ json.dump(session_data, f)
+
+ def load_session(self):
+ try:
+ with open('session.json', 'r') as f:
+ import json
+ session_data = json.load(f)
+ self.username = session_data.get('username')
+ self.user_id = session_data.get('user_id')
+ self.access_token = session_data.get('access_token')
+ self.refresh_token = session_data.get('refresh_token')
+
+ # 设置Authorization头
+ if self.access_token:
+ self.session.headers.update({
+ 'Authorization': f'Bearer {self.access_token}'
+ })
+
+ return self.is_authenticated()
+ except (FileNotFoundError, json.JSONDecodeError):
+ pass
+ return False
+
+ def clear_session(self):
+ self.username = None
+ self.user_id = None
+ self.access_token = None
+ self.refresh_token = None
+
+ # 移除Authorization头
+ if 'Authorization' in self.session.headers:
+ del self.session.headers['Authorization']
+
+ try:
+ import os
+ os.remove('session.json')
+ except:
+ pass
+
+if __name__ == '__main__':
+ app = QApplication(sys.argv)
+ window = MainWindow()
+ window.show()
+ sys.exit(app.exec_())
diff --git a/frontend/resources_page.py b/frontend/resources_page.py
new file mode 100644
index 0000000..91e03c2
--- /dev/null
+++ b/frontend/resources_page.py
@@ -0,0 +1,549 @@
+from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel,
+ QLineEdit, QPushButton, QMessageBox, QFormLayout,
+ QSpacerItem, QSizePolicy, QTableWidget, QTableWidgetItem,
+ QFileDialog, QDialog, QComboBox, QCalendarWidget, QSplitter,
+ QHeaderView, QTextEdit, QProgressBar, QFrame)
+from PyQt5.QtGui import QFont, QIcon, QPixmap, QImageReader
+from PyQt5.QtCore import Qt, pyqtSignal, QDate, QThread, pyqtSlot, QFileInfo
+import requests
+import os
+import math
+
+class ResourcesPage(QWidget):
+ def __init__(self, parent=None):
+ super().__init__(parent)
+ self.api_client = parent.login_page.api_client
+ self.initUI()
+ self.load_resources()
+
+ def initUI(self):
+ # 创建主布局
+ main_layout = QVBoxLayout()
+ main_layout.setContentsMargins(20, 20, 20, 20)
+ main_layout.setSpacing(20)
+
+ # 创建顶部布局(标题和操作按钮)
+ top_layout = QHBoxLayout()
+
+ # 标题
+ title_label = QLabel('资源管理')
+ title_label.setFont(QFont('Arial', 20, QFont.Bold))
+ top_layout.addWidget(title_label)
+
+ # 搜索框
+ self.search_edit = QLineEdit()
+ self.search_edit.setPlaceholderText('搜索资源...')
+ self.search_edit.setFont(QFont('Arial', 12))
+ self.search_edit.setFixedWidth(300)
+ self.search_edit.textChanged.connect(self.load_resources)
+ top_layout.addWidget(self.search_edit)
+
+ # 占位符
+ top_layout.addStretch()
+
+ # 上传资源按钮
+ self.upload_resource_button = QPushButton('上传资源')
+ self.upload_resource_button.setFont(QFont('Arial', 12))
+ self.upload_resource_button.setFixedHeight(35)
+ self.upload_resource_button.setStyleSheet('background-color: #4CAF50; color: white; border: none; border-radius: 5px;')
+ self.upload_resource_button.clicked.connect(self.show_upload_dialog)
+ top_layout.addWidget(self.upload_resource_button)
+
+ main_layout.addLayout(top_layout)
+
+ # 创建资源列表
+ self.resources_table = QTableWidget()
+ self.resources_table.setColumnCount(5)
+ self.resources_table.setHorizontalHeaderLabels(['名称', '类型', '大小', '上传时间', '状态'])
+ self.resources_table.horizontalHeader().setStretchLastSection(True)
+ self.resources_table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
+ self.resources_table.setSelectionBehavior(QTableWidget.SelectRows)
+ self.resources_table.setEditTriggers(QTableWidget.NoEditTriggers)
+ self.resources_table.doubleClicked.connect(self.show_resource_preview)
+ main_layout.addWidget(self.resources_table)
+
+ # 创建底部布局(操作按钮)
+ bottom_layout = QHBoxLayout()
+
+ # 激活按钮
+ self.activate_button = QPushButton('激活')
+ self.activate_button.setFont(QFont('Arial', 12))
+ self.activate_button.setFixedHeight(35)
+ self.activate_button.setStyleSheet('background-color: #2196F3; color: white; border: none; border-radius: 5px;')
+ self.activate_button.clicked.connect(self.handle_activate_resource)
+ bottom_layout.addWidget(self.activate_button)
+
+ # 删除按钮
+ self.delete_button = QPushButton('删除')
+ self.delete_button.setFont(QFont('Arial', 12))
+ self.delete_button.setFixedHeight(35)
+ self.delete_button.setStyleSheet('background-color: #F44336; color: white; border: none; border-radius: 5px;')
+ self.delete_button.clicked.connect(self.handle_delete_resource)
+ bottom_layout.addWidget(self.delete_button)
+
+ # 刷新按钮
+ self.refresh_button = QPushButton('刷新')
+ self.refresh_button.setFont(QFont('Arial', 12))
+ self.refresh_button.setFixedHeight(35)
+ self.refresh_button.setStyleSheet('background-color: #9E9E9E; color: white; border: none; border-radius: 5px;')
+ self.refresh_button.clicked.connect(self.load_resources)
+ bottom_layout.addWidget(self.refresh_button)
+
+ main_layout.addLayout(bottom_layout)
+
+ # 设置布局
+ self.setLayout(main_layout)
+
+ def load_resources(self):
+ """加载资源列表"""
+ if not self.api_client.is_authenticated():
+ return
+
+ try:
+ search_query = self.search_edit.text().strip()
+ url = f'{self.api_client.base_url}/resources'
+
+ if search_query:
+ url = f'{url}?q={search_query}'
+
+ response = self.api_client.session.get(url)
+ response.raise_for_status()
+
+ resources = response.json()
+
+ # 更新表格
+ self.resources_table.setRowCount(len(resources))
+
+ for row, resource in enumerate(resources):
+ name_item = QTableWidgetItem(resource['name'])
+ type_item = QTableWidgetItem(resource['type'])
+ size_item = QTableWidgetItem(self.format_file_size(resource['size']))
+ upload_time_item = QTableWidgetItem(resource['upload_time'].split('T')[0])
+
+ # 状态显示
+ status_item = QTableWidgetItem()
+ if resource['is_active']:
+ status_item.setText('已激活')
+ status_item.setForeground(Qt.green)
+ else:
+ status_item.setText('未激活')
+ status_item.setForeground(Qt.gray)
+
+ self.resources_table.setItem(row, 0, name_item)
+ self.resources_table.setItem(row, 1, type_item)
+ self.resources_table.setItem(row, 2, size_item)
+ self.resources_table.setItem(row, 3, upload_time_item)
+ self.resources_table.setItem(row, 4, status_item)
+
+ # 存储资源ID
+ self.resources_table.setVerticalHeaderItem(row, QTableWidgetItem(str(resource['id'])))
+
+ except requests.exceptions.RequestException as e:
+ QMessageBox.warning(self, '加载失败', f'资源加载失败: {e}')
+
+ def format_file_size(self, size_bytes):
+ """格式化文件大小为人类可读格式"""
+ if size_bytes == 0:
+ return "0 Bytes"
+
+ size_names = ["Bytes", "KB", "MB", "GB", "TB"]
+ i = int(math.floor(math.log(size_bytes, 1024)))
+ p = math.pow(1024, i)
+ s = round(size_bytes / p, 2)
+ return f"{s} {size_names[i]}"
+
+ def show_resource_preview(self, index):
+ """显示资源预览"""
+ resource_id = int(self.resources_table.verticalHeaderItem(index.row()).text())
+ resource_name = self.resources_table.item(index.row(), 0).text()
+ resource_type = self.resources_table.item(index.row(), 1).text()
+
+ try:
+ # 获取资源内容
+ response = self.api_client.session.get(f'{self.api_client.base_url}/resources/{resource_id}/download', stream=True)
+ response.raise_for_status()
+
+ # 创建临时文件
+ import tempfile
+ temp_dir = tempfile.gettempdir()
+ temp_file_path = os.path.join(temp_dir, resource_name)
+
+ # 保存文件到临时目录
+ with open(temp_file_path, 'wb') as f:
+ for chunk in response.iter_content(chunk_size=8192):
+ f.write(chunk)
+
+ # 创建预览对话框
+ preview_dialog = QDialog(self)
+ preview_dialog.setWindowTitle(f'预览: {resource_name}')
+ preview_dialog.setGeometry(200, 200, 800, 600)
+
+ layout = QVBoxLayout()
+
+ # 判断文件类型并显示相应的预览
+ if resource_type.startswith('image/'):
+ # 图片预览
+ pixmap = QPixmap(temp_file_path)
+ if not pixmap.isNull():
+ image_label = QLabel()
+ image_label.setPixmap(pixmap.scaled(800, 600, Qt.KeepAspectRatio, Qt.SmoothTransformation))
+ image_label.setAlignment(Qt.AlignCenter)
+ layout.addWidget(image_label)
+ else:
+ layout.addWidget(QLabel('无法预览该图片'))
+ elif resource_type in ['application/pdf']:
+ # PDF预览(需要PyQt5.QtWebEngineWidgets)
+ try:
+ from PyQt5.QtWebEngineWidgets import QWebEngineView
+ web_view = QWebEngineView()
+ web_view.load(QUrl.fromLocalFile(temp_file_path))
+ layout.addWidget(web_view)
+ except ImportError:
+ layout.addWidget(QLabel('无法预览PDF文件,请安装PyQt5.QtWebEngineWidgets'))
+ else:
+ # 其他类型文件显示信息
+ info_layout = QFormLayout()
+ info_layout.addRow('文件名:', QLabel(resource_name))
+ info_layout.addRow('文件类型:', QLabel(resource_type))
+ info_layout.addRow('文件大小:', self.resources_table.item(index.row(), 2))
+ info_layout.addRow('上传时间:', self.resources_table.item(index.row(), 3))
+ layout.addLayout(info_layout)
+
+ # 关闭按钮
+ close_button = QPushButton('关闭')
+ close_button.setFont(QFont('Arial', 12))
+ close_button.setFixedHeight(35)
+ close_button.clicked.connect(lambda: self.handle_close_preview(preview_dialog, temp_file_path))
+ layout.addWidget(close_button)
+
+ preview_dialog.setLayout(layout)
+ preview_dialog.exec_()
+
+ except requests.exceptions.RequestException as e:
+ QMessageBox.warning(self, '预览失败', f'资源预览失败: {e}')
+
+ def handle_close_preview(self, dialog, temp_file_path):
+ """处理关闭预览并删除临时文件"""
+ dialog.close()
+ try:
+ os.remove(temp_file_path)
+ except OSError:
+ pass
+
+ def show_upload_dialog(self):
+ """显示上传资源对话框"""
+ dialog = ResourceUploadDialog(self, self.api_client)
+ if dialog.exec_():
+ self.load_resources()
+
+ def handle_activate_resource(self):
+ """处理激活资源"""
+ selected_row = self.resources_table.currentRow()
+ if selected_row == -1:
+ QMessageBox.warning(self, '操作提示', '请先选择一个资源')
+ return
+
+ resource_id = int(self.resources_table.verticalHeaderItem(selected_row).text())
+ resource_name = self.resources_table.item(selected_row, 0).text()
+ current_status = self.resources_table.item(selected_row, 4).text()
+
+ # 如果资源已经激活,则提示用户
+ if current_status == '已激活':
+ QMessageBox.information(self, '操作提示', f'资源"{resource_name}"已经是激活状态')
+ return
+
+ reply = QMessageBox.question(self, '确认激活', f'确定要激活资源"{resource_name}"吗?',
+ QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
+
+ if reply == QMessageBox.Yes:
+ try:
+ response = self.api_client.session.post(f'{self.api_client.base_url}/resources/{resource_id}/activate')
+ response.raise_for_status()
+
+ QMessageBox.information(self, '激活成功', '资源已成功激活')
+ self.load_resources()
+
+ except requests.exceptions.RequestException as e:
+ QMessageBox.warning(self, '激活失败', f'资源激活失败: {e}')
+
+ def handle_delete_resource(self):
+ """处理删除资源"""
+ selected_row = self.resources_table.currentRow()
+ if selected_row == -1:
+ QMessageBox.warning(self, '操作提示', '请先选择一个资源')
+ return
+
+ resource_id = int(self.resources_table.verticalHeaderItem(selected_row).text())
+ resource_name = self.resources_table.item(selected_row, 0).text()
+
+ reply = QMessageBox.question(self, '确认删除', f'确定要删除资源"{resource_name}"吗?',
+ QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
+
+ if reply == QMessageBox.Yes:
+ try:
+ response = self.api_client.session.delete(f'{self.api_client.base_url}/resources/{resource_id}')
+ response.raise_for_status()
+
+ QMessageBox.information(self, '删除成功', '资源已成功删除')
+ self.load_resources()
+
+ except requests.exceptions.RequestException as e:
+ QMessageBox.warning(self, '删除失败', f'资源删除失败: {e}')
+
+class ResourceUploadDialog(QDialog):
+ def __init__(self, parent=None, api_client=None):
+ super().__init__(parent)
+ self.api_client = api_client
+ self.file_path = None
+ self.upload_thread = None
+ self.setWindowTitle('上传资源')
+ self.initUI()
+
+ def initUI(self):
+ # 创建布局
+ main_layout = QVBoxLayout()
+ main_layout.setContentsMargins(30, 30, 30, 30)
+ main_layout.setSpacing(20)
+
+ # 创建表单布局
+ form_layout = QFormLayout()
+ form_layout.setSpacing(15)
+
+ # 资源名称
+ self.name_edit = QLineEdit()
+ self.name_edit.setPlaceholderText('请输入资源名称')
+ self.name_edit.setFont(QFont('Arial', 12))
+ self.name_edit.setFixedHeight(40)
+ form_layout.addRow('资源名称:', self.name_edit)
+
+ # 资源类型
+ self.type_combo = QComboBox()
+ self.type_combo.setFont(QFont('Arial', 12))
+ self.type_combo.setFixedHeight(40)
+ self.type_combo.addItems(['皮肤', '人物模型', '场景', '音效', '其他'])
+ form_layout.addRow('资源类型:', self.type_combo)
+
+ # 描述
+ self.description_edit = QTextEdit()
+ self.description_edit.setPlaceholderText('请输入资源描述')
+ self.description_edit.setFont(QFont('Arial', 12))
+ self.description_edit.setFixedHeight(100)
+ form_layout.addRow('资源描述:', self.description_edit)
+
+ # 文件选择
+ file_layout = QHBoxLayout()
+
+ self.file_path_edit = QLineEdit()
+ self.file_path_edit.setReadOnly(True)
+ self.file_path_edit.setFont(QFont('Arial', 12))
+ self.file_path_edit.setFixedHeight(40)
+ file_layout.addWidget(self.file_path_edit)
+
+ self.browse_button = QPushButton('浏览')
+ self.browse_button.setFont(QFont('Arial', 12))
+ self.browse_button.setFixedHeight(40)
+ self.browse_button.setFixedWidth(100)
+ self.browse_button.setStyleSheet('background-color: #2196F3; color: white; border: none; border-radius: 5px;')
+ self.browse_button.clicked.connect(self.handle_browse_file)
+ file_layout.addWidget(self.browse_button)
+
+ form_layout.addRow('选择文件:', file_layout)
+
+ main_layout.addLayout(form_layout)
+
+ # 进度条
+ self.progress_bar = QProgressBar()
+ self.progress_bar.setVisible(False)
+ main_layout.addWidget(self.progress_bar)
+
+ # 创建按钮布局
+ button_layout = QHBoxLayout()
+
+ # 上传按钮
+ self.upload_button = QPushButton('上传')
+ self.upload_button.setFont(QFont('Arial', 12))
+ self.upload_button.setFixedHeight(35)
+ self.upload_button.setStyleSheet('background-color: #4CAF50; color: white; border: none; border-radius: 5px;')
+ self.upload_button.clicked.connect(self.handle_upload)
+ button_layout.addWidget(self.upload_button)
+
+ # 取消按钮
+ cancel_button = QPushButton('取消')
+ cancel_button.setFont(QFont('Arial', 12))
+ cancel_button.setFixedHeight(35)
+ cancel_button.setStyleSheet('background-color: #9E9E9E; color: white; border: none; border-radius: 5px;')
+ cancel_button.clicked.connect(self.close)
+ button_layout.addWidget(cancel_button)
+
+ main_layout.addLayout(button_layout)
+
+ # 设置布局
+ self.setLayout(main_layout)
+
+ def handle_browse_file(self):
+ """处理浏览文件"""
+ file_path, _ = QFileDialog.getOpenFileName(
+ self, '选择资源文件', '', '所有文件 (*);;图片文件 (*.png *.jpg *.jpeg);;模型文件 (*.obj *.fbx);;音频文件 (*.mp3 *.wav)')
+
+ if file_path:
+ self.file_path = file_path
+ self.file_path_edit.setText(file_path)
+
+ # 自动填充资源名称
+ if not self.name_edit.text():
+ file_info = QFileInfo(file_path)
+ self.name_edit.setText(file_info.baseName())
+
+ def handle_upload(self):
+ """处理上传资源"""
+ name = self.name_edit.text().strip()
+ resource_type = self.type_combo.currentText()
+ description = self.description_edit.toPlainText().strip()
+
+ # 基本验证
+ if not name:
+ QMessageBox.warning(self, '输入错误', '请填写资源名称')
+ return
+
+ if not self.file_path:
+ QMessageBox.warning(self, '输入错误', '请选择资源文件')
+ return
+
+ # 检查文件大小
+ max_file_size = 10 * 1024 * 1024 # 10MB
+ file_size = os.path.getsize(self.file_path)
+
+ if file_size > max_file_size:
+ QMessageBox.warning(self, '文件过大', f'文件大小不能超过10MB,当前文件大小为{self.format_file_size(file_size)}')
+ return
+
+ # 检查文件类型
+ allowed_types = [
+ ('image/png', '.png'),
+ ('image/jpeg', '.jpg'),
+ ('image/jpeg', '.jpeg'),
+ ('model/obj', '.obj'),
+ ('model/fbx', '.fbx'),
+ ('audio/mpeg', '.mp3'),
+ ('audio/wav', '.wav')
+ ]
+
+ file_ext = os.path.splitext(self.file_path)[1].lower()
+ file_type = None
+
+ for mime_type, ext in allowed_types:
+ if file_ext == ext:
+ file_type = mime_type
+ break
+
+ if not file_type:
+ QMessageBox.warning(self, '文件类型不支持', '不支持的文件类型,请选择正确的资源文件')
+ return
+
+ # 开始上传
+ self.progress_bar.setVisible(True)
+ self.upload_button.setEnabled(False)
+ self.browse_button.setEnabled(False)
+
+ # 创建上传线程
+ self.upload_thread = ResourceUploadThread(self.api_client, self.file_path, name, resource_type, description, file_type)
+ self.upload_thread.progress_updated.connect(self.update_progress)
+ self.upload_thread.upload_finished.connect(self.handle_upload_finished)
+ self.upload_thread.start()
+
+ def format_file_size(self, size_bytes):
+ """格式化文件大小为人类可读格式"""
+ if size_bytes == 0:
+ return "0 Bytes"
+
+ size_names = ["Bytes", "KB", "MB", "GB", "TB"]
+ i = int(math.floor(math.log(size_bytes, 1024)))
+ p = math.pow(1024, i)
+ s = round(size_bytes / p, 2)
+ return f"{s} {size_names[i]}"
+
+ @pyqtSlot(int)
+ def update_progress(self, progress):
+ """更新上传进度"""
+ self.progress_bar.setValue(progress)
+
+ @pyqtSlot(bool, str)
+ def handle_upload_finished(self, success, message):
+ """处理上传完成"""
+ self.progress_bar.setVisible(False)
+ self.upload_button.setEnabled(True)
+ self.browse_button.setEnabled(True)
+
+ if success:
+ QMessageBox.information(self, '上传成功', message)
+ self.accept()
+ else:
+ QMessageBox.warning(self, '上传失败', message)
+
+class ResourceUploadThread(QThread):
+ progress_updated = pyqtSignal(int)
+ upload_finished = pyqtSignal(bool, str)
+
+ def __init__(self, api_client, file_path, name, resource_type, description, file_type):
+ super().__init__()
+ self.api_client = api_client
+ self.file_path = file_path
+ self.name = name
+ self.resource_type = resource_type
+ self.description = description
+ self.file_type = file_type
+
+ def run(self):
+ """线程运行函数"""
+ try:
+ # 读取文件内容
+ with open(self.file_path, 'rb') as f:
+ file_content = f.read()
+
+ # 准备数据
+ files = {
+ 'file': (os.path.basename(self.file_path), file_content, self.file_type)
+ }
+
+ data = {
+ 'name': self.name,
+ 'type': self.resource_type,
+ 'description': self.description
+ }
+
+ # 发送上传请求
+ response = self.api_client.session.post(f'{self.api_client.base_url}/resources/upload',
+ data=data, files=files, stream=True)
+
+ # 检查响应状态
+ response.raise_for_status()
+
+ # 获取文件大小
+ file_size = len(file_content)
+
+ # 处理上传进度
+ if response.headers.get('Content-Length'):
+ total_size = int(response.headers.get('Content-Length'))
+ bytes_uploaded = 0
+
+ for chunk in response.iter_content(chunk_size=8192):
+ if chunk:
+ bytes_uploaded += len(chunk)
+ progress = int((bytes_uploaded / total_size) * 100)
+ self.progress_updated.emit(progress)
+ else:
+ # 无法获取内容长度时,直接显示100%
+ self.progress_updated.emit(100)
+
+ # 解析响应
+ result = response.json()
+
+ if result.get('success'):
+ self.upload_finished.emit(True, '资源上传成功')
+ else:
+ self.upload_finished.emit(False, f'资源上传失败: {result.get("message", "未知错误")}')
+
+ except requests.exceptions.RequestException as e:
+ self.upload_finished.emit(False, f'资源上传失败: {e}')
+ except Exception as e:
+ self.upload_finished.emit(False, f'资源上传失败: {str(e)}')
diff --git a/ke b/ke
new file mode 100644
index 0000000..e69de29
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..751b521
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,11 @@
+Flask==2.3.3
+Flask-SQLAlchemy==3.0.5
+Flask-Login==0.6.3
+Flask-WTF==1.1.1
+WTForms==3.0.1
+passlib==1.7.4
+email-validator==2.0.0.post2
+Pillow==10.0.0
+python-dotenv==1.0.0
+python-logstash-async==2.5.0
+python-dotenv==1.0.0
\ No newline at end of file