diff --git a/app/__init__.py b/app/__init__.py index 2f67fa5..da3507f 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -1,7 +1,8 @@ -from flask import Flask, render_template, session, g +from flask import Flask, render_template, session, g, Markup from app.models.user import db, User from app.controllers.user import user_bp -from app.controllers.book import book_bp # 引入图书蓝图 +from app.controllers.book import book_bp +from app.controllers.borrow import borrow_bp import os @@ -20,7 +21,7 @@ def create_app(): EMAIL_PORT=587, EMAIL_ENCRYPTION='starttls', EMAIL_USERNAME='3399560459@qq.com', - EMAIL_PASSWORD='fzwhyirhbqdzcjgf', # 这是你的SMTP授权码,不是邮箱密码 + EMAIL_PASSWORD='fzwhyirhbqdzcjgf', EMAIL_FROM='3399560459@qq.com', EMAIL_FROM_NAME='BOOKSYSTEM_OFFICIAL' ) @@ -33,7 +34,8 @@ def create_app(): # 注册蓝图 app.register_blueprint(user_bp, url_prefix='/user') - app.register_blueprint(book_bp, url_prefix='/book') # 注册图书蓝图 + app.register_blueprint(book_bp, url_prefix='/book') + app.register_blueprint(borrow_bp, url_prefix='/borrow') # 创建数据库表 with app.app_context(): @@ -44,15 +46,14 @@ def create_app(): # 创建表 db.create_all() - # 再导入依赖模型 + # 再导入依赖模型 - 但不在这里定义关系 from app.models.borrow import BorrowRecord from app.models.inventory import InventoryLog - # 现在添加反向关系 - # 这样可以确保所有类都已经定义好 - Book.borrow_records = db.relationship('BorrowRecord', backref='book', lazy='dynamic') - Book.inventory_logs = db.relationship('InventoryLog', backref='book', lazy='dynamic') - Category.books = db.relationship('Book', backref='category', lazy='dynamic') + # 移除这些重复的关系定义 + # Book.borrow_records = db.relationship('BorrowRecord', backref='book', lazy='dynamic') + # Book.inventory_logs = db.relationship('InventoryLog', backref='book', lazy='dynamic') + # Category.books = db.relationship('Book', backref='category', lazy='dynamic') # 创建默认角色 from app.models.user import Role @@ -92,7 +93,7 @@ def create_app(): db.session.commit() - # 请求前处理 + # 其余代码保持不变... @app.before_request def load_logged_in_user(): user_id = session.get('user_id') @@ -102,23 +103,20 @@ def create_app(): else: g.user = User.query.get(user_id) - # 首页路由 @app.route('/') def index(): if not g.user: return render_template('login.html') return render_template('index.html', current_user=g.user) - # 错误处理 @app.errorhandler(404) def page_not_found(e): return render_template('404.html'), 404 - # 模板过滤器 @app.template_filter('nl2br') def nl2br_filter(s): - if not s: - return s - return s.replace('\n', '
') + if s: + return Markup(s.replace('\n', '
')) + return s return app diff --git a/app/controllers/book.py b/app/controllers/book.py index 7733cc6..51eacbb 100644 --- a/app/controllers/book.py +++ b/app/controllers/book.py @@ -66,9 +66,27 @@ def book_list(): @login_required def book_detail(book_id): book = Book.query.get_or_404(book_id) - return render_template('book/detail.html', book=book, current_user=g.user) + + # 添加当前时间用于判断借阅是否逾期 + now = datetime.datetime.now() + + # 如果用户是管理员,预先查询并排序借阅记录 + borrow_records = [] + if g.user.role_id == 1: # 假设 role_id 1 为管理员 + from app.models.borrow import BorrowRecord + borrow_records = BorrowRecord.query.filter_by(book_id=book_id).order_by(BorrowRecord.borrow_date.desc()).limit( + 10).all() + + return render_template( + 'book/detail.html', + book=book, + current_user=g.user, + borrow_records=borrow_records, + now=now + ) +# 添加图书页面 # 添加图书页面 @book_bp.route('/add', methods=['GET', 'POST']) @login_required @@ -83,73 +101,137 @@ def add_book(): isbn = request.form.get('isbn') publish_year = request.form.get('publish_year') description = request.form.get('description') - stock = request.form.get('stock', type=int) + stock = request.form.get('stock', type=int, default=0) price = request.form.get('price') - if not title or not author: - flash('书名和作者不能为空', 'danger') + # 表单验证 + errors = [] + if not title: + errors.append('书名不能为空') + if not author: + errors.append('作者不能为空') + + # 检查ISBN是否已存在(如果提供了ISBN) + if isbn: + existing_book = Book.query.filter_by(isbn=isbn).first() + if existing_book: + errors.append(f'ISBN "{isbn}" 已存在,请检查ISBN或查找现有图书') + + if errors: + for error in errors: + flash(error, 'danger') categories = Category.query.all() - return render_template('book/add.html', categories=categories, current_user=g.user) + # 保留已填写的表单数据 + book_data = { + 'title': title, + 'author': author, + 'publisher': publisher, + 'category_id': category_id, + 'tags': tags, + 'isbn': isbn, + 'publish_year': publish_year, + 'description': description, + 'stock': stock, + 'price': price + } + return render_template('book/add.html', categories=categories, + current_user=g.user, book=book_data) # 处理封面图片上传 cover_url = None if 'cover' in request.files: cover_file = request.files['cover'] if cover_file and cover_file.filename != '': - filename = secure_filename(f"{uuid.uuid4()}_{cover_file.filename}") - upload_folder = os.path.join(current_app.static_folder, 'uploads/covers') + try: + # 更清晰的文件命名 + original_filename = secure_filename(cover_file.filename) + # 保留原始文件扩展名 + _, ext = os.path.splitext(original_filename) + if not ext: + ext = '.jpg' # 默认扩展名 - # 确保上传目录存在 - if not os.path.exists(upload_folder): - os.makedirs(upload_folder) + filename = f"{uuid.uuid4()}{ext}" + upload_folder = os.path.join(current_app.static_folder, 'uploads', 'covers') - file_path = os.path.join(upload_folder, filename) - cover_file.save(file_path) - cover_url = f'/static/covers/{filename}' + # 确保上传目录存在 + if not os.path.exists(upload_folder): + os.makedirs(upload_folder) - # 创建新图书 - book = Book( - title=title, - author=author, - publisher=publisher, - category_id=category_id, - tags=tags, - isbn=isbn, - publish_year=publish_year, - description=description, - cover_url=cover_url, - stock=stock, - price=price, - status=1, - created_at=datetime.datetime.now(), - updated_at=datetime.datetime.now() - ) + file_path = os.path.join(upload_folder, filename) + cover_file.save(file_path) + cover_url = f'/static/uploads/covers/{filename}' + except Exception as e: + current_app.logger.error(f"封面上传失败: {str(e)}") + flash(f"封面上传失败: {str(e)}", 'warning') - db.session.add(book) - - # 记录库存日志 - if stock and int(stock) > 0: - from app.models.inventory import InventoryLog - inventory_log = InventoryLog( - book_id=book.id, - change_type='入库', - change_amount=stock, - after_stock=stock, - operator_id=g.user.id, - remark='新书入库', - changed_at=datetime.datetime.now() + try: + # 创建新图书 + book = Book( + title=title, + author=author, + publisher=publisher, + category_id=category_id, + tags=tags, + isbn=isbn, + publish_year=publish_year, + description=description, + cover_url=cover_url, + stock=stock, + price=price, + status=1, + created_at=datetime.datetime.now(), + updated_at=datetime.datetime.now() ) - db.session.add(inventory_log) - db.session.commit() + db.session.add(book) + # 先提交以获取book的id + db.session.commit() - flash('图书添加成功', 'success') - return redirect(url_for('book.book_list')) + # 记录库存日志 - 在获取 book.id 后 + if stock and int(stock) > 0: + from app.models.inventory import InventoryLog + inventory_log = InventoryLog( + book_id=book.id, + change_type='入库', + change_amount=stock, + after_stock=stock, + operator_id=g.user.id, + remark='新书入库', + changed_at=datetime.datetime.now() + ) + db.session.add(inventory_log) + db.session.commit() + + flash(f'《{title}》添加成功', 'success') + return redirect(url_for('book.book_list')) + + except Exception as e: + db.session.rollback() + error_msg = str(e) + # 记录详细错误日志 + current_app.logger.error(f"添加图书失败: {error_msg}") + flash(f'添加图书失败: {error_msg}', 'danger') + + categories = Category.query.all() + # 保留已填写的表单数据 + book_data = { + 'title': title, + 'author': author, + 'publisher': publisher, + 'category_id': category_id, + 'tags': tags, + 'isbn': isbn, + 'publish_year': publish_year, + 'description': description, + 'stock': stock, + 'price': price + } + return render_template('book/add.html', categories=categories, + current_user=g.user, book=book_data) categories = Category.query.all() return render_template('book/add.html', categories=categories, current_user=g.user) - # 编辑图书 @book_bp.route('/edit/', methods=['GET', 'POST']) @login_required diff --git a/app/controllers/borrow.py b/app/controllers/borrow.py index e69de29..c5362a0 100644 --- a/app/controllers/borrow.py +++ b/app/controllers/borrow.py @@ -0,0 +1,82 @@ +from flask import Blueprint, request, redirect, url_for, flash, g +from app.models.book import Book +from app.models.borrow import BorrowRecord +from app.models.inventory import InventoryLog +from app.models.user import db # 修正:从 user 模型导入 db +from app.utils.auth import login_required +import datetime + +# 创建借阅蓝图 +borrow_bp = Blueprint('borrow', __name__, url_prefix='/borrow') + + +@borrow_bp.route('/book', methods=['POST']) +@login_required +def borrow_book(): + book_id = request.form.get('book_id', type=int) + borrow_days = request.form.get('borrow_days', type=int, default=14) + + if not book_id: + flash('请选择要借阅的图书', 'danger') + return redirect(url_for('book.book_list')) + + book = Book.query.get_or_404(book_id) + + # 检查库存 + if book.stock <= 0: + flash(f'《{book.title}》当前无库存,无法借阅', 'danger') + return redirect(url_for('book.book_detail', book_id=book_id)) + + # 检查当前用户是否已借阅此书 + existing_borrow = BorrowRecord.query.filter_by( + user_id=g.user.id, + book_id=book_id, + status=1 # 1表示借阅中 + ).first() + + if existing_borrow: + flash(f'您已借阅《{book.title}》,请勿重复借阅', 'warning') + return redirect(url_for('book.book_detail', book_id=book_id)) + + try: + # 创建借阅记录 + now = datetime.datetime.now() + due_date = now + datetime.timedelta(days=borrow_days) + + borrow_record = BorrowRecord( + user_id=g.user.id, + book_id=book_id, + borrow_date=now, + due_date=due_date, + status=1, # 1表示借阅中 + created_at=now, + updated_at=now + ) + + # 更新图书库存 + book.stock -= 1 + book.updated_at = now + + db.session.add(borrow_record) + db.session.commit() + + # 添加库存变更日志 + inventory_log = InventoryLog( + book_id=book_id, + change_type='借出', + change_amount=-1, + after_stock=book.stock, + operator_id=g.user.id, + remark='用户借书', + changed_at=now + ) + db.session.add(inventory_log) + db.session.commit() + + flash(f'成功借阅《{book.title}》,请在 {due_date.strftime("%Y-%m-%d")} 前归还', 'success') + + except Exception as e: + db.session.rollback() + flash(f'借阅失败: {str(e)}', 'danger') + + return redirect(url_for('book.book_detail', book_id=book_id)) diff --git a/app/models/borrow.py b/app/models/borrow.py index 4a486b2..413a501 100644 --- a/app/models/borrow.py +++ b/app/models/borrow.py @@ -19,6 +19,7 @@ class BorrowRecord(db.Model): # 添加反向关系引用 user = db.relationship('User', backref=db.backref('borrow_records', lazy='dynamic')) + book = db.relationship('Book', backref=db.backref('borrow_records', lazy='dynamic')) # book 关系会在后面步骤添加 diff --git a/app/static/css/book-form.css b/app/static/css/book-form.css index 177dbd2..d84e052 100644 --- a/app/static/css/book-form.css +++ b/app/static/css/book-form.css @@ -1,26 +1,57 @@ -/* ========== 基础样式 ========== */ +/* ========== 基础重置和变量 ========== */ +:root { + --primary-color: #3b82f6; + --primary-hover: #2563eb; + --primary-light: #eff6ff; + --danger-color: #ef4444; + --success-color: #10b981; + --warning-color: #f59e0b; + --info-color: #3b82f6; + --text-dark: #1e293b; + --text-medium: #475569; + --text-light: #64748b; + --text-muted: #94a3b8; + --border-color: #e2e8f0; + --border-focus: #bfdbfe; + --bg-white: #ffffff; + --bg-light: #f8fafc; + --bg-lightest: #f1f5f9; + --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05); + --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); + --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); + --radius-sm: 4px; + --radius-md: 6px; + --radius-lg: 8px; + --radius-xl: 12px; + --transition-fast: 0.15s ease; + --transition-base: 0.3s ease; + --transition-slow: 0.5s ease; + --font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; +} + +/* ========== 全局样式 ========== */ .book-form-container { - padding: 30px; + padding: 24px; max-width: 1400px; margin: 0 auto; - background-color: #f8f9fa; - border-radius: 10px; - box-shadow: 0 4px 20px rgba(0, 0, 0, 0.03); + font-family: var(--font-sans); + color: var(--text-dark); } /* ========== 页头样式 ========== */ .page-header-wrapper { - margin-bottom: 30px; - background: linear-gradient(135deg, #ffffff 0%, #f0f4f8 100%); - border-radius: 8px; - padding: 20px; - box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05); + margin-bottom: 24px; + background-color: var(--bg-white); + border-radius: var(--radius-lg); + box-shadow: var(--shadow-sm); + overflow: hidden; } .page-header { display: flex; justify-content: space-between; align-items: center; + padding: 24px; } .header-title-section { @@ -29,43 +60,46 @@ } .page-title { - font-size: 28px; - font-weight: 700; - color: #2c3e50; + font-size: 1.5rem; + font-weight: 600; + color: var(--text-dark); margin: 0; - display: flex; - align-items: center; -} - -.page-title i { - margin-right: 12px; - color: #3498db; - font-size: 24px; } .subtitle { - margin: 5px 0 0 0; - color: #7f8c8d; - font-size: 16px; + margin: 8px 0 0 0; + color: var(--text-medium); + font-size: 0.9rem; } .header-actions { display: flex; align-items: center; - gap: 20px; + gap: 16px; } -.btn-icon-text { +.btn-back { display: flex; align-items: center; gap: 8px; + color: var(--text-medium); + background-color: var(--bg-lightest); + border-radius: var(--radius-md); padding: 8px 16px; - border-radius: 6px; + font-size: 0.875rem; font-weight: 500; - transition: all 0.3s ease; + transition: all var(--transition-fast); + text-decoration: none; + box-shadow: var(--shadow-sm); } -.btn-icon-text i { +.btn-back:hover { + background-color: var(--border-color); + color: var(--text-dark); + text-decoration: none; +} + +.btn-back i { font-size: 14px; } @@ -74,74 +108,76 @@ min-width: 180px; } -.progress { - height: 8px; - border-radius: 4px; - background-color: #e9ecef; - margin-bottom: 5px; +.progress-bar-container { + height: 6px; + background-color: var(--bg-lightest); + border-radius: 3px; overflow: hidden; } .progress-bar { - background: linear-gradient(45deg, #3498db, #2ecc71); - transition: width 0.5s ease; + height: 100%; + background-color: var(--primary-color); + border-radius: 3px; + transition: width var(--transition-base); } .progress-text { - font-size: 12px; - color: #6c757d; + font-size: 0.75rem; + color: var(--text-light); text-align: right; display: block; + margin-top: 4px; +} + +/* ========== 表单布局 ========== */ +.form-grid { + display: grid; + grid-template-columns: 1fr 360px; + gap: 24px; +} + +.form-main-content { + display: flex; + flex-direction: column; + gap: 24px; +} + +.form-sidebar { + display: flex; + flex-direction: column; + gap: 24px; } /* ========== 表单卡片样式 ========== */ .form-card { - margin-bottom: 25px; - border: none; - border-radius: 10px; - box-shadow: 0 3px 12px rgba(0, 0, 0, 0.05); - background-color: #ffffff; - transition: all 0.3s ease; + background-color: var(--bg-white); + border-radius: var(--radius-lg); + box-shadow: var(--shadow-sm); overflow: hidden; + transition: box-shadow var(--transition-base); } .form-card:hover { - box-shadow: 0 6px 20px rgba(0, 0, 0, 0.08); - transform: translateY(-2px); + box-shadow: var(--shadow-md); } .card-header { padding: 16px 20px; - background: linear-gradient(90deg, rgba(248,249,250,1) 0%, rgba(255,255,255,1) 100%); - border-bottom: 1px solid #edf2f7; + background-color: var(--bg-white); + border-bottom: 1px solid var(--border-color); display: flex; align-items: center; } -.card-header-icon { - width: 32px; - height: 32px; - border-radius: 8px; - background-color: rgba(52, 152, 219, 0.1); - display: flex; - align-items: center; - justify-content: center; - margin-right: 12px; -} - -.card-header-icon i { - color: #3498db; - font-size: 16px; -} - -.card-header-title { +.card-title { font-weight: 600; - color: #2c3e50; - font-size: 16px; + color: var(--text-dark); + font-size: 0.9375rem; } .card-body { - padding: 25px; + padding: 20px; } .form-section { @@ -149,151 +185,298 @@ } /* ========== 表单元素样式 ========== */ -.form-label { - font-weight: 500; - color: #34495e; - margin-bottom: 8px; - display: flex; - align-items: center; -} - -.label-icon { - color: #3498db; - margin-right: 8px; - font-size: 14px; +.form-row { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 16px; + margin-bottom: 16px; } .form-group { margin-bottom: 20px; - position: relative; } -.custom-input, .custom-select, .custom-textarea { - height: calc(2.8rem + 2px); - padding: 0.75rem 1rem; - border: 1px solid #e0e6ed; - border-radius: 8px; - font-size: 15px; - transition: all 0.3s ease; - box-shadow: 0 1px 3px rgba(0,0,0,0.02); +.form-group:last-child { + margin-bottom: 0; } -.custom-input:focus, .custom-select:focus, .custom-textarea:focus { - border-color: #3498db; - box-shadow: 0 0 0 0.2rem rgba(52, 152, 219, 0.25); - outline: none; +.form-label { + display: block; + font-weight: 500; + color: var(--text-dark); + margin-bottom: 8px; + font-size: 0.9375rem; } -.custom-textarea { - height: auto; - min-height: 200px; - resize: vertical; - line-height: 1.6; +.form-control { + display: block; + width: 100%; + padding: 10px 14px; + font-size: 0.9375rem; + line-height: 1.5; + color: var(--text-dark); + background-color: var(--bg-white); + background-clip: padding-box; + border: 1px solid var(--border-color); + border-radius: var(--radius-md); + transition: border-color var(--transition-fast), box-shadow var(--transition-fast); } -.input-group-text { - background-color: #f8f9fa; - border: 1px solid #e0e6ed; - border-radius: 8px; - color: #6c757d; +.form-control:focus { + border-color: var(--border-focus); + outline: 0; + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15); } -.form-text { +.form-control::placeholder { + color: var(--text-muted); +} + +.form-control:disabled, .form-control[readonly] { + background-color: var(--bg-lightest); + opacity: 0.6; +} + +.form-help { margin-top: 6px; - font-size: 13px; - color: #6c757d; + font-size: 0.8125rem; + color: var(--text-light); } -/* 浮动标签效果 */ -.form-group.focused .floating-label { - transform: translateY(-22px) scale(0.85); - color: #3498db; - opacity: 1; -} - -.floating-label { - position: absolute; - pointer-events: none; - left: 1rem; - top: 0.75rem; - transition: 0.2s ease all; - color: #95a5a6; - opacity: 0.8; -} - -.floating-input:focus + .floating-label, -.floating-input:not(:placeholder-shown) + .floating-label { - transform: translateY(-22px) scale(0.85); - color: #3498db; - opacity: 1; -} - -.floating-input { - padding-top: 1.1rem; - padding-bottom: 0.4rem; -} - -/* 数字输入组 */ -.input-number-group { +.form-footer { display: flex; - width: 100%; + justify-content: space-between; + align-items: center; + margin-top: 8px; } -.input-number-group input { - text-align: center; - border-radius: 0; - border-left: none; - border-right: none; +.char-counter { + font-size: 0.8125rem; + color: var(--text-muted); } -.input-number-group button { - border-radius: 8px 0 0 8px; -} - -.input-number-group button:last-child { - border-radius: 0 8px 8px 0; -} - -/* 价格滑块 */ -.custom-range { - -webkit-appearance: none; - width: 100%; - height: 6px; - border-radius: 3px; - background: #e0e6ed; - outline: none; - margin: 10px 0; -} - -.custom-range::-webkit-slider-thumb { - -webkit-appearance: none; - appearance: none; - width: 20px; - height: 20px; - border-radius: 50%; - background: #3498db; - cursor: pointer; - box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); - transition: background 0.3s ease; -} - -.custom-range::-webkit-slider-thumb:hover { - background: #2980b9; -} - -/* 标签输入 */ -.tag-input-container { +/* 带按钮输入框 */ +.input-with-button { display: flex; - gap: 10px; + align-items: center; } -.tag-input { +.input-with-button .form-control { + border-top-right-radius: 0; + border-bottom-right-radius: 0; flex-grow: 1; } -.add-tag-btn { - padding: 0.5rem 0.75rem; - border-radius: 8px; +.btn-append { + height: 42px; + padding: 0 14px; + background-color: var(--bg-lightest); + border: 1px solid var(--border-color); + border-left: none; + border-top-right-radius: var(--radius-md); + border-bottom-right-radius: var(--radius-md); + color: var(--text-medium); + cursor: pointer; + transition: background-color var(--transition-fast); +} + +.btn-append:hover { + background-color: var(--border-color); + color: var(--text-dark); +} + +/* 文本域 */ +textarea.form-control { + min-height: 150px; + resize: vertical; +} + +/* 数字输入控件 */ +.number-control { + display: flex; + align-items: center; + width: 100%; + border-radius: var(--radius-md); + overflow: hidden; +} + +.number-btn { + width: 42px; + height: 42px; + display: flex; + align-items: center; + justify-content: center; + background-color: var(--bg-lightest); + border: 1px solid var(--border-color); + color: var(--text-medium); + cursor: pointer; + transition: all var(--transition-fast); + font-size: 1rem; + user-select: none; +} + +.number-btn:hover { + background-color: var(--border-color); + color: var(--text-dark); +} + +.decrement { + border-top-left-radius: var(--radius-md); + border-bottom-left-radius: var(--radius-md); +} + +.increment { + border-top-right-radius: var(--radius-md); + border-bottom-right-radius: var(--radius-md); +} + +.number-control .form-control { + flex: 1; + border-radius: 0; + border-left: none; + border-right: none; + text-align: center; + padding: 10px 0; +} + +/* 价格输入 */ +.price-input { + position: relative; +} + +.currency-symbol { + position: absolute; + left: 14px; + top: 50%; + transform: translateY(-50%); + color: var(--text-medium); +} + +.price-input .form-control { + padding-left: 30px; +} + +.price-slider { + margin-top: 16px; +} + +.range-slider { + -webkit-appearance: none; + width: 100%; + height: 4px; + border-radius: 2px; + background-color: var(--border-color); + outline: none; + margin: 14px 0; +} + +.range-slider::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: 16px; + height: 16px; + border-radius: 50%; + background: var(--primary-color); + cursor: pointer; + border: 2px solid var(--bg-white); + box-shadow: var(--shadow-sm); +} + +.slider-marks { + display: flex; + justify-content: space-between; + font-size: 0.75rem; + color: var(--text-light); +} + +/* ========== 按钮样式 ========== */ +.btn-primary { + padding: 12px 16px; + background-color: var(--primary-color); + color: white; + border: none; + border-radius: var(--radius-md); + font-weight: 500; + font-size: 0.9375rem; + cursor: pointer; + width: 100%; + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + transition: all var(--transition-fast); + box-shadow: var(--shadow-sm); +} + +.btn-primary:hover { + background-color: var(--primary-hover); + box-shadow: var(--shadow-md); + transform: translateY(-1px); +} + +.btn-primary:focus { + outline: none; + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.3); +} + +.btn-primary:active { + transform: translateY(1px); +} + +.btn-secondary { + padding: 10px 16px; + background-color: var(--bg-white); + color: var(--text-medium); + border: 1px solid var(--border-color); + border-radius: var(--radius-md); + font-weight: 500; + font-size: 0.9375rem; + cursor: pointer; + width: 100%; + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + transition: all var(--transition-fast); +} + +.btn-secondary:hover { + background-color: var(--bg-lightest); + color: var(--text-dark); +} + +.btn-secondary:focus { + outline: none; + box-shadow: 0 0 0 3px rgba(226, 232, 240, 0.5); +} + +/* ========== 标签输入样式 ========== */ +.tag-input-wrapper { + display: flex; + align-items: center; + gap: 8px; +} + +.tag-input-wrapper .form-control { + flex-grow: 1; +} + +.btn-tag-add { + width: 42px; + height: 42px; + display: flex; + align-items: center; + justify-content: center; + background-color: var(--primary-color); + border: none; + border-radius: var(--radius-md); + color: white; + cursor: pointer; + transition: all var(--transition-fast); +} + +.btn-tag-add:hover { + background-color: var(--primary-hover); } .tags-container { @@ -301,76 +484,78 @@ flex-wrap: wrap; gap: 8px; margin-top: 12px; + min-height: 32px; } -.tag-item { +.tag { display: inline-flex; align-items: center; - background-color: rgba(52, 152, 219, 0.1); - color: #3498db; - padding: 6px 12px; - border-radius: 20px; - font-size: 14px; - transition: all 0.3s ease; + background-color: var(--primary-light); + border-radius: 50px; + padding: 6px 10px 6px 14px; + font-size: 0.8125rem; + color: var(--primary-color); + transition: all var(--transition-fast); } -.tag-item:hover { - background-color: rgba(52, 152, 219, 0.2); +.tag:hover { + background-color: rgba(59, 130, 246, 0.2); } -.tag-item .remove-tag { - margin-left: 8px; +.tag-text { + margin-right: 6px; +} + +.tag-remove { + background: none; + border: none; + color: var(--primary-color); cursor: pointer; - font-size: 12px; - color: #3498db; + padding: 0; + width: 16px; + height: 16px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 0.75rem; + transition: all var(--transition-fast); } -.tag-item .remove-tag:hover { - color: #e74c3c; -} - -/* 文本计数 */ -.text-count { - font-size: 12px; - color: #7f8c8d; -} - -.text-count.text-danger { - color: #e74c3c; +.tag-remove:hover { + background-color: rgba(59, 130, 246, 0.3); + color: white; } /* ========== 封面上传区域 ========== */ .cover-preview-container { display: flex; flex-direction: column; - align-items: center; - gap: 20px; + gap: 16px; } .cover-preview { width: 100%; - height: 320px; - border: 2px dashed #e0e6ed; - border-radius: 10px; + aspect-ratio: 5/7; + background-color: var(--bg-lightest); + border-radius: var(--radius-md); overflow: hidden; - background-color: #f8f9fa; - margin-bottom: 10px; - transition: all 0.3s ease; - display: flex; - justify-content: center; - align-items: center; cursor: pointer; + transition: all var(--transition-fast); +} + +.cover-preview:hover { + background-color: var(--bg-light); } .cover-preview.dragover { - border-color: #3498db; - background-color: rgba(52, 152, 219, 0.05); + background-color: var(--primary-light); } .cover-image { width: 100%; height: 100%; - object-fit: contain; + object-fit: cover; } .no-cover-placeholder { @@ -380,165 +565,188 @@ flex-direction: column; justify-content: center; align-items: center; - color: #95a5a6; - padding: 20px; + color: var(--text-light); + padding: 24px; text-align: center; } .no-cover-placeholder i { - font-size: 64px; - margin-bottom: 20px; - color: #d0d0d0; + font-size: 48px; + margin-bottom: 16px; + color: var(--text-muted); } .placeholder-tip { - font-size: 13px; - margin-top: 10px; - color: #7f8c8d; + font-size: 0.8125rem; + margin-top: 8px; + color: var(--text-muted); } .upload-options { - width: 100%; - padding: 0 20px; + display: flex; + flex-direction: column; + gap: 12px; } .upload-btn-group { display: flex; - gap: 10px; + gap: 8px; } -.custom-upload-btn { +.btn-upload { flex-grow: 1; - padding: 10px 20px; - border-radius: 8px; - font-weight: 500; - transition: all 0.3s ease; - background-color: #3498db; - border-color: #3498db; + padding: 10px 16px; + background-color: var(--primary-color); color: white; -} - -.custom-upload-btn:hover { - background-color: #2980b9; - border-color: #2980b9; - transform: translateY(-2px); - box-shadow: 0 4px 10px rgba(52, 152, 219, 0.3); -} - -.btn-icon { - width: 42px; - padding: 0; + border: none; + border-radius: var(--radius-md); + font-weight: 500; + font-size: 0.875rem; + cursor: pointer; display: flex; align-items: center; justify-content: center; + gap: 8px; + transition: all var(--transition-fast); +} + +.btn-upload:hover { + background-color: var(--primary-hover); +} + +.btn-remove { + width: 42px; + height: 38px; + display: flex; + align-items: center; + justify-content: center; + background-color: var(--bg-white); + border: 1px solid var(--border-color); + border-radius: var(--radius-md); + color: var(--text-medium); + cursor: pointer; + transition: all var(--transition-fast); +} + +.btn-remove:hover { + background-color: #fee2e2; + border-color: #fca5a5; + color: #ef4444; +} + +.upload-tips { + text-align: center; + font-size: 0.75rem; + color: var(--text-muted); + line-height: 1.5; } /* ========== 表单提交区域 ========== */ -.form-submit-container { - margin-top: 30px; -} - -.action-buttons { +.form-actions { display: flex; flex-direction: column; - gap: 15px; + gap: 16px; } -.secondary-buttons { - display: flex; - gap: 10px; -} - -.submit-btn { - padding: 15px; - border-radius: 8px; - font-weight: 600; - background: linear-gradient(45deg, #3498db, #2980b9); - border: none; - transition: all 0.3s ease; - position: relative; - overflow: hidden; -} - -.submit-btn:hover { - transform: translateY(-3px); - box-shadow: 0 8px 25px rgba(52, 152, 219, 0.4); -} - -.submit-btn:active { - transform: translateY(-1px); -} - -.reset-btn { - padding: 12px; - border-radius: 8px; - font-weight: 500; +.secondary-actions { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 8px; } .form-tip { - margin-top: 15px; - font-size: 13px; - color: #7f8c8d; + margin-top: 8px; + font-size: 0.8125rem; + color: var(--text-muted); text-align: center; } .form-tip i { - color: #3498db; - margin-right: 5px; + color: var(--info-color); + margin-right: 4px; } /* 必填项标记 */ .required { - color: #e74c3c; + color: var(--danger-color); margin-left: 4px; - font-weight: bold; } /* 无效输入状态 */ .is-invalid { - border-color: #e74c3c !important; + border-color: var(--danger-color) !important; } .invalid-feedback { display: block; - color: #e74c3c; - font-size: 13px; - margin-top: 5px; + color: var(--danger-color); + font-size: 0.8125rem; + margin-top: 6px; +} + +/* ========== Select2 定制 ========== */ +.select2-container--classic .select2-selection--single { + height: 42px; + border: 1px solid var(--border-color); + border-radius: var(--radius-md); + background-color: var(--bg-white); +} + +.select2-container--classic .select2-selection--single .select2-selection__rendered { + line-height: 40px; + color: var(--text-dark); + padding-left: 14px; +} + +.select2-container--classic .select2-selection--single .select2-selection__arrow { + height: 40px; + border-left: 1px solid var(--border-color); +} + +.select2-container--classic .select2-selection--single:focus { + border-color: var(--border-focus); + outline: 0; + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15); } /* ========== 模态框样式 ========== */ .modal-content { border: none; - border-radius: 12px; - box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1); + border-radius: var(--radius-lg); + box-shadow: var(--shadow-lg); overflow: hidden; } .modal-header { - background-color: #f8f9fa; - border-bottom: 1px solid #edf2f7; - padding: 15px 20px; + background-color: var(--bg-white); + border-bottom: 1px solid var(--border-color); + padding: 16px 20px; } .modal-title { font-weight: 600; - color: #2c3e50; + color: var(--text-dark); + font-size: 1.125rem; } .modal-body { - padding: 25px; + padding: 20px; } .modal-footer { - border-top: 1px solid #edf2f7; - padding: 15px 20px; - justify-content: space-between; + border-top: 1px solid var(--border-color); + padding: 16px 20px; +} + +.modal-btn { + min-width: 100px; } /* 裁剪模态框 */ .img-container { max-height: 500px; overflow: hidden; + margin-bottom: 20px; } #cropperImage { @@ -548,106 +756,232 @@ .cropper-controls { display: flex; - gap: 10px; + justify-content: center; + gap: 20px; + margin-top: 16px; +} + +.control-group { + display: flex; + gap: 8px; +} + +.control-btn { + width: 40px; + height: 40px; + border-radius: var(--radius-md); + background-color: var(--bg-lightest); + border: 1px solid var(--border-color); + color: var(--text-medium); + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: all var(--transition-fast); +} + +.control-btn:hover { + background-color: var(--border-color); + color: var(--text-dark); } /* 图书预览模态框 */ -.book-preview-container { - padding: 10px; +.preview-header { + background-color: var(--bg-white); + border-bottom: 1px solid var(--border-color); +} + +.preview-body { + padding: 0; + background-color: var(--bg-lightest); +} + +/* 添加到你的CSS文件中 */ +.book-preview { + display: flex; + flex-direction: row; + gap: 20px; +} + +.preview-cover-section { + flex: 0 0 200px; +} + +.preview-details-section { + flex: 1; } .book-preview-cover { - width: 100%; - height: 300px; - border-radius: 8px; + height: 280px; + width: 200px; overflow: hidden; - box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1); - background-color: #f8f9fa; - margin-bottom: 20px; -} - -.book-preview-cover img { - width: 100%; - height: 100%; - object-fit: cover; -} - -.book-preview-details { - padding: 10px 0; -} - -.book-preview-details h3 { - margin: 0 0 5px 0; - font-weight: 600; - color: #2c3e50; -} - -.book-author { - color: #7f8c8d; - font-size: 16px; - margin-bottom: 20px; -} - -.book-info-section { - display: grid; - grid-template-columns: repeat(2, 1fr); - gap: 12px; - margin-bottom: 20px; -} - -.book-info-item { + border-radius: 4px; + border: 1px solid #ddd; display: flex; - flex-direction: column; + align-items: center; + justify-content: center; + background: #f8f9fa; } -.info-label { - font-size: 13px; - color: #7f8c8d; - margin-bottom: 3px; -} - -.info-value { - font-weight: 500; - color: #34495e; -} - -.book-tags { - display: flex; - flex-wrap: wrap; - gap: 8px; - margin-bottom: 20px; +.preview-cover-img { + max-width: 100%; + max-height: 100%; + object-fit: contain; } .preview-tag { display: inline-block; - background-color: rgba(52, 152, 219, 0.1); - color: #3498db; - padding: 5px 10px; - border-radius: 20px; - font-size: 13px; + background: #e9ecef; + color: #495057; + padding: 3px 8px; + border-radius: 12px; + font-size: 12px; + margin-right: 5px; + margin-bottom: 5px; } -.book-description { - background-color: #f8f9fa; - border-radius: 8px; - padding: 20px; +.book-tags-preview { + margin: 15px 0; +} + +.book-description-preview { margin-top: 20px; } -.book-description h4 { - margin-top: 0; +.section-title { + font-size: 16px; + margin-bottom: 10px; + color: #495057; + border-bottom: 1px solid #dee2e6; + padding-bottom: 5px; +} + +.book-meta { + margin-top: 10px; + text-align: center; +} + +.book-price { + font-size: 18px; + font-weight: bold; + color: #dc3545; +} + +.book-stock { + font-size: 14px; + color: #6c757d; +} + +/* 响应式调整 */ +@media (max-width: 768px) { + .book-preview { + flex-direction: column; + } + + .preview-cover-section { + margin: 0 auto; + } +} + +.preview-details-section { + padding: 24px; +} + +.book-title { + font-size: 1.5rem; font-weight: 600; - color: #2c3e50; - margin-bottom: 15px; + color: var(--text-dark); + margin: 0 0 8px 0; } -.book-description p { - color: #34495e; +.book-author { + color: var(--text-medium); + font-size: 1rem; + margin-bottom: 24px; +} + +.book-info-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 16px; + background-color: var(--bg-white); + border-radius: var(--radius-md); + padding: 16px; + box-shadow: var(--shadow-sm); + margin-bottom: 24px; +} + +.info-item { + display: flex; + flex-direction: column; + gap: 4px; +} + +.info-label { + font-size: 0.75rem; + color: var(--text-light); + text-transform: uppercase; +} + +.info-value { + font-weight: 500; + color: var(--text-dark); +} + +.book-tags-preview { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-bottom: 24px; +} + +.preview-tag { + display: inline-block; + background-color: var(--primary-light); + color: var(--primary-color); + padding: 4px 12px; + border-radius: 50px; + font-size: 0.8125rem; +} + +.no-tags { + font-size: 0.875rem; + color: var(--text-muted); +} + +.book-description-preview { + background-color: var(--bg-white); + border-radius: var(--radius-md); + padding: 16px; + box-shadow: var(--shadow-sm); +} + +.section-title { + font-size: 1rem; + font-weight: 600; + color: var(--text-dark); + margin: 0 0 12px 0; +} + +.description-content { + font-size: 0.9375rem; + color: var(--text-medium); line-height: 1.6; - margin: 0; } -/* ========== 自定义通知样式 ========== */ +.placeholder-text { + color: var(--text-muted); + font-style: italic; +} + +.preview-footer { + background-color: var(--bg-white); + display: flex; + justify-content: flex-end; + gap: 12px; +} + +/* ========== 通知样式 ========== */ .notification-container { position: fixed; top: 20px; @@ -655,61 +989,55 @@ z-index: 9999; display: flex; flex-direction: column; - gap: 10px; - max-width: 350px; + gap: 12px; + max-width: 320px; } -.custom-notification { - background: white; - border-radius: 10px; - padding: 15px; - box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1); +.notification { + background-color: var(--bg-white); + border-radius: var(--radius-md); + padding: 12px 16px; + box-shadow: var(--shadow-md); display: flex; align-items: center; + gap: 12px; animation-duration: 0.5s; - border-left: 4px solid #3498db; } -.notification-success { - border-color: #2ecc71; +.success-notification { + border-left: 4px solid var(--success-color); } -.notification-error { - border-color: #e74c3c; +.error-notification { + border-left: 4px solid var(--danger-color); } -.notification-warning { - border-color: #f39c12; +.warning-notification { + border-left: 4px solid var(--warning-color); } -.notification-info { - border-color: #3498db; +.info-notification { + border-left: 4px solid var(--info-color); } .notification-icon { - width: 30px; - height: 30px; - border-radius: 50%; - display: flex; - align-items: center; - justify-content: center; - margin-right: 15px; + color: var(--text-light); } -.notification-success .notification-icon i { - color: #2ecc71; +.success-notification .notification-icon { + color: var(--success-color); } -.notification-error .notification-icon i { - color: #e74c3c; +.error-notification .notification-icon { + color: var(--danger-color); } -.notification-warning .notification-icon i { - color: #f39c12; +.warning-notification .notification-icon { + color: var(--warning-color); } -.notification-info .notification-icon i { - color: #3498db; +.info-notification .notification-icon { + color: var(--info-color); } .notification-content { @@ -718,33 +1046,33 @@ .notification-content p { margin: 0; - font-size: 14px; - color: #2c3e50; + font-size: 0.875rem; + color: var(--text-dark); } .notification-close { background: none; border: none; - color: #bdc3c7; + color: var(--text-muted); cursor: pointer; padding: 5px; - transition: color 0.3s ease; + transition: color var(--transition-fast); } .notification-close:hover { - color: #7f8c8d; + color: var(--text-medium); } /* ========== 动画效果 ========== */ @keyframes pulse { 0% { - box-shadow: 0 0 0 0 rgba(52, 152, 219, 0.4); + box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.4); } 70% { - box-shadow: 0 0 0 10px rgba(52, 152, 219, 0); + box-shadow: 0 0 0 8px rgba(59, 130, 246, 0); } 100% { - box-shadow: 0 0 0 0 rgba(52, 152, 219, 0); + box-shadow: 0 0 0 0 rgba(59, 130, 246, 0); } } @@ -752,18 +1080,11 @@ animation: pulse 2s infinite; } -.pulse-icon { - animation: pulse 2s infinite; -} - /* ========== 响应式样式 ========== */ @media (max-width: 1200px) { - .book-form-container { - padding: 20px; - } - - .cover-preview { - height: 280px; + .form-grid { + grid-template-columns: 1fr 320px; + gap: 20px; } } @@ -771,7 +1092,7 @@ .page-header { flex-direction: column; align-items: flex-start; - gap: 15px; + gap: 16px; } .header-actions { @@ -779,43 +1100,73 @@ justify-content: space-between; } - .book-info-section { + .form-grid { grid-template-columns: 1fr; } + + .book-preview { + grid-template-columns: 1fr; + } + + .preview-cover-section { + border-right: none; + border-bottom: 1px solid var(--border-color); + padding-bottom: 24px; + } + + .book-preview-cover { + max-width: 240px; + margin: 0 auto; + } } @media (max-width: 768px) { .book-form-container { - padding: 15px 10px; + padding: 16px 12px; } - .page-title { - font-size: 24px; - } - - .subtitle { - font-size: 14px; - } - - .card-body { - padding: 20px 15px; - } - - .cover-preview { - height: 240px; - } - - .secondary-buttons { - flex-direction: column; + .page-header { + padding: 20px; } .form-row { - margin-right: -5px; - margin-left: -5px; + grid-template-columns: 1fr; + gap: 12px; } - .form-group { - padding-right: 5px; - padding-left: 5px; + .secondary-actions { + grid-template-columns: 1fr; + } + + .card-body { + padding: 16px; + } + + .book-info-grid { + grid-template-columns: 1fr; } } +.cover-preview { + min-height: 250px; + width: 100%; + border: 1px dashed #ccc; + border-radius: 4px; + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; + position: relative; +} +.cover-preview img.cover-image { + max-width: 100%; + max-height: 300px; + object-fit: contain; +} +.img-container { + max-height: 500px; + overflow: auto; +} +#cropperImage { + max-width: 100%; + display: block; +} \ No newline at end of file diff --git a/app/static/js/book-add.js b/app/static/js/book-add.js new file mode 100644 index 0000000..4918f24 --- /dev/null +++ b/app/static/js/book-add.js @@ -0,0 +1,644 @@ +/** + * 图书添加页面脚本 + * 处理图书表单的交互、验证和预览功能 + */ +let isSubmitting = false; +$(document).ready(function() { + // 全局变量 + let cropper; + let coverBlob; + let tags = []; + const coverPreview = $('#coverPreview'); + const coverInput = $('#cover'); + const tagInput = $('#tagInput'); + const tagsContainer = $('#tagsContainer'); + const tagsHiddenInput = $('#tags'); + + // 初始化函数 + function initialize() { + initSelect2(); + initFormProgress(); + initTagsFromInput(); + initCoverHandlers(); + initNumberControls(); + initPriceSlider(); + initCharCounter(); + initFormValidation(); + attachEventListeners(); + } + + // ========== 组件初始化 ========== + + // 初始化Select2 + function initSelect2() { + $('.select2').select2({ + placeholder: "选择分类...", + allowClear: true, + theme: "classic", + width: '100%' + }); + } + + // 初始化表单进度条 + function initFormProgress() { + updateFormProgress(); + $('input, textarea, select').on('change keyup', function() { + updateFormProgress(); + }); + } + + // 初始化标签(从隐藏输入字段) + function initTagsFromInput() { + const tagsValue = $('#tags').val(); + if (tagsValue) { + tags = tagsValue.split(','); + renderTags(); + } + } + + // 初始化封面处理 + function initCoverHandlers() { + // 拖放上传功能 + coverPreview.on('dragover', function(e) { + e.preventDefault(); + $(this).addClass('dragover'); + }).on('dragleave drop', function(e) { + e.preventDefault(); + $(this).removeClass('dragover'); + }).on('drop', function(e) { + e.preventDefault(); + const file = e.originalEvent.dataTransfer.files[0]; + if (file && file.type.match('image.*')) { + coverInput[0].files = e.originalEvent.dataTransfer.files; + coverInput.trigger('change'); + } + }).on('click', function() { + if (!$(this).find('img').length) { + coverInput.click(); + } + }); + + // 重置页面加载完后的字符计数 + if ($('#description').val()) { + $('#charCount').text($('#description').val().length); + } + } + + // 初始化数字控制 + function initNumberControls() { + $('#stockDecrement').on('click', function() { + const input = $('#stock'); + const value = parseInt(input.val()); + if (value > parseInt(input.attr('min'))) { + input.val(value - 1).trigger('change'); + } + }); + + $('#stockIncrement').on('click', function() { + const input = $('#stock'); + const value = parseInt(input.val()); + input.val(value + 1).trigger('change'); + }); + } + + // 初始化价格滑块 + function initPriceSlider() { + $('#priceRange').on('input', function() { + $('#price').val($(this).val()); + }); + + $('#price').on('input', function() { + const value = parseFloat($(this).val()) || 0; + $('#priceRange').val(Math.min(value, 500)); + }); + } + + // 初始化字符计数器 + function initCharCounter() { + $('#description').on('input', function() { + const count = $(this).val().length; + $('#charCount').text(count); + if (count > 2000) { + $('#charCount').addClass('text-danger'); + } else { + $('#charCount').removeClass('text-danger'); + } + }); + } + + // 初始化表单验证 + ffunction initFormValidation() { + $('#bookForm').on('submit', function(e) { + // 如果表单正在提交中,阻止重复提交 + if (isSubmitting) { + e.preventDefault(); + showNotification('表单正在提交中,请勿重复点击', 'warning'); + return false; + } + let isValid = true; + $('[required]').each(function() { + if (!$(this).val().trim()) { + isValid = false; + $(this).addClass('is-invalid'); + // 添加错误提示 + if (!$(this).next('.invalid-feedback').length) { + $(this).after(`
此字段不能为空
`); + } + } else { + $(this).removeClass('is-invalid').next('.invalid-feedback').remove(); + } + }); + // 验证ISBN格式(如果已填写) + const isbn = $('#isbn').val().trim(); + if (isbn) { + // 移除所有非数字、X和x字符后检查 + const cleanIsbn = isbn.replace(/[^0-9Xx]/g, ''); + const isbnRegex = /^(?:\d{10}|\d{13})$|^(?:\d{9}[Xx])$/; + if (!isbnRegex.test(cleanIsbn)) { + isValid = false; + $('#isbn').addClass('is-invalid'); + if (!$('#isbn').next('.invalid-feedback').length) { + $('#isbn').after(`
ISBN格式不正确,应为10位或13位
`); + } + } + } + if (!isValid) { + e.preventDefault(); + // 滚动到第一个错误字段 + $('html, body').animate({ + scrollTop: $('.is-invalid:first').offset().top - 100 + }, 500); + showNotification('请正确填写所有标记的字段', 'error'); + } else { + // 设置表单锁定状态 + isSubmitting = true; + + // 修改提交按钮样式 + const submitBtn = $(this).find('button[type="submit"]'); + const originalHtml = submitBtn.html(); + submitBtn.prop('disabled', true) + .html(' 保存中...'); + + // 显示提交中通知 + showNotification('表单提交中...', 'info'); + + // 如果表单提交时间过长,30秒后自动解锁 + setTimeout(function() { + if (isSubmitting) { + isSubmitting = false; + submitBtn.prop('disabled', false).html(originalHtml); + showNotification('提交超时,请重试', 'warning'); + } + }, 30000); + } + }); + // 输入时移除错误样式 + $('input, textarea, select').on('input change', function() { + $(this).removeClass('is-invalid').next('.invalid-feedback').remove(); + }); + } + // 还需要在服务端处理成功后重置状态 + // 在页面加载完成时,添加监听服务器重定向事件 + $(window).on('pageshow', function(event) { + if (event.originalEvent.persisted || + (window.performance && window.performance.navigation.type === 2)) { + // 如果页面是从缓存加载的或通过后退按钮回到的 + isSubmitting = false; + $('button[type="submit"]').prop('disabled', false) + .html(' 保存图书'); + } + }); + + // 绑定事件监听器 + function attachEventListeners() { + // 文件选择处理 + coverInput.on('change', handleCoverSelect); + + // 裁剪控制 + $('#rotateLeft').on('click', function() { cropper && cropper.rotate(-90); }); + $('#rotateRight').on('click', function() { cropper && cropper.rotate(90); }); + $('#zoomIn').on('click', function() { cropper && cropper.zoom(0.1); }); + $('#zoomOut').on('click', function() { cropper && cropper.zoom(-0.1); }); + $('#cropImage').on('click', applyCrop); + $('#removeCover').on('click', removeCover); + + // 标签处理 + tagInput.on('keydown', handleTagKeydown); + $('#addTagBtn').on('click', addTag); + $(document).on('click', '.tag-remove', removeTag); + + // ISBN查询 + $('#isbnLookup').on('click', lookupISBN); + + // 预览按钮 + $('#previewBtn').on('click', showPreview); + + // 表单重置 + $('#resetBtn').on('click', confirmReset); + } + + // ========== 功能函数 ========== + + // 更新表单进度条 + function updateFormProgress() { + const requiredFields = $('[required]'); + const filledFields = requiredFields.filter(function() { + return $(this).val() !== ''; + }); + + const otherFields = $('input:not([required]), textarea:not([required]), select:not([required])').not('[type="file"]'); + const filledOtherFields = otherFields.filter(function() { + return $(this).val() !== ''; + }); + + let requiredWeight = 70; // 必填字段权重70% + let otherWeight = 30; // 非必填字段权重30% + + let requiredProgress = requiredFields.length ? (filledFields.length / requiredFields.length) * requiredWeight : requiredWeight; + let otherProgress = otherFields.length ? (filledOtherFields.length / otherFields.length) * otherWeight : 0; + + let totalProgress = Math.floor(requiredProgress + otherProgress); + + $('#formProgress').css('width', totalProgress + '%').attr('aria-valuenow', totalProgress); + $('#progressText').text('完成 ' + totalProgress + '%'); + + if (totalProgress >= 100) { + $('.btn-primary').addClass('pulse'); + } else { + $('.btn-primary').removeClass('pulse'); + } + } + + // 处理封面选择 + function handleCoverSelect(e) { + const file = e.target.files[0]; + if (!file) return; + // 验证文件类型 + if (!file.type.match('image.*')) { + showNotification('请选择图片文件', 'warning'); + return; + } + // 验证文件大小(最大5MB) + if (file.size > 5 * 1024 * 1024) { + showNotification('图片大小不能超过5MB', 'warning'); + return; + } + const reader = new FileReader(); + reader.onload = function(e) { + // 先显示在预览框中,确保用户能立即看到上传的图片 + coverPreview.html(`图书封面预览`); + + // 准备裁剪图片 + $('#cropperImage').attr('src', e.target.result); + + // 确保图片加载完成后再显示模态框 + $('#cropperImage').on('load', function() { + // 打开模态框 + $('#cropperModal').modal('show'); + + // 在模态框完全显示后初始化裁剪器 + $('#cropperModal').on('shown.bs.modal', function() { + if (cropper) { + cropper.destroy(); + } + + try { + cropper = new Cropper(document.getElementById('cropperImage'), { + aspectRatio: 5 / 7, + viewMode: 2, + responsive: true, + guides: true, + background: true, + ready: function() { + console.log('Cropper初始化成功'); + } + }); + } catch (err) { + console.error('Cropper初始化失败:', err); + showNotification('图片处理工具初始化失败,请重试', 'error'); + } + }); + }); + }; + + // 处理读取错误 + reader.onerror = function() { + showNotification('读取图片失败,请重试', 'error'); + }; + + reader.readAsDataURL(file); + } + // 应用裁剪 + function applyCrop() { + if (!cropper) { + showNotification('图片处理工具未就绪,请重新上传', 'error'); + $('#cropperModal').modal('hide'); + return; + } + + try { + const canvas = cropper.getCroppedCanvas({ + width: 500, + height: 700, + fillColor: '#fff', + imageSmoothingEnabled: true, + imageSmoothingQuality: 'high', + }); + if (!canvas) { + throw new Error('无法生成裁剪后的图片'); + } + canvas.toBlob(function(blob) { + if (!blob) { + showNotification('图片处理失败,请重试', 'error'); + return; + } + + const url = URL.createObjectURL(blob); + coverPreview.html(`图书封面`); + coverBlob = blob; + // 模拟File对象 + const fileList = new DataTransfer(); + const file = new File([blob], "cover.jpg", {type: "image/jpeg"}); + fileList.items.add(file); + document.getElementById('cover').files = fileList.files; + $('#cropperModal').modal('hide'); + showNotification('封面图片已更新', 'success'); + }, 'image/jpeg', 0.95); + } catch (err) { + console.error('裁剪失败:', err); + showNotification('图片裁剪失败,请重试', 'error'); + $('#cropperModal').modal('hide'); + } + } + + // 移除封面 + function removeCover() { + coverPreview.html(` +
+ + 暂无封面 +

