First Version
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
FROM python:3.11-slim
|
||||
|
||||
# Set environment variables
|
||||
ENV PYTHONDONTWRITEBYTECODE=1
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
ENV FLASK_APP=app.py
|
||||
|
||||
# Install system dependencies
|
||||
RUN apt-get update && apt-get install -y \
|
||||
gcc \
|
||||
g++ \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Create app directory
|
||||
WORKDIR /app
|
||||
|
||||
# Copy requirements and install dependencies
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# Copy application
|
||||
COPY . .
|
||||
|
||||
# Create non-root user
|
||||
RUN useradd -m -u 1000 flaskuser && \
|
||||
chown -R flaskuser:flaskuser /app
|
||||
USER flaskuser
|
||||
|
||||
# Expose port
|
||||
EXPOSE 5000
|
||||
|
||||
# Run with gunicorn
|
||||
CMD ["gunicorn", "--timeout", "300", "--workers", "3", "--bind", "0.0.0.0:5000", "--preload", "app:app"]
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,43 @@
|
||||
from flask import Flask
|
||||
from flask_login import LoginManager
|
||||
from model.models import db, User, bcrypt
|
||||
from auth import auth
|
||||
from admin import admin_blueprint
|
||||
from dashboard import dashboard_blueprint
|
||||
from config import Config
|
||||
|
||||
def create_app():
|
||||
app = Flask(__name__)
|
||||
app.config.from_object(Config)
|
||||
|
||||
print(f"DEBUG: SECRET_KEY = {app.config.get('SECRET_KEY')}")
|
||||
|
||||
db.init_app(app)
|
||||
bcrypt.init_app(app)
|
||||
|
||||
login_manager = LoginManager()
|
||||
login_manager.init_app(app)
|
||||
login_manager.login_view = 'auth.login'
|
||||
login_manager.login_message = "Please log in to access this page."
|
||||
|
||||
app.config['SESSION_COOKIE_NAME'] = 'flask_session'
|
||||
|
||||
@login_manager.user_loader
|
||||
def load_user(user_id):
|
||||
user = db.session.get(User, int(user_id))
|
||||
print(f"DEBUG: Loading user {user_id} -> {user}")
|
||||
return user
|
||||
|
||||
app.register_blueprint(admin_blueprint)
|
||||
app.register_blueprint(auth, url_prefix='/auth')
|
||||
app.register_blueprint(dashboard_blueprint)
|
||||
|
||||
with app.app_context():
|
||||
db.create_all()
|
||||
|
||||
return app
|
||||
|
||||
app = create_app()
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(host='0.0.0.0', port=5000, debug=False)
|
||||
@@ -0,0 +1,44 @@
|
||||
from flask import Blueprint, render_template, request, redirect, url_for, flash
|
||||
from flask_login import login_user, logout_user, login_required
|
||||
from model.models import db, User
|
||||
|
||||
auth = Blueprint('auth', __name__)
|
||||
|
||||
@auth.route('/login', methods=['GET', 'POST'])
|
||||
def login():
|
||||
if request.method == 'POST':
|
||||
username = request.form['username']
|
||||
password = request.form['password']
|
||||
user = User.query.filter_by(username=username).first()
|
||||
if user and user.check_password(password):
|
||||
login_user(user)
|
||||
if user.role == "admin":
|
||||
return redirect(url_for('admin.admin_dashboard'))
|
||||
return redirect(url_for('dashboard.dashboard'))
|
||||
flash('نام کاربری یا رمز عبور اشتباه است.', 'error')
|
||||
return render_template('index.html')
|
||||
|
||||
@auth.route('/register', methods=['GET', 'POST'])
|
||||
def register():
|
||||
if request.method == 'POST':
|
||||
username = request.form['username']
|
||||
email = request.form['email']
|
||||
password = request.form['password']
|
||||
|
||||
if User.query.filter_by(username=username).first():
|
||||
flash('این نام کاربری قبلاً استفاده شده.', 'error')
|
||||
return render_template('index.html')
|
||||
|
||||
user = User(username=username, email=email)
|
||||
user.set_password(password)
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
flash('ثبتنام با موفقیت انجام شد. وارد شوید.', 'success')
|
||||
return render_template('index.html')
|
||||
return render_template('index.html')
|
||||
|
||||
@auth.route('/logout')
|
||||
@login_required
|
||||
def logout():
|
||||
logout_user()
|
||||
return render_template('index.html')
|
||||
@@ -0,0 +1,20 @@
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
|
||||
|
||||
load_dotenv(os.path.join(os.path.dirname(__file__), '.env'))
|
||||
|
||||
class Config:
|
||||
SECRET_KEY = os.environ.get('SECRET_KEY') or 'dev-fallback-key-for-testing-12345'
|
||||
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or 'sqlite:///yara.db'
|
||||
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
||||
RAG_API_URL = os.environ.get('RAG_API_URL') or 'http://localhost:8000/'
|
||||
|
||||
SESSION_COOKIE_SECURE = False
|
||||
SESSION_COOKIE_HTTPONLY = True
|
||||
SESSION_COOKIE_SAMESITE = 'Lax'
|
||||
SESSION_COOKIE_DOMAIN = None
|
||||
|
||||
UPLOAD_FOLDER = 'uploads'
|
||||
|
||||
PERMANENT_SESSION_LIFETIME = 86400*10
|
||||
@@ -0,0 +1,833 @@
|
||||
from flask import Blueprint, render_template, request, jsonify, Response, stream_with_context, session
|
||||
from flask_login import login_required, current_user
|
||||
from model.models import db, ChatSession, Message, Article, ArticleResponse, CorrectionLog
|
||||
from datetime import datetime
|
||||
import requests
|
||||
import time
|
||||
import json
|
||||
import uuid
|
||||
import os
|
||||
|
||||
dashboard_blueprint = Blueprint('dashboard', __name__)
|
||||
|
||||
RAG_API_URL = "http://localhost:8000/"
|
||||
|
||||
@dashboard_blueprint.route('/')
|
||||
def index():
|
||||
return render_template('index.html')
|
||||
|
||||
@dashboard_blueprint.route('/dashboard')
|
||||
@login_required
|
||||
def dashboard():
|
||||
session_obj = ChatSession.query.filter_by(user_id=current_user.id).order_by(ChatSession.created_at.desc()).first()
|
||||
if not session_obj:
|
||||
session_obj = ChatSession(user_id=current_user.id)
|
||||
db.session.add(session_obj)
|
||||
db.session.commit()
|
||||
|
||||
messages = Message.query.filter_by(session_id=session_obj.id).order_by(Message.created_at).all()
|
||||
messages_data = [
|
||||
{
|
||||
"id": msg.id,
|
||||
"session_id": msg.session_id,
|
||||
"role": msg.role,
|
||||
"content": msg.content,
|
||||
"created_at": msg.created_at.isoformat() if msg.created_at else None
|
||||
}
|
||||
for msg in messages
|
||||
]
|
||||
|
||||
return render_template('dashboard/dashboard.html', messages=messages_data)
|
||||
|
||||
@dashboard_blueprint.route('/send_message', methods=['POST'])
|
||||
@login_required
|
||||
def send_message():
|
||||
user_message = request.json.get('message', '').strip()
|
||||
article_id = request.json.get('article_id', '')
|
||||
|
||||
if not user_message:
|
||||
return jsonify({"error": "پیام خالی است"}), 400
|
||||
|
||||
# Get PDF path from database based on article_id
|
||||
pdf_path = ""
|
||||
if article_id:
|
||||
article = Article.query.filter_by(id=article_id, user_id=current_user.id).first()
|
||||
if article and article.pdf_path:
|
||||
pdf_path = article.pdf_path
|
||||
|
||||
session_obj = ChatSession.query.filter_by(user_id=current_user.id).order_by(ChatSession.created_at.desc()).first()
|
||||
if not session_obj:
|
||||
session_obj = ChatSession(user_id=current_user.id)
|
||||
db.session.add(session_obj)
|
||||
db.session.commit()
|
||||
|
||||
user_msg = Message(session_id=session_obj.id, role='user', content=user_message)
|
||||
db.session.add(user_msg)
|
||||
db.session.commit()
|
||||
|
||||
try:
|
||||
response = requests.post("http://localhost:8000/query", json={"message": user_message, "pdf_path": pdf_path}, timeout=60)
|
||||
if response.status_code != 200:
|
||||
raise Exception(f"Backend error: {response.text}")
|
||||
|
||||
result = response.json()
|
||||
full_response = result["response"]
|
||||
|
||||
assistant_msg = Message(session_id=session_obj.id, role='assistant', content=full_response)
|
||||
db.session.add(assistant_msg)
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({
|
||||
"content": full_response})
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"خطا در ارتباط با سرور: {str(e)}"
|
||||
err_msg = Message(session_id=session_obj.id, role='assistant', content=error_msg)
|
||||
db.session.add(err_msg)
|
||||
db.session.commit()
|
||||
return jsonify({"content": error_msg, "chunks": []}), 500
|
||||
|
||||
@dashboard_blueprint.route('/clear_chat', methods=['POST'])
|
||||
@login_required
|
||||
def clear_chat():
|
||||
new_session = ChatSession(user_id=current_user.id)
|
||||
db.session.add(new_session)
|
||||
db.session.commit()
|
||||
return jsonify({"status": "cleared", "new_session_id": new_session.id})
|
||||
|
||||
@dashboard_blueprint.route('/api/current_user')
|
||||
@login_required
|
||||
def get_current_user():
|
||||
"""دریافت اطلاعات کاربر جاری"""
|
||||
return jsonify({
|
||||
'id': current_user.id,
|
||||
'username': current_user.username,
|
||||
'email': getattr(current_user, 'email', ''),
|
||||
'name': getattr(current_user, 'name', current_user.username),
|
||||
'role': getattr(current_user, 'role', 'ویراستار')
|
||||
})
|
||||
|
||||
@dashboard_blueprint.route('/articles')
|
||||
@login_required
|
||||
def articles_dashboard():
|
||||
"""صفحه اصلی نمایشگر معیار مقالات"""
|
||||
user_data = {
|
||||
'name': getattr(current_user, 'name', current_user.username),
|
||||
'email': getattr(current_user, 'email', ''),
|
||||
'role': getattr(current_user, 'role', 'ویراستار')
|
||||
}
|
||||
return render_template('dashboard/articles_dashboard.html', user_data=user_data)
|
||||
|
||||
@dashboard_blueprint.route('/api/generate_response', methods=['POST'])
|
||||
@login_required
|
||||
def generate_response():
|
||||
"""تولید پاسخ جدید با هوش مصنوعی برای یک معیار"""
|
||||
try:
|
||||
data = request.get_json()
|
||||
if not data:
|
||||
return jsonify({'error': 'دادههای JSON دریافت نشد'}), 400
|
||||
|
||||
article = data.get('article')
|
||||
user_edited_response = data.get('user_edited_response')
|
||||
measure_text = data.get('measure_text', '')
|
||||
measure_type = data.get('measure_type', '')
|
||||
|
||||
if not article:
|
||||
return jsonify({'error': 'دادههای مقاله دریافت نشد'}), 400
|
||||
|
||||
if not user_edited_response:
|
||||
return jsonify({'error': 'پاسخ ویرایش شده دریافت نشد'}), 400
|
||||
|
||||
article_id = (
|
||||
article.get('id') or
|
||||
article.get('article_id') or
|
||||
article.get('article id')
|
||||
)
|
||||
|
||||
if article_id is None:
|
||||
return jsonify({'error': 'شناسه مقاله در دادهها یافت نشد'}), 400
|
||||
|
||||
# Get PDF path from database
|
||||
pdf_path = ""
|
||||
if article.get('id'):
|
||||
db_article = Article.query.filter_by(id=article['id'], user_id=current_user.id).first()
|
||||
if db_article and db_article.pdf_path:
|
||||
pdf_path = db_article.pdf_path
|
||||
else:
|
||||
# Fallback to article name if no PDF path in database
|
||||
pdf_path = article.get('name', '').strip() + ".pdf"
|
||||
|
||||
# Find the index of the measure text and get the current response
|
||||
response_measure = ""
|
||||
try:
|
||||
if measure_type == 'positive':
|
||||
measures = article.get('measures_positive', [])
|
||||
responses = article.get('responses_positive', [])
|
||||
if measure_text in measures:
|
||||
index = measures.index(measure_text)
|
||||
response_measure = responses[index] if index < len(responses) else ""
|
||||
else:
|
||||
measures = article.get('measures_negative', [])
|
||||
responses = article.get('responses_negative', [])
|
||||
if measure_text in measures:
|
||||
index = measures.index(measure_text)
|
||||
response_measure = responses[index] if index < len(responses) else ""
|
||||
except (ValueError, IndexError):
|
||||
response_measure = ""
|
||||
|
||||
prompt = f"""
|
||||
چکیده مقاله: {article.get('abstract', '')}
|
||||
معیار {measure_type}: {measure_text}
|
||||
|
||||
پاسخ قبلی: {response_measure}
|
||||
|
||||
پاسخ ویرایش شده توسط کاربر: {user_edited_response}
|
||||
|
||||
لطفاً بر اساس معیار فوق و پاسخ ویرایش شده کاربر، یک پاسخ بهبود یافته و حرفهایتر ارائه دهید که:
|
||||
1. کاملاً با معیار داده شده مرتبط باشد
|
||||
2. ساختار منظمتری داشته باشد
|
||||
3. از لحاظ علمی دقیق و معتبر باشد
|
||||
4. از پاسخ کاربر به عنوان پایه استفاده کند اما آن را ارتقا دهد
|
||||
5. مستقیماً به معیار اشاره شده پاسخ دهد
|
||||
|
||||
پاسخ بهبود یافته:
|
||||
"""
|
||||
|
||||
try:
|
||||
rag_response = requests.post(
|
||||
"http://localhost:8000/query",
|
||||
json={
|
||||
"message": prompt,
|
||||
"pdf_path": pdf_path
|
||||
},
|
||||
timeout=30000
|
||||
)
|
||||
|
||||
if rag_response.status_code == 200:
|
||||
result = rag_response.json()
|
||||
ai_response = result.get("response", user_edited_response)
|
||||
else:
|
||||
print(f"RAG error: {rag_response.status_code} - {rag_response.text}")
|
||||
ai_response = user_edited_response
|
||||
|
||||
except Exception as e:
|
||||
print(f"خطا در ارتباط با RAG: {e}")
|
||||
ai_response = user_edited_response
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'ai_response': ai_response,
|
||||
'message': 'پاسخ با موفقیت توسط هوش مصنوعی تولید شد'
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error in generate_response: {str(e)}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return jsonify({'error': f'خطا در تولید پاسخ: {str(e)}'}), 500
|
||||
|
||||
@dashboard_blueprint.route('/api/delete_article/<int:article_id>', methods=['DELETE'])
|
||||
@login_required
|
||||
def delete_article(article_id):
|
||||
"""حذف مقاله"""
|
||||
article = Article.query.filter_by(id=article_id, user_id=current_user.id).first()
|
||||
if not article:
|
||||
return jsonify({'error': 'مقاله یافت نشد'}), 404
|
||||
|
||||
try:
|
||||
# Delete related responses first
|
||||
ArticleResponse.query.filter_by(article_id=article_id).delete()
|
||||
db.session.delete(article)
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({'success': True, 'message': 'مقاله با موفقیت حذف شد'})
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
return jsonify({'error': f'خطا در حذف مقاله: {str(e)}'}), 500
|
||||
|
||||
@dashboard_blueprint.route('/api/upload_articles', methods=['POST'])
|
||||
@login_required
|
||||
def upload_articles():
|
||||
"""آپلود فایل JSON مقالات"""
|
||||
if 'file' not in request.files:
|
||||
return jsonify({'error': 'فایلی انتخاب نشده است'}), 400
|
||||
|
||||
file = request.files['file']
|
||||
if file.filename == '':
|
||||
return jsonify({'error': 'فایلی انتخاب نشده است'}), 400
|
||||
|
||||
if not file.filename.endswith('.json'):
|
||||
return jsonify({'error': 'فقط فایلهای JSON پشتیبانی میشوند'}), 400
|
||||
|
||||
try:
|
||||
articles_data = json.load(file)
|
||||
uploaded_count = 0
|
||||
|
||||
for article_data in articles_data:
|
||||
existing_article = Article.query.filter_by(
|
||||
user_id=current_user.id,
|
||||
article_id=article_data.get('article id', str(uuid.uuid4()))
|
||||
).first()
|
||||
|
||||
if existing_article:
|
||||
continue
|
||||
|
||||
# Extract PDF path from article data or create from name
|
||||
pdf_path = article_data.get('pdf_path', '')
|
||||
if not pdf_path and 'article name' in article_data:
|
||||
pdf_path = f"{article_data['article name'].strip()}.pdf"
|
||||
|
||||
article = Article(
|
||||
user_id=current_user.id,
|
||||
article_id=article_data.get('article id', str(uuid.uuid4())),
|
||||
name=article_data.get('article name', 'بدون نام'),
|
||||
institute=article_data.get('article institute', 'نامشخص'),
|
||||
abstract=article_data.get('article abstract', 'چکیدهای موجود نیست'),
|
||||
pdf_path=pdf_path, # Store PDF path
|
||||
measures_positive=json.dumps(article_data.get('measures_positive', []), ensure_ascii=False),
|
||||
measures_negative=json.dumps(article_data.get('measures_negative', []), ensure_ascii=False),
|
||||
responses_positive=json.dumps(article_data.get('responses_positive', []), ensure_ascii=False),
|
||||
responses_negative=json.dumps(article_data.get('responses_negative', []), ensure_ascii=False),
|
||||
scores_positive=json.dumps(article_data.get('scores_positive', []), ensure_ascii=False),
|
||||
scores_negative=json.dumps(article_data.get('scores_negative', []), ensure_ascii=False)
|
||||
)
|
||||
db.session.add(article)
|
||||
uploaded_count += 1
|
||||
|
||||
db.session.commit()
|
||||
|
||||
new_articles = Article.query.filter_by(user_id=current_user.id).all()
|
||||
articles_list = []
|
||||
for article in new_articles:
|
||||
articles_list.append({
|
||||
'id': article.id,
|
||||
'article_id': article.article_id,
|
||||
'name': article.name,
|
||||
'institute': article.institute,
|
||||
'abstract': article.abstract,
|
||||
'pdf_path': article.pdf_path, # Include PDF path in response
|
||||
'measures_positive': json.loads(article.measures_positive) if article.measures_positive else [],
|
||||
'measures_negative': json.loads(article.measures_negative) if article.measures_negative else [],
|
||||
'responses_positive': json.loads(article.responses_positive) if article.responses_positive else [],
|
||||
'responses_negative': json.loads(article.responses_negative) if article.responses_negative else [],
|
||||
'scores_positive': json.loads(article.scores_positive) if article.scores_positive else [],
|
||||
'scores_negative': json.loads(article.scores_negative) if article.scores_negative else []
|
||||
})
|
||||
|
||||
return jsonify({
|
||||
'message': f'{uploaded_count} مقاله با موفقیت آپلود شد',
|
||||
'articles': articles_list
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error in upload_articles: {str(e)}")
|
||||
return jsonify({'error': f'خطا در پردازش فایل: {str(e)}'}), 500
|
||||
|
||||
@dashboard_blueprint.route('/my_articles')
|
||||
@login_required
|
||||
def my_articles():
|
||||
"""مقالات تخصیص داده شده به کاربر"""
|
||||
from model.models import Article
|
||||
|
||||
status_filter = request.args.get('status', 'all')
|
||||
query = Article.query.filter_by(user_id=current_user.id)
|
||||
|
||||
if status_filter != 'all':
|
||||
query = query.filter_by(correction_status=status_filter)
|
||||
|
||||
articles = query.order_by(Article.updated_at.desc()).all()
|
||||
|
||||
return render_template('user/my_articles.html',
|
||||
articles=articles,
|
||||
status_filter=status_filter)
|
||||
|
||||
@dashboard_blueprint.route('/correct_article/<int:article_id>')
|
||||
@login_required
|
||||
def correct_article(article_id):
|
||||
"""صفحه تصحیح مقاله"""
|
||||
from model.models import Article, CorrectionLog, db
|
||||
|
||||
article = Article.query.filter_by(id=article_id, user_id=current_user.id).first_or_404()
|
||||
|
||||
if article.correction_status == 'pending':
|
||||
article.correction_status = 'in_progress'
|
||||
article.corrected_by = current_user.id
|
||||
db.session.commit()
|
||||
|
||||
return render_template('user/correct_article.html', article=article)
|
||||
|
||||
@dashboard_blueprint.route('/api/save_correction', methods=['POST'])
|
||||
@login_required
|
||||
def save_correction():
|
||||
"""ذخیره تصحیح کاربر"""
|
||||
from model.models import Article, CorrectionLog, db
|
||||
import json
|
||||
|
||||
data = request.json
|
||||
article_id = data.get('article_id')
|
||||
field = data.get('field')
|
||||
new_value = data.get('value')
|
||||
|
||||
article = Article.query.filter_by(id=article_id, user_id=current_user.id).first_or_404()
|
||||
|
||||
try:
|
||||
# ذخیره مقدار قدیمی برای لاگ
|
||||
old_value = getattr(article, field, '')
|
||||
|
||||
# بهروزرسانی فیلد
|
||||
setattr(article, field, new_value)
|
||||
article.correction_status = 'completed'
|
||||
article.corrected_at = datetime.utcnow()
|
||||
|
||||
# ایجاد لاگ
|
||||
log = CorrectionLog(
|
||||
article_id=article.id,
|
||||
user_id=current_user.id,
|
||||
action='corrected',
|
||||
field_name=field,
|
||||
old_value=old_value,
|
||||
new_value=new_value
|
||||
)
|
||||
db.session.add(log)
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({'success': True, 'message': 'تغییرات ذخیره شد'})
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
@dashboard_blueprint.route('/api/update_measure_completion', methods=['POST'])
|
||||
@login_required
|
||||
def update_measure_completion():
|
||||
"""بهروزرسانی وضعیت تکمیل یک معیار"""
|
||||
try:
|
||||
data = request.get_json()
|
||||
if not data:
|
||||
return jsonify({'error': 'دادههای JSON دریافت نشد'}), 400
|
||||
|
||||
article_id = data.get('article_id')
|
||||
measure_index = data.get('measure_index')
|
||||
is_completed = data.get('is_completed')
|
||||
|
||||
if article_id is None:
|
||||
return jsonify({'error': 'شناسه مقاله ارسال نشد'}), 400
|
||||
|
||||
if measure_index is None:
|
||||
return jsonify({'error': 'شماره معیار ارسال نشد'}), 400
|
||||
|
||||
if is_completed is None:
|
||||
return jsonify({'error': 'وضعیت تکمیل ارسال نشد'}), 400
|
||||
|
||||
# پیدا کردن مقاله
|
||||
article = Article.query.filter_by(id=article_id, user_id=current_user.id).first()
|
||||
if not article:
|
||||
return jsonify({'error': 'مقاله یافت نشد'}), 404
|
||||
|
||||
# دریافت وضعیتهای تکمیل فعلی
|
||||
measures_completed = []
|
||||
if article.measures_completed:
|
||||
try:
|
||||
measures_completed = json.loads(article.measures_completed)
|
||||
except json.JSONDecodeError:
|
||||
measures_completed = []
|
||||
|
||||
# اطمینان از اندازه آرایه
|
||||
max_measures = max(
|
||||
len(json.loads(article.measures_positive)) if article.measures_positive else 0,
|
||||
len(json.loads(article.measures_negative)) if article.measures_negative else 0
|
||||
)
|
||||
|
||||
while len(measures_completed) <= measure_index:
|
||||
measures_completed.append(False)
|
||||
|
||||
# بهروزرسانی وضعیت معیار
|
||||
measures_completed[measure_index] = bool(is_completed)
|
||||
|
||||
# ذخیره در دیتابیس
|
||||
article.measures_completed = json.dumps(measures_completed, ensure_ascii=False)
|
||||
|
||||
# محاسبه و بهروزرسانی وضعیت کلی مقاله
|
||||
total_measures = max_measures
|
||||
completed_count = sum(measures_completed[:total_measures]) if total_measures > 0 else 0
|
||||
|
||||
if total_measures == 0:
|
||||
article.correction_status = 'pending'
|
||||
elif completed_count == 0:
|
||||
article.correction_status = 'pending'
|
||||
elif completed_count < total_measures:
|
||||
article.correction_status = 'in_progress'
|
||||
else:
|
||||
article.correction_status = 'completed'
|
||||
|
||||
article.updated_at = datetime.utcnow()
|
||||
db.session.commit()
|
||||
|
||||
# ایجاد لاگ برای ذخیره تغییرات
|
||||
try:
|
||||
log = CorrectionLog(
|
||||
article_id=article.id,
|
||||
user_id=current_user.id,
|
||||
action='measure_completion_updated',
|
||||
field_name=f'measure_{measure_index}_completed',
|
||||
old_value='False' if not is_completed else 'True',
|
||||
new_value='True' if is_completed else 'False',
|
||||
details=json.dumps({
|
||||
'measure_index': measure_index,
|
||||
'is_completed': is_completed,
|
||||
'total_completed': completed_count,
|
||||
'total_measures': total_measures
|
||||
}, ensure_ascii=False)
|
||||
)
|
||||
db.session.add(log)
|
||||
db.session.commit()
|
||||
except Exception as log_error:
|
||||
print(f"خطا در ذخیره لاگ: {log_error}")
|
||||
# در صورت خطا در لاگ، commit اصلی را rollback نکن
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': 'وضعیت تکمیل معیار با موفقیت بهروزرسانی شد',
|
||||
'data': {
|
||||
'article_id': article.id,
|
||||
'measure_index': measure_index,
|
||||
'is_completed': is_completed,
|
||||
'total_completed': completed_count,
|
||||
'total_measures': total_measures,
|
||||
'article_status': article.correction_status
|
||||
}
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error in update_measure_completion: {str(e)}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
db.session.rollback()
|
||||
return jsonify({'error': f'خطا در بهروزرسانی وضعیت تکمیل: {str(e)}'}), 500
|
||||
|
||||
@dashboard_blueprint.route('/api/articles')
|
||||
@login_required
|
||||
def get_articles():
|
||||
"""دریافت لیست مقالات کاربر"""
|
||||
articles = Article.query.filter_by(user_id=current_user.id).all()
|
||||
|
||||
articles_list = []
|
||||
for article in articles:
|
||||
# محاسبه وضعیت بر اساس معیارهای تکمیل شده
|
||||
measures_completed = []
|
||||
if article.measures_completed:
|
||||
try:
|
||||
measures_completed = json.loads(article.measures_completed)
|
||||
except json.JSONDecodeError:
|
||||
measures_completed = []
|
||||
|
||||
# محاسبه وضعیت کلی
|
||||
max_measures = max(
|
||||
len(json.loads(article.measures_positive)) if article.measures_positive else 0,
|
||||
len(json.loads(article.measures_negative)) if article.measures_negative else 0
|
||||
)
|
||||
|
||||
completed_count = sum(measures_completed[:max_measures]) if max_measures > 0 else 0
|
||||
|
||||
# تعیین وضعیت مقاله
|
||||
if max_measures == 0:
|
||||
article_status = 'pending'
|
||||
elif completed_count == 0:
|
||||
article_status = 'pending'
|
||||
elif completed_count < max_measures:
|
||||
article_status = 'in_progress'
|
||||
else:
|
||||
article_status = 'completed'
|
||||
|
||||
articles_list.append({
|
||||
'id': article.id,
|
||||
'article_id': article.article_id,
|
||||
'name': article.name,
|
||||
'institute': article.institute,
|
||||
'abstract': article.abstract,
|
||||
'pdf_path': article.pdf_path,
|
||||
'measures_positive': json.loads(article.measures_positive) if article.measures_positive else [],
|
||||
'measures_negative': json.loads(article.measures_negative) if article.measures_negative else [],
|
||||
'responses_positive': json.loads(article.responses_positive) if article.responses_positive else [],
|
||||
'responses_negative': json.loads(article.responses_negative) if article.responses_negative else [],
|
||||
'scores_positive': json.loads(article.scores_positive) if article.scores_positive else [],
|
||||
'scores_negative': json.loads(article.scores_negative) if article.scores_negative else [],
|
||||
'measures_completed': measures_completed, # وضعیت تکمیل هر معیار
|
||||
'status': article_status, # وضعیت کلی مقاله
|
||||
'correction_status': article.correction_status,
|
||||
'measures_count': max_measures, # تعداد کل معیارها
|
||||
'completed_measures': completed_count # تعداد معیارهای تکمیل شده
|
||||
})
|
||||
|
||||
return jsonify(articles_list)
|
||||
|
||||
@dashboard_blueprint.route('/api/update_response', methods=['POST'])
|
||||
@login_required
|
||||
def update_response():
|
||||
"""ذخیره مستقیم پاسخ ویرایش شده و امتیازها"""
|
||||
data = request.json
|
||||
article_id = data.get('article_id')
|
||||
measure_index = data.get('measure_index')
|
||||
measure_type = data.get('measure_type')
|
||||
edited_response = data.get('edited_response')
|
||||
|
||||
# دریافت امتیازهای جدید (اگر ارسال شده باشند)
|
||||
positive_score = data.get('positive_score')
|
||||
negative_score = data.get('negative_score')
|
||||
score = data.get('score') # امتیاز کلی برای backward compatibility
|
||||
|
||||
if not all([article_id, measure_index is not None, measure_type, edited_response]):
|
||||
return jsonify({'error': 'دادههای ناقص ارسال شده است'}), 400
|
||||
|
||||
try:
|
||||
article = Article.query.filter_by(id=article_id, user_id=current_user.id).first()
|
||||
if not article:
|
||||
return jsonify({'error': 'مقاله یافت نشد'}), 404
|
||||
|
||||
# ذخیره پاسخ
|
||||
if measure_type == 'positive':
|
||||
responses = json.loads(article.responses_positive) if article.responses_positive else []
|
||||
if len(responses) > measure_index:
|
||||
responses[measure_index] = edited_response
|
||||
else:
|
||||
while len(responses) <= measure_index:
|
||||
responses.append("")
|
||||
responses[measure_index] = edited_response
|
||||
article.responses_positive = json.dumps(responses, ensure_ascii=False)
|
||||
|
||||
# ذخیره امتیاز مثبت اگر ارسال شده باشد
|
||||
if positive_score is not None:
|
||||
scores = json.loads(article.scores_positive) if article.scores_positive else []
|
||||
if len(scores) > measure_index:
|
||||
scores[measure_index] = float(positive_score)
|
||||
else:
|
||||
while len(scores) <= measure_index:
|
||||
scores.append(0.0)
|
||||
scores[measure_index] = float(positive_score)
|
||||
article.scores_positive = json.dumps(scores, ensure_ascii=False)
|
||||
else:
|
||||
responses = json.loads(article.responses_negative) if article.responses_negative else []
|
||||
if len(responses) > measure_index:
|
||||
responses[measure_index] = edited_response
|
||||
else:
|
||||
while len(responses) <= measure_index:
|
||||
responses.append("")
|
||||
responses[measure_index] = edited_response
|
||||
article.responses_negative = json.dumps(responses, ensure_ascii=False)
|
||||
|
||||
# ذخیره امتیاز منفی اگر ارسال شده باشد
|
||||
if negative_score is not None:
|
||||
scores = json.loads(article.scores_negative) if article.scores_negative else []
|
||||
if len(scores) > measure_index:
|
||||
scores[measure_index] = float(negative_score)
|
||||
else:
|
||||
while len(scores) <= measure_index:
|
||||
scores.append(0.0)
|
||||
scores[measure_index] = float(negative_score)
|
||||
article.scores_negative = json.dumps(scores, ensure_ascii=False)
|
||||
|
||||
# همچنین ذخیره امتیاز کلی برای backward compatibility
|
||||
if score is not None and measure_type == 'positive':
|
||||
scores = json.loads(article.scores_positive) if article.scores_positive else []
|
||||
if len(scores) > measure_index:
|
||||
scores[measure_index] = float(score)
|
||||
else:
|
||||
while len(scores) <= measure_index:
|
||||
scores.append(0.0)
|
||||
scores[measure_index] = float(score)
|
||||
article.scores_positive = json.dumps(scores, ensure_ascii=False)
|
||||
elif score is not None and measure_type == 'negative':
|
||||
scores = json.loads(article.scores_negative) if article.scores_negative else []
|
||||
if len(scores) > measure_index:
|
||||
scores[measure_index] = float(score)
|
||||
else:
|
||||
while len(scores) <= measure_index:
|
||||
scores.append(0.0)
|
||||
scores[measure_index] = float(score)
|
||||
article.scores_negative = json.dumps(scores, ensure_ascii=False)
|
||||
|
||||
# بررسی تکمیل خودکار معیار
|
||||
responses_positive = json.loads(article.responses_positive) if article.responses_positive else []
|
||||
responses_negative = json.loads(article.responses_negative) if article.responses_negative else []
|
||||
measures_completed = json.loads(article.measures_completed) if article.measures_completed else []
|
||||
|
||||
# اگر هر دو پاسخ برای این معیار پر شدهاند، به صورت خودکار تکمیل را علامت بزن
|
||||
if measure_index < len(responses_positive) and measure_index < len(responses_negative):
|
||||
pos_response = responses_positive[measure_index] if measure_index < len(responses_positive) else ""
|
||||
neg_response = responses_negative[measure_index] if measure_index < len(responses_negative) else ""
|
||||
|
||||
if pos_response.strip() and pos_response != "پاسخی موجود نیست" and \
|
||||
neg_response.strip() and neg_response != "پاسخی موجود نیست":
|
||||
|
||||
# اطمینان از اندازه آرایه
|
||||
while len(measures_completed) <= measure_index:
|
||||
measures_completed.append(False)
|
||||
|
||||
measures_completed[measure_index] = True
|
||||
article.measures_completed = json.dumps(measures_completed, ensure_ascii=False)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
# ایجاد لاگ
|
||||
try:
|
||||
log = CorrectionLog(
|
||||
article_id=article.id,
|
||||
user_id=current_user.id,
|
||||
action='response_updated',
|
||||
field_name=f'{measure_type}_response_{measure_index}',
|
||||
old_value='',
|
||||
new_value=edited_response[:200], # فقط 200 کاراکتر اول
|
||||
details=json.dumps({
|
||||
'measure_index': measure_index,
|
||||
'measure_type': measure_type,
|
||||
'positive_score': positive_score,
|
||||
'negative_score': negative_score
|
||||
}, ensure_ascii=False)
|
||||
)
|
||||
db.session.add(log)
|
||||
db.session.commit()
|
||||
except Exception as log_error:
|
||||
print(f"خطا در ذخیره لاگ: {log_error}")
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': 'پاسخ با موفقیت بهروزرسانی شد'
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error in update_response: {str(e)}")
|
||||
db.session.rollback()
|
||||
return jsonify({'error': f'خطا در بهروزرسانی پاسخ: {str(e)}'}), 500
|
||||
|
||||
@dashboard_blueprint.route('/api/article/<int:article_id>')
|
||||
@login_required
|
||||
def get_article_details(article_id):
|
||||
"""دریافت جزئیات یک مقاله خاص"""
|
||||
article = Article.query.filter_by(id=article_id, user_id=current_user.id).first()
|
||||
|
||||
if not article:
|
||||
return jsonify({'error': 'مقاله یافت نشد'}), 404
|
||||
|
||||
# محاسبه وضعیت بر اساس معیارهای تکمیل شده
|
||||
measures_completed = []
|
||||
if article.measures_completed:
|
||||
try:
|
||||
measures_completed = json.loads(article.measures_completed)
|
||||
except json.JSONDecodeError:
|
||||
measures_completed = []
|
||||
|
||||
# محاسبه وضعیت کلی
|
||||
max_measures = max(
|
||||
len(json.loads(article.measures_positive)) if article.measures_positive else 0,
|
||||
len(json.loads(article.measures_negative)) if article.measures_negative else 0
|
||||
)
|
||||
|
||||
completed_count = sum(measures_completed[:max_measures]) if max_measures > 0 else 0
|
||||
|
||||
# تعیین وضعیت مقاله
|
||||
if max_measures == 0:
|
||||
article_status = 'pending'
|
||||
elif completed_count == 0:
|
||||
article_status = 'pending'
|
||||
elif completed_count < max_measures:
|
||||
article_status = 'in_progress'
|
||||
else:
|
||||
article_status = 'completed'
|
||||
|
||||
article_data = {
|
||||
'id': article.id,
|
||||
'article_id': article.article_id,
|
||||
'name': article.name,
|
||||
'institute': article.institute,
|
||||
'abstract': article.abstract,
|
||||
'pdf_path': article.pdf_path,
|
||||
'measures_positive': json.loads(article.measures_positive) if article.measures_positive else [],
|
||||
'measures_negative': json.loads(article.measures_negative) if article.measures_negative else [],
|
||||
'responses_positive': json.loads(article.responses_positive) if article.responses_positive else [],
|
||||
'responses_negative': json.loads(article.responses_negative) if article.responses_negative else [],
|
||||
'scores_positive': json.loads(article.scores_positive) if article.scores_positive else [],
|
||||
'scores_negative': json.loads(article.scores_negative) if article.scores_negative else [],
|
||||
'measures_completed': measures_completed,
|
||||
'status': article_status,
|
||||
'correction_status': article.correction_status,
|
||||
'measures_count': max_measures,
|
||||
'completed_measures': completed_count,
|
||||
'progress_percentage': round((completed_count / max_measures) * 100, 2) if max_measures > 0 else 0,
|
||||
'created_at': article.created_at.isoformat() if article.created_at else None,
|
||||
'updated_at': article.updated_at.isoformat() if article.updated_at else None
|
||||
}
|
||||
|
||||
return jsonify(article_data)
|
||||
|
||||
@dashboard_blueprint.route('/api/sync_article_status/<int:article_id>', methods=['POST'])
|
||||
@login_required
|
||||
def sync_article_status(article_id):
|
||||
"""همگامسازی وضعیت مقاله بر اساس پاسخها و وضعیت تکمیل"""
|
||||
article = Article.query.filter_by(id=article_id, user_id=current_user.id).first()
|
||||
|
||||
if not article:
|
||||
return jsonify({'error': 'مقاله یافت نشد'}), 404
|
||||
|
||||
try:
|
||||
# دریافت دادههای فعلی
|
||||
measures_positive = json.loads(article.measures_positive) if article.measures_positive else []
|
||||
measures_negative = json.loads(article.measures_negative) if article.measures_negative else []
|
||||
responses_positive = json.loads(article.responses_positive) if article.responses_positive else []
|
||||
responses_negative = json.loads(article.responses_negative) if article.responses_negative else []
|
||||
measures_completed = json.loads(article.measures_completed) if article.measures_completed else []
|
||||
|
||||
max_measures = max(len(measures_positive), len(measures_negative))
|
||||
|
||||
# بررسی وضعیت تکمیل بر اساس پاسخها
|
||||
updated_measures_completed = []
|
||||
for i in range(max_measures):
|
||||
# اگر از قبل تکمیل علامت خورده، آن را حفظ کن
|
||||
if i < len(measures_completed) and measures_completed[i]:
|
||||
updated_measures_completed.append(True)
|
||||
continue
|
||||
|
||||
# بررسی پاسخها
|
||||
pos_response = responses_positive[i] if i < len(responses_positive) else ""
|
||||
neg_response = responses_negative[i] if i < len(responses_negative) else ""
|
||||
|
||||
is_pos_valid = pos_response and pos_response.strip() and pos_response != "پاسخی موجود نیست"
|
||||
is_neg_valid = neg_response and neg_response.strip() and neg_response != "پاسخی موجود نیست"
|
||||
|
||||
updated_measures_completed.append(is_pos_valid and is_neg_valid)
|
||||
|
||||
# ذخیره وضعیت بهروزرسانی شده
|
||||
article.measures_completed = json.dumps(updated_measures_completed, ensure_ascii=False)
|
||||
|
||||
# محاسبه وضعیت کلی
|
||||
completed_count = sum(updated_measures_completed)
|
||||
|
||||
if max_measures == 0:
|
||||
article.correction_status = 'pending'
|
||||
elif completed_count == 0:
|
||||
article.correction_status = 'pending'
|
||||
elif completed_count < max_measures:
|
||||
article.correction_status = 'in_progress'
|
||||
else:
|
||||
article.correction_status = 'completed'
|
||||
|
||||
article.updated_at = datetime.utcnow()
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': 'وضعیت مقاله همگامسازی شد',
|
||||
'data': {
|
||||
'article_id': article.id,
|
||||
'total_measures': max_measures,
|
||||
'completed_measures': completed_count,
|
||||
'article_status': article.correction_status,
|
||||
'progress_percentage': round((completed_count / max_measures) * 100, 2) if max_measures > 0 else 0
|
||||
}
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error in sync_article_status: {str(e)}")
|
||||
db.session.rollback()
|
||||
return jsonify({'error': f'خطا در همگامسازی وضعیت: {str(e)}'}), 500
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
from datetime import datetime
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from flask_login import UserMixin
|
||||
from flask_bcrypt import Bcrypt
|
||||
|
||||
db = SQLAlchemy()
|
||||
bcrypt = Bcrypt()
|
||||
|
||||
class User(UserMixin, db.Model):
|
||||
__tablename__ = 'users'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
username = db.Column(db.String(80), unique=True, nullable=False)
|
||||
email = db.Column(db.String(120), unique=True, nullable=False)
|
||||
password_hash = db.Column(db.String(255), nullable=False)
|
||||
|
||||
role = db.Column(db.String(20), default='user') # 'admin', 'user'
|
||||
is_active = db.Column(db.Boolean, default=True)
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
|
||||
def set_password(self, password):
|
||||
"""Hash and set the password"""
|
||||
self.password_hash = bcrypt.generate_password_hash(password).decode('utf-8')
|
||||
|
||||
def check_password(self, password):
|
||||
"""Check if the provided password matches the hash"""
|
||||
return bcrypt.check_password_hash(self.password_hash, password)
|
||||
|
||||
def __repr__(self):
|
||||
return f'<User {self.username}>'
|
||||
|
||||
class DataFile(db.Model):
|
||||
__tablename__ = 'data_files'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
filename = db.Column(db.String(255), nullable=False)
|
||||
original_filename = db.Column(db.String(255), nullable=False)
|
||||
file_path = db.Column(db.String(500), nullable=False)
|
||||
file_size = db.Column(db.Integer, nullable=False)
|
||||
file_type = db.Column(db.String(50), nullable=False) # 'json', 'pdf', etc.
|
||||
uploader_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
|
||||
status = db.Column(db.String(20), default='pending') # 'pending', 'processing', 'completed', 'error'
|
||||
description = db.Column(db.Text)
|
||||
uploaded_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
processed_at = db.Column(db.DateTime)
|
||||
|
||||
uploader = db.relationship('User', backref='uploaded_files')
|
||||
|
||||
class Article(db.Model):
|
||||
__tablename__ = 'articles'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
|
||||
article_id = db.Column(db.String(255), nullable=False)
|
||||
name = db.Column(db.String(500), nullable=False)
|
||||
institute = db.Column(db.String(500))
|
||||
abstract = db.Column(db.Text)
|
||||
|
||||
measures_positive = db.Column(db.Text)
|
||||
measures_negative = db.Column(db.Text)
|
||||
responses_positive = db.Column(db.Text)
|
||||
responses_negative = db.Column(db.Text)
|
||||
scores_positive = db.Column(db.Text)
|
||||
scores_negative = db.Column(db.Text)
|
||||
|
||||
measures_completed = db.Column(db.Text)
|
||||
|
||||
pdf_path = db.Column(db.String(500))
|
||||
data_file_id = db.Column(db.Integer, db.ForeignKey('data_files.id'))
|
||||
word_count = db.Column(db.Integer)
|
||||
page_count = db.Column(db.Integer)
|
||||
file_size = db.Column(db.Integer)
|
||||
|
||||
correction_status = db.Column(db.String(20), default='pending')
|
||||
corrected_by = db.Column(db.Integer, db.ForeignKey('users.id'))
|
||||
corrected_at = db.Column(db.DateTime)
|
||||
reviewer_id = db.Column(db.Integer, db.ForeignKey('users.id'))
|
||||
reviewed_at = db.Column(db.DateTime)
|
||||
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
|
||||
user = db.relationship('User', foreign_keys=[user_id], backref=db.backref('articles', lazy=True))
|
||||
|
||||
corrector = db.relationship('User', foreign_keys=[corrected_by], backref='corrected_articles')
|
||||
reviewer = db.relationship('User', foreign_keys=[reviewer_id], backref='reviewed_articles')
|
||||
data_file = db.relationship('DataFile', backref='articles')
|
||||
|
||||
class ArticleResponse(db.Model):
|
||||
__tablename__ = 'article_responses'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
article_id = db.Column(db.Integer, db.ForeignKey('articles.id'), nullable=False)
|
||||
measure_index = db.Column(db.Integer, nullable=False)
|
||||
measure_type = db.Column(db.String(10), nullable=False)
|
||||
original_response = db.Column(db.Text)
|
||||
edited_response = db.Column(db.Text)
|
||||
ai_generated_response = 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)
|
||||
|
||||
article = db.relationship('Article', backref=db.backref('responses', lazy=True))
|
||||
|
||||
class ChatSession(db.Model):
|
||||
__tablename__ = 'chat_sessions'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
|
||||
|
||||
article_id = db.Column(db.Integer, db.ForeignKey('articles.id'))
|
||||
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
user = db.relationship('User', backref=db.backref('chat_sessions', lazy=True))
|
||||
messages = db.relationship('Message', backref='session', lazy=True, cascade='all, delete-orphan')
|
||||
|
||||
article = db.relationship('Article', backref='chat_sessions')
|
||||
|
||||
class Message(db.Model):
|
||||
__tablename__ = 'messages'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
session_id = db.Column(db.Integer, db.ForeignKey('chat_sessions.id'), nullable=False)
|
||||
role = db.Column(db.String(20), nullable=False)
|
||||
content = db.Column(db.Text, nullable=False)
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
|
||||
class CorrectionLog(db.Model):
|
||||
__tablename__ = 'correction_logs'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
article_id = db.Column(db.Integer, db.ForeignKey('articles.id'), nullable=False)
|
||||
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
|
||||
action = db.Column(db.String(50), nullable=False) # 'created', 'updated', 'corrected', 'reviewed'
|
||||
field_name = db.Column(db.String(100))
|
||||
old_value = db.Column(db.Text)
|
||||
new_value = db.Column(db.Text)
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
|
||||
# ارتباطها
|
||||
article = db.relationship('Article', backref='correction_logs')
|
||||
user = db.relationship('User', backref='correction_actions')
|
||||
@@ -0,0 +1,10 @@
|
||||
flask==3.0.0
|
||||
gunicorn==21.2.0
|
||||
requests==2.31.0
|
||||
jinja2==3.1.2
|
||||
werkzeug==3.0.1
|
||||
flask-cors==4.0.0
|
||||
flask_login
|
||||
flask_sqlalchemy
|
||||
flask_bcrypt
|
||||
python-dotenv
|
||||
@@ -0,0 +1,34 @@
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.append(os.path.join(os.path.dirname(__file__)))
|
||||
|
||||
from app import create_app
|
||||
from model.models import db, User
|
||||
|
||||
def setup_admin():
|
||||
app = create_app()
|
||||
|
||||
with app.app_context():
|
||||
try:
|
||||
db.drop_all()
|
||||
|
||||
db.create_all()
|
||||
|
||||
admin_user = User(
|
||||
username='admin',
|
||||
email='admin@admin.com',
|
||||
role='admin'
|
||||
)
|
||||
admin_user.set_password('admin123')
|
||||
|
||||
db.session.add(admin_user)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
except Exception as e:
|
||||
print(f'❌ خطا در راهاندازی: {e}')
|
||||
db.session.rollback()
|
||||
|
||||
if __name__ == '__main__':
|
||||
setup_admin()
|
||||
File diff suppressed because one or more lines are too long
Vendored
+9
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 261 KiB |
@@ -0,0 +1,257 @@
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const chatContainer = document.getElementById('chatContainer');
|
||||
const userInput = document.getElementById('userInput');
|
||||
const sendBtn = document.getElementById('sendBtn');
|
||||
const clearBtn = document.getElementById('clearChat');
|
||||
const topKSlider = document.getElementById('top_k_slider');
|
||||
const topKValueDisplay = document.getElementById('top_k_value');
|
||||
|
||||
let currentTopK = parseInt(topKSlider.value) || 5;
|
||||
|
||||
topKSlider.addEventListener('input', function () {
|
||||
currentTopK = parseInt(this.value);
|
||||
topKValueDisplay.textContent = currentTopK;
|
||||
});
|
||||
|
||||
function markdownToHtml(text) {
|
||||
if (typeof text !== 'string') return text;
|
||||
|
||||
text = text.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
|
||||
text = text.replace(/\*(.*?)\*/g, '<em>$1</em>');
|
||||
text = text.replace(/`(.*?)`/g, '<code>$1</code>');
|
||||
text = text.replace(/^\s*\*\s+(.*)$/gm, '<li>$1</li>');
|
||||
if (text.includes('<li>')) {
|
||||
text = text.replace(/(<li>.*<\/li>)/gs, '<ul>$1</ul>');
|
||||
}
|
||||
text = text.replace(/\n/g, '<br>');
|
||||
return text;
|
||||
}
|
||||
|
||||
function addMessage(role, content, isStreaming = false) {
|
||||
const messageDiv = document.createElement('div');
|
||||
messageDiv.className = `flex ${role === 'user' ? 'justify-end' : 'justify-start'} mb-4 animate-fadeIn`;
|
||||
|
||||
if (role === 'assistant' && !isStreaming) {
|
||||
messageDiv.innerHTML = `
|
||||
<div class="max-w-[85%] flex gap-3">
|
||||
<div class="w-9 h-9 rounded-full bg-gradient-to-br from-iran-green to-green-600 flex items-center justify-center text-white">🤖</div>
|
||||
<div class="bg-white border border-gray-200 p-4 rounded-2xl rounded-bl-none shadow-sm break-words">
|
||||
<strong class="text-iran-green block mb-1">یارا</strong>
|
||||
${simpleMarkdownToHtml(content)}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
} else if (role === 'user') {
|
||||
messageDiv.innerHTML = `
|
||||
<div class="max-w-[85%] flex gap-3">
|
||||
<div class="bg-blue-500 text-white p-4 rounded-2xl rounded-br-none shadow-sm break-words">
|
||||
${simpleMarkdownToHtml(content)}
|
||||
</div>
|
||||
<div class="w-9 h-9 rounded-full bg-blue-600 flex items-center justify-center text-white">👤</div>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
messageDiv.innerHTML = `
|
||||
<div class="max-w-[85%] flex gap-3">
|
||||
<div class="w-9 h-9 rounded-full bg-gradient-to-br from-iran-green to-green-600 flex items-center justify-center text-white">🤖</div>
|
||||
<div class="bg-white border border-gray-200 p-4 rounded-2xl rounded-bl-none shadow-sm break-words">
|
||||
<strong class="text-iran-green block mb-1">یارا</strong>
|
||||
${content}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
chatContainer.appendChild(messageDiv);
|
||||
chatContainer.scrollTop = chatContainer.scrollHeight;
|
||||
}
|
||||
|
||||
function addTypingIndicator() {
|
||||
const messageDiv = document.createElement('div');
|
||||
messageDiv.className = 'chat-message assistant-message';
|
||||
messageDiv.id = 'typingPlaceholder';
|
||||
|
||||
const avatarDiv = document.createElement('div');
|
||||
avatarDiv.className = 'avatar';
|
||||
avatarDiv.textContent = '🤖';
|
||||
|
||||
const contentDiv = document.createElement('div');
|
||||
contentDiv.className = 'message-content';
|
||||
contentDiv.innerHTML = `<strong>یارا:</strong> <span class="typing-dots">در حال پاسخدهی...</span>`;
|
||||
|
||||
messageDiv.appendChild(avatarDiv);
|
||||
messageDiv.appendChild(contentDiv);
|
||||
chatContainer.appendChild(messageDiv);
|
||||
chatContainer.scrollTop = chatContainer.scrollHeight;
|
||||
}
|
||||
|
||||
function updateTypingContent(text) {
|
||||
const placeholder = document.getElementById('typingPlaceholder');
|
||||
if (placeholder) {
|
||||
// ✅ تبدیل Markdown به HTML حتی در حالت استریم
|
||||
placeholder.querySelector('.message-content').innerHTML = `<strong>یارا:</strong> ${markdownToHtml(text)}`;
|
||||
}
|
||||
}
|
||||
|
||||
function removeTypingIndicator() {
|
||||
const placeholder = document.getElementById('typingPlaceholder');
|
||||
if (placeholder) placeholder.remove();
|
||||
}
|
||||
|
||||
function displayChunks(chunks) {
|
||||
const existingSection = document.querySelector('.chunks-section');
|
||||
if (existingSection) existingSection.remove();
|
||||
|
||||
if (!chunks || chunks.length === 0) return;
|
||||
|
||||
const section = document.createElement('div');
|
||||
section.className = 'chunks-section';
|
||||
|
||||
// ✅ جعبه توضیحی با Markdown
|
||||
const explanation = document.createElement('div');
|
||||
explanation.className = 'explanation-box';
|
||||
explanation.innerHTML = `
|
||||
<p>🧠 <strong>پاسخ من بر اساس این اسناد تولید شده:</strong></p>
|
||||
<ul>
|
||||
${chunks.map((chunk, i) => {
|
||||
const source = chunk.metadata?.source || 'نامشخص';
|
||||
const page = chunk.metadata?.page || '?';
|
||||
const score = chunk.metadata?.score?.toFixed(2) || 'N/A';
|
||||
return `<li>📄 <strong>${source}</strong> (صفحه ${page}) — امتیاز: ${score}</li>`;
|
||||
}).join('')}
|
||||
</ul>
|
||||
`;
|
||||
section.appendChild(explanation);
|
||||
|
||||
// ✅ جزئیات چانکها با Markdown
|
||||
const detailsHeader = document.createElement('h4');
|
||||
detailsHeader.textContent = '📄 محتوای کامل اسناد مرتبط:';
|
||||
section.appendChild(detailsHeader);
|
||||
|
||||
chunks.forEach(chunk => {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'chunk-card';
|
||||
|
||||
const score = chunk.metadata?.score !== undefined ? chunk.metadata.score.toFixed(2) : 'N/A';
|
||||
const source = chunk.metadata?.source || 'نامشخص';
|
||||
const page = chunk.metadata?.page || 'نامشخص';
|
||||
|
||||
// ✅ تبدیل Markdown در محتوای چانکها
|
||||
card.innerHTML = `
|
||||
<div class="chunk-meta">
|
||||
<span class="chunk-score">امتیاز شباهت: ${score}</span>
|
||||
<span class="source-tag">📄 منبع: ${source}</span>
|
||||
<span class="source-tag">📄 صفحه: ${page}</span>
|
||||
</div>
|
||||
<div class="chunk-content">${markdownToHtml(truncateText(chunk.page_content, 400))}</div>
|
||||
`;
|
||||
section.appendChild(card);
|
||||
});
|
||||
|
||||
chatContainer.insertAdjacentElement('afterend', section);
|
||||
}
|
||||
|
||||
function truncateText(text, maxLength) {
|
||||
if (text.length <= maxLength) return text;
|
||||
return text.slice(0, maxLength) + '...';
|
||||
}
|
||||
|
||||
async function sendMessage() {
|
||||
const message = userInput.value.trim();
|
||||
if (!message) return;
|
||||
|
||||
addMessage('user', message);
|
||||
userInput.value = '';
|
||||
addTypingIndicator();
|
||||
|
||||
try {
|
||||
const response = await fetch('/send_message', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
message: message,
|
||||
top_k: currentTopK
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder('utf-8');
|
||||
let buffer = '';
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
|
||||
let boundary = buffer.indexOf('\n\n');
|
||||
while (boundary !== -1) {
|
||||
let line = buffer.slice(0, boundary).trim();
|
||||
buffer = buffer.slice(boundary + 2);
|
||||
|
||||
if (line.startsWith('data: ')) {
|
||||
try {
|
||||
const jsonStr = line.slice(6); // remove "data: "
|
||||
const data = JSON.parse(jsonStr);
|
||||
|
||||
if (data.type === 'stream') {
|
||||
updateTypingContent(data.content);
|
||||
} else if (data.type === 'final') {
|
||||
removeTypingIndicator();
|
||||
addMessage('assistant', data.content);
|
||||
} else if (data.type === 'chunks') {
|
||||
displayChunks(data.content);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("JSON parse error:", e);
|
||||
}
|
||||
}
|
||||
|
||||
boundary = buffer.indexOf('\n\n');
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error("Fetch error:", error);
|
||||
removeTypingIndicator();
|
||||
addMessage('assistant', "❌ خطایی در ارتباط با سرور رخ داد. لطفاً دوباره تلاش کنید.");
|
||||
}
|
||||
}
|
||||
|
||||
sendBtn.addEventListener('click', sendMessage);
|
||||
|
||||
userInput.addEventListener('keypress', function (e) {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
sendMessage();
|
||||
}
|
||||
});
|
||||
|
||||
clearBtn.addEventListener('click', async function () {
|
||||
try {
|
||||
const response = await fetch('/clear_chat', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
chatContainer.innerHTML = '';
|
||||
const existingSection = document.querySelector('.chunks-section');
|
||||
if (existingSection) existingSection.remove();
|
||||
|
||||
addMessage('assistant', "سلام! 😊 من یارا هستم — دستیار هوشمند شما. چطور میتونم کمکتون کنم؟");
|
||||
} else {
|
||||
alert("خطا در پاککردن گفتگو");
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Clear chat error:", e);
|
||||
alert("اتصال به سرور برقرار نشد");
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,493 @@
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Vazirmatn', 'Segoe UI', Tahoma, sans-serif;
|
||||
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
|
||||
color: #2c3e50;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* ✅ Fix: app-container should be ROW to place sidebar next to main */
|
||||
.app-container {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
flex-direction: row; /* 👈 Was column — now fixed to row */
|
||||
}
|
||||
|
||||
/* Sidebar */
|
||||
.sidebar {
|
||||
width: 300px;
|
||||
background: #fff;
|
||||
padding: 2rem;
|
||||
box-shadow: 2px 0 10px rgba(0,0,0,0.05);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
overflow-y: auto;
|
||||
flex-shrink: 0; /* Prevent shrinking on small screens */
|
||||
}
|
||||
|
||||
.sidebar img {
|
||||
width: 100%;
|
||||
border-radius: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.sidebar h2 {
|
||||
color: #1a237e;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.caption {
|
||||
color: #78909c;
|
||||
font-size: 0.9rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.info-box {
|
||||
background: #e3f2fd;
|
||||
padding: 1rem;
|
||||
border-radius: 1rem;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.info-box ul {
|
||||
padding-right: 1.5rem;
|
||||
}
|
||||
|
||||
/* Main Content Wrapper (now a COLUMN inside the row) */
|
||||
.main-wrapper {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.main {
|
||||
flex: 1; /* Takes remaining vertical space */
|
||||
padding: 2rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-y: auto; /* Allow scrolling if content is long */
|
||||
}
|
||||
|
||||
.main-header {
|
||||
background: linear-gradient(120deg, #1a237e, #3949ab);
|
||||
padding: 1.5rem;
|
||||
border-radius: 1.5rem;
|
||||
color: white;
|
||||
margin-bottom: 2rem;
|
||||
box-shadow: 0 8px 30px rgba(26, 35, 126, 0.2);
|
||||
}
|
||||
|
||||
.header-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.header-content .emoji {
|
||||
font-size: 3rem;
|
||||
}
|
||||
|
||||
.header-content h1 {
|
||||
margin: 0;
|
||||
font-size: 2.2rem;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.header-content p {
|
||||
margin: 0.5rem 0 0 0;
|
||||
opacity: 0.9;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
/* Chat Container */
|
||||
.chat-container {
|
||||
flex: 1; /* Fill available space in .main */
|
||||
overflow-y: auto;
|
||||
padding: 1rem 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.2rem;
|
||||
min-height: 0; /* Important: allows scrolling inside flex child */
|
||||
}
|
||||
|
||||
.chat-message {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 1rem;
|
||||
padding: 1.5rem;
|
||||
border-radius: 1.5rem;
|
||||
box-shadow: 0 5px 15px rgba(0,0,0,0.06);
|
||||
animation: fadeInUp 0.6s ease-out;
|
||||
max-width: 85%;
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from { opacity: 0; transform: translateY(20px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 50%;
|
||||
background: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 40px;
|
||||
box-shadow: 0 4px 10px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.assistant-message .avatar {
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% { box-shadow: 0 0 0 0 rgba(85, 139, 47, 0.4); }
|
||||
70% { box-shadow: 0 0 0 10px rgba(85, 139, 47, 0); }
|
||||
100% { box-shadow: 0 0 0 0 rgba(85, 139, 47, 0); }
|
||||
}
|
||||
|
||||
.user-message {
|
||||
background: linear-gradient(135deg, #e3f2fd 0%, #bbdefb 100%);
|
||||
border-right: 5px solid #1565c0;
|
||||
margin-right: auto;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.assistant-message {
|
||||
background: linear-gradient(135deg, #f1f8e9 0%, #dcedc8 100%);
|
||||
border-right: 5px solid #558b2f;
|
||||
margin-left: auto;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.message-content {
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* Typing indicator */
|
||||
.typing-indicator {
|
||||
display: inline-block;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background-color: #558b2f;
|
||||
margin: 0 3px;
|
||||
animation: typing 1s infinite;
|
||||
}
|
||||
|
||||
.typing-indicator:nth-child(2) { animation-delay: 0.2s; }
|
||||
.typing-indicator:nth-child(3) { animation-delay: 0.4s; }
|
||||
|
||||
@keyframes typing {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(-5px); }
|
||||
}
|
||||
|
||||
/* Retrieved Chunks */
|
||||
.chunks-section {
|
||||
margin: 2rem 0;
|
||||
}
|
||||
|
||||
.chunks-section h3 {
|
||||
color: #1a237e;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.chunk-card {
|
||||
background: white;
|
||||
padding: 1.5rem;
|
||||
border-radius: 1.2rem;
|
||||
margin: 1rem 0;
|
||||
border: 1px solid #e0e0e0;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.04);
|
||||
transition: all 0.3s ease;
|
||||
border-left: 4px solid #ff8a65;
|
||||
}
|
||||
|
||||
.chunk-card:hover {
|
||||
box-shadow: 0 8px 20px rgba(0,0,0,0.08);
|
||||
transform: translateX(8px);
|
||||
border-left-color: #e64a19;
|
||||
}
|
||||
|
||||
.chunk-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.7rem;
|
||||
margin-bottom: 1rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.chunk-score {
|
||||
font-weight: 800;
|
||||
color: #e64a19;
|
||||
font-size: 1.2rem;
|
||||
background: linear-gradient(135deg, #fff3e0, #ffe0b2);
|
||||
padding: 0.4rem 1rem;
|
||||
border-radius: 1.5rem;
|
||||
display: inline-block;
|
||||
box-shadow: 0 2px 5px rgba(230, 74, 25, 0.15);
|
||||
}
|
||||
|
||||
.source-tag {
|
||||
background: linear-gradient(135deg, #e3f2fd, #bbdefb);
|
||||
color: #0d47a1;
|
||||
padding: 0.4rem 1rem;
|
||||
border-radius: 1.5rem;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
display: inline-block;
|
||||
box-shadow: 0 2px 4px rgba(13, 71, 161, 0.1);
|
||||
}
|
||||
|
||||
.chunk-content {
|
||||
line-height: 1.8;
|
||||
font-size: 1.05rem;
|
||||
color: #455a64;
|
||||
background: #fafafa;
|
||||
padding: 1rem;
|
||||
border-radius: 0.8rem;
|
||||
border-right: 3px solid #e0e0e0;
|
||||
}
|
||||
|
||||
/* Input Area */
|
||||
.input-area {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-top: 1rem;
|
||||
align-items: flex-end;
|
||||
padding: 1rem 0;
|
||||
background: #fff; /* Optional: prevent overlap with gradient */
|
||||
}
|
||||
|
||||
.input-area textarea {
|
||||
flex: 1;
|
||||
border-radius: 2rem;
|
||||
padding: 1rem 1.5rem;
|
||||
font-size: 1.1rem;
|
||||
border: 2px solid #cfd8dc;
|
||||
box-shadow: inset 0 2px 5px rgba(0,0,0,0.05);
|
||||
min-height: 100px;
|
||||
resize: none;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.input-area textarea:focus {
|
||||
outline: none;
|
||||
border-color: #4caf50;
|
||||
box-shadow: 0 0 0 3px rgba(76, 175, 80, 0.25), inset 0 2px 5px rgba(0,0,0,0.08);
|
||||
}
|
||||
|
||||
.input-area button {
|
||||
border-radius: 2rem;
|
||||
padding: 0.7rem 2rem;
|
||||
font-weight: 700;
|
||||
font-size: 1.1rem;
|
||||
background: linear-gradient(135deg, #4caf50, #2e7d32);
|
||||
color: white;
|
||||
border: none;
|
||||
box-shadow: 0 5px 15px rgba(46, 125, 50, 0.3);
|
||||
cursor: pointer;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.input-area button:hover {
|
||||
transform: translateY(-3px) scale(1.03);
|
||||
box-shadow: 0 8px 25px rgba(46, 125, 50, 0.5);
|
||||
background: linear-gradient(135deg, #66bb6a, #388e3c);
|
||||
}
|
||||
|
||||
/* Footer — now inside main-wrapper, pushed down by flex:1 on .main */
|
||||
footer {
|
||||
text-align: center;
|
||||
padding: 2rem 0;
|
||||
color: #78909c;
|
||||
font-size: 0.95rem;
|
||||
border-top: 1px solid #eceff1;
|
||||
background: #fff;
|
||||
margin-top: auto; /* Works now because main-wrapper is column */
|
||||
}
|
||||
|
||||
footer a {
|
||||
color: #1e88e5;
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.btn-secondary {
|
||||
background: linear-gradient(135deg, #ff7043, #d84315);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.7rem 1rem;
|
||||
border-radius: 2rem;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: linear-gradient(135deg, #ff8a65, #e64a19);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
/* === زیباسازی تنظیمات عددی === */
|
||||
.setting-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin: 1.5rem 0;
|
||||
padding: 0.8rem 0;
|
||||
}
|
||||
|
||||
.setting-label {
|
||||
font-weight: 600;
|
||||
color: #1a237e;
|
||||
font-size: 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.number-input {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: 2px solid #cfd8dc;
|
||||
border-radius: 2rem;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 5px rgba(0,0,0,0.05);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.number-input:focus-within {
|
||||
border-color: #4caf50;
|
||||
box-shadow: 0 0 0 3px rgba(76, 175, 80, 0.25);
|
||||
}
|
||||
|
||||
.number-input input {
|
||||
width: 50px;
|
||||
text-align: center;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 700;
|
||||
color: #2c3e50;
|
||||
background: #fafafa;
|
||||
border: none;
|
||||
padding: 0.7rem 0;
|
||||
outline: none;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.number-input button {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: none;
|
||||
background: #e3f2fd;
|
||||
color: #1565c0;
|
||||
font-size: 1.2rem;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.number-input button:hover {
|
||||
background: #bbdefb;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.number-input button:active {
|
||||
background: #90caf9;
|
||||
}
|
||||
|
||||
.number-input button.minus {
|
||||
border-left: 1px solid #bbdefb;
|
||||
}
|
||||
|
||||
.number-input button.plus {
|
||||
border-right: 1px solid #bbdefb;
|
||||
}
|
||||
|
||||
.slider {
|
||||
width: 150px;
|
||||
height: 8px;
|
||||
border-radius: 1rem;
|
||||
background: #e0e0e0;
|
||||
outline: none;
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
.slider::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
background: #4caf50;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 2px 6px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.slider::-moz-range-thumb {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
background: #4caf50;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
box-shadow: 0 2px 6px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.typing-dots::after {
|
||||
content: '...';
|
||||
animation: blink 1s infinite;
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
0%, 100% { opacity: 0.2; }
|
||||
50% { opacity: 1; }
|
||||
}
|
||||
|
||||
.explanation-box {
|
||||
background: #f0f7ff;
|
||||
border: 1px solid #bee3f8;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
margin: 20px 0;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
color: #2c5282;
|
||||
}
|
||||
|
||||
.explanation-box ul {
|
||||
margin: 8px 0 0 20px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.explanation-box li {
|
||||
margin: 4px 0;
|
||||
padding: 4px 0;
|
||||
border-bottom: 1px dashed #a0aec0;
|
||||
}
|
||||
|
||||
.explanation-box strong {
|
||||
color: #2b6cb0;
|
||||
}
|
||||
|
||||
.chunks-section {
|
||||
display: block !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
.chunk-card {
|
||||
display: block !important;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"liveServer.settings.port": 5501
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fa" dir="rtl">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>جزئیات مقاله - پنل مدیریت</title>
|
||||
<script src="/static/3.4.17.es"></script>
|
||||
<link rel="stylesheet" href="/static/all.min.css">
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css2?family=Vazir&display=swap');
|
||||
body { font-family: 'Vazir', Tahoma, sans-serif; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-gray-50 min-h-screen">
|
||||
<div class="flex">
|
||||
<aside class="w-64 bg-green-800 text-white min-h-screen">
|
||||
<div class="p-4">
|
||||
<h1 class="text-xl font-bold text-center border-b border-green-700 pb-4">
|
||||
<i class="fas fa-cogs ml-2"></i>
|
||||
پنل مدیریت
|
||||
</h1>
|
||||
<nav class="mt-8">
|
||||
<ul class="space-y-2">
|
||||
<li><a href="{{ url_for('admin.admin_dashboard') }}" class="flex items-center p-3 rounded-lg hover:bg-green-700 transition-colors"><i class="fas fa-tachometer-alt ml-3"></i>داشبورد</a></li>
|
||||
<li><a href="{{ url_for('admin.files_management') }}" class="flex items-center p-3 rounded-lg hover:bg-green-700 transition-colors"><i class="fas fa-file-upload ml-3"></i>مدیریت فایلها</a></li>
|
||||
<li><a href="{{ url_for('admin.articles_management') }}" class="flex items-center p-3 rounded-lg bg-green-700 transition-colors"><i class="fas fa-book ml-3"></i>مدیریت مقالات</a></li>
|
||||
<li><a href="{{ url_for('admin.users_management') }}" class="flex items-center p-3 rounded-lg hover:bg-green-700 transition-colors"><i class="fas fa-users ml-3"></i>مدیریت کاربران</a></li>
|
||||
<li><a href="{{ url_for('admin.corrections_management') }}" class="flex items-center p-3 rounded-lg hover:bg-green-700 transition-colors"><i class="fas fa-edit ml-3"></i>نظارت بر تصحیحها</a></li>
|
||||
<li class="pt-4 border-t border-green-700 mt-4"><a href="{{ url_for('dashboard.index') }}" class="flex items-center p-3 rounded-lg hover:bg-green-700 transition-colors"><i class="fas fa-arrow-right ml-3"></i>بازگشت به سایت</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<main class="flex-1 p-6">
|
||||
<header class="bg-white rounded-lg shadow-sm p-4 mb-6">
|
||||
<div class="flex justify-between items-center">
|
||||
<h2 class="text-2xl font-bold text-gray-800">جزئیات مقاله</h2>
|
||||
<div class="flex items-center space-x-4 space-x-reverse">
|
||||
<span class="text-gray-600">خوش آمدید، {{ current_user.username }}</span>
|
||||
<div class="w-8 h-8 bg-green-500 rounded-full flex items-center justify-center text-white">
|
||||
<i class="fas fa-user"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="bg-white rounded-lg shadow-sm p-6">
|
||||
<h3 class="text-xl font-bold text-gray-800 mb-4">{{ article.name }}</h3>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700">موسسه</label>
|
||||
<p class="mt-1 text-gray-900">{{ article.institute }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700">چکیده</label>
|
||||
<p class="mt-1 text-gray-900">{{ article.abstract }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700">وضعیت تصحیح</label>
|
||||
<span class="mt-1 px-2 py-1 text-xs rounded-full
|
||||
{% if article.correction_status == 'completed' %}bg-green-100 text-green-800
|
||||
{% elif article.correction_status == 'in_progress' %}bg-yellow-100 text-yellow-800
|
||||
{% else %}bg-red-100 text-red-800{% endif %}">
|
||||
{{ article.correction_status }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,971 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fa" dir="rtl">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>مدیریت مقالات - پنل مدیریت</title>
|
||||
<script src="/static/3.4.17.es"></script>
|
||||
<link rel="stylesheet" href="/static/all.min.css">
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css2?family=Vazir&display=swap');
|
||||
|
||||
body {
|
||||
font-family: 'Vazir', Tahoma, sans-serif;
|
||||
}
|
||||
|
||||
.fade-in {
|
||||
animation: fadeIn 0.5s ease-in;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.truncate-2 {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.assigned-badge {
|
||||
font-size: 0.7rem;
|
||||
padding: 2px 8px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.debug-info {
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
background: #f5f5f5;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
margin: 10px 0;
|
||||
border-right: 3px solid #f39c12;
|
||||
}
|
||||
|
||||
/* استایل برای مودال */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 1000;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.modal-overlay.active {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background-color: white;
|
||||
border-radius: 10px;
|
||||
width: 90%;
|
||||
max-width: 700px;
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
transform: translateY(-20px);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.modal-overlay.active .modal-content {
|
||||
transform: translateY(0);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body class="bg-gray-50 min-h-screen">
|
||||
<div class="flex">
|
||||
<!-- نوار کناری -->
|
||||
<aside class="w-64 bg-green-800 text-white min-h-screen">
|
||||
<div class="p-4">
|
||||
<h1 class="text-xl font-bold text-center border-b border-green-700 pb-4">
|
||||
<i class="fas fa-cogs ml-2"></i>
|
||||
پنل مدیریت
|
||||
</h1>
|
||||
<nav class="mt-8">
|
||||
<ul class="space-y-2">
|
||||
<li><a href="{{ url_for('admin.admin_dashboard') }}"
|
||||
class="flex items-center p-3 rounded-lg hover:bg-green-700 transition-colors"><i
|
||||
class="fas fa-tachometer-alt ml-3"></i>داشبورد</a></li>
|
||||
<li><a href="{{ url_for('admin.files_management') }}"
|
||||
class="flex items-center p-3 rounded-lg hover:bg-green-700 transition-colors"><i
|
||||
class="fas fa-file-upload ml-3"></i>مدیریت فایلها</a></li>
|
||||
<li><a href="{{ url_for('admin.articles_management') }}"
|
||||
class="flex items-center p-3 rounded-lg bg-green-700 transition-colors"><i
|
||||
class="fas fa-book ml-3"></i>مدیریت مقالات</a></li>
|
||||
<li><a href="{{ url_for('admin.users_management') }}"
|
||||
class="flex items-center p-3 rounded-lg hover:bg-green-700 transition-colors"><i
|
||||
class="fas fa-users ml-3"></i>مدیریت کاربران</a></li>
|
||||
<li><a href="{{ url_for('admin.corrections_management') }}"
|
||||
class="flex items-center p-3 rounded-lg hover:bg-green-700 transition-colors"><i
|
||||
class="fas fa-edit ml-3"></i>نظارت بر تصحیحها</a></li>
|
||||
<li class="pt-4 border-t border-green-700 mt-4"><a href="{{ url_for('dashboard.index') }}"
|
||||
class="flex items-center p-3 rounded-lg hover:bg-green-700 transition-colors"><i
|
||||
class="fas fa-arrow-right ml-3"></i>بازگشت به سایت</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- محتوای اصلی -->
|
||||
<main class="flex-1 p-6">
|
||||
<!-- هدر صفحه -->
|
||||
<header class="bg-white rounded-2xl shadow-sm p-6 mb-6 fade-in">
|
||||
<div class="flex justify-between items-center">
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold text-gray-800">مدیریت مقالات</h2>
|
||||
<p class="text-gray-600 mt-1">مدیریت و بازبینی مقالات سیستم</p>
|
||||
</div>
|
||||
<div class="flex items-center space-x-4 space-x-reverse">
|
||||
<div
|
||||
class="w-10 h-10 bg-gradient-to-r from-green-500 to-emerald-600 rounded-full flex items-center justify-center text-white shadow-lg">
|
||||
<i class="fas fa-user text-sm"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- فیلترها و جستجو -->
|
||||
<div class="bg-white rounded-2xl shadow-sm p-6 mb-6 fade-in">
|
||||
<div class="flex flex-col md:flex-row md:items-center md:justify-between space-y-4 md:space-y-0">
|
||||
<div class="flex items-center space-x-4 space-x-reverse">
|
||||
<span class="text-sm font-medium text-gray-700">فیلتر بر اساس وضعیت:</span>
|
||||
<div class="flex space-x-2 space-x-reverse">
|
||||
<a href="{{ url_for('admin.articles_management') }}?status=all"
|
||||
class="px-3 py-1 text-sm rounded-full {% if status_filter == 'all' %}bg-green-100 text-green-800{% else %}bg-gray-100 text-gray-600 hover:bg-gray-200{% endif %} transition-colors">
|
||||
همه
|
||||
</a>
|
||||
<a href="{{ url_for('admin.articles_management') }}?status=pending"
|
||||
class="px-3 py-1 text-sm rounded-full {% if status_filter == 'pending' %}bg-red-100 text-red-800{% else %}bg-gray-100 text-gray-600 hover:bg-gray-200{% endif %} transition-colors">
|
||||
در انتظار
|
||||
</a>
|
||||
<a href="{{ url_for('admin.articles_management') }}?status=in_progress"
|
||||
class="px-3 py-1 text-sm rounded-full {% if status_filter == 'in_progress' %}bg-yellow-100 text-yellow-800{% else %}bg-gray-100 text-gray-600 hover:bg-gray-200{% endif %} transition-colors">
|
||||
در حال تصحیح
|
||||
</a>
|
||||
<a href="{{ url_for('admin.articles_management') }}?status=completed"
|
||||
class="px-3 py-1 text-sm rounded-full {% if status_filter == 'completed' %}bg-green-100 text-green-800{% else %}bg-gray-100 text-gray-600 hover:bg-gray-200{% endif %} transition-colors">
|
||||
تکمیل شده
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="relative">
|
||||
<input type="text" id="searchInput" placeholder="جستجو در مقالات..."
|
||||
class="w-full md:w-64 px-4 py-2 pr-10 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-green-500">
|
||||
<i class="fas fa-search absolute right-3 top-3 text-gray-400"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- لیست مقالات -->
|
||||
<div class="bg-white rounded-2xl shadow-sm p-6 fade-in">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h3 class="text-xl font-bold text-gray-800 flex items-center">
|
||||
<i class="fas fa-list ml-2 text-blue-600"></i>
|
||||
لیست مقالات
|
||||
</h3>
|
||||
<div class="text-sm text-gray-500">
|
||||
{% if articles %}
|
||||
صفحه {{ articles.page }} از {{ articles.pages }} | مجموع: {{ articles.total }} مقاله
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- بخش دیباگ برای بررسی دادهها -->
|
||||
{% if debug_mode %}
|
||||
<div class="debug-info mb-4">
|
||||
<strong>اطلاعات دیباگ:</strong><br>
|
||||
- مقالات موجود: {{ articles.total if articles else 0 }}<br>
|
||||
- تعداد آیتمها: {{ articles.items|length if articles and articles.items else 0 }}<br>
|
||||
- وضعیت فیلتر: {{ status_filter }}<br>
|
||||
- تعداد کاربران ارسال شده: {{ users|length if users else 0 }}<br>
|
||||
{% if articles and articles.items %}
|
||||
- نمونه از اولین مقاله:<br>
|
||||
ID: {{ articles.items[0].id }}<br>
|
||||
نام: {{ articles.items[0].name }}<br>
|
||||
user_id: {{ articles.items[0].user_id }}<br>
|
||||
correction_status: {{ articles.items[0].correction_status }}<br>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if articles and articles.items %}
|
||||
<!-- ساخت دیکشنری کاربران برای دسترسی آسان -->
|
||||
{% if users %}
|
||||
{% set user_dict = {} %}
|
||||
{% for user in users %}
|
||||
{% set _ = user_dict.update({user.id: user}) %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm text-right text-gray-700">
|
||||
<thead class="text-xs text-gray-700 uppercase bg-gray-50">
|
||||
<tr>
|
||||
<th class="px-4 py-3">عنوان مقاله</th>
|
||||
<th class="px-4 py-3">موسسه</th>
|
||||
<th class="px-4 py-3">وضعیت تصحیح</th>
|
||||
<th class="px-4 py-3">کاربر اختصاصیافته</th>
|
||||
<th class="px-4 py-3">تاریخ ایجاد</th>
|
||||
<th class="px-4 py-3">عملیات</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="articlesTableBody">
|
||||
{% for article in articles.items %}
|
||||
<tr class="border-b hover:bg-gray-50 transition-colors">
|
||||
<td class="px-4 py-3">
|
||||
<div class="flex items-start">
|
||||
<i class="fas fa-file-alt text-gray-400 ml-3 mt-1"></i>
|
||||
<div>
|
||||
<!-- استفاده از فیلدهای مختلف برای عنوان -->
|
||||
<p class="font-medium text-gray-900 truncate-2">
|
||||
{{ article.name or article.title or article.filename or 'بدون عنوان' }}
|
||||
</p>
|
||||
<p class="text-xs text-gray-500 mt-1">
|
||||
ID: {{ article.id }}
|
||||
{% if article.article_id %}({{ article.article_id }}){% endif %}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
<p class="text-gray-600">{{ article.institute or 'نامشخص' }}</p>
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
{% set correction_status = article.correction_status or article.status or 'pending' %}
|
||||
<span class="px-3 py-1 text-xs rounded-full
|
||||
{% if correction_status == 'completed' %}bg-green-100 text-green-800 border border-green-200
|
||||
{% elif correction_status == 'in_progress' %}bg-yellow-100 text-yellow-800 border border-yellow-200
|
||||
{% else %}bg-red-100 text-red-800 border border-red-200{% endif %}">
|
||||
<i class="fas
|
||||
{% if correction_status == 'completed' %}fa-check-circle
|
||||
{% elif correction_status == 'in_progress' %}fa-spinner
|
||||
{% else %}fa-clock{% endif %} ml-1">
|
||||
</i>
|
||||
{% if correction_status == 'completed' %}تکمیل شده
|
||||
{% elif correction_status == 'in_progress' %}در حال تصحیح
|
||||
{% else %}در انتظار تصحیح{% endif %}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
{% if article.user_id %}
|
||||
{% if users and user_dict %}
|
||||
{% set assigned_user = user_dict.get(article.user_id) %}
|
||||
{% if assigned_user %}
|
||||
<div class="flex items-center">
|
||||
<i class="fas fa-user-check text-green-600 ml-2"></i>
|
||||
<div>
|
||||
<span class="text-sm font-medium">{{ assigned_user.fullname or assigned_user.username or assigned_user.email }}</span>
|
||||
<p class="text-xs text-gray-500 mt-1">
|
||||
{% if assigned_user.email %}{{ assigned_user.email }}{% endif %}
|
||||
{% if assigned_user.role %} ({{ assigned_user.role }}){% endif %}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="flex items-center">
|
||||
<i class="fas fa-user-clock text-yellow-600 ml-2"></i>
|
||||
<span class="text-sm">کاربر ID: {{ article.user_id }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<!-- اگر لیست کاربران موجود نیست -->
|
||||
<div class="flex items-center">
|
||||
<i class="fas fa-user-clock text-blue-600 ml-2"></i>
|
||||
<span class="text-sm">کاربر ID: {{ article.user_id }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class="flex items-center">
|
||||
<i class="fas fa-user-times text-gray-400 ml-2"></i>
|
||||
<span class="text-sm text-gray-500">اختصاص داده نشده</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- دکمه اختصاص مقاله -->
|
||||
<button onclick="assignArticle({{ article.id }})"
|
||||
class="mt-2 inline-flex items-center text-xs text-blue-600 hover:text-blue-800 transition-colors">
|
||||
<i class="fas fa-user-plus ml-1"></i>
|
||||
{% if article.user_id %}تغییر{% else %}اختصاص{% endif %}
|
||||
</button>
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
{% if article.created_at %}
|
||||
<div class="text-sm">
|
||||
<p>{{ article.created_at.strftime('%Y/%m/%d') }}</p>
|
||||
<p class="text-gray-500">{{ article.created_at.strftime('%H:%M') }}</p>
|
||||
</div>
|
||||
{% else %}
|
||||
<span class="text-gray-500">نامشخص</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
<div class="flex items-center space-x-2 space-x-reverse">
|
||||
<!-- تغییر لینک مشاهده جزئیات به استفاده از مودال -->
|
||||
<button onclick="showArticleDetails({{ article.id }})"
|
||||
class="text-blue-600 hover:text-blue-800 transition-colors"
|
||||
title="مشاهده جزئیات">
|
||||
<i class="fas fa-eye"></i>
|
||||
</button>
|
||||
{% if article.pdf_path %}
|
||||
<a href="{{ article.pdf_path }}"
|
||||
class="text-green-600 hover:text-green-800 transition-colors"
|
||||
title="دانلود مقاله" target="_blank">
|
||||
<i class="fas fa-download"></i>
|
||||
</a>
|
||||
{% else %}
|
||||
<span class="text-gray-400 cursor-not-allowed" title="فایل PDF موجود نیست">
|
||||
<i class="fas fa-download"></i>
|
||||
</span>
|
||||
{% endif %}
|
||||
<form action="{{ url_for('admin.delete_article', article_id=article.id) }}"
|
||||
method="POST"
|
||||
onsubmit="return confirmDeleteArticle(event, {{ article.id }}, '{{ article.name or 'بدون عنوان' }}');"
|
||||
class="inline">
|
||||
<button type="submit"
|
||||
class="text-red-600 hover:text-red-800 transition-colors"
|
||||
title="حذف مقاله">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- صفحهبندی -->
|
||||
{% if articles.pages > 1 %}
|
||||
<div class="flex justify-center items-center space-x-2 space-x-reverse mt-6">
|
||||
{% if articles.has_prev %}
|
||||
<a href="{{ url_for('admin.articles_management', page=articles.prev_num, status=status_filter) }}"
|
||||
class="px-4 py-2 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors">
|
||||
<i class="fas fa-chevron-right"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{% for page_num in articles.iter_pages(left_edge=2, left_current=2, right_current=2, right_edge=2) %}
|
||||
{% if page_num %}
|
||||
{% if page_num == articles.page %}
|
||||
<span class="px-4 py-2 bg-green-600 text-white rounded-lg">{{ page_num }}</span>
|
||||
{% else %}
|
||||
<a href="{{ url_for('admin.articles_management', page=page_num, status=status_filter) }}"
|
||||
class="px-4 py-2 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors">
|
||||
{{ page_num }}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<span class="px-2">...</span>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% if articles.has_next %}
|
||||
<a href="{{ url_for('admin.articles_management', page=articles.next_num, status=status_filter) }}"
|
||||
class="px-4 py-2 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors">
|
||||
<i class="fas fa-chevron-left"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% else %}
|
||||
<div class="text-center py-8">
|
||||
<i class="fas fa-book-open text-4xl text-gray-400 mb-4"></i>
|
||||
<h4 class="text-lg font-medium text-gray-700">هیچ مقالهای یافت نشد</h4>
|
||||
<p class="text-gray-500 mt-2">
|
||||
{% if status_filter != 'all' %}
|
||||
هیچ مقالهای با وضعیت "{{ status_filter }}" وجود ندارد
|
||||
{% else %}
|
||||
هنوز مقالهای در سیستم ثبت نشده است
|
||||
{% endif %}
|
||||
</p>
|
||||
<a href="{{ url_for('admin.files_management') }}"
|
||||
class="mt-4 inline-flex items-center px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors">
|
||||
<i class="fas fa-upload ml-2"></i>
|
||||
آپلود فایل جدید
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- مودال اختصاص مقاله -->
|
||||
<div id="assignModal" class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full hidden z-50">
|
||||
<div class="relative top-20 mx-auto p-5 border w-96 shadow-lg rounded-md bg-white">
|
||||
<div class="mt-3">
|
||||
<h3 class="text-lg font-medium text-gray-900 mb-4">اختصاص مقاله</h3>
|
||||
<form id="assignForm" method="POST">
|
||||
<input type="hidden" id="articleId" name="article_id">
|
||||
<div class="mb-4">
|
||||
<label for="userId" class="block text-sm font-medium text-gray-700 mb-2">انتخاب کاربر:</label>
|
||||
<select id="userId" name="user_id"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-green-500 focus:border-green-500">
|
||||
<option value="">-- انتخاب کاربر --</option>
|
||||
{% if users %}
|
||||
{% for user in users %}
|
||||
<option value="{{ user.id }}">
|
||||
{{ user.fullname or user.username }} - {{ user.email }}
|
||||
{% if user.role %}({{ user.role }}){% endif %}
|
||||
</option>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<option value="1">مدیر سیستم</option>
|
||||
<option value="2">کاربر نمونه ۱</option>
|
||||
<option value="3">کاربر نمونه ۲</option>
|
||||
{% endif %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex justify-end space-x-3 space-x-reverse mt-6">
|
||||
<button type="button" onclick="closeAssignModal()"
|
||||
class="px-4 py-2 bg-gray-300 text-gray-700 rounded-md hover:bg-gray-400 transition-colors">
|
||||
انصراف
|
||||
</button>
|
||||
<button type="submit"
|
||||
class="px-4 py-2 bg-green-600 text-white rounded-md hover:bg-green-700 transition-colors">
|
||||
اختصاص
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- مودال نمایش جزئیات مقاله -->
|
||||
<div id="articleDetailModal" class="modal-overlay">
|
||||
<div class="modal-content">
|
||||
<div class="p-6">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h3 class="text-xl font-bold text-gray-800 flex items-center">
|
||||
<i class="fas fa-file-alt text-blue-600 ml-2"></i>
|
||||
جزئیات مقاله
|
||||
</h3>
|
||||
<button onclick="closeArticleDetailModal()"
|
||||
class="text-gray-400 hover:text-gray-600 transition-colors">
|
||||
<i class="fas fa-times text-lg"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="articleDetailContent">
|
||||
<!-- محتوای جزئیات مقاله به صورت داینامیک لود میشود -->
|
||||
<div class="text-center py-8">
|
||||
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-green-600 mx-auto mb-4"></div>
|
||||
<p class="text-gray-600">در حال بارگذاری اطلاعات مقاله...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end mt-6 pt-4 border-t border-gray-200">
|
||||
<button onclick="closeArticleDetailModal()"
|
||||
class="px-5 py-2 bg-gray-200 text-gray-700 rounded-lg hover:bg-gray-300 transition-colors">
|
||||
بستن
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// تابع تأیید حذف مقاله
|
||||
function confirmDeleteArticle(event, articleId, articleName) {
|
||||
event.preventDefault();
|
||||
const confirmation = confirm(`آیا از حذف مقاله "${articleName}" با شناسه ${articleId} اطمینان دارید؟\nاین عمل غیرقابل بازگشت است.`);
|
||||
|
||||
if (confirmation) {
|
||||
event.target.closest('form').submit();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// توابع مربوط به مودال اختصاص مقاله
|
||||
function assignArticle(articleId) {
|
||||
console.log('اختصاص مقاله با ID:', articleId);
|
||||
document.getElementById('articleId').value = articleId;
|
||||
document.getElementById('assignModal').classList.remove('hidden');
|
||||
|
||||
// برای UX بهتر، اسکرول به بالای مودال
|
||||
document.getElementById('assignModal').scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
|
||||
function closeAssignModal() {
|
||||
document.getElementById('assignModal').classList.add('hidden');
|
||||
// بازنشانی فرم
|
||||
document.getElementById('assignForm').reset();
|
||||
}
|
||||
|
||||
// ارسال فرم اختصاص مقاله
|
||||
document.getElementById('assignForm').addEventListener('submit', function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
const articleId = document.getElementById('articleId').value;
|
||||
const userId = document.getElementById('userId').value;
|
||||
|
||||
if (!userId) {
|
||||
alert('لطفاً یک کاربر انتخاب کنید');
|
||||
return;
|
||||
}
|
||||
|
||||
// نمایش پیام در حال پردازش
|
||||
const submitBtn = this.querySelector('button[type="submit"]');
|
||||
const originalText = submitBtn.innerHTML;
|
||||
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin ml-1"></i> در حال پردازش...';
|
||||
submitBtn.disabled = true;
|
||||
|
||||
console.log('ارسال درخواست اختصاص:', { article_id: articleId, user_id: userId });
|
||||
|
||||
// ارسال درخواست به سرور
|
||||
fetch('{{ url_for("admin.assign_article") }}', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
article_id: articleId,
|
||||
user_id: userId
|
||||
})
|
||||
})
|
||||
.then(response => {
|
||||
console.log('پاسخ دریافت شد:', response);
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
console.log('دادههای پاسخ:', data);
|
||||
if (data.success) {
|
||||
alert(data.message);
|
||||
closeAssignModal();
|
||||
// تأخیر کوتاه قبل از ریلود برای نمایش پیام
|
||||
setTimeout(() => {
|
||||
location.reload();
|
||||
}, 1000);
|
||||
} else {
|
||||
alert('خطا: ' + data.message);
|
||||
submitBtn.innerHTML = originalText;
|
||||
submitBtn.disabled = false;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('خطا در ارتباط با سرور: ' + error.message);
|
||||
submitBtn.innerHTML = originalText;
|
||||
submitBtn.disabled = false;
|
||||
});
|
||||
});
|
||||
|
||||
// توابع مربوط به مودال نمایش جزئیات مقاله
|
||||
function showArticleDetails(articleId) {
|
||||
console.log('نمایش جزئیات مقاله با ID:', articleId);
|
||||
|
||||
// نمایش مودال
|
||||
const modal = document.getElementById('articleDetailModal');
|
||||
modal.classList.add('active');
|
||||
|
||||
// نمایش اسکلت لودینگ
|
||||
document.getElementById('articleDetailContent').innerHTML = `
|
||||
<div class="text-center py-8">
|
||||
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-green-600 mx-auto mb-4"></div>
|
||||
<p class="text-gray-600">در حال بارگذاری اطلاعات مقاله...</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// درخواست اطلاعات مقاله از سرور
|
||||
setTimeout(() => {
|
||||
loadArticleDetails(articleId);
|
||||
}, 500);
|
||||
}
|
||||
|
||||
function loadArticleDetails(articleId) {
|
||||
// درخواست به API برای دریافت جزئیات مقاله
|
||||
fetch(`/admin/api/article/${articleId}/details`)
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error('مقاله یافت نشد');
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
if (!data.success) {
|
||||
throw new Error(data.error || 'خطا در دریافت اطلاعات');
|
||||
}
|
||||
|
||||
const article = data.article;
|
||||
|
||||
// وضعیت تصحیح
|
||||
let statusBadge = '';
|
||||
if (article.correction_status === 'completed') {
|
||||
statusBadge = '<span class="px-3 py-1 text-xs rounded-full bg-green-100 text-green-800 border border-green-200">تکمیل شده</span>';
|
||||
} else if (article.correction_status === 'in_progress') {
|
||||
statusBadge = '<span class="px-3 py-1 text-xs rounded-full bg-yellow-100 text-yellow-800 border border-yellow-200">در حال تصحیح</span>';
|
||||
} else {
|
||||
statusBadge = '<span class="px-3 py-1 text-xs rounded-full bg-red-100 text-red-800 border border-red-200">در انتظار تصحیح</span>';
|
||||
}
|
||||
|
||||
// کاربر اختصاصیافته
|
||||
let assignedUserHtml = '';
|
||||
if (article.assigned_user) {
|
||||
assignedUserHtml = `
|
||||
<div class="flex items-center">
|
||||
<div class="w-8 h-8 bg-green-100 rounded-full flex items-center justify-center ml-2">
|
||||
<i class="fas fa-user text-green-600 text-sm"></i>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-gray-900 font-medium">${article.assigned_user.fullname || article.assigned_user.username}</p>
|
||||
<p class="text-xs text-gray-500">${article.assigned_user.email || ''} ${article.assigned_user.role ? '(' + article.assigned_user.role + ')' : ''}</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
assignedUserHtml = `
|
||||
<div class="flex items-center">
|
||||
<div class="w-8 h-8 bg-gray-100 rounded-full flex items-center justify-center ml-2">
|
||||
<i class="fas fa-user-times text-gray-400 text-sm"></i>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-gray-900 font-medium">اختصاص داده نشده</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// تصحیحها
|
||||
let correctionsHtml = '';
|
||||
if (article.corrections && article.corrections.length > 0) {
|
||||
correctionsHtml = `
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">تاریخچه تصحیحها:</label>
|
||||
<div class="space-y-2">
|
||||
${article.corrections.map(correction => `
|
||||
<div class="p-3 bg-gray-50 rounded-lg border border-gray-200">
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-sm text-gray-700">
|
||||
${correction.user ? correction.user.fullname || correction.user.username : 'کاربر نامشخص'}
|
||||
</span>
|
||||
<span class="px-2 py-1 text-xs rounded-full
|
||||
${correction.status === 'completed' ? 'bg-green-100 text-green-800' :
|
||||
correction.status === 'in_progress' ? 'bg-yellow-100 text-yellow-800' :
|
||||
'bg-blue-100 text-blue-800'}">
|
||||
${correction.status === 'completed' ? 'تکمیل شده' :
|
||||
correction.status === 'in_progress' ? 'در حال تصحیح' : 'اختصاص داده شده'}
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 mt-1">
|
||||
${correction.created_at || ''}
|
||||
${correction.updated_at ? ' - بروزرسانی: ' + correction.updated_at : ''}
|
||||
</p>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// متادیتا
|
||||
let metadataHtml = '';
|
||||
if (article.metadata) {
|
||||
const metadata = article.metadata;
|
||||
metadataHtml = `
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
${metadata.page_count ? `
|
||||
<div class="text-center p-3 bg-blue-50 rounded-lg">
|
||||
<p class="text-sm text-gray-600">تعداد صفحات</p>
|
||||
<p class="text-lg font-bold text-blue-700">${metadata.page_count}</p>
|
||||
</div>
|
||||
` : ''}
|
||||
${metadata.word_count ? `
|
||||
<div class="text-center p-3 bg-green-50 rounded-lg">
|
||||
<p class="text-sm text-gray-600">تعداد کلمات</p>
|
||||
<p class="text-lg font-bold text-green-700">${metadata.word_count}</p>
|
||||
</div>
|
||||
` : ''}
|
||||
${metadata.file_size ? `
|
||||
<div class="text-center p-3 bg-purple-50 rounded-lg">
|
||||
<p class="text-sm text-gray-600">حجم فایل</p>
|
||||
<p class="text-lg font-bold text-purple-700">${formatFileSize(metadata.file_size)}</p>
|
||||
</div>
|
||||
` : ''}
|
||||
<div class="text-center p-3 bg-yellow-50 rounded-lg">
|
||||
<p class="text-sm text-gray-600">تعداد تصحیحها</p>
|
||||
<p class="text-lg font-bold text-yellow-700">${article.corrections_count || 0}</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
const articleContent = `
|
||||
<div class="space-y-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">عنوان مقاله:</label>
|
||||
<div class="p-3 bg-gray-50 rounded-lg">
|
||||
<p class="text-gray-900 font-medium">${article.title || 'بدون عنوان'}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">شناسه مقاله:</label>
|
||||
<div class="p-3 bg-gray-50 rounded-lg">
|
||||
<p class="text-gray-900">${article.id}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">موسسه:</label>
|
||||
<div class="p-3 bg-gray-50 rounded-lg">
|
||||
<p class="text-gray-900">${article.institute || 'نامشخص'}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">وضعیت تصحیح:</label>
|
||||
<div class="p-3 bg-gray-50 rounded-lg">
|
||||
${statusBadge}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${metadataHtml}
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">کاربر اختصاصیافته:</label>
|
||||
<div class="p-3 bg-gray-50 rounded-lg">
|
||||
${assignedUserHtml}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">تاریخ ایجاد:</label>
|
||||
<div class="p-3 bg-gray-50 rounded-lg">
|
||||
<p class="text-gray-900">${article.created_at || 'نامشخص'}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">آخرین بروزرسانی:</label>
|
||||
<div class="p-3 bg-gray-50 rounded-lg">
|
||||
<p class="text-gray-900">${article.updated_at || 'نامشخص'}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">نام فایل اصلی:</label>
|
||||
<div class="p-3 bg-gray-50 rounded-lg">
|
||||
<p class="text-gray-900">${article.original_filename || 'نامشخص'}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">مسیر فایل:</label>
|
||||
<div class="p-3 bg-gray-50 rounded-lg">
|
||||
<p class="text-gray-900 truncate">${article.file_path || 'نامشخص'}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${correctionsHtml}
|
||||
|
||||
<div class="flex flex-wrap gap-3 space-x-reverse mt-6">
|
||||
<button onclick="assignArticle(${article.id})"
|
||||
class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors">
|
||||
<i class="fas fa-user-plus ml-1"></i>
|
||||
${article.assigned_user ? 'تغییر کاربر' : 'اختصاص به کاربر'}
|
||||
</button>
|
||||
${article.pdf_path ? `
|
||||
<a href="${article.pdf_path}"
|
||||
target="_blank"
|
||||
class="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors">
|
||||
<i class="fas fa-download ml-1"></i>
|
||||
دانلود PDF
|
||||
</a>
|
||||
` : ''}
|
||||
<button onclick="updateArticleStatus(${article.id}, 'pending')"
|
||||
class="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors">
|
||||
<i class="fas fa-clock ml-1"></i>
|
||||
در انتظار
|
||||
</button>
|
||||
<button onclick="updateArticleStatus(${article.id}, 'in_progress')"
|
||||
class="px-4 py-2 bg-yellow-600 text-white rounded-lg hover:bg-yellow-700 transition-colors">
|
||||
<i class="fas fa-spinner ml-1"></i>
|
||||
در حال تصحیح
|
||||
</button>
|
||||
<button onclick="updateArticleStatus(${article.id}, 'completed')"
|
||||
class="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors">
|
||||
<i class="fas fa-check ml-1"></i>
|
||||
تکمیل شده
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.getElementById('articleDetailContent').innerHTML = articleContent;
|
||||
})
|
||||
.catch(error => {
|
||||
document.getElementById('articleDetailContent').innerHTML = `
|
||||
<div class="text-center py-8">
|
||||
<i class="fas fa-exclamation-triangle text-4xl text-yellow-500 mb-4"></i>
|
||||
<h4 class="text-lg font-medium text-gray-700">خطا در بارگذاری اطلاعات</h4>
|
||||
<p class="text-gray-500 mt-2">${error.message}</p>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
}
|
||||
|
||||
// تابع کمکی برای فرمتبندی حجم فایل
|
||||
function formatFileSize(bytes) {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
const k = 1024;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
// تابع برای بهروزرسانی وضعیت مقاله
|
||||
function updateArticleStatus(articleId, status) {
|
||||
if (!confirm('آیا از تغییر وضعیت مقاله اطمینان دارید؟')) {
|
||||
return;
|
||||
}
|
||||
|
||||
fetch(`/admin/api/article/${articleId}/status`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ status: status })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
alert(data.message);
|
||||
// بستن مودال و رفرش صفحه
|
||||
closeArticleDetailModal();
|
||||
setTimeout(() => {
|
||||
location.reload();
|
||||
}, 1000);
|
||||
} else {
|
||||
alert('خطا: ' + data.error);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
alert('خطا در ارتباط با سرور: ' + error.message);
|
||||
});
|
||||
}
|
||||
|
||||
function closeArticleDetailModal() {
|
||||
const modal = document.getElementById('articleDetailModal');
|
||||
modal.classList.remove('active');
|
||||
// پاک کردن محتوا بعد از بستن مودال
|
||||
setTimeout(() => {
|
||||
document.getElementById('articleDetailContent').innerHTML = `
|
||||
<div class="text-center py-8">
|
||||
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-green-600 mx-auto mb-4"></div>
|
||||
<p class="text-gray-600">در حال بارگذاری اطلاعات مقاله...</p>
|
||||
</div>
|
||||
`;
|
||||
}, 300);
|
||||
}
|
||||
|
||||
// بستن مودالها با کلیک خارج از آنها
|
||||
window.onclick = function (event) {
|
||||
const assignModal = document.getElementById('assignModal');
|
||||
const articleDetailModal = document.getElementById('articleDetailModal');
|
||||
|
||||
if (event.target === assignModal) {
|
||||
closeAssignModal();
|
||||
}
|
||||
|
||||
if (event.target === articleDetailModal) {
|
||||
closeArticleDetailModal();
|
||||
}
|
||||
}
|
||||
|
||||
// بستن مودالها با کلید ESC
|
||||
document.addEventListener('keydown', function (event) {
|
||||
if (event.key === 'Escape') {
|
||||
closeAssignModal();
|
||||
closeArticleDetailModal();
|
||||
}
|
||||
});
|
||||
|
||||
// جستجوی مقالات
|
||||
const searchInput = document.getElementById('searchInput');
|
||||
const articlesTableBody = document.getElementById('articlesTableBody');
|
||||
|
||||
if (searchInput && articlesTableBody) {
|
||||
searchInput.addEventListener('input', function (e) {
|
||||
const searchTerm = e.target.value.toLowerCase();
|
||||
const rows = articlesTableBody.querySelectorAll('tr');
|
||||
|
||||
rows.forEach(row => {
|
||||
const title = row.querySelector('td:first-child .font-medium').textContent.toLowerCase();
|
||||
const institute = row.querySelector('td:nth-child(2)').textContent.toLowerCase();
|
||||
const assignedUser = row.querySelector('td:nth-child(4)').textContent.toLowerCase();
|
||||
|
||||
if (title.includes(searchTerm) || institute.includes(searchTerm) || assignedUser.includes(searchTerm)) {
|
||||
row.style.display = '';
|
||||
} else {
|
||||
row.style.display = 'none';
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// انیمیشن
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const fadeElements = document.querySelectorAll('.fade-in');
|
||||
fadeElements.forEach((element, index) => {
|
||||
element.style.animationDelay = `${index * 0.1}s`;
|
||||
});
|
||||
});
|
||||
|
||||
// خودکار پنهان کردن پیامها
|
||||
setTimeout(() => {
|
||||
const flashMessages = document.querySelectorAll('.fixed div');
|
||||
flashMessages.forEach(msg => {
|
||||
if (msg.classList.contains('alert')) {
|
||||
msg.style.transition = 'all 0.5s ease';
|
||||
msg.style.opacity = '0';
|
||||
msg.style.transform = 'translateX(-100%)';
|
||||
setTimeout(() => msg.remove(), 500);
|
||||
}
|
||||
});
|
||||
}, 5000);
|
||||
|
||||
// افزودن event listener برای بررسی وضعیت
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// بررسی وجود مقالات
|
||||
const articleCount = {{ articles.total if articles else 0 }};
|
||||
console.log('تعداد مقالات:', articleCount);
|
||||
|
||||
// بررسی وجود کاربران
|
||||
const userCount = {{ users|length if users else 0 }};
|
||||
console.log('تعداد کاربران:', userCount);
|
||||
|
||||
// بررسی endpoint اختصاص
|
||||
console.log('Endpoint اختصاص:', '{{ url_for("admin.assign_article") }}');
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,323 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fa" dir="rtl">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>نظارت بر تصحیحها - پنل مدیریت</title>
|
||||
<script src="/static/3.4.17.es"></script>
|
||||
<link rel="stylesheet" href="/static/all.min.css">
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css2?family=Vazir&display=swap');
|
||||
body { font-family: 'Vazir', Tahoma, sans-serif; }
|
||||
|
||||
.fade-in {
|
||||
animation: fadeIn 0.5s ease-in;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
height: 8px;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
background-color: #e5e7eb;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-gray-50 min-h-screen">
|
||||
<div class="flex">
|
||||
<!-- نوار کناری -->
|
||||
<aside class="w-64 bg-green-800 text-white min-h-screen">
|
||||
<div class="p-4">
|
||||
<h1 class="text-xl font-bold text-center border-b border-green-700 pb-4">
|
||||
<i class="fas fa-cogs ml-2"></i>
|
||||
پنل مدیریت
|
||||
</h1>
|
||||
<nav class="mt-8">
|
||||
<ul class="space-y-2">
|
||||
<li><a href="{{ url_for('admin.admin_dashboard') }}" class="flex items-center p-3 rounded-lg hover:bg-green-700 transition-colors"><i class="fas fa-tachometer-alt ml-3"></i>داشبورد</a></li>
|
||||
<li><a href="{{ url_for('admin.files_management') }}" class="flex items-center p-3 rounded-lg hover:bg-green-700 transition-colors"><i class="fas fa-file-upload ml-3"></i>مدیریت فایلها</a></li>
|
||||
<li><a href="{{ url_for('admin.articles_management') }}" class="flex items-center p-3 rounded-lg hover:bg-green-700 transition-colors"><i class="fas fa-book ml-3"></i>مدیریت مقالات</a></li>
|
||||
<li><a href="{{ url_for('admin.users_management') }}" class="flex items-center p-3 rounded-lg hover:bg-green-700 transition-colors"><i class="fas fa-users ml-3"></i>مدیریت کاربران</a></li>
|
||||
<li><a href="{{ url_for('admin.corrections_management') }}" class="flex items-center p-3 rounded-lg bg-green-700 transition-colors"><i class="fas fa-edit ml-3"></i>نظارت بر تصحیحها</a></li>
|
||||
<li class="pt-4 border-t border-green-700 mt-4"><a href="{{ url_for('dashboard.index') }}" class="flex items-center p-3 rounded-lg hover:bg-green-700 transition-colors"><i class="fas fa-arrow-right ml-3"></i>بازگشت به سایت</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- محتوای اصلی -->
|
||||
<main class="flex-1 p-6">
|
||||
<!-- هدر صفحه -->
|
||||
<header class="bg-white rounded-2xl shadow-sm p-6 mb-6 fade-in">
|
||||
<div class="flex justify-between items-center">
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold text-gray-800">نظارت بر تصحیح مقالات</h2>
|
||||
<p class="text-gray-600 mt-1">پیگیری وضعیت تصحیح و بازبینی مقالات</p>
|
||||
</div>
|
||||
<div class="flex items-center space-x-4 space-x-reverse">
|
||||
<div class="w-10 h-10 bg-gradient-to-r from-green-500 to-emerald-600 rounded-full flex items-center justify-center text-white shadow-lg">
|
||||
<i class="fas fa-user text-sm"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- آمار تصحیحها -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-6">
|
||||
<div class="bg-white rounded-2xl shadow-sm p-6 fade-in">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-gray-500 text-sm">کل مقالات</p>
|
||||
<p class="text-2xl font-bold text-gray-800">{{ stats.total }}</p>
|
||||
</div>
|
||||
<div class="w-12 h-12 bg-blue-100 rounded-full flex items-center justify-center">
|
||||
<i class="fas fa-book text-blue-500 text-xl"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-2xl shadow-sm p-6 fade-in">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-gray-500 text-sm">در انتظار تصحیح</p>
|
||||
<p class="text-2xl font-bold text-gray-800">{{ stats.pending }}</p>
|
||||
</div>
|
||||
<div class="w-12 h-12 bg-yellow-100 rounded-full flex items-center justify-center">
|
||||
<i class="fas fa-clock text-yellow-500 text-xl"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-2xl shadow-sm p-6 fade-in">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-gray-500 text-sm">در حال تصحیح</p>
|
||||
<p class="text-2xl font-bold text-gray-800">{{ stats.in_progress }}</p>
|
||||
</div>
|
||||
<div class="w-12 h-12 bg-orange-100 rounded-full flex items-center justify-center">
|
||||
<i class="fas fa-spinner text-orange-500 text-xl"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-2xl shadow-sm p-6 fade-in">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-gray-500 text-sm">تکمیل شده</p>
|
||||
<p class="text-2xl font-bold text-gray-800">{{ stats.completed }}</p>
|
||||
</div>
|
||||
<div class="w-12 h-12 bg-green-100 rounded-full flex items-center justify-center">
|
||||
<i class="fas fa-check-circle text-green-500 text-xl"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<a href="/admin/report">
|
||||
<button class="relative px-8 py-4 font-bold text-white rounded-full bg-gradient-to-r from-purple-500 to-pink-500 hover:from-purple-600 hover:to-pink-600 shadow-lg hover:shadow-xl transform hover:scale-105 transition-all duration-300 ease-in-out focus:outline-none focus:ring-4 focus:ring-purple-300 mb-3">
|
||||
<span class="flex items-center gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
گزارش جامع دانشگاه ها
|
||||
</span>
|
||||
</a>
|
||||
</button>
|
||||
</div>
|
||||
<!-- نوار پیشرفت -->
|
||||
<div class="bg-white rounded-2xl shadow-sm p-6 mb-6 fade-in">
|
||||
<h3 class="text-lg font-bold text-gray-800 mb-4 flex items-center">
|
||||
<i class="fas fa-chart-line ml-2 text-green-600"></i>
|
||||
پیشرفت کلی تصحیح مقالات
|
||||
</h3>
|
||||
|
||||
{% if stats.total > 0 %}
|
||||
<div class="space-y-4">
|
||||
<div class="flex justify-between text-sm text-gray-600">
|
||||
<span>درصد پیشرفت</span>
|
||||
<span>{{ "%.1f"|format((stats.completed / stats.total) * 100) }}%</span>
|
||||
</div>
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill bg-green-500"
|
||||
style="width: {{ (stats.completed / stats.total) * 100 }}%"></div>
|
||||
</div>
|
||||
<div class="flex justify-between text-xs text-gray-500">
|
||||
<span>{{ stats.completed }} مقاله تکمیل شده</span>
|
||||
<span>{{ stats.total - stats.completed }} مقاله باقیمانده</span>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="text-gray-500 text-center py-4">هیچ مقالهای برای نمایش وجود ندارد</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- توزیع وضعیتها -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
|
||||
<!-- نمودار دایرهای وضعیتها -->
|
||||
<div class="bg-white rounded-2xl shadow-sm p-6 fade-in">
|
||||
<h3 class="text-lg font-bold text-gray-800 mb-4 flex items-center">
|
||||
<i class="fas fa-chart-pie ml-2 text-purple-600"></i>
|
||||
توزیع وضعیت تصحیح
|
||||
</h3>
|
||||
<div class="flex justify-center items-center h-64">
|
||||
<div class="relative w-48 h-48">
|
||||
<!-- نمودار دایرهای ساده -->
|
||||
<svg viewBox="0 0 42 42" class="w-full h-full">
|
||||
{% set total = stats.total %}
|
||||
{% if total > 0 %}
|
||||
{% set completed_angle = (stats.completed / total) * 360 %}
|
||||
{% set in_progress_angle = (stats.in_progress / total) * 360 %}
|
||||
{% set pending_angle = (stats.pending / total) * 360 %}
|
||||
|
||||
<circle cx="21" cy="21" r="15.9155" fill="transparent"
|
||||
stroke="#10b981" stroke-width="8"
|
||||
stroke-dasharray="{{ completed_angle / 360 * 100 }} {{ 100 - completed_angle / 360 * 100 }}"
|
||||
stroke-dashoffset="25"/>
|
||||
|
||||
<circle cx="21" cy="21" r="15.9155" fill="transparent"
|
||||
stroke="#f59e0b" stroke-width="8"
|
||||
stroke-dasharray="{{ in_progress_angle / 360 * 100 }} {{ 100 - in_progress_angle / 360 * 100 }}"
|
||||
stroke-dashoffset="{{ 25 - completed_angle / 360 * 100 }}"/>
|
||||
|
||||
<circle cx="21" cy="21" r="15.9155" fill="transparent"
|
||||
stroke="#ef4444" stroke-width="8"
|
||||
stroke-dasharray="{{ pending_angle / 360 * 100 }} {{ 100 - pending_angle / 360 * 100 }}"
|
||||
stroke-dashoffset="{{ 25 - (completed_angle + in_progress_angle) / 360 * 100 }}"/>
|
||||
{% endif %}
|
||||
</svg>
|
||||
<div class="absolute inset-0 flex items-center justify-center">
|
||||
<div class="text-center">
|
||||
<p class="text-2xl font-bold text-gray-800">
|
||||
{% if total > 0 %}{{ "%.0f"|format((stats.completed / total) * 100) }}%{% else %}0%{% endif %}
|
||||
</p>
|
||||
<p class="text-xs text-gray-500">تکمیل شده</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-3 gap-4 mt-4 text-center">
|
||||
<div>
|
||||
<div class="w-3 h-3 bg-green-500 rounded-full mx-auto mb-1"></div>
|
||||
<p class="text-xs text-gray-600">تکمیل شده</p>
|
||||
<p class="font-bold">{{ stats.completed }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<div class="w-3 h-3 bg-yellow-500 rounded-full mx-auto mb-1"></div>
|
||||
<p class="text-xs text-gray-600">در حال تصحیح</p>
|
||||
<p class="font-bold">{{ stats.in_progress }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<div class="w-3 h-3 bg-red-500 rounded-full mx-auto mb-1"></div>
|
||||
<p class="text-xs text-gray-600">در انتظار</p>
|
||||
<p class="font-bold">{{ stats.pending }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- مقالات نیازمند بازبینی -->
|
||||
<div class="bg-white rounded-2xl shadow-sm p-6 fade-in">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h3 class="text-lg font-bold text-gray-800 flex items-center">
|
||||
<i class="fas fa-eye ml-2 text-blue-600"></i>
|
||||
مقالات نیازمند بازبینی
|
||||
</h3>
|
||||
<a href="{{ url_for('admin.articles_management') }}?status=completed"
|
||||
class="text-blue-600 hover:text-blue-700 text-sm">
|
||||
مشاهده همه
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3">
|
||||
{% if articles_for_review %}
|
||||
{% for article in articles_for_review %}
|
||||
<div class="flex items-center justify-between p-3 bg-gray-50 rounded-lg border border-gray-200">
|
||||
<div class="flex items-center">
|
||||
<i class="fas fa-file-alt text-gray-400 ml-3"></i>
|
||||
<div>
|
||||
<p class="font-medium text-gray-800 text-sm">{{ article.name[:50] }}{% if article.name|length > 50 %}...{% endif %}</p>
|
||||
<p class="text-xs text-gray-500">
|
||||
{% if article.updated_at %}
|
||||
آخرین بروزرسانی: {{ article.updated_at.strftime('%Y/%m/%d %H:%M') }}
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<a href="{{ url_for('admin.article_detail', article_id=article.id) }}"
|
||||
class="text-blue-600 hover:text-blue-800 text-sm">
|
||||
<i class="fas fa-external-link-alt"></i>
|
||||
</a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<div class="text-center py-8">
|
||||
<i class="fas fa-check-circle text-3xl text-green-400 mb-3"></i>
|
||||
<p class="text-gray-500">همه مقالات بازبینی شدهاند</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- اقدامات سریع -->
|
||||
<div class="bg-white rounded-2xl shadow-sm p-6 fade-in">
|
||||
<h3 class="text-lg font-bold text-gray-800 mb-4 flex items-center">
|
||||
<i class="fas fa-bolt ml-2 text-yellow-600"></i>
|
||||
اقدامات سریع
|
||||
</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<a href="{{ url_for('admin.articles_management') }}?status=pending"
|
||||
class="p-4 bg-red-50 border border-red-200 rounded-lg hover:bg-red-100 transition-colors text-center group">
|
||||
<i class="fas fa-clock text-2xl text-red-500 mb-2 group-hover:scale-110 transition-transform"></i>
|
||||
<p class="font-medium text-red-800">مقالات در انتظار</p>
|
||||
<p class="text-sm text-red-600">{{ stats.pending }} مقاله</p>
|
||||
</a>
|
||||
|
||||
<a href="{{ url_for('admin.articles_management') }}?status=in_progress"
|
||||
class="p-4 bg-yellow-50 border border-yellow-200 rounded-lg hover:bg-yellow-100 transition-colors text-center group">
|
||||
<i class="fas fa-spinner text-2xl text-yellow-500 mb-2 group-hover:scale-110 transition-transform"></i>
|
||||
<p class="font-medium text-yellow-800">در حال تصحیح</p>
|
||||
<p class="text-sm text-yellow-600">{{ stats.in_progress }} مقاله</p>
|
||||
</a>
|
||||
|
||||
<a href="{{ url_for('admin.files_management') }}"
|
||||
class="p-4 bg-blue-50 border border-blue-200 rounded-lg hover:bg-blue-100 transition-colors text-center group">
|
||||
<i class="fas fa-upload text-2xl text-blue-500 mb-2 group-hover:scale-110 transition-transform"></i>
|
||||
<p class="font-medium text-blue-800">آپلود فایل جدید</p>
|
||||
<p class="text-sm text-blue-600">افزودن مقالات</p>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// انیمیشن
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const fadeElements = document.querySelectorAll('.fade-in');
|
||||
fadeElements.forEach((element, index) => {
|
||||
element.style.animationDelay = `${index * 0.1}s`;
|
||||
});
|
||||
});
|
||||
|
||||
// خودکار پنهان کردن پیامها
|
||||
setTimeout(() => {
|
||||
const flashMessages = document.querySelectorAll('.fixed div');
|
||||
flashMessages.forEach(msg => {
|
||||
msg.style.transition = 'all 0.5s ease';
|
||||
msg.style.opacity = '0';
|
||||
msg.style.transform = 'translateX(-100%)';
|
||||
setTimeout(() => msg.remove(), 500);
|
||||
});
|
||||
}, 5000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,264 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fa" dir="rtl">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>پنل مدیریت - نمایشگر معیار مقالات</title>
|
||||
<script src="/static/3.4.17.es"></script>
|
||||
<link rel="stylesheet" href="/static/all.min.css">
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css2?family=Vazir&display=swap');
|
||||
|
||||
body {
|
||||
font-family: 'Vazir', Tahoma, sans-serif;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body class="bg-gray-50 min-h-screen">
|
||||
<!-- نوار کناری -->
|
||||
<div class="flex">
|
||||
<aside class="w-64 bg-green-800 text-white min-h-screen">
|
||||
<div class="p-4">
|
||||
<h1 class="text-xl font-bold text-center border-b border-green-700 pb-4">
|
||||
<i class="fas fa-cogs ml-2"></i>
|
||||
پنل مدیریت
|
||||
</h1>
|
||||
|
||||
<!-- بخش منوی نوار کناری با اصلاحات -->
|
||||
<nav class="mt-8">
|
||||
<ul class="space-y-2">
|
||||
<li>
|
||||
<a href="{{ url_for('admin.admin_dashboard') }}"
|
||||
class="flex items-center p-3 rounded-lg bg-green-700 transition-colors">
|
||||
<i class="fas fa-tachometer-alt ml-3"></i>
|
||||
داشبورد
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{{ url_for('admin.files_management') }}"
|
||||
class="flex items-center p-3 rounded-lg hover:bg-green-700 transition-colors">
|
||||
<i class="fas fa-file-upload ml-3"></i>
|
||||
مدیریت فایلها
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{{ url_for('admin.articles_management') }}"
|
||||
class="flex items-center p-3 rounded-lg hover:bg-green-700 transition-colors">
|
||||
<i class="fas fa-book ml-3"></i>
|
||||
مدیریت مقالات
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{{ url_for('admin.users_management') }}"
|
||||
class="flex items-center p-3 rounded-lg hover:bg-green-700 transition-colors">
|
||||
<i class="fas fa-users ml-3"></i>
|
||||
مدیریت کاربران
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{{ url_for('admin.corrections_management') }}"
|
||||
class="flex items-center p-3 rounded-lg hover:bg-green-700 transition-colors">
|
||||
<i class="fas fa-edit ml-3"></i>
|
||||
نظارت بر تصحیحها
|
||||
</a>
|
||||
</li>
|
||||
<li class="pt-4 border-t border-green-700 mt-4">
|
||||
<a href="{{ url_for('dashboard.index') }}"
|
||||
class="flex items-center p-3 rounded-lg hover:bg-green-700 transition-colors">
|
||||
<i class="fas fa-arrow-right ml-3"></i>
|
||||
بازگشت به سایت
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- محتوای اصلی -->
|
||||
<main class="flex-1 p-6">
|
||||
<!-- هدر -->
|
||||
<header class="bg-white rounded-lg shadow-sm p-4 mb-6">
|
||||
<div class="flex justify-between items-center">
|
||||
<h2 class="text-2xl font-bold text-gray-800">داشبورد مدیریت</h2>
|
||||
<div class="flex items-center space-x-4 space-x-reverse">
|
||||
<span class="text-gray-600">خوش آمدید، {{ current_user.username }}</span>
|
||||
<div class="w-8 h-8 bg-green-500 rounded-full flex items-center justify-center text-white">
|
||||
<i class="fas fa-user"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- کارتهای آمار -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
||||
<div class="bg-white rounded-lg shadow-sm p-6 border-r-4 border-blue-500 stats-card">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-gray-500 text-sm">کل کاربران</p>
|
||||
<p class="text-2xl font-bold text-gray-800">{{ stats.total_users }}</p>
|
||||
</div>
|
||||
<div class="w-12 h-12 bg-blue-100 rounded-full flex items-center justify-center">
|
||||
<i class="fas fa-users text-blue-500 text-xl"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-lg shadow-sm p-6 border-r-4 border-green-500 stats-card">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-gray-500 text-sm">کل مقالات</p>
|
||||
<p class="text-2xl font-bold text-gray-800">{{ stats.total_articles }}</p>
|
||||
</div>
|
||||
<div class="w-12 h-12 bg-green-100 rounded-full flex items-center justify-center">
|
||||
<i class="fas fa-book text-green-500 text-xl"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-lg shadow-sm p-6 border-r-4 border-yellow-500 stats-card">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-gray-500 text-sm">در انتظار تصحیح</p>
|
||||
<p class="text-2xl font-bold text-gray-800">{{ stats.pending_corrections }}</p>
|
||||
</div>
|
||||
<div class="w-12 h-12 bg-yellow-100 rounded-full flex items-center justify-center">
|
||||
<i class="fas fa-clock text-yellow-500 text-xl"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-lg shadow-sm p-6 border-r-4 border-purple-500 stats-card">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-gray-500 text-sm">فایلهای داده</p>
|
||||
<p class="text-2xl font-bold text-gray-800">{{ stats.total_files }}</p>
|
||||
</div>
|
||||
<div class="w-12 h-12 bg-purple-100 rounded-full flex items-center justify-center">
|
||||
<i class="fas fa-file-upload text-purple-500 text-xl"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- بخشهای دیگر -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<!-- فایلهای اخیر -->
|
||||
<div class="bg-white rounded-lg shadow-sm p-6">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h3 class="text-lg font-bold text-gray-800">آخرین فایلهای آپلود شده</h3>
|
||||
<a href="{{ url_for('admin.files_management') }}"
|
||||
class="text-green-600 hover:text-green-700 text-sm">
|
||||
مشاهده همه
|
||||
</a>
|
||||
</div>
|
||||
<div class="space-y-3">
|
||||
{% if recent_files %}
|
||||
{% for file in recent_files %}
|
||||
<div class="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
|
||||
<div class="flex items-center">
|
||||
<i class="fas fa-file text-gray-400 ml-3"></i>
|
||||
<div>
|
||||
<p class="font-medium text-gray-800">{{
|
||||
file.original_filename|default(file.filename, true) }}</p>
|
||||
<p class="text-xs text-gray-500">
|
||||
{% if file.uploaded_at %}
|
||||
{{ file.uploaded_at.strftime('%Y/%m/%d') }}
|
||||
{% else %}
|
||||
تاریخ نامشخص
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<span class="px-2 py-1 text-xs rounded-full
|
||||
{% if file.status == 'completed' %}bg-green-100 text-green-800
|
||||
{% elif file.status == 'processing' %}bg-yellow-100 text-yellow-800
|
||||
{% else %}bg-red-100 text-red-800{% endif %}">
|
||||
{% if file.status == 'completed' %}تکمیل شده
|
||||
{% elif file.status == 'processing' %}در حال پردازش
|
||||
{% else %}خطا{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<p class="text-gray-500 text-center py-4">هیچ فایلی یافت نشد</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- کاربران اخیر -->
|
||||
<div class="bg-white rounded-lg shadow-sm p-6">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h3 class="text-lg font-bold text-gray-800">کاربران جدید</h3>
|
||||
<a href="{{ url_for('admin.users_management') }}"
|
||||
class="text-green-600 hover:text-green-700 text-sm">
|
||||
مشاهده همه
|
||||
</a>
|
||||
</div>
|
||||
<div class="space-y-3">
|
||||
{% if recent_users %}
|
||||
{% for user in recent_users %}
|
||||
<div class="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
|
||||
<div class="flex items-center">
|
||||
<div class="w-8 h-8 bg-green-100 rounded-full flex items-center justify-center ml-3">
|
||||
<i class="fas fa-user text-green-500 text-sm"></i>
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-medium text-gray-800">{{ user.username }}</p>
|
||||
<p class="text-xs text-gray-500">{{ user.email }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<span class="px-2 py-1 text-xs rounded-full
|
||||
{% if user.is_active %}bg-green-100 text-green-800
|
||||
{% else %}bg-red-100 text-red-800{% endif %}">
|
||||
{{ 'فعال' if user.is_active else 'غیرفعال' }}
|
||||
</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<p class="text-gray-500 text-center py-4">هیچ کاربری یافت نشد</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- اسکریپتها -->
|
||||
<script>
|
||||
// بهروزرسانی آمار هر 30 ثانیه
|
||||
function updateStats() {
|
||||
fetch('/admin/api/stats')
|
||||
.then(response => {
|
||||
if (!response.ok) throw new Error('Network error');
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
// بهروزرسانی کارتهای آمار
|
||||
document.querySelectorAll('.stats-card').forEach((card, index) => {
|
||||
const valueElement = card.querySelector('.text-2xl');
|
||||
if (valueElement) {
|
||||
const values = [
|
||||
data.total_users,
|
||||
data.total_articles,
|
||||
data.pending_corrections,
|
||||
data.total_files
|
||||
];
|
||||
if (values[index] !== undefined) {
|
||||
valueElement.textContent = values[index];
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch(error => console.error('Error updating stats:', error));
|
||||
}
|
||||
|
||||
// فقط اگر کاربر در صفحه داشبورد است، آمار را بهروزرسانی کن
|
||||
if (window.location.pathname === '/admin/' || window.location.pathname === '/admin') {
|
||||
setInterval(updateStats, 30000);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,373 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fa" dir="rtl">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>مدیریت فایلها - پنل مدیریت</title>
|
||||
<script src="/static/3.4.17.es"></script>
|
||||
<link rel="stylesheet" href="/static/all.min.css">
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css2?family=Vazir&display=swap');
|
||||
body { font-family: 'Vazir', Tahoma, sans-serif; }
|
||||
|
||||
.fade-in {
|
||||
animation: fadeIn 0.5s ease-in;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.upload-area {
|
||||
border: 2px dashed #d1d5db;
|
||||
border-radius: 0.75rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.upload-area.dragover {
|
||||
border-color: #10b981;
|
||||
background-color: #f0fdf4;
|
||||
}
|
||||
|
||||
.file-size {
|
||||
font-family: monospace;
|
||||
background-color: #f3f4f6;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 0.375rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-gray-50 min-h-screen">
|
||||
<div class="flex">
|
||||
<!-- نوار کناری -->
|
||||
<aside class="w-64 bg-green-800 text-white min-h-screen">
|
||||
<div class="p-4">
|
||||
<h1 class="text-xl font-bold text-center border-b border-green-700 pb-4">
|
||||
<i class="fas fa-cogs ml-2"></i>
|
||||
پنل مدیریت
|
||||
</h1>
|
||||
<nav class="mt-8">
|
||||
<ul class="space-y-2">
|
||||
<li><a href="{{ url_for('admin.admin_dashboard') }}" class="flex items-center p-3 rounded-lg hover:bg-green-700 transition-colors"><i class="fas fa-tachometer-alt ml-3"></i>داشبورد</a></li>
|
||||
<li><a href="{{ url_for('admin.files_management') }}" class="flex items-center p-3 rounded-lg bg-green-700 transition-colors"><i class="fas fa-file-upload ml-3"></i>مدیریت فایلها</a></li>
|
||||
<li><a href="{{ url_for('admin.articles_management') }}" class="flex items-center p-3 rounded-lg hover:bg-green-700 transition-colors"><i class="fas fa-book ml-3"></i>مدیریت مقالات</a></li>
|
||||
<li><a href="{{ url_for('admin.users_management') }}" class="flex items-center p-3 rounded-lg hover:bg-green-700 transition-colors"><i class="fas fa-users ml-3"></i>مدیریت کاربران</a></li>
|
||||
<li><a href="{{ url_for('admin.corrections_management') }}" class="flex items-center p-3 rounded-lg hover:bg-green-700 transition-colors"><i class="fas fa-edit ml-3"></i>نظارت بر تصحیحها</a></li>
|
||||
<li class="pt-4 border-t border-green-700 mt-4"><a href="{{ url_for('dashboard.index') }}" class="flex items-center p-3 rounded-lg hover:bg-green-700 transition-colors"><i class="fas fa-arrow-right ml-3"></i>بازگشت به سایت</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- محتوای اصلی -->
|
||||
<main class="flex-1 p-6">
|
||||
<!-- هدر صفحه -->
|
||||
<header class="bg-white rounded-2xl shadow-sm p-6 mb-6 fade-in">
|
||||
<div class="flex justify-between items-center">
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold text-gray-800">مدیریت فایلهای داده</h2>
|
||||
<p class="text-gray-600 mt-1">آپلود و مدیریت فایلهای JSON حاوی مقالات</p>
|
||||
</div>
|
||||
<div class="flex items-center space-x-4 space-x-reverse">
|
||||
<div class="w-10 h-10 bg-gradient-to-r from-green-500 to-emerald-600 rounded-full flex items-center justify-center text-white shadow-lg">
|
||||
<i class="fas fa-user text-sm"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- بخش آپلود فایل -->
|
||||
<div class="bg-white rounded-2xl shadow-sm p-6 mb-6 fade-in">
|
||||
<h3 class="text-xl font-bold text-gray-800 mb-4 flex items-center">
|
||||
<i class="fas fa-cloud-upload-alt ml-2 text-green-600"></i>
|
||||
آپلود فایل جدید
|
||||
</h3>
|
||||
|
||||
<form id="uploadForm" action="{{ url_for('admin.upload_file') }}" method="POST" enctype="multipart/form-data" class="space-y-4">
|
||||
<div id="uploadArea" class="upload-area p-8 text-center cursor-pointer">
|
||||
<i class="fas fa-file-upload text-4xl text-gray-400 mb-4"></i>
|
||||
<p class="text-gray-600 mb-2">فایل JSON خود را اینجا رها کنید یا برای انتخاب کلیک کنید</p>
|
||||
<p class="text-sm text-gray-500">فرمتهای مجاز: JSON | حداکثر حجم: 50 مگابایت</p>
|
||||
<input type="file" name="file" id="fileInput" accept=".json" class="hidden" required>
|
||||
<button type="button" onclick="document.getElementById('fileInput').click()"
|
||||
class="mt-4 bg-green-600 text-white px-6 py-2 rounded-lg hover:bg-green-700 transition-colors">
|
||||
انتخاب فایل
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="fileInfo" class="hidden p-4 bg-green-50 border border-green-200 rounded-lg">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<i class="fas fa-file text-green-600 ml-3"></i>
|
||||
<div>
|
||||
<p id="fileName" class="font-medium text-green-800"></p>
|
||||
<p id="fileSize" class="text-sm text-green-600"></p>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" onclick="clearFile()" class="text-red-500 hover:text-red-700">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="description" class="block text-sm font-medium text-gray-700 mb-2">توضیحات (اختیاری)</label>
|
||||
<textarea name="description" id="description" rows="3"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-green-500"
|
||||
placeholder="توضیحات درباره محتوای فایل..."></textarea>
|
||||
</div>
|
||||
|
||||
<button type="submit" id="uploadBtn"
|
||||
class="w-full bg-green-600 text-white py-3 px-4 rounded-lg hover:bg-green-700 transition-colors flex items-center justify-center disabled:opacity-50 disabled:cursor-not-allowed">
|
||||
<i class="fas fa-upload ml-2"></i>
|
||||
آپلود و پردازش فایل
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- لیست فایلها -->
|
||||
<div class="bg-white rounded-2xl shadow-sm p-6 fade-in">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h3 class="text-xl font-bold text-gray-800 flex items-center">
|
||||
<i class="fas fa-list ml-2 text-blue-600"></i>
|
||||
فایلهای آپلود شده
|
||||
</h3>
|
||||
<div class="text-sm text-gray-500">
|
||||
مجموع: {{ files.total }} فایل
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if files.items %}
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm text-right text-gray-700">
|
||||
<thead class="text-xs text-gray-700 uppercase bg-gray-50">
|
||||
<tr>
|
||||
<th class="px-4 py-3">نام فایل</th>
|
||||
<th class="px-4 py-3">حجم</th>
|
||||
<th class="px-4 py-3">وضعیت</th>
|
||||
<th class="px-4 py-3">تاریخ آپلود</th>
|
||||
<th class="px-4 py-3">عملیات</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for file in files.items %}
|
||||
<tr class="border-b hover:bg-gray-50 transition-colors">
|
||||
<td class="px-4 py-3">
|
||||
<div class="flex items-center">
|
||||
<i class="fas fa-file-json text-yellow-500 ml-3"></i>
|
||||
<div>
|
||||
<p class="font-medium text-gray-900">{{ file.original_filename }}</p>
|
||||
{% if file.description %}
|
||||
<p class="text-xs text-gray-500">{{ file.description }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
<span class="file-size">
|
||||
{% if file.file_size %}
|
||||
{{ "%.2f"|format(file.file_size / 1024 / 1024) }} مگابایت
|
||||
{% else %}
|
||||
نامشخص
|
||||
{% endif %}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
<span class="px-3 py-1 text-xs rounded-full
|
||||
{% if file.status == 'completed' %}bg-green-100 text-green-800
|
||||
{% elif file.status == 'processing' %}bg-yellow-100 text-yellow-800
|
||||
{% elif file.status == 'error' %}bg-red-100 text-red-800
|
||||
{% else %}bg-gray-100 text-gray-800{% endif %}">
|
||||
<i class="fas
|
||||
{% if file.status == 'completed' %}fa-check-circle
|
||||
{% elif file.status == 'processing' %}fa-spinner fa-spin
|
||||
{% elif file.status == 'error' %}fa-exclamation-circle
|
||||
{% else %}fa-question-circle{% endif %} ml-1">
|
||||
</i>
|
||||
{% if file.status == 'completed' %}تکمیل شده
|
||||
{% elif file.status == 'processing' %}در حال پردازش
|
||||
{% elif file.status == 'error' %}خطا
|
||||
{% else %}نامشخص{% endif %}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
{% if file.uploaded_at %}
|
||||
<div class="text-sm">
|
||||
<p>{{ file.uploaded_at.strftime('%Y/%m/%d') }}</p>
|
||||
<p class="text-gray-500">{{ file.uploaded_at.strftime('%H:%M') }}</p>
|
||||
</div>
|
||||
{% else %}
|
||||
<span class="text-gray-500">نامشخص</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
<form action="{{ url_for('admin.delete_file', file_id=file.id) }}" method="POST"
|
||||
onsubmit="return confirm('آیا از حذف این فایل اطمینان دارید؟ این عمل غیرقابل بازگشت است.');"
|
||||
class="inline">
|
||||
<button type="submit" class="text-red-600 hover:text-red-800 transition-colors">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- صفحهبندی -->
|
||||
{% if files.pages > 1 %}
|
||||
<div class="flex justify-center items-center space-x-2 space-x-reverse mt-6">
|
||||
{% if files.has_prev %}
|
||||
<a href="{{ url_for('admin.files_management', page=files.prev_num) }}"
|
||||
class="px-4 py-2 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors">
|
||||
<i class="fas fa-chevron-right"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{% for page_num in files.iter_pages() %}
|
||||
{% if page_num %}
|
||||
{% if page_num == files.page %}
|
||||
<span class="px-4 py-2 bg-green-600 text-white rounded-lg">{{ page_num }}</span>
|
||||
{% else %}
|
||||
<a href="{{ url_for('admin.files_management', page=page_num) }}"
|
||||
class="px-4 py-2 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors">
|
||||
{{ page_num }}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<span class="px-2">...</span>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% if files.has_next %}
|
||||
<a href="{{ url_for('admin.files_management', page=files.next_num) }}"
|
||||
class="px-4 py-2 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors">
|
||||
<i class="fas fa-chevron-left"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% else %}
|
||||
<div class="text-center py-8">
|
||||
<i class="fas fa-folder-open text-4xl text-gray-400 mb-4"></i>
|
||||
<h4 class="text-lg font-medium text-gray-700">هیچ فایلی یافت نشد</h4>
|
||||
<p class="text-gray-500 mt-2">هنوز فایلی آپلود نکردهاید</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// مدیریت آپلود فایل
|
||||
const uploadArea = document.getElementById('uploadArea');
|
||||
const fileInput = document.getElementById('fileInput');
|
||||
const fileInfo = document.getElementById('fileInfo');
|
||||
const fileName = document.getElementById('fileName');
|
||||
const fileSize = document.getElementById('fileSize');
|
||||
const uploadBtn = document.getElementById('uploadBtn');
|
||||
const uploadForm = document.getElementById('uploadForm');
|
||||
|
||||
// رویدادهای درگ و دراپ
|
||||
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
|
||||
uploadArea.addEventListener(eventName, preventDefaults, false);
|
||||
});
|
||||
|
||||
function preventDefaults(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
['dragenter', 'dragover'].forEach(eventName => {
|
||||
uploadArea.addEventListener(eventName, highlight, false);
|
||||
});
|
||||
|
||||
['dragleave', 'drop'].forEach(eventName => {
|
||||
uploadArea.addEventListener(eventName, unhighlight, false);
|
||||
});
|
||||
|
||||
function highlight() {
|
||||
uploadArea.classList.add('dragover');
|
||||
}
|
||||
|
||||
function unhighlight() {
|
||||
uploadArea.classList.remove('dragover');
|
||||
}
|
||||
|
||||
uploadArea.addEventListener('drop', handleDrop, false);
|
||||
|
||||
function handleDrop(e) {
|
||||
const dt = e.dataTransfer;
|
||||
const files = dt.files;
|
||||
handleFiles(files);
|
||||
}
|
||||
|
||||
fileInput.addEventListener('change', function() {
|
||||
handleFiles(this.files);
|
||||
});
|
||||
|
||||
function handleFiles(files) {
|
||||
if (files.length > 0) {
|
||||
const file = files[0];
|
||||
|
||||
// بررسی فرمت فایل
|
||||
if (!file.name.toLowerCase().endsWith('.json')) {
|
||||
alert('لطفاً فقط فایلهای JSON آپلود کنید.');
|
||||
return;
|
||||
}
|
||||
|
||||
// بررسی حجم فایل (50MB)
|
||||
if (file.size > 50 * 1024 * 1024) {
|
||||
alert('حجم فایل باید کمتر از 50 مگابایت باشد.');
|
||||
return;
|
||||
}
|
||||
|
||||
// نمایش اطلاعات فایل
|
||||
fileName.textContent = file.name;
|
||||
fileSize.textContent = formatFileSize(file.size);
|
||||
fileInfo.classList.remove('hidden');
|
||||
uploadBtn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
function clearFile() {
|
||||
fileInput.value = '';
|
||||
fileInfo.classList.add('hidden');
|
||||
uploadBtn.disabled = true;
|
||||
}
|
||||
|
||||
function formatFileSize(bytes) {
|
||||
if (bytes === 0) return '0 بایت';
|
||||
const k = 1024;
|
||||
const sizes = ['بایت', 'کیلوبایت', 'مگابایت', 'گیگابایت'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
// مدیریت ارسال فرم
|
||||
uploadForm.addEventListener('submit', function(e) {
|
||||
const file = fileInput.files[0];
|
||||
if (!file) {
|
||||
e.preventDefault();
|
||||
alert('لطفاً یک فایل انتخاب کنید.');
|
||||
return;
|
||||
}
|
||||
|
||||
uploadBtn.disabled = true;
|
||||
uploadBtn.innerHTML = '<i class="fas fa-spinner fa-spin ml-2"></i>در حال آپلود...';
|
||||
});
|
||||
|
||||
// انیمیشن
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const fadeElements = document.querySelectorAll('.fade-in');
|
||||
fadeElements.forEach((element, index) => {
|
||||
element.style.animationDelay = `${index * 0.1}s`;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,261 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fa" dir="rtl">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>مدیریت کاربران - پنل مدیریت</title>
|
||||
<script src="/static/3.4.17.es"></script>
|
||||
<link rel="stylesheet" href="/static/all.min.css">
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css2?family=Vazir&display=swap');
|
||||
body { font-family: 'Vazir', Tahoma, sans-serif; }
|
||||
|
||||
.fade-in {
|
||||
animation: fadeIn 0.5s ease-in;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-gray-50 min-h-screen">
|
||||
<div class="flex">
|
||||
<!-- نوار کناری -->
|
||||
<aside class="w-64 bg-green-800 text-white min-h-screen">
|
||||
<div class="p-4">
|
||||
<h1 class="text-xl font-bold text-center border-b border-green-700 pb-4">
|
||||
<i class="fas fa-cogs ml-2"></i>
|
||||
پنل مدیریت
|
||||
</h1>
|
||||
<nav class="mt-8">
|
||||
<ul class="space-y-2">
|
||||
<li><a href="{{ url_for('admin.admin_dashboard') }}" class="flex items-center p-3 rounded-lg hover:bg-green-700 transition-colors"><i class="fas fa-tachometer-alt ml-3"></i>داشبورد</a></li>
|
||||
<li><a href="{{ url_for('admin.files_management') }}" class="flex items-center p-3 rounded-lg hover:bg-green-700 transition-colors"><i class="fas fa-file-upload ml-3"></i>مدیریت فایلها</a></li>
|
||||
<li><a href="{{ url_for('admin.articles_management') }}" class="flex items-center p-3 rounded-lg hover:bg-green-700 transition-colors"><i class="fas fa-book ml-3"></i>مدیریت مقالات</a></li>
|
||||
<li><a href="{{ url_for('admin.users_management') }}" class="flex items-center p-3 rounded-lg bg-green-700 transition-colors"><i class="fas fa-users ml-3"></i>مدیریت کاربران</a></li>
|
||||
<li><a href="{{ url_for('admin.corrections_management') }}" class="flex items-center p-3 rounded-lg hover:bg-green-700 transition-colors"><i class="fas fa-edit ml-3"></i>نظارت بر تصحیحها</a></li>
|
||||
<li class="pt-4 border-t border-green-700 mt-4"><a href="{{ url_for('dashboard.index') }}" class="flex items-center p-3 rounded-lg hover:bg-green-700 transition-colors"><i class="fas fa-arrow-right ml-3"></i>بازگشت به سایت</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- محتوای اصلی -->
|
||||
<main class="flex-1 p-6">
|
||||
<!-- هدر صفحه -->
|
||||
<header class="bg-white rounded-2xl shadow-sm p-6 mb-6 fade-in">
|
||||
<div class="flex justify-between items-center">
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold text-gray-800">مدیریت کاربران</h2>
|
||||
<p class="text-gray-600 mt-1">مدیریت حسابهای کاربری و سطوح دسترسی</p>
|
||||
</div>
|
||||
<div class="flex items-center space-x-4 space-x-reverse">
|
||||
<div class="w-10 h-10 bg-gradient-to-r from-green-500 to-emerald-600 rounded-full flex items-center justify-center text-white shadow-lg">
|
||||
<i class="fas fa-user text-sm"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- آمار کاربران -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-6">
|
||||
<div class="bg-white rounded-2xl shadow-sm p-6 text-center fade-in">
|
||||
<div class="w-12 h-12 bg-blue-100 rounded-full flex items-center justify-center mx-auto mb-3">
|
||||
<i class="fas fa-users text-blue-500 text-xl"></i>
|
||||
</div>
|
||||
<p class="text-gray-500 text-sm">کل کاربران</p>
|
||||
<p class="text-2xl font-bold text-gray-800">{{ users.total }}</p>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-2xl shadow-sm p-6 text-center fade-in">
|
||||
<div class="w-12 h-12 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-3">
|
||||
<i class="fas fa-user-check text-green-500 text-xl"></i>
|
||||
</div>
|
||||
<p class="text-gray-500 text-sm">کاربران فعال</p>
|
||||
<p class="text-2xl font-bold text-gray-800" id="activeUsersCount">...</p>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-2xl shadow-sm p-6 text-center fade-in">
|
||||
<div class="w-12 h-12 bg-red-100 rounded-full flex items-center justify-center mx-auto mb-3">
|
||||
<i class="fas fa-user-slash text-red-500 text-xl"></i>
|
||||
</div>
|
||||
<p class="text-gray-500 text-sm">کاربران غیرفعال</p>
|
||||
<p class="text-2xl font-bold text-gray-800" id="inactiveUsersCount">...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- لیست کاربران -->
|
||||
<div class="bg-white rounded-2xl shadow-sm p-6 fade-in">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h3 class="text-xl font-bold text-gray-800 flex items-center">
|
||||
<i class="fas fa-list ml-2 text-blue-600"></i>
|
||||
لیست کاربران
|
||||
</h3>
|
||||
<div class="text-sm text-gray-500">
|
||||
صفحه {{ users.page }} از {{ users.pages }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if users.items %}
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm text-right text-gray-700">
|
||||
<thead class="text-xs text-gray-700 uppercase bg-gray-50">
|
||||
<tr>
|
||||
<th class="px-4 py-3">کاربر</th>
|
||||
<th class="px-4 py-3">ایمیل</th>
|
||||
<th class="px-4 py-3">نقش</th>
|
||||
<th class="px-4 py-3">وضعیت</th>
|
||||
<th class="px-4 py-3">تاریخ ثبتنام</th>
|
||||
<th class="px-4 py-3">عملیات</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for user in users.items %}
|
||||
<tr class="border-b hover:bg-gray-50 transition-colors">
|
||||
<td class="px-4 py-3">
|
||||
<div class="flex items-center">
|
||||
<div class="w-8 h-8 bg-gradient-to-r from-green-400 to-blue-500 rounded-full flex items-center justify-center text-white text-sm font-bold ml-3">
|
||||
{{ user.username[0]|upper }}
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-medium text-gray-900">{{ user.username }}</p>
|
||||
<p class="text-xs text-gray-500">ID: {{ user.id }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
<p class="text-gray-600">{{ user.email }}</p>
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
<span class="px-3 py-1 text-xs rounded-full
|
||||
{% if user.role == 'admin' %}bg-purple-100 text-purple-800
|
||||
{% else %}bg-blue-100 text-blue-800{% endif %}">
|
||||
<i class="fas
|
||||
{% if user.role == 'admin' %}fa-crown
|
||||
{% else %}fa-user{% endif %} ml-1">
|
||||
</i>
|
||||
{{ 'مدیر' if user.role == 'admin' else 'کاربر عادی' }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
<span class="px-3 py-1 text-xs rounded-full
|
||||
{% if user.is_active %}bg-green-100 text-green-800
|
||||
{% else %}bg-red-100 text-red-800{% endif %}">
|
||||
<i class="fas
|
||||
{% if user.is_active %}fa-check-circle
|
||||
{% else %}fa-times-circle{% endif %} ml-1">
|
||||
</i>
|
||||
{{ 'فعال' if user.is_active else 'غیرفعال' }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
{% if user.created_at %}
|
||||
<div class="text-sm">
|
||||
<p>{{ user.created_at.strftime('%Y/%m/%d') }}</p>
|
||||
<p class="text-gray-500">{{ user.created_at.strftime('%H:%M') }}</p>
|
||||
</div>
|
||||
{% else %}
|
||||
<span class="text-gray-500">نامشخص</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
{% if user.id != current_user.id %}
|
||||
<form action="{{ url_for('admin.toggle_user', user_id=user.id) }}" method="POST"
|
||||
onsubmit="return confirm('آیا از {{ 'غیرفعال' if user.is_active else 'فعال' }} کردن این کاربر اطمینان دارید؟');"
|
||||
class="inline">
|
||||
<button type="submit"
|
||||
class="{% if user.is_active %}text-red-600 hover:text-red-800{% else %}text-green-600 hover:text-green-800{% endif %} transition-colors ml-3">
|
||||
<i class="fas {% if user.is_active %}fa-user-slash{% else %}fa-user-check{% endif %}"></i>
|
||||
</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<span class="text-gray-400 text-sm">حساب شما</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- صفحهبندی -->
|
||||
{% if users.pages > 1 %}
|
||||
<div class="flex justify-center items-center space-x-2 space-x-reverse mt-6">
|
||||
{% if users.has_prev %}
|
||||
<a href="{{ url_for('admin.users_management', page=users.prev_num) }}"
|
||||
class="px-4 py-2 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors">
|
||||
<i class="fas fa-chevron-right"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{% for page_num in users.iter_pages() %}
|
||||
{% if page_num %}
|
||||
{% if page_num == users.page %}
|
||||
<span class="px-4 py-2 bg-green-600 text-white rounded-lg">{{ page_num }}</span>
|
||||
{% else %}
|
||||
<a href="{{ url_for('admin.users_management', page=page_num) }}"
|
||||
class="px-4 py-2 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors">
|
||||
{{ page_num }}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<span class="px-2">...</span>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% if users.has_next %}
|
||||
<a href="{{ url_for('admin.users_management', page=users.next_num) }}"
|
||||
class="px-4 py-2 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors">
|
||||
<i class="fas fa-chevron-left"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% else %}
|
||||
<div class="text-center py-8">
|
||||
<i class="fas fa-users text-4xl text-gray-400 mb-4"></i>
|
||||
<h4 class="text-lg font-medium text-gray-700">هیچ کاربری یافت نشد</h4>
|
||||
<p class="text-gray-500 mt-2">هنوز کاربری ثبتنام نکرده است</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// محاسبه آمار کاربران
|
||||
function calculateUserStats() {
|
||||
const activeUsers = document.querySelectorAll('tbody tr').length -
|
||||
document.querySelectorAll('tbody tr:has(.bg-red-100)').length;
|
||||
const inactiveUsers = document.querySelectorAll('tbody tr:has(.bg-red-100)').length;
|
||||
|
||||
document.getElementById('activeUsersCount').textContent = activeUsers;
|
||||
document.getElementById('inactiveUsersCount').textContent = inactiveUsers;
|
||||
}
|
||||
|
||||
// انیمیشن
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const fadeElements = document.querySelectorAll('.fade-in');
|
||||
fadeElements.forEach((element, index) => {
|
||||
element.style.animationDelay = `${index * 0.1}s`;
|
||||
});
|
||||
|
||||
calculateUserStats();
|
||||
});
|
||||
|
||||
// خودکار پنهان کردن پیامها
|
||||
setTimeout(() => {
|
||||
const flashMessages = document.querySelectorAll('.fixed div');
|
||||
flashMessages.forEach(msg => {
|
||||
msg.style.transition = 'all 0.5s ease';
|
||||
msg.style.opacity = '0';
|
||||
msg.style.transform = 'translateX(-100%)';
|
||||
setTimeout(() => msg.remove(), 500);
|
||||
});
|
||||
}, 5000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,805 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fa" dir="rtl">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>معیارسنج</title>
|
||||
<link rel="stylesheet" href="/static/all.min.css">
|
||||
<link href="https://cdn.jsdelivr.net/gh/rastikerdar/vazirmatn@v33.003/Vazirmatn-font-face.css" rel="stylesheet">
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
font-family: 'Vazirmatn', 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
}
|
||||
|
||||
:root {
|
||||
--primary-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
--glass-bg: rgba(255, 255, 255, 0.08);
|
||||
--glass-border: rgba(255, 255, 255, 0.1);
|
||||
--text-primary: #ffffff;
|
||||
--text-secondary: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--primary-gradient);
|
||||
color: var(--text-primary);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* گرادیان پویا و بهینهشده */
|
||||
.gradient-bg {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: -1;
|
||||
background:
|
||||
radial-gradient(circle at 20% 80%, rgba(120, 119, 198, 0.15) 0%, transparent 40%),
|
||||
radial-gradient(circle at 80% 20%, rgba(255, 119, 198, 0.1) 0%, transparent 40%),
|
||||
radial-gradient(circle at 40% 40%, rgba(120, 219, 255, 0.1) 0%, transparent 40%);
|
||||
animation: gradientShift 20s ease-in-out infinite alternate;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
@keyframes gradientShift {
|
||||
0% {
|
||||
transform: scale(1) rotate(0deg);
|
||||
opacity: 0.8;
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.1) rotate(180deg);
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
transform: scale(1) rotate(360deg);
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
/* نوار ناوبری بهینهشده */
|
||||
nav {
|
||||
padding: 1.5rem 2rem;
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.auth-buttons {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 12px 30px;
|
||||
border: none;
|
||||
border-radius: 50px;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
font-size: 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.btn::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s;
|
||||
border-radius: 50px;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
color: var(--text-primary);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.btn-outline {
|
||||
background: transparent;
|
||||
color: var(--text-primary);
|
||||
border: 2px solid rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 15px 30px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.btn:hover::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
|
||||
.btn-outline:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
/* بخش اصلی بهینهشده */
|
||||
.main-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.site-title {
|
||||
font-size: clamp(3rem, 8vw, 5rem);
|
||||
font-weight: 800;
|
||||
letter-spacing: -1px;
|
||||
line-height: 1.1;
|
||||
background: linear-gradient(135deg, #ffffff 0%, #e6e6e6 50%, #b3b3b3 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
text-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
|
||||
animation: titleEntrance 1s ease-out;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
@keyframes titleEntrance {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(30px) scale(0.9);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.floating {
|
||||
animation: floating 6s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes floating {
|
||||
0%, 100% {
|
||||
transform: translateY(0px) rotate(0deg);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-25px) rotate(1deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* صفحات ورود و ثبتنام بهینهشده */
|
||||
.auth-page {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 100;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: all 0.3s ease-out;
|
||||
backdrop-filter: blur(5px);
|
||||
-webkit-backdrop-filter: blur(5px);
|
||||
}
|
||||
|
||||
.auth-page.active {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.auth-card {
|
||||
background: var(--glass-bg);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
border-radius: 24px;
|
||||
padding: 40px;
|
||||
width: 90%;
|
||||
max-width: 450px;
|
||||
border: 1px solid var(--glass-border);
|
||||
box-shadow:
|
||||
0 20px 40px rgba(0, 0, 0, 0.3),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
||||
transform: translateY(30px) scale(0.95);
|
||||
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.auth-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 1px;
|
||||
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent);
|
||||
}
|
||||
|
||||
.auth-page.active .auth-card {
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
|
||||
.auth-header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.auth-header h2 {
|
||||
font-size: 2.2rem;
|
||||
margin-bottom: 10px;
|
||||
background: linear-gradient(135deg, #ffffff 0%, #e6e6e6 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.auth-header p {
|
||||
color: var(--text-secondary);
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
left: 20px;
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-primary);
|
||||
font-size: 1.5rem;
|
||||
cursor: pointer;
|
||||
opacity: 0.7;
|
||||
transition: opacity 0.3s;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.close-btn:hover {
|
||||
opacity: 1;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
width: 100%;
|
||||
padding: 16px 20px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 12px;
|
||||
color: var(--text-primary);
|
||||
font-size: 1rem;
|
||||
transition: all 0.3s;
|
||||
backdrop-filter: blur(5px);
|
||||
}
|
||||
|
||||
.form-control::placeholder {
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
outline: none;
|
||||
border-color: rgba(255, 255, 255, 0.3);
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.auth-btn {
|
||||
width: 100%;
|
||||
padding: 16px;
|
||||
background: linear-gradient(135deg, rgba(255, 255, 255, 0.2), rgba(255, 255, 255, 0.1));
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 12px;
|
||||
color: var(--text-primary);
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
margin-top: 10px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.auth-btn::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
|
||||
transition: left 0.6s;
|
||||
}
|
||||
|
||||
.auth-btn:hover {
|
||||
background: linear-gradient(135deg, rgba(255, 255, 255, 0.3), rgba(255, 255, 255, 0.2));
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.auth-btn:hover::before {
|
||||
left: 100%;
|
||||
}
|
||||
|
||||
.auth-btn.loading {
|
||||
opacity: 0.8;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* پیامها */
|
||||
.message {
|
||||
padding: 12px 16px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
text-align: center;
|
||||
font-weight: 500;
|
||||
display: none;
|
||||
animation: slideIn 0.3s ease-out;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.error {
|
||||
background: linear-gradient(135deg, rgba(231, 76, 60, 0.2), rgba(192, 57, 43, 0.2));
|
||||
border: 1px solid rgba(231, 76, 60, 0.3);
|
||||
color: #ff9c8e;
|
||||
}
|
||||
|
||||
.success {
|
||||
background: linear-gradient(135deg, rgba(46, 204, 113, 0.2), rgba(39, 174, 96, 0.2));
|
||||
border: 1px solid rgba(46, 204, 113, 0.3);
|
||||
color: #a3ffce;
|
||||
}
|
||||
|
||||
/* افکتهای داینامیک */
|
||||
.floating-particles {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.particle {
|
||||
position: absolute;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 50%;
|
||||
animation: floatParticle 15s infinite linear;
|
||||
}
|
||||
|
||||
@keyframes floatParticle {
|
||||
0% {
|
||||
transform: translateY(100vh) translateX(0) rotate(0deg);
|
||||
opacity: 0;
|
||||
}
|
||||
10% {
|
||||
opacity: 0.3;
|
||||
}
|
||||
90% {
|
||||
opacity: 0.3;
|
||||
}
|
||||
100% {
|
||||
transform: translateY(-100px) translateX(100px) rotate(360deg);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* رسپانسیو */
|
||||
@media (max-width: 768px) {
|
||||
nav {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.auth-buttons {
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.auth-card {
|
||||
padding: 30px 20px;
|
||||
margin: 1rem;
|
||||
}
|
||||
|
||||
.site-title {
|
||||
font-size: 3.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.site-title {
|
||||
font-size: 2.8rem;
|
||||
}
|
||||
|
||||
.auth-header h2 {
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
padding: 14px 16px;
|
||||
}
|
||||
}
|
||||
|
||||
/* اسکرول بار زیبا */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<!-- گرادیان پسزمینه -->
|
||||
<div class="gradient-bg"></div>
|
||||
|
||||
<!-- ذرات شناور -->
|
||||
<div class="floating-particles" id="particles"></div>
|
||||
|
||||
<!-- نوار ناوبری -->
|
||||
<nav>
|
||||
<div class="auth-buttons">
|
||||
<button class="btn btn-primary" onclick="showLogin()">
|
||||
<i class="fas fa-sign-in-alt"></i>
|
||||
ورود
|
||||
</button>
|
||||
<button class="btn btn-outline" onclick="showRegister()">
|
||||
<i class="fas fa-user-plus"></i>
|
||||
ثبت نام
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- محتوای اصلی -->
|
||||
<main class="main-content">
|
||||
<h1 class="site-title floating">معیارسنج</h1>
|
||||
</main>
|
||||
|
||||
<!-- صفحه ورود -->
|
||||
<div id="login-page" class="auth-page">
|
||||
<div class="auth-card">
|
||||
<button class="close-btn" onclick="hideAuthPages()">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
<div class="auth-header">
|
||||
<h2>ورود به سامانه</h2>
|
||||
<p>لطفا اطلاعات حساب کاربری خود را وارد کنید</p>
|
||||
</div>
|
||||
|
||||
<!-- پیامها -->
|
||||
<div id="login-message" class="message"></div>
|
||||
|
||||
<form id="login-form">
|
||||
<div class="form-group">
|
||||
<input type="text" id="login-username" class="form-control" placeholder="نام کاربری" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="password" id="login-password" class="form-control" placeholder="رمز عبور" required>
|
||||
</div>
|
||||
<button type="submit" class="auth-btn" id="login-submit">
|
||||
<span id="login-text">ورود به سامانه</span>
|
||||
<span id="login-loading" style="display: none;">
|
||||
<i class="fas fa-spinner fa-spin"></i> در حال ورود...
|
||||
</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- صفحه ثبت نام -->
|
||||
<div id="register-page" class="auth-page">
|
||||
<div class="auth-card">
|
||||
<button class="close-btn" onclick="hideAuthPages()">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
<div class="auth-header">
|
||||
<h2>ثبت نام در سامانه</h2>
|
||||
<p>فرم زیر را برای ایجاد حساب کاربری تکمیل کنید</p>
|
||||
</div>
|
||||
|
||||
<!-- پیامها -->
|
||||
<div id="register-message" class="message"></div>
|
||||
|
||||
<form id="register-form">
|
||||
<div class="form-group">
|
||||
<input type="text" id="register-username" class="form-control" placeholder="نام کاربری" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="email" id="register-email" class="form-control" placeholder="ایمیل" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="password" id="register-password" class="form-control" placeholder="رمز عبور" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="password" id="register-confirm-password" class="form-control"
|
||||
placeholder="تکرار رمز عبور" required>
|
||||
</div>
|
||||
<button type="submit" class="auth-btn" id="register-submit">
|
||||
<span id="register-text">ثبت نام در سامانه</span>
|
||||
<span id="register-loading" style="display: none;">
|
||||
<i class="fas fa-spinner fa-spin"></i> در حال ثبت نام...
|
||||
</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// ایجاد ذرات شناور
|
||||
function createParticles() {
|
||||
const particlesContainer = document.getElementById('particles');
|
||||
const particleCount = 20;
|
||||
|
||||
for (let i = 0; i < particleCount; i++) {
|
||||
const particle = document.createElement('div');
|
||||
particle.classList.add('particle');
|
||||
|
||||
// اندازه تصادفی
|
||||
const size = Math.random() * 60 + 10;
|
||||
particle.style.width = `${size}px`;
|
||||
particle.style.height = `${size}px`;
|
||||
|
||||
// موقعیت تصادفی
|
||||
particle.style.left = `${Math.random() * 100}%`;
|
||||
particle.style.top = `${Math.random() * 100}%`;
|
||||
|
||||
// تاخیر و مدت زمان انیمیشن تصادفی
|
||||
const delay = Math.random() * 5;
|
||||
const duration = 15 + Math.random() * 10;
|
||||
particle.style.animationDelay = `${delay}s`;
|
||||
particle.style.animationDuration = `${duration}s`;
|
||||
|
||||
particlesContainer.appendChild(particle);
|
||||
}
|
||||
}
|
||||
|
||||
// فراخوانی پس از لود صفحه
|
||||
document.addEventListener('DOMContentLoaded', createParticles);
|
||||
|
||||
// توابع نمایش/پنهان کردن صفحات
|
||||
function showLogin() {
|
||||
document.getElementById('login-page').classList.add('active');
|
||||
document.getElementById('login-form').reset();
|
||||
hideMessage('login-message');
|
||||
}
|
||||
|
||||
function showRegister() {
|
||||
document.getElementById('register-page').classList.add('active');
|
||||
document.getElementById('register-form').reset();
|
||||
hideMessage('register-message');
|
||||
}
|
||||
|
||||
function hideAuthPages() {
|
||||
document.querySelectorAll('.auth-page').forEach(page => {
|
||||
page.classList.remove('active');
|
||||
});
|
||||
}
|
||||
|
||||
// بستن صفحات با کلیک خارج از کارت
|
||||
document.querySelectorAll('.auth-page').forEach(page => {
|
||||
page.addEventListener('click', function (e) {
|
||||
if (e.target === this) {
|
||||
hideAuthPages();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// مدیریت فرم لاگین
|
||||
document.getElementById('login-form').addEventListener('submit', function (e) {
|
||||
e.preventDefault();
|
||||
loginUser();
|
||||
});
|
||||
|
||||
// مدیریت فرم ثبت نام
|
||||
document.getElementById('register-form').addEventListener('submit', function (e) {
|
||||
e.preventDefault();
|
||||
registerUser();
|
||||
});
|
||||
|
||||
// تابع نمایش پیام
|
||||
function showMessage(elementId, message, type) {
|
||||
const messageElement = document.getElementById(elementId);
|
||||
messageElement.textContent = message;
|
||||
messageElement.className = `message ${type}`;
|
||||
messageElement.style.display = 'block';
|
||||
}
|
||||
|
||||
// تابع پنهان کردن پیام
|
||||
function hideMessage(elementId) {
|
||||
const messageElement = document.getElementById(elementId);
|
||||
messageElement.style.display = 'none';
|
||||
}
|
||||
|
||||
async function loginUser() {
|
||||
const username = document.getElementById('login-username').value;
|
||||
const password = document.getElementById('login-password').value;
|
||||
|
||||
if (!username || !password) {
|
||||
showMessage('login-message', 'لطفا تمام فیلدها را پر کنید', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById('login-text').style.display = 'none';
|
||||
document.getElementById('login-loading').style.display = 'inline';
|
||||
document.getElementById('login-submit').classList.add('loading');
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('username', username);
|
||||
formData.append('password', password);
|
||||
|
||||
const response = await fetch('/auth/login', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
if (response.redirected) {
|
||||
window.location.href = response.url;
|
||||
} else {
|
||||
const html = await response.text();
|
||||
showMessage('login-message', 'نام کاربری یا رمز عبور اشتباه است', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
showMessage('login-message', 'خطا در ارتباط با سرور', 'error');
|
||||
} finally {
|
||||
document.getElementById('login-text').style.display = 'inline';
|
||||
document.getElementById('login-loading').style.display = 'none';
|
||||
document.getElementById('login-submit').classList.remove('loading');
|
||||
}
|
||||
}
|
||||
|
||||
async function registerUser() {
|
||||
const username = document.getElementById('register-username').value;
|
||||
const email = document.getElementById('register-email').value;
|
||||
const password = document.getElementById('register-password').value;
|
||||
const confirmPassword = document.getElementById('register-confirm-password').value;
|
||||
|
||||
if (!username || !email || !password || !confirmPassword) {
|
||||
showMessage('register-message', 'لطفا تمام فیلدها را پر کنید', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (password !== confirmPassword) {
|
||||
showMessage('register-message', 'رمز عبور و تکرار آن مطابقت ندارند', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (password.length < 6) {
|
||||
showMessage('register-message', 'رمز عبور باید حداقل 6 کاراکتر باشد', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!emailRegex.test(email)) {
|
||||
showMessage('register-message', 'لطفا یک ایمیل معتبر وارد کنید', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById('register-text').style.display = 'none';
|
||||
document.getElementById('register-loading').style.display = 'inline';
|
||||
document.getElementById('register-submit').classList.add('loading');
|
||||
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('username', username);
|
||||
formData.append('email', email);
|
||||
formData.append('password', password);
|
||||
|
||||
const response = await fetch('/auth/register', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
const responseText = await response.text();
|
||||
|
||||
let responseData;
|
||||
try {
|
||||
responseData = JSON.parse(responseText);
|
||||
} catch (e) {
|
||||
console.log('Response is not JSON:', responseText);
|
||||
if (response.ok && (responseText.includes('success') || responseText.includes('موفق'))) {
|
||||
showMessage('register-message', 'ثبت نام با موفقیت انجام شد!', 'success');
|
||||
setTimeout(() => {
|
||||
hideAuthPages();
|
||||
showLogin();
|
||||
}, 2000);
|
||||
} else {
|
||||
showMessage('register-message', 'پاسخ غیرمنتظره از سرور', 'error');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.ok) {
|
||||
if (responseData.success) {
|
||||
showMessage('register-message', 'ثبت نام با موفقیت انجام شد!', 'success');
|
||||
setTimeout(() => {
|
||||
hideAuthPages();
|
||||
showLogin();
|
||||
}, 2000);
|
||||
} else {
|
||||
showMessage('register-message', responseData.message || 'خطا در ثبت نام', 'error');
|
||||
}
|
||||
} else {
|
||||
let errorMessage = 'خطا در ثبت نام';
|
||||
if (response.status === 400) {
|
||||
errorMessage = 'اطلاعات وارد شده معتبر نیست';
|
||||
} else if (response.status === 409) {
|
||||
errorMessage = 'نام کاربری یا ایمیل قبلاً استفاده شده است';
|
||||
} else if (response.status === 500) {
|
||||
errorMessage = 'خطای سرور لطفاً بعداً تلاش کنید';
|
||||
}
|
||||
showMessage('register-message', responseData?.message || errorMessage, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Network error:', error);
|
||||
showMessage('register-message', 'خطا در ارتباط با سرور', 'error');
|
||||
} finally {
|
||||
document.getElementById('register-text').style.display = 'inline';
|
||||
document.getElementById('register-loading').style.display = 'none';
|
||||
document.getElementById('register-submit').classList.remove('loading');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,615 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fa" dir="rtl">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>معیارسنج</title>
|
||||
<link rel="stylesheet" href="/static/all.min.css">
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
font-family: 'Vazirmatn', 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
background: url('static/images/bg.jpg');
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-attachment: fixed;
|
||||
color: white;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* افکتهای گرادیان پویا */
|
||||
body::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
background:
|
||||
radial-gradient(circle at 20% 80%, rgba(120, 119, 198, 0.3) 0%, transparent 25%),
|
||||
radial-gradient(circle at 80% 20%, rgba(255, 119, 198, 0.2) 0%, transparent 25%),
|
||||
radial-gradient(circle at 40% 40%, rgba(120, 219, 255, 0.2) 0%, transparent 25%);
|
||||
z-index: -1;
|
||||
animation: gradientShift 15s ease infinite;
|
||||
}
|
||||
|
||||
@keyframes gradientShift {
|
||||
0% {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
/* نوار ناوبری */
|
||||
nav {
|
||||
padding: 1.5rem 2rem;
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.auth-buttons {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 12px 30px;
|
||||
border: none;
|
||||
border-radius: 50px;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s;
|
||||
font-size: 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: rgba(207, 206, 206, 0);
|
||||
color: rgb(255, 255, 255);
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.btn-outline {
|
||||
background: transparent;
|
||||
color: white;
|
||||
border: 2px solid rgba(255, 255, 255, 0.308);
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.btn-outline:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
/* بخش اصلی با نام سامانه */
|
||||
.main-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.site-title {
|
||||
font-size: 5rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: -2px;
|
||||
line-height: 1.1;
|
||||
text-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
|
||||
background: linear-gradient(to right, #fff, #f0f0f0);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
animation: titleGlow 3s ease-in-out infinite alternate;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@keyframes titleGlow {
|
||||
from {
|
||||
text-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
to {
|
||||
text-shadow: 0 5px 25px rgba(0, 0, 0, 0.4), 0 0 30px rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
/* صفحات ورود و ثبت نام */
|
||||
.auth-page {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 100;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.auth-page.active {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.auth-card {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
backdrop-filter: blur(15px);
|
||||
border-radius: 20px;
|
||||
padding: 40px;
|
||||
width: 90%;
|
||||
max-width: 450px;
|
||||
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.3);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
transform: translateY(20px);
|
||||
transition: transform 0.4s;
|
||||
}
|
||||
|
||||
.auth-page.active .auth-card {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.auth-header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.auth-header h2 {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
left: 20px;
|
||||
background: none;
|
||||
border: none;
|
||||
color: white;
|
||||
font-size: 1.5rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
width: 100%;
|
||||
padding: 15px 20px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 10px;
|
||||
color: white;
|
||||
font-size: 1rem;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.form-control::placeholder {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
outline: none;
|
||||
border-color: rgba(255, 255, 255, 0.5);
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
.auth-btn {
|
||||
width: 100%;
|
||||
padding: 15px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
color: white;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
margin-top: 10px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.auth-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.auth-btn.loading {
|
||||
opacity: 0.7;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.floating {
|
||||
animation: floating 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes floating {
|
||||
0% {
|
||||
transform: translateY(0px);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: translateY(-20px);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translateY(0px);
|
||||
}
|
||||
}
|
||||
|
||||
/* استایل برای پیامها */
|
||||
.message {
|
||||
padding: 10px 15px;
|
||||
border-radius: 5px;
|
||||
margin-bottom: 15px;
|
||||
text-align: center;
|
||||
font-weight: 500;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.error {
|
||||
background: rgba(231, 76, 60, 0.2);
|
||||
border: 1px solid rgba(231, 76, 60, 0.5);
|
||||
color: #e74c3c;
|
||||
}
|
||||
|
||||
.success {
|
||||
background: rgba(46, 204, 113, 0.2);
|
||||
border: 1px solid rgba(46, 204, 113, 0.5);
|
||||
color: #2ecc71;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.site-title {
|
||||
font-size: 3.5rem;
|
||||
}
|
||||
|
||||
nav {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 10px 20px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.site-title {
|
||||
font-size: 2.8rem;
|
||||
}
|
||||
|
||||
.auth-buttons {
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.auth-card {
|
||||
padding: 30px 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<!-- نوار ناوبری -->
|
||||
<nav>
|
||||
<div class="auth-buttons">
|
||||
<button class="btn btn-primary" onclick="showLogin()">
|
||||
<i class="fas fa-sign-in-alt"></i>
|
||||
ورود
|
||||
</button>
|
||||
<button class="btn btn-outline" onclick="showRegister()">
|
||||
<i class="fas fa-user-plus"></i>
|
||||
ثبت نام
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- محتوای اصلی -->
|
||||
<main class="main-content">
|
||||
<h1 class="site-title floating">معیارسنج</h1>
|
||||
</main>
|
||||
|
||||
<!-- صفحه ورود -->
|
||||
<div id="login-page" class="auth-page">
|
||||
<div class="auth-card">
|
||||
<button class="close-btn" onclick="hideAuthPages()">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
<div class="auth-header">
|
||||
<h2>ورود به سامانه</h2>
|
||||
<p>لطفا اطلاعات حساب کاربری خود را وارد کنید</p>
|
||||
</div>
|
||||
|
||||
<!-- پیامها -->
|
||||
<div id="login-message" class="message"></div>
|
||||
|
||||
<form id="login-form">
|
||||
<div class="form-group">
|
||||
<input type="text" id="login-username" class="form-control" placeholder="نام کاربری" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="password" id="login-password" class="form-control" placeholder="رمز عبور" required>
|
||||
</div>
|
||||
<button type="submit" class="auth-btn" id="login-submit">
|
||||
<span id="login-text">ورود به سامانه</span>
|
||||
<span id="login-loading" style="display: none;">
|
||||
<i class="fas fa-spinner fa-spin"></i> در حال ورود...
|
||||
</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- صفحه ثبت نام -->
|
||||
<div id="register-page" class="auth-page">
|
||||
<div class="auth-card">
|
||||
<button class="close-btn" onclick="hideAuthPages()">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
<div class="auth-header">
|
||||
<h2>ثبت نام در سامانه</h2>
|
||||
<p>فرم زیر را برای ایجاد حساب کاربری تکمیل کنید</p>
|
||||
</div>
|
||||
|
||||
<!-- پیامها -->
|
||||
<div id="register-message" class="message"></div>
|
||||
|
||||
<form id="register-form">
|
||||
<div class="form-group">
|
||||
<input type="text" id="register-username" class="form-control" placeholder="نام کاربری" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="email" id="register-email" class="form-control" placeholder="ایمیل" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="password" id="register-password" class="form-control" placeholder="رمز عبور" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="password" id="register-confirm-password" class="form-control"
|
||||
placeholder="تکرار رمز عبور" required>
|
||||
</div>
|
||||
<button type="submit" class="auth-btn" id="register-submit">
|
||||
<span id="register-text">ثبت نام در سامانه</span>
|
||||
<span id="register-loading" style="display: none;">
|
||||
<i class="fas fa-spinner fa-spin"></i> در حال ثبت نام...
|
||||
</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// توابع نمایش/پنهان کردن صفحات
|
||||
function showLogin() {
|
||||
document.getElementById('login-page').classList.add('active');
|
||||
// پاک کردن فرم و پیامها
|
||||
document.getElementById('login-form').reset();
|
||||
hideMessage('login-message');
|
||||
}
|
||||
|
||||
function showRegister() {
|
||||
document.getElementById('register-page').classList.add('active');
|
||||
// پاک کردن فرم و پیامها
|
||||
document.getElementById('register-form').reset();
|
||||
hideMessage('register-message');
|
||||
}
|
||||
|
||||
function hideAuthPages() {
|
||||
document.querySelectorAll('.auth-page').forEach(page => {
|
||||
page.classList.remove('active');
|
||||
});
|
||||
}
|
||||
|
||||
// بستن صفحات با کلیک خارج از کارت
|
||||
document.querySelectorAll('.auth-page').forEach(page => {
|
||||
page.addEventListener('click', function (e) {
|
||||
if (e.target === this) {
|
||||
hideAuthPages();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// مدیریت فرم لاگین
|
||||
document.getElementById('login-form').addEventListener('submit', function (e) {
|
||||
e.preventDefault();
|
||||
loginUser();
|
||||
});
|
||||
|
||||
// مدیریت فرم ثبت نام
|
||||
document.getElementById('register-form').addEventListener('submit', function (e) {
|
||||
e.preventDefault();
|
||||
registerUser();
|
||||
});
|
||||
|
||||
// تابع نمایش پیام
|
||||
function showMessage(elementId, message, type) {
|
||||
const messageElement = document.getElementById(elementId);
|
||||
messageElement.textContent = message;
|
||||
messageElement.className = `message ${type}`;
|
||||
messageElement.style.display = 'block';
|
||||
}
|
||||
|
||||
// تابع پنهان کردن پیام
|
||||
function hideMessage(elementId) {
|
||||
const messageElement = document.getElementById(elementId);
|
||||
messageElement.style.display = 'none';
|
||||
}
|
||||
|
||||
async function loginUser() {
|
||||
const username = document.getElementById('login-username').value;
|
||||
const password = document.getElementById('login-password').value;
|
||||
|
||||
if (!username || !password) {
|
||||
showMessage('login-message', 'لطفا تمام فیلدها را پر کنید', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById('login-text').style.display = 'none';
|
||||
document.getElementById('login-loading').style.display = 'inline';
|
||||
document.getElementById('login-submit').classList.add('loading');
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('username', username);
|
||||
formData.append('password', password);
|
||||
|
||||
const response = await fetch('/auth/login', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
console.log(response)
|
||||
if (response.redirected) {
|
||||
window.location.href = response.url;
|
||||
} else {
|
||||
const html = await response.text();
|
||||
showMessage('login-message', 'نام کاربری یا رمز عبور اشتباه است', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
showMessage('login-message', 'خطا در ارتباط با سرور', 'error');
|
||||
} finally {
|
||||
document.getElementById('login-text').style.display = 'inline';
|
||||
document.getElementById('login-loading').style.display = 'none';
|
||||
document.getElementById('login-submit').classList.remove('loading');
|
||||
}
|
||||
}
|
||||
|
||||
async function registerUser() {
|
||||
const username = document.getElementById('register-username').value;
|
||||
const email = document.getElementById('register-email').value;
|
||||
const password = document.getElementById('register-password').value;
|
||||
const confirmPassword = document.getElementById('register-confirm-password').value;
|
||||
|
||||
// Fixed validation - removed fullname reference
|
||||
if (!username || !email || !password || !confirmPassword) {
|
||||
showMessage('register-message', 'لطفا تمام فیلدها را پر کنید', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (password !== confirmPassword) {
|
||||
showMessage('register-message', 'رمز عبور و تکرار آن مطابقت ندارند', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (password.length < 6) {
|
||||
showMessage('register-message', 'رمز عبور باید حداقل 6 کاراکتر باشد', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Email validation
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!emailRegex.test(email)) {
|
||||
showMessage('register-message', 'لطفا یک ایمیل معتبر وارد کنید', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById('register-text').style.display = 'none';
|
||||
document.getElementById('register-loading').style.display = 'inline';
|
||||
document.getElementById('register-submit').classList.add('loading');
|
||||
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('username', username);
|
||||
formData.append('email', email);
|
||||
formData.append('password', password);
|
||||
|
||||
const response = await fetch('/auth/register', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
// Read the response once and store it
|
||||
const responseText = await response.text();
|
||||
|
||||
let responseData;
|
||||
try {
|
||||
responseData = JSON.parse(responseText);
|
||||
} catch (e) {
|
||||
console.log('Response is not JSON:', responseText);
|
||||
// If it's not JSON, check if it's HTML or other content
|
||||
if (response.ok && responseText.includes('success') || responseText.includes('موفق')) {
|
||||
showMessage('register-message', 'ثبت نام با موفقیت انجام شد!', 'success');
|
||||
setTimeout(() => {
|
||||
hideAuthPages();
|
||||
showLogin();
|
||||
}, 2000);
|
||||
} else {
|
||||
showMessage('register-message', 'پاسخ غیرمنتظره از سرور', 'error');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Now handle the parsed JSON data
|
||||
if (response.ok) {
|
||||
if (responseData.success) {
|
||||
showMessage('register-message', 'ثبت نام با موفقیت انجام شد!', 'success');
|
||||
|
||||
setTimeout(() => {
|
||||
hideAuthPages();
|
||||
showLogin();
|
||||
}, 2000);
|
||||
} else {
|
||||
showMessage('register-message', responseData.message || 'خطا در ثبت نام', 'error');
|
||||
}
|
||||
} else {
|
||||
// Handle different HTTP status codes
|
||||
let errorMessage = 'خطا در ثبت نام';
|
||||
|
||||
if (response.status === 400) {
|
||||
errorMessage = 'اطلاعات وارد شده معتبر نیست';
|
||||
} else if (response.status === 409) {
|
||||
errorMessage = 'نام کاربری یا ایمیل قبلاً استفاده شده است';
|
||||
} else if (response.status === 500) {
|
||||
errorMessage = 'خطای سرور لطفاً بعداً تلاش کنید';
|
||||
}
|
||||
|
||||
showMessage('register-message', responseData?.message || errorMessage, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Network error:', error);
|
||||
showMessage('register-message', 'خطا در ارتباط با سرور', 'error');
|
||||
} finally {
|
||||
document.getElementById('register-text').style.display = 'inline';
|
||||
document.getElementById('register-loading').style.display = 'none';
|
||||
document.getElementById('register-submit').classList.remove('loading');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user