一、项目背景与需求分析
在日常企业管理场景中,HR 或管理人员需要快速地对员工信息进行增删改查、搜索筛选等操作。为了提升效率并降低人工维护成本,我们基于 Flask + SQLAlchemy 构建了后端 API,配合 Bootstrap + 原生 JavaScript 实现一个简洁易用的“员工信息管理系统”。
主要需求包括:
-
员工信息维护:新增、更新、删除员工记录;
-
列表展示与搜索:分页/前端过滤展示所有员工数据;
-
表单校验:前端强制字段(姓名、职位、邮箱、电话)必填,且电话必须 11 位数字;
-
界面美观:响应式布局、交互提示、表格与按钮样式优化;
-
后端健壮性:字段约束、唯一索引、错误处理与分页支持。
二、技术选型
层级 | 技术栈 | 说明 |
---|---|---|
前端 | HTML5 + Bootstrap 5 + 原生 JS | 快速布局、响应式、无需额外框架 |
后端 | Flask 2.x + Flask-SQLAlchemy | 轻量级微框架 + ORM,提高开发效率 |
数据库 | MySQL(InnoDB) + SQLAlchemy ORM | 事务安全、自增主键、UTF8MB4 支持 |
开发/部署 | Python 3.8+ + pipenv/venv + WSGI Server | 便于虚拟环境管理,建议生产环境使用 Gunicorn/Nginx |
本人配置的项目环境如下:
三、核心功能详解
3.1 前端页面结构
<div class="container py-4">
<h1 class="mb-4">员工信息管理系统</h1>
<!-- 搜索框 -->
<div class="mb-3">
<input id="search" class="form-control search-input"
placeholder="搜索员工(姓名/职位/邮箱/电话)"
oninput="filterEmployees()">
</div>
<!-- 员工表单 -->
<form id="employeeForm" class="row g-2 mb-4 needs-validation" novalidate>
<!-- ... 姓名、职位、邮箱、电话输入框,均带 required 与 invalid-feedback 提示 ... -->
</form>
<!-- 员工列表 -->
<table class="table table-striped">
<thead>
<tr><th>ID</th><th>姓名</th><th>职位</th><th>邮箱</th><th>电话</th><th>操作</th></tr>
</thead>
<tbody id="employeeTable"></tbody>
</table>
</div>
-
搜索框:
oninput
触发filterEmployees()
,基于前端数组做实时过滤; -
Bootstrap 校验:通过
needs-validation
+novalidate
,结合 JS 调用checkValidity()
控制表单提交;
3.2 前端逻辑核心
// 加载所有员工
function loadEmployees() {
fetch('/api/employees')
.then(res => res.json())
.then(data => {
employees = data;
renderTable(employees);
});
}
// 渲染表格
function renderTable(list) {
const tbody = document.getElementById('employeeTable');
tbody.innerHTML = list.map(emp => `
<tr>
<td>${emp.id}</td>
<td>${emp.name}</td>
<td>${emp.position}</td>
<td>${emp.email}</td>
<td>${emp.phone}</td>
<td>
<button class="btn btn-sm btn-outline-primary" onclick="startEdit(${emp.id})">编辑</button>
<button class="btn btn-sm btn-outline-danger" onclick="deleteEmployee(${emp.id})">删除</button>
</td>
</tr>
`).join('');
}
-
CRUD 操作:
POST
/PUT
/DELETE
对应/api/employees
接口; -
表单切换:编辑时将数据写回输入框,按钮文本与取消按钮状态动态控制。
四、后端源码解析
4.1 数据库表结构
CREATE TABLE `employee` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`name` VARCHAR(80) NOT NULL,
`position` VARCHAR(120) NOT NULL,
`email` VARCHAR(120) NOT NULL,
`phone` VARCHAR(20) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uniq_email` (`email`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='员工信息表';
-
NOT NULL
:所有业务字段强制必填; -
UNIQUE(email)
:邮箱全局唯一。
4.2 Flask + SQLAlchemy 模型
class Employee(db.Model):
__tablename__ = 'employee'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), nullable=False)
position = db.Column(db.String(120), nullable=False)
email = db.Column(db.String(120), nullable=False, unique=True)
phone = db.Column(db.String(20), nullable=False)
def to_dict(self):
return { c.name: getattr(self, c.name) for c in self.__table__.columns }
4.3 RESTful 接口示例
@app.route('/api/employees', methods=['POST'])
def add_employee():
data = request.get_json() or {}
# 校验必填
for f in ('name','position','email','phone'):
if not data.get(f):
return jsonify({'error': f'{f} 为必填'}), 400
emp = Employee(**data)
db.session.add(emp)
try:
db.session.commit()
except IntegrityError:
db.session.rollback()
return jsonify({'error': '邮箱已存在'}), 409
return jsonify(emp.to_dict()), 201
-
捕获
IntegrityError
:避免重复邮箱导致 500 报错。
五、部署与优化方案
-
生产环境:推荐使用 Gunicorn + Nginx 做 WSGI 容器与反向代理,关闭 Flask
debug
; -
配置管理:将数据库 URI、调试开关等放入环境变量或
config.py
; -
日志:接入
logging
模块或 ELK,记录增删查改操作; -
分页/搜索:后端可扩展服务器端分页或模糊查询,提升数据量大时的性能;
-
安全:启用 HTTPS、添加 CSRF 防护(WTForms 或 Flask-WTF)、接口鉴权(Token/JWT)。
六、完整代码:
首先,创建MySQL数据库employee_db,并创建数据表employee。
然后编写后端代码:
# app.py
from flask import Flask, render_template, request, jsonify, redirect, url_for
from flask_sqlalchemy import SQLAlchemy
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql+pymysql://root:xiaozeng @localhost/employee_db' # root:后面是自己的密码,localhost后面就是需要用的数据库
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db = SQLAlchemy(app)
class Employee(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), nullable=False)
position = db.Column(db.String(120))
email = db.Column(db.String(120), unique=True)
phone = db.Column(db.String(20))
def to_dict(self):
return {
"id": self.id,
"name": self.name,
"position": self.position,
"email": self.email,
"phone": self.phone
}
@app.route('/')
def index():
return render_template('employee.html')
# 获取所有员工
@app.route('/api/employees', methods=['GET'])
def get_employees():
employees = Employee.query.all()
return jsonify([e.to_dict() for e in employees])
# 新增员工
@app.route('/api/employees', methods=['POST'])
def add_employee():
data = request.get_json()
new_emp = Employee(
name=data['name'],
position=data['position'],
email=data['email'],
phone=data['phone']
)
db.session.add(new_emp)
db.session.commit()
return jsonify(new_emp.to_dict()), 201
# 更新员工
@app.route('/api/employees/<int:id>', methods=['PUT'])
def update_employee(id):
emp = Employee.query.get_or_404(id)
data = request.get_json()
emp.name = data.get('name', emp.name)
emp.position = data.get('position', emp.position)
emp.email = data.get('email', emp.email)
emp.phone = data.get('phone', emp.phone)
db.session.commit()
return jsonify(emp.to_dict())
# 删除员工
@app.route('/api/employees/<int:id>', methods=['DELETE'])
def delete_employee(id):
emp = Employee.query.get_or_404(id)
db.session.delete(emp)
db.session.commit()
return '', 204
if __name__ == '__main__':
with app.app_context():
db.create_all()
app.run(debug=True)
最后,编写前端代码:
<!-- templates/index.html -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>员工信息管理系统</title>
<!-- 引入 Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
/* 自定义样式 */
.search-input {
width: 100%;
max-width: 400px;
padding: 6px 8px;
box-sizing: border-box;
}
table tr:hover {
background: #f1f1f1;
}
</style>
</head>
<body>
<div class="container py-4">
<h1 class="mb-4">员工信息管理系统</h1>
<!-- 搜索 -->
<div class="mb-3">
<input type="text"
id="search"
class="form-control search-input"
placeholder="搜索员工(姓名/职位/邮箱/电话)"
oninput="filterEmployees()">
</div>
<!-- 新增/编辑表单 -->
<form id="employeeForm" class="row g-2 mb-4 needs-validation" novalidate>
<div class="col-md-3">
<input type="text"
id="name"
class="form-control"
placeholder="姓名"
required>
<div class="invalid-feedback">请输入姓名。</div>
</div>
<div class="col-md-3">
<input type="text"
id="position"
class="form-control"
placeholder="职位"
required>
<div class="invalid-feedback">请输入职位。</div>
</div>
<div class="col-md-3">
<input type="email"
id="email"
class="form-control"
placeholder="邮箱"
required>
<div class="invalid-feedback">请输入有效的邮箱地址。</div>
</div>
<div class="col-md-3">
<input type="tel"
id="phone"
class="form-control"
placeholder="电话(11 位数字)"
pattern="\d{11}"
required>
<div class="invalid-feedback">请输入 11 位数字电话号码。</div>
</div>
<div class="col-auto">
<button type="button"
id="submitBtn"
class="btn btn-primary"
onclick="submitEmployee()">新增员工</button>
</div>
<div class="col-auto">
<button type="button"
id="cancelBtn"
class="btn btn-secondary d-none"
onclick="cancelEdit()">取消编辑</button>
</div>
</form>
<!-- 员工列表 -->
<table class="table table-striped">
<thead>
<tr>
<th>ID</th>
<th>姓名</th>
<th>职位</th>
<th>邮箱</th>
<th>电话</th>
<th>操作</th>
</tr>
</thead>
<tbody id="employeeTable"></tbody>
</table>
</div>
<!-- 引入 Bootstrap JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
let employees = [];
let editingId = null;
// 启用表单 Bootstrap 校验样式
(function () {
'use strict';
const forms = document.querySelectorAll('.needs-validation');
Array.from(forms).forEach(form => {
form.addEventListener('submit', event => {
if (!form.checkValidity()) {
event.preventDefault();
event.stopPropagation();
}
form.classList.add('was-validated');
}, false);
});
})();
// 加载员工列表
function loadEmployees() {
fetch('/api/employees')
.then(res => res.json())
.then(data => {
employees = data;
renderTable(employees);
});
}
// 渲染表格
function renderTable(list) {
const tbody = document.querySelector('#employeeTable');
tbody.innerHTML = list.map(emp => `
<tr>
<td>${emp.id}</td>
<td>${emp.name}</td>
<td>${emp.position}</td>
<td>${emp.email}</td>
<td>${emp.phone}</td>
<td>
<button class="btn btn-sm btn-outline-primary me-1" onclick="startEdit(${emp.id})">编辑</button>
<button class="btn btn-sm btn-outline-danger" onclick="deleteEmployee(${emp.id})">删除</button>
</td>
</tr>
`).join('');
}
// 提交(新增或更新)
function submitEmployee() {
const form = document.getElementById('employeeForm');
if (!form.checkValidity()) {
form.classList.add('was-validated');
return;
}
const data = {
name: document.getElementById('name').value.trim(),
position: document.getElementById('position').value.trim(),
email: document.getElementById('email').value.trim(),
phone: document.getElementById('phone').value.trim()
};
const method = editingId ? 'PUT' : 'POST';
const url = editingId ? `/api/employees/${editingId}` : '/api/employees';
fetch(url, {
method,
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(data)
}).then(() => {
resetForm();
loadEmployees();
});
}
// 开始编辑
function startEdit(id) {
const emp = employees.find(e => e.id === id);
if (!emp) return;
editingId = id;
document.getElementById('name').value = emp.name;
document.getElementById('position').value = emp.position;
document.getElementById('email').value = emp.email;
document.getElementById('phone').value = emp.phone;
document.getElementById('submitBtn').textContent = '保存修改';
document.getElementById('cancelBtn').classList.remove('d-none');
}
// 取消编辑
function cancelEdit() {
resetForm();
}
// 重置表单
function resetForm() {
editingId = null;
document.getElementById('employeeForm').reset();
document.getElementById('submitBtn').textContent = '新增员工';
document.getElementById('cancelBtn').classList.add('d-none');
document.getElementById('employeeForm').classList.remove('was-validated');
}
// 删除员工
function deleteEmployee(id) {
if (confirm('确定删除该员工?')) {
fetch(`/api/employees/${id}`, {method: 'DELETE'})
.then(loadEmployees);
}
}
// 搜索过滤
function filterEmployees() {
const term = document.getElementById('search').value.toLowerCase();
const filtered = employees.filter(emp =>
emp.name.toLowerCase().includes(term) ||
emp.position.toLowerCase().includes(term) ||
emp.email.toLowerCase().includes(term) ||
emp.phone.toLowerCase().includes(term)
);
renderTable(filtered);
}
// 初始化加载
loadEmployees();
</script>
</body>
</html>
运行结果:
七、总结与展望
本文介绍了一个基于 Flask + MySQL + Bootstrap 的“员工信息管理系统”完整实现,从前端页面、交互逻辑、后端模型到部署优化,覆盖了典型的 CRUD 应用场景。该项目代码量小、逻辑清晰,适合作为入门级示例或公司内部小工具。
未来可进一步迭代:
-
增加导出 Excel/CSV 功能;
-
角色权限管理(管理员 vs 普通用户);
-
接入第三方登录/单点登录;
-
前端重构:React/Vue + Ant Design 等框架。
欢迎关注、点赞并留言交流,这个是我自己有些用ai写的,后面会慢慢改进!
写在最后:
我们可以在这里学习C++知识: