<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