关于fetch属性的问题备注

本文探讨了Hibernate中懒加载导致的错误及解决方案,详细解释了如何通过调整FetchType来解决懒加载问题,并避免因此引发的记录删除难题。

fetch的默认设置:
JPA接口默认为一对多为Lazy,多对一为Eager,但是Hibernate反向工程生成Entity时,多对一为Lazy,需要手动改为Eager。 

问题描述:
一开始因为使用的是Hibernate的默认设置,出现因为懒加载报错的问题,
由于是懒加载,关联对象不立刻加载(即从数据库读入到内存),在使用到的时候加载;
如果在session中(session未关闭),什么时候要用,就什么时候取(再次访问数据库),这个没有问题, 
但是,在session外(session已关闭),就不能再取了。如果这时候要去取,就会报错,没有session可用。

解决方法:

就是把@ManyToOne中的fetch设置为FetchType.EAGER ,用EAGER时,因为在内存里,所以在session外也可以取。


之前用的是,网上搜索的方法,在主表的@OneToMany中加上fetch=FetchType.EAGER,报错解除。
紧接着出现,该表的记录不能删除的问题,网上查了一下,说是因为加的那个fetch=FetchType.EAGER的问题,
接着按上面的描述重新修改了一下,两个问题都没有了。

<think> 我们正在开发一个精灵图鉴网站,使用MySQL数据库,处理约10000个精灵信息。主要功能包括: - 单页显示100个精灵 - 精灵信息字段:ID、精灵名称、属性、获取途径、获取状态、备注 - 功能:搜索、筛选、编辑更新、获取状态用复选框 - 可导入CSV文件追加和更新信息 - 精灵信息点击后以卡片样式展示并可编辑 - 属性通过下拉选择实现,且客户可自行添加属性选项 我们将分为以下几个步骤: 1. 数据库和开发环境的安装部署(MySQL、Python环境、Flask框架等) 2. 数据库表的设计与创建 3. 后端代码(Flask)实现 4. 前端代码(HTML/CSS/JavaScript)实现 5. CSV导入功能的实现 6. 分页、搜索、筛选功能的实现 7. 精灵卡片的展示与编辑 由于代码量较大,我们将分块展示,并尽可能详细。 步骤1:环境准备 - 安装MySQL:参考官方文档安装MySQL服务器和客户端。 - 安装Python:建议Python 3.6以上版本。 - 安装必要的Python包:flask, flask-sqlalchemy, pymysql, flask-cors(如果前后端分离)等。 步骤2:数据库表设计 - 我们需要两张表: 1. `spirits`表:存储精灵信息。 2. `attributes`表:存储属性选项,用于下拉选择。 创建数据库和表: 创建数据库: ```sql CREATE DATABASE spirit_encyclopedia; USE spirit_encyclopedia; ``` 创建`attributes`表(用于存储属性选项): ```sql CREATE TABLE attributes ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(50) NOT NULL UNIQUE ); ``` 创建`spirits`表: ```sql CREATE TABLE spirits ( id INT PRIMARY KEY, -- 注意:因为我们要导入CSV,可能已经指定ID,所以不使用自增,但确保唯一 name VARCHAR(100) NOT NULL, attribute_id INT, -- 关联属性表 obtain_method VARCHAR(255), obtained BOOLEAN DEFAULT 0, -- 获取状态,0表示未获取,1表示已获取 notes TEXT, FOREIGN KEY (attribute_id) REFERENCES attributes(id) -- 外键约束 ); ``` 注意:属性表用于存储可选的属性(如:火、水、草等)。在精灵表中,我们用attribute_id关联。 步骤3:后端代码(使用Flask) 文件结构: - app.py # 主应用 - config.py # 配置文件 - models.py # 数据库模型 - requirements.txt # 依赖包 我们先创建`requirements.txt`,内容如下: ``` Flask==2.0.2 Flask-SQLAlchemy==2.5.1 PyMySQL==1.0.2 Flask-Cors==3.0.10 ``` 安装依赖:`pip install -r requirements.txt` 创建`config.py`: ```python class Config: SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://username:password@localhost/spirit_encyclopedia' SQLALCHEMY_TRACK_MODIFICATIONS = False ``` 创建`models.py`: ```python from flask_sqlalchemy import SQLAlchemy db = SQLAlchemy() class Attribute(db.Model): __tablename__ = 'attributes' id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(50), unique=True, nullable=False) def __repr__(self): return f'<Attribute {self.name}>' class Spirit(db.Model): __tablename__ = 'spirits' id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(100), nullable=False) attribute_id = db.Column(db.Integer, db.ForeignKey('attributes.id')) attribute = db.relationship('Attribute', backref='spirits') # 建立关系,方便通过spirit.attribute访问属性名 obtain_method = db.Column(db.String(255)) obtained = db.Column(db.Boolean, default=False) notes = db.Column(db.Text) def __repr__(self): return f'<Spirit {self.name}>' ``` 创建`app.py`(主要后端逻辑): 由于代码较长,我们分函数实现,包括: - 初始化应用 - 分页获取精灵列表 - 搜索、筛选 - 更新精灵信息(包括获取状态) - 添加/编辑精灵 - 导入CSV - 属性管理(获取所有属性、添加属性) 以下是`app.py`的代码框架: ```python from flask import Flask, request, jsonify, render_template, redirect, url_for from flask_sqlalchemy import SQLAlchemy from models import db, Attribute, Spirit import config import csv from io import TextIOWrapper app = Flask(__name__) app.config.from_object(Config) db.init_app(app) # 创建数据库表(如果不存在) with app.app_context(): db.create_all() # 首页路由,将渲染前端页面 @app.route('/') def index(): return render_template('index.html') # 获取精灵列表(分页、搜索、筛选) @app.route('/api/spirits', methods=['GET']) def get_spirits(): page = request.args.get('page', 1, type=int) per_page = 100 # 每页100个 search = request.args.get('search', None) attribute_id = request.args.get('attribute_id', None, type=int) obtained = request.args.get('obtained', None) # 可以是'true'或'false' query = Spirit.query # 搜索条件:精灵名称 if search: query = query.filter(Spirit.name.like(f'%{search}%')) # 筛选条件:属性 if attribute_id is not None: query = query.filter(Spirit.attribute_id == attribute_id) # 筛选条件:获取状态 if obtained is not None: # 将字符串转换为布尔值 obtained_bool = obtained.lower() == 'true' query = query.filter(Spirit.obtained == obtained_bool) pagination = query.paginate(page=page, per_page=per_page, error_out=False) spirits = pagination.items # 构建返回的JSON数据 spirits_data = [] for spirit in spirits: spirits_data.append({ 'id': spirit.id, 'name': spirit.name, 'attribute': spirit.attribute.name if spirit.attribute else None, 'attribute_id': spirit.attribute_id, 'obtain_method': spirit.obtain_method, 'obtained': spirit.obtained, 'notes': spirit.notes }) return jsonify({ 'spirits': spirits_data, 'total_pages': pagination.pages, 'current_page': page }) # 更新精灵信息(包括获取状态) @app.route('/api/spirits/<int:id>', methods=['PUT']) def update_spirit(id): spirit = Spirit.query.get_or_404(id) data = request.get_json() # 更新字段 if 'name' in data: spirit.name = data['name'] if 'attribute_id' in data: spirit.attribute_id = data['attribute_id'] if 'obtain_method' in data: spirit.obtain_method = data['obtain_method'] if 'obtained' in data: spirit.obtained = data['obtained'] if 'notes' in data: spirit.notes = data['notes'] db.session.commit() return jsonify({'message': '更新成功'}) # 添加新精灵 @app.route('/api/spirits', methods=['POST']) def add_spirit(): data = request.get_json() new_spirit = Spirit( id=data['id'], name=data['name'], attribute_id=data.get('attribute_id'), obtain_method=data.get('obtain_method'), obtained=data.get('obtained', False), notes=data.get('notes', '') ) db.session.add(new_spirit) db.session.commit() return jsonify({'message': '添加成功'}), 201 # 导入CSV文件 @app.route('/api/spirits/import', methods=['POST']) def import_csv(): if 'file' not in request.files: return jsonify({'error': '未上传文件'}), 400 file = request.files['file'] if file.filename == '': return jsonify({'error': '未选择文件'}), 400 if file: # 读取CSV文件 csv_file = TextIOWrapper(file, encoding='utf-8') reader = csv.DictReader(csv_file) for row in reader: # 检查精灵是否已存在(根据ID) spirit = Spirit.query.get(int(row['id'])) if spirit: # 更新现有记录 spirit.name = row['name'] # 属性处理:需要根据属性名查找属性ID,如果属性不存在则创建 attribute_name = row['attribute'] attribute = Attribute.query.filter_by(name=attribute_name).first() if not attribute: attribute = Attribute(name=attribute_name) db.session.add(attribute) db.session.flush() # 获取新属性的ID spirit.attribute_id = attribute.id spirit.obtain_method = row['obtain_method'] spirit.obtained = row['obtained'].lower() == 'true' # CSV中应为'true'或'false' spirit.notes = row['notes'] else: # 创建新记录 # 同样处理属性 attribute_name = row['attribute'] attribute = Attribute.query.filter_by(name=attribute_name).first() if not attribute: attribute = Attribute(name=attribute_name) db.session.add(attribute) db.session.flush() # 获取新属性的ID new_spirit = Spirit( id=int(row['id']), name=row['name'], attribute_id=attribute.id, obtain_method=row['obtain_method'], obtained=row['obtained'].lower() == 'true', notes=row['notes'] ) db.session.add(new_spirit) db.session.commit() return jsonify({'message': '导入成功'}), 200 # 属性管理:获取所有属性 @app.route('/api/attributes', methods=['GET']) def get_attributes(): attributes = Attribute.query.all() attributes_data = [{'id': attr.id, 'name': attr.name} for attr in attributes] return jsonify(attributes_data) # 属性管理:添加属性 @app.route('/api/attributes', methods=['POST']) def add_attribute(): data = request.get_json() name = data['name'] if not name: return jsonify({'error': '属性名不能为空'}), 400 # 检查是否已存在 existing = Attribute.query.filter_by(name=name).first() if existing: return jsonify({'error': '属性已存在'}), 400 new_attr = Attribute(name=name) db.session.add(new_attr) db.session.commit() return jsonify({'message': '添加成功'}), 201 if __name__ == '__main__': app.run(debug=True) ``` 步骤4:前端代码 我们将创建一个简单的单页面应用(SPA),使用纯JavaScript(也可以使用Vue或React,但这里为了简化使用原生JS)。 文件结构(在项目根目录创建templates和static文件夹): - templates/index.html - static/css/style.css - static/js/main.js 由于前端代码量非常大,我们只展示关键部分: `index.html`主要结构: ```html <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>精灵图鉴</title> <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}"> </head> <body> <div class="container"> <h1>精灵图鉴</h1> <!-- 搜索和筛选区域 --> <div class="filters"> <input type="text" id="search-input" placeholder="搜索精灵名称"> <select id="attribute-select"> <option value="">所有属性</option> <!-- 属性选项将通过JS动态加载 --> </select> <select id="obtained-select"> <option value="">所有状态</option> <option value="true">已获取</option> <option value="false">未获取</option> </select> <button id="add-spirit-btn">添加精灵</button> <!-- CSV导入 --> <input type="file" id="csv-file" accept=".csv"> <button id="import-btn">导入CSV</button> </div> <!-- 精灵列表表格 --> <table id="spirits-table"> <thead> <tr> <th>ID</th> <th>名称</th> <th>属性</th> <th>获取途径</th> <th>获取状态</th> <th>备注</th> <th>操作</th> </tr> </thead> <tbody> <!-- 动态填充 --> </tbody> </table> <!-- 分页控件 --> <div class="pagination"> <button id="prev-page">上一页</button> <span id="current-page">1</span>/<span id="total-pages">1</span> <button id="next-page">下一页</button> </div> <!-- 精灵卡片(模态框) --> <div class="modal" id="spirit-modal" style="display:none;"> <div class="modal-content"> <h2>精灵详情</h2> <form id="spirit-form"> <input type="hidden" id="spirit-id"> <label for="spirit-name">名称:</label> <input type="text" id="spirit-name" required> <label for="spirit-attribute">属性:</label> <select id="spirit-attribute"> <!-- 属性选项动态加载 --> </select> <label for="spirit-obtain-method">获取途径:</label> <input type="text" id="spirit-obtain-method"> <label for="spirit-obtained">已获取:</label> <input type="checkbox" id="spirit-obtained"> <label for="spirit-notes">备注:</label> <textarea id="spirit-notes"></textarea> <button type="submit">保存</button> <button type="button" id="cancel-edit">取消</button> </form> </div> </div> <!-- 添加属性的模态框 --> <div class="modal" id="attribute-modal" style="display:none;"> <div class="modal-content"> <h2>添加属性</h2> <input type="text" id="new-attribute-name" placeholder="输入新属性名称"> <button id="save-attribute">添加</button> <button id="cancel-attribute">取消</button> </div> </div> </div> <script src="{{ url_for('static', filename='js/main.js') }}"></script> </body> </html> ``` `static/css/style.css` 提供基本样式(略,可根据需要自行设计) `static/js/main.js` 实现前端逻辑: - 加载精灵列表(分页) - 搜索、筛选 - 打开精灵卡片进行编辑 - 更新获取状态(复选框) - 导入CSV - 添加新属性 由于JavaScript代码较长,这里只给出关键逻辑的伪代码和部分实现: ```javascript // 全局变量 let currentPage = 1; let totalPages = 1; // 页面加载完成后执行 document.addEventListener('DOMContentLoaded', () => { // 加载属性选项(用于筛选下拉框和编辑表单) loadAttributes(); // 加载第一页的精灵 loadSpirits(currentPage); // 绑定事件 document.getElementById('search-input').addEventListener('input', searchHandler); document.getElementById('attribute-select').addEventListener('change', filterHandler); document.getElementById('obtained-select').addEventListener('change', filterHandler); document.getElementById('prev-page').addEventListener('click', () => changePage(currentPage - 1)); document.getElementById('next-page').addEventListener('click', () => changePage(currentPage + 1)); document.getElementById('import-btn').addEventListener('click', importCSV); document.getElementById('add-spirit-btn').addEventListener('click', openAddSpiritModal); document.getElementById('cancel-edit').addEventListener('click', closeModal); document.getElementById('spirit-form').addEventListener('submit', saveSpirit); // 其他事件... }); // 加载属性选项 function loadAttributes() { fetch('/api/attributes') .then(response => response.json()) .then(attributes => { const attributeSelect = document.getElementById('attribute-select'); const spiritAttributeSelect = document.getElementById('spirit-attribute'); // 清空并加入默认选项 attributeSelect.innerHTML = '<option value="">所有属性</option>'; spiritAttributeSelect.innerHTML = ''; attributes.forEach(attr => { const option = document.createElement('option'); option.value = attr.id; option.textContent = attr.name; attributeSelect.appendChild(option.cloneNode(true)); spiritAttributeSelect.appendChild(option); }); }); } // 加载精灵列表 function loadSpirits(page) { const search = document.getElementById('search-input').value; const attributeId = document.getElementById('attribute-select').value; const obtained = document.getElementById('obtained-select').value; let url = `/api/spirits?page=${page}`; if (search) url += `&search=${search}`; if (attributeId) url += `&attribute_id=${attributeId}`; if (obtained) url += `&obtained=${obtained}`; fetch(url) .then(response => response.json()) .then(data => { currentPage = data.current_page; totalPages = data.total_pages; updatePagination(); renderSpirits(data.spirits); }); } // 渲染精灵列表到表格 function renderSpirits(spirits) { const tbody = document.querySelector('#spirits-table tbody'); tbody.innerHTML = ''; spirits.forEach(spirit => { const row = document.createElement('tr'); row.innerHTML = ` <td>${spirit.id}</td> <td>${spirit.name}</td> <td>${spirit.attribute || '无'}</td> <td>${spirit.obtain_method || ''}</td> <td><input type="checkbox" class="obtained-checkbox" data-id="${spirit.id}" ${spirit.obtained ? 'checked' : ''}></td> <td>${spirit.notes || ''}</td> <td><button class="edit-btn" data-id="${spirit.id}">编辑</button></td> `; tbody.appendChild(row); }); // 绑定复选框事件 document.querySelectorAll('.obtained-checkbox').forEach(checkbox => { checkbox.addEventListener('change', function() { updateObtainedStatus(this.dataset.id, this.checked); }); }); // 绑定编辑按钮事件 document.querySelectorAll('.edit-btn').forEach(button => { button.addEventListener('click', function() { openEditModal(this.dataset.id); }); }); } // 更新获取状态 function updateObtainedStatus(id, obtained) { fetch(`/api/spirits/${id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ obtained }) }) .then(response => response.json()) .then(data => { console.log('更新成功'); }); } // 打开编辑模态框(添加和编辑共用) function openEditModal(id) { // 如果是添加,id为0,否则获取精灵信息 if (id) { fetch(`/api/spirits/${id}`) .then(response => response.json()) .then(spirit => { document.getElementById('spirit-id').value = spirit.id; document.getElementById('spirit-name').value = spirit.name; document.getElementById('spirit-attribute').value = spirit.attribute_id || ''; document.getElementById('spirit-obtain-method').value = spirit.obtain_method || ''; document.getElementById('spirit-obtained').checked = spirit.obtained; document.getElementById('spirit-notes').value = spirit.notes || ''; document.getElementById('spirit-modal').style.display = 'block'; }); } else { // 添加精灵 document.getElementById('spirit-id').value = ''; document.getElementById('spirit-name').value = ''; document.getElementById('spirit-attribute').value = ''; document.getElementById('spirit-obtain-method').value = ''; document.getElementById('spirit-obtained').checked = false; document.getElementById('spirit-notes').value = ''; document.getElementById('spirit-modal').style.display = 'block'; } } function openAddSpiritModal() { openEditModal(null); } // 保存精灵(添加或更新) function saveSpirit(event) { event.preventDefault(); const id = document.getElementById('spirit-id').value; const name = document.getElementById('spirit-name').value; const attribute_id = document.getElementById('spirit-attribute').value || null; const obtain_method = document.getElementById('spirit-obtain-method').value; const obtained = document.getElementById('spirit-obtained').checked; const notes = document.getElementById('spirit-notes').value; const spiritData = { name, attribute_id: attribute_id ? parseInt(attribute_id) : null, obtain_method, obtained, notes }; let url, method; if (id) { url = `/api/spirits/${id}`; method = 'PUT'; } else { url = '/api/spirits'; method = 'POST'; } fetch(url, { method: method, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(spiritData) }) .then(response => response.json()) .then(data => { closeModal(); loadSpirits(currentPage); // 重新加载当前页 }); } // 导入CSV function importCSV() { const fileInput = document.getElementById('csv-file'); if (fileInput.files.length === 0) { alert('请选择CSV文件'); return; } const formData = new FormData(); formData.append('file', fileInput.files[0]); fetch('/api/spirits/import', { method: 'POST', body: formData }) .then(response => response.json()) .then(data => { alert(data.message); loadSpirits(currentPage); }); } // 其他辅助函数... ``` 以上代码是一个基本框架,实际开发中还需要考虑错误处理、样式美化、用户体验优化等。 由于篇幅限制,我们无法提供每一行代码,但以上已经涵盖了核心功能。您可以根据需要扩展和调整。 注意:在生产环境中,还需要考虑安全性(如SQL注入、CSRF保护、文件上传验证等)、性能(数据库索引等)和错误处理。 另外,在`spirits`表中,由于`attribute_id`是外键,我们在导入CSV时假设CSV中提供的是属性名,我们会自动查找或创建属性。CSV文件格式示例: ``` id,name,attribute,obtain_method,obtained,notes 1,小火龙,火,野外捕捉,true,初始精灵 2,杰尼龟,水,赠送,false,... ``` 最后,请确保在MySQL数据库的`spirits`表的`id`列上有索引,并且对外键`attribute_id`也有索引以提高查询效率。 希望这个详细的指南能帮助您开发精灵图鉴网站! </think> 以下是开发精灵图鉴网站的完整解决方案,包含数据库设计、后端代码(Python/Flask)、前端实现(HTML/JS)及部署指南: ### 一、数据库设计与部署(MySQL) 1. **安装MySQL** ```bash sudo apt update sudo apt install mysql-server sudo mysql_secure_installation ``` 2. **创建数据库和表** ```sql CREATE DATABASE spirit_db; USE spirit_db; -- 属性表(用于下拉选项) CREATE TABLE attributes ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(50) NOT NULL UNIQUE ); -- 精灵主表 CREATE TABLE spirits ( id INT PRIMARY KEY, name VARCHAR(100) NOT NULL, attribute_id INT, obtain_method VARCHAR(255), obtained BOOLEAN DEFAULT 0, notes TEXT, FOREIGN KEY (attribute_id) REFERENCES attributes(id) ); ``` 3. **初始化属性数据** ```sql INSERT INTO attributes (name) VALUES ('火'),('水'),('草'),('电'),('超能'), ('格斗'),('幽灵'),('龙'),('恶'),('妖精'); ``` --- ### 二、后端实现(Python/Flask) 文件结构: ``` spirit_app/ ├── app.py ├── requirements.txt └── templates/ └── index.html ``` 1. **安装依赖** (`requirements.txt`) ``` Flask==2.3.2 Flask-SQLAlchemy==3.0.3 Flask-Cors==3.0.10 pandas==1.5.3 PyMySQL==1.0.2 ``` 2. **后端核心代码** (`app.py`) ```python from flask import Flask, request, jsonify, render_template from flask_sqlalchemy import SQLAlchemy from flask_cors import CORS import pandas as pd import os app = Flask(__name__) CORS(app) app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql+pymysql://username:password@localhost/spirit_db' db = SQLAlchemy(app) class Spirit(db.Model): __tablename__ = 'spirits' id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(100), nullable=False) attribute_id = db.Column(db.Integer, db.ForeignKey('attributes.id')) obtain_method = db.Column(db.String(255)) obtained = db.Column(db.Boolean, default=False) notes = db.Column(db.Text) attribute = db.relationship('Attribute', backref='spirits') class Attribute(db.Model): __tablename__ = 'attributes' id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(50), unique=True, nullable=False) @app.route('/') def index(): return render_template('index.html') @app.route('/api/spirits', methods=['GET']) def get_spirits(): page = request.args.get('page', 1, type=int) per_page = 100 search = request.args.get('search', '') attribute_id = request.args.get('attribute', type=int) query = Spirit.query if search: query = query.filter(Spirit.name.like(f'%{search}%')) if attribute_id: query = query.filter(Spirit.attribute_id == attribute_id) pagination = query.paginate(page=page, per_page=per_page, error_out=False) spirits = [ { 'id': s.id, 'name': s.name, 'attribute': s.attribute.name if s.attribute else None, 'obtain_method': s.obtain_method, 'obtained': s.obtained, 'notes': s.notes } for s in pagination.items ] return jsonify({ 'spirits': spirits, 'total_pages': pagination.pages, 'current_page': page }) @app.route('/api/spirits/<int:id>', methods=['PUT']) def update_spirit(id): spirit = Spirit.query.get_or_404(id) data = request.get_json() spirit.obtained = data.get('obtained', spirit.obtained) spirit.notes = data.get('notes', spirit.notes) db.session.commit() return jsonify({'message': '更新成功'}) @app.route('/api/spirits/import', methods=['POST']) def import_csv(): file = request.files['file'] df = pd.read_csv(file) for _, row in df.iterrows(): spirit = Spirit.query.get(row['id']) if spirit: # 更新现有记录 spirit.name = row['name'] spirit.obtain_method = row['obtain_method'] spirit.obtained = bool(row['obtained']) spirit.notes = row.get('notes', '') else: # 新增记录 new_spirit = Spirit( id=row['id'], name=row['name'], obtain_method=row['obtain_method'], obtained=bool(row['obtained']), notes=row.get('notes', '') ) db.session.add(new_spirit) db.session.commit() return jsonify({'message': f'成功导入/更新 {len(df)} 条数据'}) @app.route('/api/attributes', methods=['GET', 'POST']) def handle_attributes(): if request.method == 'POST': name = request.json.get('name') if not name: return jsonify({'error': '属性名不能为空'}), 400 if Attribute.query.filter_by(name=name).first(): return jsonify({'error': '属性已存在'}), 400 new_attr = Attribute(name=name) db.session.add(new_attr) db.session.commit() return jsonify({'message': '属性添加成功'}), 201 else: attributes = Attribute.query.all() return jsonify([{'id': a.id, 'name': a.name} for a in attributes]) if __name__ == '__main__': app.run(host='0.0.0.0', port=5000, debug=True) ``` --- ### 三、前端实现 (`templates/index.html`) ```html <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>精灵图鉴系统</title> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet"> <style> .spirit-card { transition: transform 0.2s; cursor: pointer; } .spirit-card:hover { transform: translateY(-5px); box-shadow: 0 10px 20px rgba(0,0,0,0.1); } .modal-card-img { max-height: 200px; object-fit: contain; } </style> </head> <body> <div class="container py-4"> <h1 class="text-center mb-4">精灵图鉴系统</h1> <!-- 搜索和筛选区 --> <div class="row mb-3"> <div class="col-md-6"> <input type="text" id="search-input" class="form-control" placeholder="搜索精灵名称..."> </div> <div class="col-md-3"> <select id="attribute-filter" class="form-select"> <option value="">全部属性</option> </select> </div> <div class="col-md-3"> <button class="btn btn-primary w-100" data-bs-toggle="modal" data-bs-target="#importModal"> 导入CSV </button> </div> </div> <!-- 精灵列表 --> <div id="spirits-container" class="row row-cols-2 row-cols-md-4 row-cols-lg-5 g-4"></div> <!-- 分页控件 --> <nav class="mt-4"> <ul class="pagination justify-content-center"></ul> </nav> </div> <!-- 精灵详情模态框 --> <div class="modal fade" id="detailModal" tabindex="-1"> <div class="modal-dialog modal-lg"> <div class="modal-content"> <div class="modal-header"> <h5 class="modal-title" id="spirit-name"></h5> <button type="button" class="btn-close" data-bs-dismiss="modal"></button> </div> <div class="modal-body"> <div class="row"> <div class="col-md-4"> <img src="" class="img-fluid modal-card-img" id="spirit-image"> </div> <div class="col-md-8"> <div class="mb-3"> <label class="form-label">属性</label> <select id="spirit-attribute" class="form-select"></select> </div> <div class="mb-3"> <label class="form-label">获取途径</label> <input type="text" id="spirit-obtain-method" class="form-control"> </div> <div class="mb-3 form-check"> <input type="checkbox" id="spirit-obtained" class="form-check-input"> <label class="form-check-label">已获取</label> </div> <div class="mb-3"> <label class="form-label">备注</label> <textarea id="spirit-notes" class="form-control" rows="3"></textarea> </div> </div> </div> </div> <div class="modal-footer"> <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button> <button type="button" class="btn btn-primary" id="save-btn">保存更改</button> <button type="button" class="btn btn-success" data-bs-toggle="modal" data-bs-target="#addAttributeModal"> 添加新属性 </button> </div> </div> </div> </div> <!-- CSV导入模态框 --> <div class="modal fade" id="importModal" tabindex="-1"> <div class="modal-dialog"> <div class="modal-content"> <div class="modal-header"> <h5 class="modal-title">导入CSV文件</h5> <button type="button" class="btn-close" data-bs-dismiss="modal"></button> </div> <div class="modal-body"> <div class="mb-3"> <label class="form-label">选择CSV文件</label> <input type="file" class="form-control" id="csv-file" accept=".csv"> <div class="form-text">CSV格式:id,name,attribute,obtain_method,obtained,notes</div> </div> <div class="alert alert-info"> 提示:系统将根据ID自动更新或创建新精灵 </div> </div> <div class="modal-footer"> <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button> <button type="button" class="btn btn-primary" id="import-confirm">导入</button> </div> </div> </div> </div> <!-- 添加属性模态框 --> <div class="modal fade" id="addAttributeModal" tabindex="-1"> <div class="modal-dialog"> <div class="modal-content"> <div class="modal-header"> <h5 class="modal-title">添加新属性</h5> <button type="button" class="btn-close" data-bs-dismiss="modal"></button> </div> <div class="modal-body"> <div class="mb-3"> <label class="form-label">属性名称</label> <input type="text" class="form-control" id="new-attribute-name"> </div> </div> <div class="modal-footer"> <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button> <button type="button" class="btn btn-primary" id="add-attribute-btn">添加</button> </div> </div> </div> </div> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script> <script> let currentPage = 1; let totalPages = 1; let currentSpiritId = null; // 初始化页面 document.addEventListener('DOMContentLoaded', () => { loadAttributes(); loadSpirits(); // 绑定事件监听器 document.getElementById('search-input').addEventListener('input', debounce(loadSpirits, 300)); document.getElementById('attribute-filter').addEventListener('change', loadSpirits); document.getElementById('save-btn').addEventListener('click', saveSpirit); document.getElementById('import-confirm').addEventListener('click', importCSV); document.getElementById('add-attribute-btn').addEventListener('click', addAttribute); }); // 加载属性下拉菜单 async function loadAttributes() { const response = await fetch('/api/attributes'); const attributes = await response.json(); const filterSelect = document.getElementById('attribute-filter'); const attributeSelect = document.getElementById('spirit-attribute'); filterSelect.innerHTML = '<option value="">全部属性</option>'; attributeSelect.innerHTML = ''; attributes.forEach(attr => { const option = document.createElement('option'); option.value = attr.id; option.textContent = attr.name; filterSelect.appendChild(option.cloneNode(true)); attributeSelect.appendChild(option); }); } // 加载精灵数据 async function loadSpirits(page = 1) { const search = document.getElementById('search-input').value; const attribute = document.getElementById('attribute-filter').value; const url = `/api/spirits?page=${page}&search=${encodeURIComponent(search)}&attribute=${attribute}`; const response = await fetch(url); const data = await response.json(); renderSpirits(data.spirits); renderPagination(data.total_pages, page); } // 渲染精灵卡片 function renderSpirits(spirits) { const container = document.getElementById('spirits-container'); container.innerHTML = ''; spirits.forEach(spirit => { const card = document.createElement('div'); card.className = 'col'; card.innerHTML = ` <div class="card h-100 spirit-card" data-id="${spirit.id}"> <div class="card-body"> <h5 class="card-title">${spirit.name}</h5> <p class="card-text"> <span class="badge bg-primary">${spirit.attribute || '无属性'}</span> <span class="badge ${spirit.obtained ? 'bg-success' : 'bg-secondary'}"> ${spirit.obtained ? '已获取' : '未获取'} </span> </p> <p class="card-text text-truncate">${spirit.obtain_method || '无获取途径'}</p> </div> </div> `; card.querySelector('.card').addEventListener('click', () => openDetailModal(spirit)); container.appendChild(card); }); } // 打开详情模态框 async function openDetailModal(spirit) { currentSpiritId = spirit.id; document.getElementById('spirit-name').textContent = spirit.name; document.getElementById('spirit-attribute').value = spirit.attribute_id || ''; document.getElementById('spirit-obtain-method').value = spirit.obtain_method || ''; document.getElementById('spirit-obtained').checked = spirit.obtained; document.getElementById('spirit-notes').value = spirit.notes || ''; const modal = new bootstrap.Modal(document.getElementById('detailModal')); modal.show(); } // 保存精灵更新 async function saveSpirit() { const data = { obtained: document.getElementById('spirit-obtained').checked, notes: document.getElementById('spirit-notes').value }; await fetch(`/api/spirits/${currentSpiritId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); bootstrap.Modal.getInstance(document.getElementById('detailModal')).hide(); loadSpirits(currentPage); } // 导入CSV async function importCSV() { const fileInput = document.getElementById('csv-file'); if (!fileInput.files.length) return alert('请选择CSV文件'); const formData = new FormData(); formData.append('file', fileInput.files[0]); const response = await fetch('/api/spirits/import', { method: 'POST', body: formData }); const result = await response.json(); alert(result.message); bootstrap.Modal.getInstance(document.getElementById('importModal')).hide(); loadSpirits(currentPage); } // 添加新属性 async function addAttribute() { const name = document.getElementById('new-attribute-name').value.trim(); if (!name) return alert('请输入属性名称'); const response = await fetch('/api/attributes', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name }) }); if (response.ok) { alert('属性添加成功'); loadAttributes(); bootstrap.Modal.getInstance(document.getElementById('addAttributeModal')).hide(); document.getElementById('new-attribute-name').value = ''; } else { const error = await response.json(); alert(error.error); } } // 辅助函数:防抖 function debounce(func, timeout = 300) { let timer; return (...args) => { clearTimeout(timer); timer = setTimeout(() => { func.apply(this, args); }, timeout); }; } </script> </body> </html> ``` --- ### 四、部署与运行 1. **启动后端服务** ```bash pip install -r requirements.txt
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值