点击上传或拖放图片至此处

+
+ `); + coverInput.val(''); + coverBlob = null; + } + + // 渲染标签 + function renderTags() { + tagsContainer.empty(); + tags.forEach(tag => { + tagsContainer.append(` +
+ ${tag} + +
+ `); + }); + tagsHiddenInput.val(tags.join(',')); + } + + // 添加标签 + function addTag() { + const tag = tagInput.val().trim(); + if (tag && !tags.includes(tag)) { + tags.push(tag); + renderTags(); + tagInput.val('').focus(); + } + } + + // 处理标签输入键盘事件 + function handleTagKeydown(e) { + if (e.key === 'Enter' || e.key === ',') { + e.preventDefault(); + addTag(); + } + } + + // 移除标签 + function removeTag() { + const tagToRemove = $(this).data('tag'); + tags = tags.filter(t => t !== tagToRemove); + renderTags(); + } + + // ISBN查询 + function lookupISBN() { + const isbn = $('#isbn').val().trim(); + if (!isbn) { + showNotification('请先输入ISBN', 'warning'); + return; + } + + // 验证ISBN格式 + const cleanIsbn = isbn.replace(/[^0-9Xx]/g, ''); + const isbnRegex = /^(?:\d{10}|\d{13})$|^(?:\d{9}[Xx])$/; + if (!isbnRegex.test(cleanIsbn)) { + showNotification('ISBN格式不正确,应为10位或13位', 'warning'); + return; + } + + $(this).html(''); + + // 先检查ISBN是否已存在 + $.get('/book/api/check-isbn', {isbn: isbn}, function(data) { + if (data.exists) { + $('#isbnLookup').html(''); + showNotification(`ISBN "${isbn}" 已存在: 《${data.book_title}》`, 'warning'); + $('#isbn').addClass('is-invalid'); + if (!$('#isbn').next('.invalid-feedback').length) { + $('#isbn').after(`
此ISBN已被图书《${data.book_title}》使用
`); + } + } else { + // 继续查询外部API(模拟) + simulateISBNLookup(isbn); + } + }).fail(function() { + $('#isbnLookup').html(''); + showNotification('服务器查询失败,请稍后再试', 'error'); + }); + } + + // 模拟ISBN查询 + function simulateISBNLookup(isbn) { + // 模拟API查询延迟 + setTimeout(() => { + // 模拟查到的数据 + if (isbn === '9787020002207') { + $('#title').val('红楼梦').trigger('blur'); + $('#author').val('曹雪芹').trigger('blur'); + $('#publisher').val('人民文学出版社').trigger('blur'); + $('#publish_year').val('1996').trigger('blur'); + $('#category_id').val('1').trigger('change'); + tags = ['中国文学', '古典', '名著']; + renderTags(); + $('#description').val('《红楼梦》是中国古代章回体长篇小说,中国古典四大名著之一,通行本共120回,一般认为前80回是清代作家曹雪芹所著,后40回作者有争议。小说以贾、史、王、薛四大家族的兴衰为背景,以贾府的家庭琐事、闺阁闲情为脉络,以贾宝玉、林黛玉、薛宝钗的爱情婚姻悲剧为主线,刻画了以贾宝玉和金陵十二钗为中心的正邪两赋有情人的人性美和悲剧美。').trigger('input'); + $('#price').val('59.70').trigger('input'); + $('#priceRange').val('59.70'); + + showNotification('ISBN查询成功', 'success'); + } else if (isbn === '9787544270878') { + $('#title').val('挪威的森林').trigger('blur'); + $('#author').val('村上春树').trigger('blur'); + $('#publisher').val('南海出版社').trigger('blur'); + $('#publish_year').val('2017').trigger('blur'); + $('#category_id').val('2').trigger('change'); + tags = ['外国文学', '日本', '小说']; + renderTags(); + $('#description').val('《挪威的森林》是日本作家村上春树创作的长篇小说,首次出版于1987年。小说讲述了一个悲伤的爱情故事,背景设定在20世纪60年代末的日本。主人公渡边纠缠在与平静的直子和开朗的绿子两人的感情中,最终选择了生活。').trigger('input'); + $('#price').val('39.50').trigger('input'); + $('#priceRange').val('39.50'); + + showNotification('ISBN查询成功', 'success'); + } else { + showNotification('未找到相关图书信息', 'warning'); + } + + $('#isbnLookup').html(''); + updateFormProgress(); + }, 1500); + } + + // 显示预览 + function showPreview() { + // 检查必填字段 + if (!$('#title').val().trim() || !$('#author').val().trim()) { + showNotification('请至少填写书名和作者后再预览', 'warning'); + return; + } + try { + // 确保所有值都有默认值,防止undefined错误 + const title = $('#title').val() || '未填写标题'; + const author = $('#author').val() || '未填写作者'; + const publisher = $('#publisher').val() || '-'; + const isbn = $('#isbn').val() || '-'; + const publishYear = $('#publish_year').val() || '-'; + const description = $('#description').val() || ''; + const stock = $('#stock').val() || '0'; + let price = parseFloat($('#price').val()) || 0; + + // 填充预览内容 + $('#previewTitle').text(title); + $('#previewAuthor').text(author ? '作者: ' + author : '未填写作者'); + $('#previewPublisher').text(publisher); + $('#previewISBN').text(isbn); + $('#previewYear').text(publishYear); + // 获取分类文本 + const categoryId = $('#category_id').val(); + const categoryText = categoryId ? $('#category_id option:selected').text() : '-'; + $('#previewCategory').text(categoryText); + // 价格和库存 + $('#previewPrice').text(price ? '¥' + price.toFixed(2) : '¥0.00'); + $('#previewStock').text('库存: ' + stock); + // 标签 + const previewTags = $('#previewTags'); + previewTags.empty(); + if (tags && tags.length > 0) { + tags.forEach(tag => { + previewTags.append(`${tag}`); + }); + } else { + previewTags.append('暂无标签'); + } + // 描述 + if (description) { + $('#previewDescription').html(`

${description.replace(/\n/g, '
')}

`); + } else { + $('#previewDescription').html(`

暂无简介内容

`); + } + // 封面 + const previewCover = $('#previewCover'); + previewCover.empty(); // 清空现有内容 + + if ($('#coverPreview img').length) { + const coverSrc = $('#coverPreview img').attr('src'); + previewCover.html(`封面预览`); + } else { + previewCover.html(` +
+ + 暂无封面 +
+ `); + } + // 显示预览模态框 + $('#previewModal').modal('show'); + + console.log('预览模态框已显示'); + } catch (err) { + console.error('生成预览时发生错误:', err); + showNotification('生成预览时出错,请重试', 'error'); + } + } + + // 确认重置表单 + function confirmReset() { + if (confirm('确定要重置表单吗?所有已填写的内容将被清空。')) { + $('#bookForm')[0].reset(); + removeCover(); + tags = []; + renderTags(); + updateFormProgress(); + $('.select2').val(null).trigger('change'); + $('#charCount').text('0'); + + showNotification('表单已重置', 'info'); + } + } + + // 通知提示函数 + function showNotification(message, type) { + // 创建通知元素 + const notification = $(` +
+
+ +
+
+

${message}

+
+ +
+ `); + + // 添加到页面 + if ($('.notification-container').length === 0) { + $('body').append('
'); + } + $('.notification-container').append(notification); + + // 自动关闭 + setTimeout(() => { + notification.removeClass('animate__fadeInRight').addClass('animate__fadeOutRight'); + setTimeout(() => { + notification.remove(); + }, 500); + }, 5000); + + // 点击关闭 + notification.find('.notification-close').on('click', function() { + notification.removeClass('animate__fadeInRight').addClass('animate__fadeOutRight'); + setTimeout(() => { + notification.remove(); + }, 500); + }); + } + + function getIconForType(type) { + switch(type) { + case 'success': return 'fa-check-circle'; + case 'warning': return 'fa-exclamation-triangle'; + case 'error': return 'fa-times-circle'; + case 'info': + default: return 'fa-info-circle'; + } + } + + // 初始化页面 + initialize(); +}); diff --git a/app/static/uploads/covers/110aad2c-1b7d-4a26-b23c-8efe1e971127_.jpg b/app/static/uploads/covers/110aad2c-1b7d-4a26-b23c-8efe1e971127_.jpg new file mode 100644 index 0000000..72cbaec Binary files /dev/null and b/app/static/uploads/covers/110aad2c-1b7d-4a26-b23c-8efe1e971127_.jpg differ diff --git a/app/static/uploads/covers/69a1f7af-2b9c-4354-9af9-f3bb909acad4_.jpg b/app/static/uploads/covers/69a1f7af-2b9c-4354-9af9-f3bb909acad4_.jpg new file mode 100644 index 0000000..72cbaec Binary files /dev/null and b/app/static/uploads/covers/69a1f7af-2b9c-4354-9af9-f3bb909acad4_.jpg differ diff --git a/app/static/uploads/covers/e189fca6-09ee-4c97-937c-2307c9440f7f_.jpg b/app/static/uploads/covers/e189fca6-09ee-4c97-937c-2307c9440f7f_.jpg new file mode 100644 index 0000000..72cbaec Binary files /dev/null and b/app/static/uploads/covers/e189fca6-09ee-4c97-937c-2307c9440f7f_.jpg differ diff --git a/app/static/uploads/covers/e74584c6-807f-478c-a3eb-36b5008a75a5.jpg b/app/static/uploads/covers/e74584c6-807f-478c-a3eb-36b5008a75a5.jpg new file mode 100644 index 0000000..72cbaec Binary files /dev/null and b/app/static/uploads/covers/e74584c6-807f-478c-a3eb-36b5008a75a5.jpg differ diff --git a/app/templates/base.html b/app/templates/base.html index 7a1b0e3..af5c51b 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -5,6 +5,7 @@ {% block title %}图书管理系统{% endblock %} + @@ -95,7 +96,8 @@ - + + - + {% endblock %} diff --git a/app/templates/book/detail.html b/app/templates/book/detail.html index b57bbf6..2c3b31d 100644 --- a/app/templates/book/detail.html +++ b/app/templates/book/detail.html @@ -109,8 +109,6 @@ {% if current_user.role_id == 1 %}

借阅历史

- {% set borrow_records = book.borrow_records.order_by(BorrowRecord.borrow_date.desc()).limit(10).all() %} - {% if borrow_records %} @@ -151,7 +149,7 @@ -