user
This commit is contained in:
parent
0c1d1b0d19
commit
29009ef7de
@ -1,12 +1,16 @@
|
||||
from flask import Flask, render_template, session, g, Markup
|
||||
from flask_login import LoginManager
|
||||
from app.models.user import db, User
|
||||
from app.controllers.user import user_bp
|
||||
from app.controllers.book import book_bp
|
||||
from app.controllers.borrow import borrow_bp
|
||||
from flask_login import LoginManager, current_user
|
||||
import os
|
||||
|
||||
login_manager = LoginManager()
|
||||
|
||||
def create_app():
|
||||
|
||||
def create_app(config=None):
|
||||
app = Flask(__name__)
|
||||
|
||||
# 配置应用
|
||||
@ -32,6 +36,14 @@ def create_app():
|
||||
# 初始化数据库
|
||||
db.init_app(app)
|
||||
|
||||
# 初始化 Flask-Login
|
||||
login_manager.init_app(app)
|
||||
login_manager.login_view = 'user.login'
|
||||
|
||||
@login_manager.user_loader
|
||||
def load_user(user_id):
|
||||
return User.query.get(int(user_id))
|
||||
|
||||
# 注册蓝图
|
||||
app.register_blueprint(user_bp, url_prefix='/user')
|
||||
app.register_blueprint(book_bp, url_prefix='/book')
|
||||
@ -105,9 +117,9 @@ def create_app():
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
if not g.user:
|
||||
if not current_user.is_authenticated:
|
||||
return render_template('login.html')
|
||||
return render_template('index.html', current_user=g.user)
|
||||
return render_template('index.html') # 无需传递current_user,Flask-Login自动提供
|
||||
|
||||
@app.errorhandler(404)
|
||||
def page_not_found(e):
|
||||
|
||||
@ -19,7 +19,8 @@ def book_list():
|
||||
page = request.args.get('page', 1, type=int)
|
||||
per_page = request.args.get('per_page', 10, type=int)
|
||||
|
||||
query = Book.query
|
||||
# 只显示状态为1的图书(未下架的图书)
|
||||
query = Book.query.filter_by(status=1)
|
||||
|
||||
# 搜索功能
|
||||
search = request.args.get('search', '')
|
||||
|
||||
@ -6,6 +6,9 @@ import logging
|
||||
from functools import wraps
|
||||
import time
|
||||
from datetime import datetime, timedelta
|
||||
from app.services.user_service import UserService
|
||||
from flask_login import login_user, logout_user, current_user, login_required
|
||||
from app.models.user import User
|
||||
|
||||
# 创建蓝图
|
||||
user_bp = Blueprint('user', __name__)
|
||||
@ -46,11 +49,13 @@ class VerificationStore:
|
||||
verification_codes = VerificationStore()
|
||||
|
||||
|
||||
def login_required(f):
|
||||
# 添加管理员权限检查装饰器
|
||||
def admin_required(f):
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
if 'user_id' not in session:
|
||||
return redirect(url_for('user.login'))
|
||||
if not current_user.is_authenticated or current_user.role_id != 1:
|
||||
flash('您没有管理员权限', 'error')
|
||||
return redirect(url_for('index'))
|
||||
return f(*args, **kwargs)
|
||||
|
||||
return decorated_function
|
||||
@ -58,7 +63,10 @@ def login_required(f):
|
||||
|
||||
@user_bp.route('/login', methods=['GET', 'POST'])
|
||||
def login():
|
||||
# 保持原代码不变
|
||||
# 如果用户已经登录,直接重定向到首页
|
||||
if current_user.is_authenticated:
|
||||
return redirect(url_for('index'))
|
||||
|
||||
if request.method == 'POST':
|
||||
username = request.form.get('username')
|
||||
password = request.form.get('password')
|
||||
@ -76,26 +84,30 @@ def login():
|
||||
if user.status == 0:
|
||||
return render_template('login.html', error='账号已被禁用,请联系管理员')
|
||||
|
||||
# 登录成功,保存用户信息到会话
|
||||
session['user_id'] = user.id
|
||||
# 使用 Flask-Login 的 login_user 函数
|
||||
login_user(user, remember=remember_me)
|
||||
|
||||
# 这些session信息仍然可以保留,但不再用于认证
|
||||
session['username'] = user.username
|
||||
session['role_id'] = user.role_id
|
||||
|
||||
if remember_me:
|
||||
# 设置会话过期时间为7天
|
||||
session.permanent = True
|
||||
# 获取登录后要跳转的页面
|
||||
next_page = request.args.get('next')
|
||||
if not next_page or not next_page.startswith('/'):
|
||||
next_page = url_for('index')
|
||||
|
||||
# 记录登录日志(可选)
|
||||
# log_user_action('用户登录')
|
||||
|
||||
# 重定向到首页
|
||||
return redirect(url_for('index'))
|
||||
# 重定向到首页或其他请求的页面
|
||||
return redirect(next_page)
|
||||
|
||||
return render_template('login.html')
|
||||
|
||||
|
||||
@user_bp.route('/register', methods=['GET', 'POST'])
|
||||
def register():
|
||||
# 如果用户已登录,重定向到首页
|
||||
if current_user.is_authenticated:
|
||||
return redirect(url_for('index'))
|
||||
|
||||
if request.method == 'POST':
|
||||
username = request.form.get('username')
|
||||
email = request.form.get('email')
|
||||
@ -147,11 +159,9 @@ def register():
|
||||
|
||||
|
||||
@user_bp.route('/logout')
|
||||
@login_required
|
||||
def logout():
|
||||
# 清除会话数据
|
||||
session.pop('user_id', None)
|
||||
session.pop('username', None)
|
||||
session.pop('role_id', None)
|
||||
logout_user()
|
||||
return redirect(url_for('user.login'))
|
||||
|
||||
|
||||
@ -179,3 +189,194 @@ def send_verification_code():
|
||||
return jsonify({'success': True, 'message': '验证码已发送'})
|
||||
else:
|
||||
return jsonify({'success': False, 'message': '邮件发送失败,请稍后重试'})
|
||||
|
||||
|
||||
# 用户管理列表
|
||||
@user_bp.route('/manage')
|
||||
@login_required
|
||||
@admin_required
|
||||
def user_list():
|
||||
page = request.args.get('page', 1, type=int)
|
||||
search = request.args.get('search', '')
|
||||
status = request.args.get('status', type=int)
|
||||
role_id = request.args.get('role_id', type=int)
|
||||
|
||||
pagination = UserService.get_users(
|
||||
page=page,
|
||||
per_page=10,
|
||||
search_query=search,
|
||||
status=status,
|
||||
role_id=role_id
|
||||
)
|
||||
|
||||
roles = UserService.get_all_roles()
|
||||
|
||||
return render_template(
|
||||
'user/list.html',
|
||||
pagination=pagination,
|
||||
search=search,
|
||||
status=status,
|
||||
role_id=role_id,
|
||||
roles=roles
|
||||
)
|
||||
|
||||
|
||||
# 用户详情/编辑页面
|
||||
@user_bp.route('/edit/<int:user_id>', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
@admin_required
|
||||
def user_edit(user_id):
|
||||
user = UserService.get_user_by_id(user_id)
|
||||
if not user:
|
||||
flash('用户不存在', 'error')
|
||||
return redirect(url_for('user.user_list'))
|
||||
|
||||
roles = UserService.get_all_roles()
|
||||
|
||||
if request.method == 'POST':
|
||||
data = {
|
||||
'email': request.form.get('email'),
|
||||
'phone': request.form.get('phone'),
|
||||
'nickname': request.form.get('nickname'),
|
||||
'role_id': int(request.form.get('role_id')),
|
||||
'status': int(request.form.get('status')),
|
||||
}
|
||||
|
||||
password = request.form.get('password')
|
||||
if password:
|
||||
data['password'] = password
|
||||
|
||||
success, message = UserService.update_user(user_id, data)
|
||||
if success:
|
||||
flash(message, 'success')
|
||||
return redirect(url_for('user.user_list'))
|
||||
else:
|
||||
flash(message, 'error')
|
||||
|
||||
return render_template('user/edit.html', user=user, roles=roles)
|
||||
|
||||
|
||||
# 用户状态管理API
|
||||
@user_bp.route('/status/<int:user_id>', methods=['POST'])
|
||||
@login_required
|
||||
@admin_required
|
||||
def user_status(user_id):
|
||||
data = request.get_json()
|
||||
status = data.get('status')
|
||||
|
||||
if status is None or status not in [0, 1]:
|
||||
return jsonify({'success': False, 'message': '无效的状态值'})
|
||||
|
||||
# 不能修改自己的状态
|
||||
if user_id == current_user.id:
|
||||
return jsonify({'success': False, 'message': '不能修改自己的状态'})
|
||||
|
||||
success, message = UserService.change_user_status(user_id, status)
|
||||
return jsonify({'success': success, 'message': message})
|
||||
|
||||
|
||||
# 用户删除API
|
||||
@user_bp.route('/delete/<int:user_id>', methods=['POST'])
|
||||
@login_required
|
||||
@admin_required
|
||||
def user_delete(user_id):
|
||||
# 不能删除自己
|
||||
if user_id == current_user.id:
|
||||
return jsonify({'success': False, 'message': '不能删除自己的账号'})
|
||||
|
||||
success, message = UserService.delete_user(user_id)
|
||||
return jsonify({'success': success, 'message': message})
|
||||
|
||||
|
||||
# 个人中心页面
|
||||
@user_bp.route('/profile', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def user_profile():
|
||||
user = current_user
|
||||
|
||||
if request.method == 'POST':
|
||||
data = {
|
||||
'email': request.form.get('email'),
|
||||
'phone': request.form.get('phone'),
|
||||
'nickname': request.form.get('nickname')
|
||||
}
|
||||
|
||||
current_password = request.form.get('current_password')
|
||||
new_password = request.form.get('new_password')
|
||||
confirm_password = request.form.get('confirm_password')
|
||||
|
||||
# 如果用户想要修改密码
|
||||
if current_password and new_password:
|
||||
if not user.check_password(current_password):
|
||||
flash('当前密码不正确', 'error')
|
||||
return render_template('user/profile.html', user=user)
|
||||
|
||||
if new_password != confirm_password:
|
||||
flash('两次输入的新密码不匹配', 'error')
|
||||
return render_template('user/profile.html', user=user)
|
||||
|
||||
data['password'] = new_password
|
||||
|
||||
success, message = UserService.update_user(user.id, data)
|
||||
if success:
|
||||
flash(message, 'success')
|
||||
else:
|
||||
flash(message, 'error')
|
||||
|
||||
return render_template('user/profile.html', user=user)
|
||||
|
||||
|
||||
# 角色管理页面
|
||||
@user_bp.route('/roles', methods=['GET'])
|
||||
@login_required
|
||||
@admin_required
|
||||
def role_list():
|
||||
roles = UserService.get_all_roles()
|
||||
return render_template('user/roles.html', roles=roles)
|
||||
|
||||
|
||||
# 创建/编辑角色API
|
||||
@user_bp.route('/role/save', methods=['POST'])
|
||||
@login_required
|
||||
@admin_required
|
||||
def role_save():
|
||||
data = request.get_json()
|
||||
role_id = data.get('id')
|
||||
role_name = data.get('role_name')
|
||||
description = data.get('description')
|
||||
|
||||
if not role_name:
|
||||
return jsonify({'success': False, 'message': '角色名不能为空'})
|
||||
|
||||
if role_id: # 更新
|
||||
success, message = UserService.update_role(role_id, role_name, description)
|
||||
else: # 创建
|
||||
success, message = UserService.create_role(role_name, description)
|
||||
|
||||
return jsonify({'success': success, 'message': message})
|
||||
|
||||
"""
|
||||
@user_bp.route('/api/role/<int:role_id>/user-count')
|
||||
@login_required
|
||||
@admin_required
|
||||
def get_role_user_count(role_id):
|
||||
count = User.query.filter_by(role_id=role_id).count()
|
||||
return jsonify({'count': count})
|
||||
"""
|
||||
@user_bp.route('/user/role/<int:role_id>/count', methods=['GET'])
|
||||
@login_required
|
||||
@admin_required
|
||||
def get_role_user_count(role_id):
|
||||
"""获取指定角色的用户数量"""
|
||||
try:
|
||||
count = User.query.filter_by(role_id=role_id).count()
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'count': count
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': f"查询失败: {str(e)}",
|
||||
'count': 0
|
||||
}), 500
|
||||
@ -1,13 +1,13 @@
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from werkzeug.security import generate_password_hash, check_password_hash
|
||||
from datetime import datetime
|
||||
from flask_login import UserMixin
|
||||
|
||||
db = SQLAlchemy()
|
||||
|
||||
|
||||
class User(db.Model):
|
||||
class User(db.Model, UserMixin):
|
||||
__tablename__ = 'users'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
||||
username = db.Column(db.String(64), unique=True, nullable=False)
|
||||
password = db.Column(db.String(255), nullable=False)
|
||||
@ -27,6 +27,9 @@ class User(db.Model):
|
||||
self.nickname = nickname
|
||||
self.role_id = role_id
|
||||
|
||||
def is_active(self):
|
||||
return self.status == 1
|
||||
|
||||
def set_password(self, password):
|
||||
"""设置密码,使用哈希加密"""
|
||||
self.password = generate_password_hash(password)
|
||||
|
||||
@ -0,0 +1,163 @@
|
||||
# app/services/user_service.py
|
||||
|
||||
from app.models.user import User, Role, db
|
||||
from sqlalchemy import or_
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class UserService:
|
||||
@staticmethod
|
||||
def get_users(page=1, per_page=10, search_query=None, status=None, role_id=None):
|
||||
"""
|
||||
获取用户列表,支持分页、搜索和过滤
|
||||
"""
|
||||
query = User.query
|
||||
|
||||
# 搜索条件
|
||||
if search_query:
|
||||
query = query.filter(or_(
|
||||
User.username.like(f'%{search_query}%'),
|
||||
User.email.like(f'%{search_query}%'),
|
||||
User.nickname.like(f'%{search_query}%'),
|
||||
User.phone.like(f'%{search_query}%')
|
||||
))
|
||||
|
||||
# 状态过滤
|
||||
if status is not None:
|
||||
query = query.filter(User.status == status)
|
||||
|
||||
# 角色过滤
|
||||
if role_id is not None:
|
||||
query = query.filter(User.role_id == role_id)
|
||||
|
||||
# 分页
|
||||
pagination = query.order_by(User.id.desc()).paginate(
|
||||
page=page, per_page=per_page, error_out=False
|
||||
)
|
||||
|
||||
return pagination
|
||||
|
||||
@staticmethod
|
||||
def get_user_by_id(user_id):
|
||||
"""通过ID获取用户"""
|
||||
return User.query.get(user_id)
|
||||
|
||||
@staticmethod
|
||||
def update_user(user_id, data):
|
||||
"""更新用户信息"""
|
||||
user = User.query.get(user_id)
|
||||
if not user:
|
||||
return False, "用户不存在"
|
||||
|
||||
try:
|
||||
# 更新可编辑字段
|
||||
if 'email' in data:
|
||||
# 检查邮箱是否已被其他用户使用
|
||||
existing = User.query.filter(User.email == data['email'], User.id != user_id).first()
|
||||
if existing:
|
||||
return False, "邮箱已被使用"
|
||||
user.email = data['email']
|
||||
|
||||
if 'phone' in data:
|
||||
# 检查手机号是否已被其他用户使用
|
||||
existing = User.query.filter(User.phone == data['phone'], User.id != user_id).first()
|
||||
if existing:
|
||||
return False, "手机号已被使用"
|
||||
user.phone = data['phone']
|
||||
|
||||
if 'nickname' in data and data['nickname']:
|
||||
user.nickname = data['nickname']
|
||||
|
||||
# 只有管理员可以修改这些字段
|
||||
if 'role_id' in data:
|
||||
user.role_id = data['role_id']
|
||||
|
||||
if 'status' in data:
|
||||
user.status = data['status']
|
||||
|
||||
if 'password' in data and data['password']:
|
||||
user.set_password(data['password'])
|
||||
|
||||
user.updated_at = datetime.now()
|
||||
db.session.commit()
|
||||
return True, "用户信息更新成功"
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
return False, f"更新失败: {str(e)}"
|
||||
|
||||
@staticmethod
|
||||
def change_user_status(user_id, status):
|
||||
"""变更用户状态 (启用/禁用)"""
|
||||
user = User.query.get(user_id)
|
||||
if not user:
|
||||
return False, "用户不存在"
|
||||
|
||||
try:
|
||||
user.status = status
|
||||
user.updated_at = datetime.now()
|
||||
db.session.commit()
|
||||
status_text = "启用" if status == 1 else "禁用"
|
||||
return True, f"用户已{status_text}"
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
return False, f"状态变更失败: {str(e)}"
|
||||
|
||||
@staticmethod
|
||||
def delete_user(user_id):
|
||||
"""删除用户 (软删除,将状态设为-1)"""
|
||||
user = User.query.get(user_id)
|
||||
if not user:
|
||||
return False, "用户不存在"
|
||||
|
||||
try:
|
||||
user.status = -1 # 软删除,设置状态为-1
|
||||
user.updated_at = datetime.now()
|
||||
db.session.commit()
|
||||
return True, "用户已删除"
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
return False, f"删除失败: {str(e)}"
|
||||
|
||||
@staticmethod
|
||||
def get_all_roles():
|
||||
"""获取所有角色"""
|
||||
return Role.query.all()
|
||||
|
||||
@staticmethod
|
||||
def create_role(role_name, description=None):
|
||||
"""创建新角色"""
|
||||
existing = Role.query.filter_by(role_name=role_name).first()
|
||||
if existing:
|
||||
return False, "角色名已存在"
|
||||
|
||||
try:
|
||||
role = Role(role_name=role_name, description=description)
|
||||
db.session.add(role)
|
||||
db.session.commit()
|
||||
return True, "角色创建成功"
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
return False, f"创建失败: {str(e)}"
|
||||
|
||||
@staticmethod
|
||||
def update_role(role_id, role_name, description=None):
|
||||
"""更新角色信息"""
|
||||
role = Role.query.get(role_id)
|
||||
if not role:
|
||||
return False, "角色不存在"
|
||||
|
||||
# 检查角色名是否已被使用
|
||||
existing = Role.query.filter(Role.role_name == role_name, Role.id != role_id).first()
|
||||
if existing:
|
||||
return False, "角色名已存在"
|
||||
|
||||
try:
|
||||
role.role_name = role_name
|
||||
if description is not None:
|
||||
role.description = description
|
||||
db.session.commit()
|
||||
return True, "角色更新成功"
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
return False, f"更新失败: {str(e)}"
|
||||
240
app/static/css/user-edit.css
Normal file
240
app/static/css/user-edit.css
Normal file
@ -0,0 +1,240 @@
|
||||
/* 用户编辑页面样式 */
|
||||
.user-edit-container {
|
||||
padding: 20px;
|
||||
background-color: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
/* 页面标题和操作按钮 */
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 25px;
|
||||
padding-bottom: 15px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.page-header h1 {
|
||||
font-size: 1.8rem;
|
||||
color: #333;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.page-header .actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
/* 卡片样式 */
|
||||
.card {
|
||||
border: none;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 25px;
|
||||
}
|
||||
|
||||
/* 表单样式 */
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
font-weight: 500;
|
||||
margin-bottom: 8px;
|
||||
color: #333;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
height: auto;
|
||||
padding: 10px 15px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 0.95rem;
|
||||
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
border-color: #4c84ff;
|
||||
box-shadow: 0 0 0 0.2rem rgba(76, 132, 255, 0.25);
|
||||
}
|
||||
|
||||
.form-control[readonly] {
|
||||
background-color: #f8f9fa;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.form-text {
|
||||
font-size: 0.85rem;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
margin-right: -15px;
|
||||
margin-left: -15px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.col-md-6 {
|
||||
flex: 0 0 50%;
|
||||
max-width: 50%;
|
||||
padding-right: 15px;
|
||||
padding-left: 15px;
|
||||
}
|
||||
|
||||
.col-md-12 {
|
||||
flex: 0 0 100%;
|
||||
max-width: 100%;
|
||||
padding-right: 15px;
|
||||
padding-left: 15px;
|
||||
}
|
||||
|
||||
/* 用户信息框 */
|
||||
.user-info-box {
|
||||
margin-top: 20px;
|
||||
margin-bottom: 20px;
|
||||
padding: 20px;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
flex: 0 0 auto;
|
||||
margin-right: 30px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
font-weight: 500;
|
||||
color: #666;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* 表单操作区域 */
|
||||
.form-actions {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
gap: 10px;
|
||||
margin-top: 30px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: #4c84ff;
|
||||
border-color: #4c84ff;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: #3a70e9;
|
||||
border-color: #3a70e9;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background-color: #f8f9fa;
|
||||
border-color: #ddd;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background-color: #e9ecef;
|
||||
border-color: #ccc;
|
||||
}
|
||||
|
||||
.btn-outline-secondary {
|
||||
color: #6c757d;
|
||||
border-color: #6c757d;
|
||||
}
|
||||
|
||||
.btn-outline-secondary:hover {
|
||||
color: #fff;
|
||||
background-color: #6c757d;
|
||||
border-color: #6c757d;
|
||||
}
|
||||
|
||||
/* 表单分隔线 */
|
||||
.form-divider {
|
||||
height: 1px;
|
||||
background-color: #f0f0f0;
|
||||
margin: 30px 0;
|
||||
}
|
||||
|
||||
/* 警告和错误状态 */
|
||||
.is-invalid {
|
||||
border-color: #dc3545 !important;
|
||||
}
|
||||
|
||||
.invalid-feedback {
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin-top: 5px;
|
||||
font-size: 0.85rem;
|
||||
color: #dc3545;
|
||||
}
|
||||
|
||||
/* 成功消息样式 */
|
||||
.alert-success {
|
||||
color: #155724;
|
||||
background-color: #d4edda;
|
||||
border-color: #c3e6cb;
|
||||
padding: 15px;
|
||||
margin-bottom: 20px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* 错误消息样式 */
|
||||
.alert-error, .alert-danger {
|
||||
color: #721c24;
|
||||
background-color: #f8d7da;
|
||||
border-color: #f5c6cb;
|
||||
padding: 15px;
|
||||
margin-bottom: 20px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* 响应式调整 */
|
||||
@media (max-width: 768px) {
|
||||
.form-row {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.col-md-6, .col-md-12 {
|
||||
flex: 0 0 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.page-header .actions {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.user-info-box {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
244
app/static/css/user-list.css
Normal file
244
app/static/css/user-list.css
Normal file
@ -0,0 +1,244 @@
|
||||
/* 用户列表页面样式 */
|
||||
.user-list-container {
|
||||
padding: 20px;
|
||||
background-color: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
/* 页面标题和操作按钮 */
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 25px;
|
||||
padding-bottom: 15px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.page-header h1 {
|
||||
font-size: 1.8rem;
|
||||
color: #333;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.page-header .actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
/* 搜索和筛选区域 */
|
||||
.search-filter-container {
|
||||
margin-bottom: 20px;
|
||||
padding: 20px;
|
||||
background-color: #f9f9f9;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.search-filter-form .form-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
min-width: 250px;
|
||||
}
|
||||
|
||||
.search-box input {
|
||||
padding-right: 40px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.btn-search {
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
top: 5px;
|
||||
background: none;
|
||||
border: none;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.filter-box {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.filter-box select {
|
||||
min-width: 120px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #ddd;
|
||||
padding: 5px 10px;
|
||||
}
|
||||
|
||||
.btn-filter, .btn-reset {
|
||||
padding: 6px 15px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.btn-filter {
|
||||
background-color: #4c84ff;
|
||||
color: white;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.btn-reset {
|
||||
background-color: #f8f9fa;
|
||||
color: #333;
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
|
||||
/* 表格样式 */
|
||||
.table {
|
||||
width: 100%;
|
||||
margin-bottom: 0;
|
||||
color: #333;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.table th {
|
||||
background-color: #f8f9fa;
|
||||
padding: 12px 15px;
|
||||
font-weight: 600;
|
||||
text-align: left;
|
||||
border-top: 1px solid #dee2e6;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
.table td {
|
||||
padding: 12px 15px;
|
||||
vertical-align: middle;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.table tr:hover {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
/* 状态标签 */
|
||||
.status-badge {
|
||||
display: inline-block;
|
||||
padding: 5px 10px;
|
||||
border-radius: 20px;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status-badge.active {
|
||||
background-color: #e8f5e9;
|
||||
color: #43a047;
|
||||
}
|
||||
|
||||
.status-badge.inactive {
|
||||
background-color: #ffebee;
|
||||
color: #e53935;
|
||||
}
|
||||
|
||||
/* 操作按钮 */
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.actions .btn {
|
||||
padding: 5px 8px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* 分页控件 */
|
||||
.pagination-container {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
display: flex;
|
||||
padding-left: 0;
|
||||
list-style: none;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
.page-item {
|
||||
margin: 0 2px;
|
||||
}
|
||||
|
||||
.page-link {
|
||||
position: relative;
|
||||
display: block;
|
||||
padding: 0.5rem 0.75rem;
|
||||
margin-left: -1px;
|
||||
color: #4c84ff;
|
||||
background-color: #fff;
|
||||
border: 1px solid #dee2e6;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.page-item.active .page-link {
|
||||
z-index: 3;
|
||||
color: #fff;
|
||||
background-color: #4c84ff;
|
||||
border-color: #4c84ff;
|
||||
}
|
||||
|
||||
.page-item.disabled .page-link {
|
||||
color: #aaa;
|
||||
pointer-events: none;
|
||||
background-color: #f8f9fa;
|
||||
border-color: #dee2e6;
|
||||
}
|
||||
|
||||
/* 通知样式 */
|
||||
.alert-box {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
z-index: 1050;
|
||||
}
|
||||
|
||||
.alert-box .alert {
|
||||
margin-bottom: 10px;
|
||||
padding: 10px 15px;
|
||||
border-radius: 4px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.alert-box .fade-in {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.alert-box .fade-out {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* 响应式调整 */
|
||||
@media (max-width: 992px) {
|
||||
.search-filter-form .form-row {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.search-box, .filter-box {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.table {
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
375
app/static/css/user-profile.css
Normal file
375
app/static/css/user-profile.css
Normal file
@ -0,0 +1,375 @@
|
||||
/* 用户个人中心页面样式 */
|
||||
.profile-container {
|
||||
padding: 20px;
|
||||
background-color: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
/* 页面标题 */
|
||||
.page-header {
|
||||
margin-bottom: 25px;
|
||||
padding-bottom: 15px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.page-header h1 {
|
||||
font-size: 1.8rem;
|
||||
color: #333;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* 个人中心内容布局 */
|
||||
.profile-content {
|
||||
display: flex;
|
||||
gap: 30px;
|
||||
}
|
||||
|
||||
/* 左侧边栏 */
|
||||
.profile-sidebar {
|
||||
flex: 0 0 300px;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
padding: 25px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
/* 右侧主要内容 */
|
||||
.profile-main {
|
||||
flex: 1;
|
||||
min-width: 0; /* 防止内容溢出 */
|
||||
}
|
||||
|
||||
/* 用户头像容器 */
|
||||
.user-avatar-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-bottom: 25px;
|
||||
padding-bottom: 25px;
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
/* 大头像样式 */
|
||||
.user-avatar.large {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
border-radius: 50%;
|
||||
background-color: #4c84ff;
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 3rem;
|
||||
margin-bottom: 15px;
|
||||
box-shadow: 0 4px 8px rgba(76, 132, 255, 0.2);
|
||||
}
|
||||
|
||||
.user-name {
|
||||
font-size: 1.5rem;
|
||||
margin: 10px 0 5px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.user-role {
|
||||
font-size: 0.9rem;
|
||||
color: #6c757d;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* 用户统计信息 */
|
||||
.user-stats {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 25px;
|
||||
padding-bottom: 25px;
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
text-align: center;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 1.8rem;
|
||||
font-weight: 600;
|
||||
color: #4c84ff;
|
||||
line-height: 1;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 0.85rem;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
/* 账户信息样式 */
|
||||
.account-info {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.account-info .info-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 15px;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.account-info .info-label {
|
||||
color: #6c757d;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.account-info .info-value {
|
||||
color: #333;
|
||||
text-align: right;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
/* 选项卡导航样式 */
|
||||
.nav-tabs {
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.nav-tabs .nav-link {
|
||||
border: none;
|
||||
color: #6c757d;
|
||||
padding: 12px 15px;
|
||||
margin-right: 5px;
|
||||
border-bottom: 2px solid transparent;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.nav-tabs .nav-link:hover {
|
||||
color: #4c84ff;
|
||||
border-bottom-color: #4c84ff;
|
||||
}
|
||||
|
||||
.nav-tabs .nav-link.active {
|
||||
font-weight: 500;
|
||||
color: #4c84ff;
|
||||
border-bottom: 2px solid #4c84ff;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.nav-tabs .nav-link i {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
/* 表单区域 */
|
||||
.form-section {
|
||||
padding: 20px;
|
||||
background-color: #f9f9fb;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-section h4 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 20px;
|
||||
color: #333;
|
||||
font-size: 1.2rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
font-weight: 500;
|
||||
margin-bottom: 8px;
|
||||
color: #333;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
height: auto;
|
||||
padding: 10px 15px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 0.95rem;
|
||||
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
border-color: #4c84ff;
|
||||
box-shadow: 0 0 0 0.2rem rgba(76, 132, 255, 0.25);
|
||||
}
|
||||
|
||||
.form-text {
|
||||
font-size: 0.85rem;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
/* 表单操作区域 */
|
||||
.form-actions {
|
||||
margin-top: 25px;
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 10px 20px;
|
||||
border-radius: 4px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: #4c84ff;
|
||||
border-color: #4c84ff;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: #3a70e9;
|
||||
border-color: #3a70e9;
|
||||
}
|
||||
|
||||
/* 活动记录选项卡 */
|
||||
.activity-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.activity-filter {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.activity-filter label {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.activity-filter select {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
/* 活动时间线 */
|
||||
.activity-timeline {
|
||||
padding: 20px;
|
||||
background-color: #f9f9fb;
|
||||
border-radius: 8px;
|
||||
min-height: 300px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.timeline-loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 250px;
|
||||
}
|
||||
|
||||
.timeline-loading p {
|
||||
margin-top: 15px;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.timeline-item {
|
||||
position: relative;
|
||||
padding-left: 30px;
|
||||
padding-bottom: 25px;
|
||||
border-left: 2px solid #dee2e6;
|
||||
}
|
||||
|
||||
.timeline-item:last-child {
|
||||
border-left: none;
|
||||
}
|
||||
|
||||
.timeline-icon {
|
||||
position: absolute;
|
||||
left: -10px;
|
||||
top: 0;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
background-color: #4c84ff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.timeline-content {
|
||||
background-color: white;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.timeline-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.timeline-title {
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.timeline-time {
|
||||
font-size: 0.85rem;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.timeline-details {
|
||||
color: #555;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.timeline-type-login .timeline-icon {
|
||||
background-color: #4caf50;
|
||||
}
|
||||
|
||||
.timeline-type-borrow .timeline-icon {
|
||||
background-color: #2196f3;
|
||||
}
|
||||
|
||||
.timeline-type-return .timeline-icon {
|
||||
background-color: #ff9800;
|
||||
}
|
||||
|
||||
/* 通知样式 */
|
||||
.alert {
|
||||
padding: 15px;
|
||||
margin-bottom: 20px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.alert-success {
|
||||
color: #155724;
|
||||
background-color: #d4edda;
|
||||
border: 1px solid #c3e6cb;
|
||||
}
|
||||
|
||||
.alert-error, .alert-danger {
|
||||
color: #721c24;
|
||||
background-color: #f8d7da;
|
||||
border: 1px solid #f5c6cb;
|
||||
}
|
||||
|
||||
/* 响应式调整 */
|
||||
@media (max-width: 992px) {
|
||||
.profile-content {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.profile-sidebar {
|
||||
flex: none;
|
||||
width: 100%;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.user-stats {
|
||||
justify-content: space-around;
|
||||
}
|
||||
}
|
||||
253
app/static/css/user-roles.css
Normal file
253
app/static/css/user-roles.css
Normal file
@ -0,0 +1,253 @@
|
||||
/* 角色管理页面样式 */
|
||||
.roles-container {
|
||||
padding: 20px;
|
||||
background-color: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
/* 页面标题 */
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 25px;
|
||||
padding-bottom: 15px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.page-header h1 {
|
||||
font-size: 1.8rem;
|
||||
color: #333;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.page-header .actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
/* 角色列表 */
|
||||
.role-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 20px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
/* 角色卡片 */
|
||||
.role-card {
|
||||
background-color: #f9f9fb;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.role-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.role-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.role-name {
|
||||
font-size: 1.3rem;
|
||||
color: #333;
|
||||
margin: 0;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.role-actions {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.role-actions .btn {
|
||||
padding: 5px 8px;
|
||||
color: #6c757d;
|
||||
background: none;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.role-actions .btn:hover {
|
||||
color: #4c84ff;
|
||||
}
|
||||
|
||||
.role-actions .btn-delete-role:hover {
|
||||
color: #dc3545;
|
||||
}
|
||||
|
||||
.role-description {
|
||||
color: #555;
|
||||
margin-bottom: 20px;
|
||||
min-height: 50px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.role-stats {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 0.9rem;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.stat-item i {
|
||||
color: #4c84ff;
|
||||
}
|
||||
|
||||
/* 角色标签 */
|
||||
.role-badge {
|
||||
display: inline-block;
|
||||
padding: 5px 12px;
|
||||
border-radius: 20px;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.role-badge.admin {
|
||||
background-color: #e3f2fd;
|
||||
color: #1976d2;
|
||||
}
|
||||
|
||||
.role-badge.user {
|
||||
background-color: #e8f5e9;
|
||||
color: #43a047;
|
||||
}
|
||||
|
||||
.role-badge.custom {
|
||||
background-color: #fff3e0;
|
||||
color: #ef6c00;
|
||||
}
|
||||
|
||||
/* 无数据提示 */
|
||||
.no-data-message {
|
||||
grid-column: 1 / -1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 50px 0;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.no-data-message i {
|
||||
font-size: 3rem;
|
||||
margin-bottom: 15px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* 权限信息部分 */
|
||||
.permissions-info {
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.permissions-info h3 {
|
||||
font-size: 1.4rem;
|
||||
margin-bottom: 15px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.permission-table {
|
||||
width: 100%;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.permission-table th {
|
||||
background-color: #f8f9fa;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.permission-table td, .permission-table th {
|
||||
padding: 12px 15px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.permission-table td:first-child {
|
||||
text-align: left;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.text-success {
|
||||
color: #28a745;
|
||||
}
|
||||
|
||||
.text-danger {
|
||||
color: #dc3545;
|
||||
}
|
||||
|
||||
/* 模态框样式 */
|
||||
.modal-header {
|
||||
background-color: #f8f9fa;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
border-top: 1px solid #f0f0f0;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
/* 表单样式 */
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
font-weight: 500;
|
||||
margin-bottom: 8px;
|
||||
color: #333;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
height: auto;
|
||||
padding: 10px 15px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 0.95rem;
|
||||
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
border-color: #4c84ff;
|
||||
box-shadow: 0 0 0 0.2rem rgba(76, 132, 255, 0.25);
|
||||
}
|
||||
|
||||
/* 响应式调整 */
|
||||
@media (max-width: 992px) {
|
||||
.role-list {
|
||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
.role-list {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.page-header .actions {
|
||||
margin-top: 15px;
|
||||
}
|
||||
}
|
||||
@ -40,7 +40,7 @@ $(document).ready(function() {
|
||||
// 处理删除图书
|
||||
let bookIdToDelete = null;
|
||||
|
||||
$('.delete-btn').click(function(e) {
|
||||
$('.delete-book').click(function(e) {
|
||||
e.preventDefault();
|
||||
bookIdToDelete = $(this).data('id');
|
||||
const bookTitle = $(this).data('title');
|
||||
@ -60,21 +60,28 @@ $(document).ready(function() {
|
||||
// 显示成功消息
|
||||
showNotification(response.message, 'success');
|
||||
// 移除图书卡片
|
||||
$(`.book-card[data-id="${bookIdToDelete}"]`).fadeOut(300, function() {
|
||||
$(this).remove();
|
||||
});
|
||||
setTimeout(() => {
|
||||
location.reload();
|
||||
}, 800);
|
||||
if ($('.book-card').length === 0) {
|
||||
location.reload(); // 如果没有图书了,刷新页面显示"无图书"提示
|
||||
}
|
||||
}, 500);
|
||||
} else {
|
||||
$('#deleteModal').modal('hide');
|
||||
showNotification(response.message, 'error');
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
$('#deleteModal').modal('hide');
|
||||
showNotification('删除操作失败,请稍后重试', 'error');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 处理借阅图书
|
||||
$('.borrow-btn').click(function(e) {
|
||||
$('.borrow-book').click(function(e) {
|
||||
e.preventDefault();
|
||||
const bookId = $(this).data('id');
|
||||
|
||||
|
||||
91
app/static/js/user-edit.js
Normal file
91
app/static/js/user-edit.js
Normal file
@ -0,0 +1,91 @@
|
||||
// 用户编辑页面交互
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const passwordField = document.getElementById('password');
|
||||
const confirmPasswordGroup = document.getElementById('confirmPasswordGroup');
|
||||
const confirmPasswordField = document.getElementById('confirm_password');
|
||||
const userEditForm = document.getElementById('userEditForm');
|
||||
|
||||
// 如果输入密码,显示确认密码字段
|
||||
passwordField.addEventListener('input', function() {
|
||||
if (this.value.trim() !== '') {
|
||||
confirmPasswordGroup.style.display = 'block';
|
||||
} else {
|
||||
confirmPasswordGroup.style.display = 'none';
|
||||
confirmPasswordField.value = '';
|
||||
}
|
||||
});
|
||||
|
||||
// 表单提交验证
|
||||
userEditForm.addEventListener('submit', function(event) {
|
||||
let valid = true;
|
||||
|
||||
// 清除之前的错误提示
|
||||
const invalidFields = document.querySelectorAll('.is-invalid');
|
||||
const feedbackElements = document.querySelectorAll('.invalid-feedback');
|
||||
|
||||
invalidFields.forEach(field => {
|
||||
field.classList.remove('is-invalid');
|
||||
});
|
||||
|
||||
feedbackElements.forEach(element => {
|
||||
element.parentNode.removeChild(element);
|
||||
});
|
||||
|
||||
// 邮箱格式验证
|
||||
const emailField = document.getElementById('email');
|
||||
if (emailField.value.trim() !== '') {
|
||||
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!emailPattern.test(emailField.value.trim())) {
|
||||
showError(emailField, '请输入有效的邮箱地址');
|
||||
valid = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 手机号码格式验证
|
||||
const phoneField = document.getElementById('phone');
|
||||
if (phoneField.value.trim() !== '') {
|
||||
const phonePattern = /^1[3456789]\d{9}$/;
|
||||
if (!phonePattern.test(phoneField.value.trim())) {
|
||||
showError(phoneField, '请输入有效的手机号码');
|
||||
valid = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 密码验证
|
||||
if (passwordField.value.trim() !== '') {
|
||||
if (passwordField.value.length < 6) {
|
||||
showError(passwordField, '密码长度至少为6个字符');
|
||||
valid = false;
|
||||
}
|
||||
|
||||
if (passwordField.value !== confirmPasswordField.value) {
|
||||
showError(confirmPasswordField, '两次输入的密码不一致');
|
||||
valid = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!valid) {
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
|
||||
// 显示表单字段错误
|
||||
function showError(field, message) {
|
||||
field.classList.add('is-invalid');
|
||||
|
||||
const feedback = document.createElement('div');
|
||||
feedback.className = 'invalid-feedback';
|
||||
feedback.innerText = message;
|
||||
|
||||
field.parentNode.appendChild(feedback);
|
||||
}
|
||||
|
||||
// 处理表单提交后的成功反馈
|
||||
const successAlert = document.querySelector('.alert-success');
|
||||
if (successAlert) {
|
||||
// 如果有成功消息,显示成功对话框
|
||||
setTimeout(() => {
|
||||
$('#successModal').modal('show');
|
||||
}, 500);
|
||||
}
|
||||
});
|
||||
124
app/static/js/user-list.js
Normal file
124
app/static/js/user-list.js
Normal file
@ -0,0 +1,124 @@
|
||||
// 用户列表页面交互
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// 处理状态切换按钮
|
||||
const toggleStatusButtons = document.querySelectorAll('.toggle-status');
|
||||
toggleStatusButtons.forEach(button => {
|
||||
button.addEventListener('click', function() {
|
||||
const userId = this.getAttribute('data-id');
|
||||
const newStatus = parseInt(this.getAttribute('data-status'));
|
||||
const statusText = newStatus === 1 ? '启用' : '禁用';
|
||||
|
||||
if (confirm(`确定要${statusText}该用户吗?`)) {
|
||||
toggleUserStatus(userId, newStatus);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 处理删除按钮
|
||||
const deleteButtons = document.querySelectorAll('.delete-user');
|
||||
const deleteModal = $('#deleteModal');
|
||||
let userIdToDelete = null;
|
||||
|
||||
deleteButtons.forEach(button => {
|
||||
button.addEventListener('click', function() {
|
||||
userIdToDelete = this.getAttribute('data-id');
|
||||
deleteModal.modal('show');
|
||||
});
|
||||
});
|
||||
|
||||
// 确认删除按钮
|
||||
document.getElementById('confirmDelete').addEventListener('click', function() {
|
||||
if (userIdToDelete) {
|
||||
deleteUser(userIdToDelete);
|
||||
deleteModal.modal('hide');
|
||||
}
|
||||
});
|
||||
|
||||
// 自动提交表单的下拉菜单
|
||||
const autoSubmitSelects = document.querySelectorAll('select[name="status"], select[name="role_id"]');
|
||||
autoSubmitSelects.forEach(select => {
|
||||
select.addEventListener('change', function() {
|
||||
this.closest('form').submit();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// 切换用户状态
|
||||
function toggleUserStatus(userId, status) {
|
||||
fetch(`/user/status/${userId}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
},
|
||||
body: JSON.stringify({ status: status })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showAlert(data.message, 'success');
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 1500);
|
||||
} else {
|
||||
showAlert(data.message, 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
showAlert('操作失败,请稍后重试', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
// 删除用户
|
||||
function deleteUser(userId) {
|
||||
fetch(`/user/delete/${userId}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showAlert(data.message, 'success');
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 1500);
|
||||
} else {
|
||||
showAlert(data.message, 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
showAlert('操作失败,请稍后重试', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
// 显示通知
|
||||
function showAlert(message, type) {
|
||||
// 检查是否已有通知元素
|
||||
let alertBox = document.querySelector('.alert-box');
|
||||
if (!alertBox) {
|
||||
alertBox = document.createElement('div');
|
||||
alertBox.className = 'alert-box';
|
||||
document.body.appendChild(alertBox);
|
||||
}
|
||||
|
||||
// 创建新的通知
|
||||
const alert = document.createElement('div');
|
||||
alert.className = `alert alert-${type === 'success' ? 'success' : 'danger'} fade-in`;
|
||||
alert.innerHTML = message;
|
||||
|
||||
// 添加到通知框中
|
||||
alertBox.appendChild(alert);
|
||||
|
||||
// 自动关闭
|
||||
setTimeout(() => {
|
||||
alert.classList.add('fade-out');
|
||||
setTimeout(() => {
|
||||
alertBox.removeChild(alert);
|
||||
}, 500);
|
||||
}, 3000);
|
||||
}
|
||||
275
app/static/js/user-profile.js
Normal file
275
app/static/js/user-profile.js
Normal file
@ -0,0 +1,275 @@
|
||||
// 用户个人中心页面交互
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// 获取表单对象
|
||||
const profileForm = document.getElementById('profileForm');
|
||||
const passwordForm = document.getElementById('passwordForm');
|
||||
|
||||
// 表单验证逻辑
|
||||
if (profileForm) {
|
||||
profileForm.addEventListener('submit', function(event) {
|
||||
let valid = true;
|
||||
|
||||
// 清除之前的错误提示
|
||||
clearValidationErrors();
|
||||
|
||||
// 验证邮箱
|
||||
const emailField = document.getElementById('email');
|
||||
if (emailField.value.trim() !== '') {
|
||||
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!emailPattern.test(emailField.value.trim())) {
|
||||
showError(emailField, '请输入有效的邮箱地址');
|
||||
valid = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 验证手机号
|
||||
const phoneField = document.getElementById('phone');
|
||||
if (phoneField.value.trim() !== '') {
|
||||
const phonePattern = /^1[3456789]\d{9}$/;
|
||||
if (!phonePattern.test(phoneField.value.trim())) {
|
||||
showError(phoneField, '请输入有效的手机号码');
|
||||
valid = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!valid) {
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 密码修改表单验证
|
||||
if (passwordForm) {
|
||||
passwordForm.addEventListener('submit', function(event) {
|
||||
let valid = true;
|
||||
|
||||
// 清除之前的错误提示
|
||||
clearValidationErrors();
|
||||
|
||||
// 验证当前密码
|
||||
const currentPasswordField = document.getElementById('current_password');
|
||||
if (currentPasswordField.value.trim() === '') {
|
||||
showError(currentPasswordField, '请输入当前密码');
|
||||
valid = false;
|
||||
}
|
||||
|
||||
// 验证新密码
|
||||
const newPasswordField = document.getElementById('new_password');
|
||||
if (newPasswordField.value.trim() === '') {
|
||||
showError(newPasswordField, '请输入新密码');
|
||||
valid = false;
|
||||
} else if (newPasswordField.value.length < 6) {
|
||||
showError(newPasswordField, '密码长度至少为6个字符');
|
||||
valid = false;
|
||||
}
|
||||
|
||||
// 验证确认密码
|
||||
const confirmPasswordField = document.getElementById('confirm_password');
|
||||
if (confirmPasswordField.value.trim() === '') {
|
||||
showError(confirmPasswordField, '请确认新密码');
|
||||
valid = false;
|
||||
} else if (confirmPasswordField.value !== newPasswordField.value) {
|
||||
showError(confirmPasswordField, '两次输入的密码不一致');
|
||||
valid = false;
|
||||
}
|
||||
|
||||
if (!valid) {
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 获取用户统计数据
|
||||
fetchUserStats();
|
||||
|
||||
// 获取用户活动记录
|
||||
const activityFilter = document.getElementById('activityFilter');
|
||||
if (activityFilter) {
|
||||
// 初始加载
|
||||
fetchUserActivities('all');
|
||||
|
||||
// 监听过滤器变化
|
||||
activityFilter.addEventListener('change', function() {
|
||||
fetchUserActivities(this.value);
|
||||
});
|
||||
}
|
||||
|
||||
// 处理URL中的tab参数
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const tabParam = urlParams.get('tab');
|
||||
if (tabParam) {
|
||||
const tabElement = document.getElementById(`${tabParam}-tab`);
|
||||
if (tabElement) {
|
||||
$('#profileTabs a[href="#' + tabParam + '"]').tab('show');
|
||||
}
|
||||
}
|
||||
|
||||
// 清除表单验证错误
|
||||
function clearValidationErrors() {
|
||||
const invalidFields = document.querySelectorAll('.is-invalid');
|
||||
const feedbackElements = document.querySelectorAll('.invalid-feedback');
|
||||
|
||||
invalidFields.forEach(field => {
|
||||
field.classList.remove('is-invalid');
|
||||
});
|
||||
|
||||
feedbackElements.forEach(element => {
|
||||
element.parentNode.removeChild(element);
|
||||
});
|
||||
}
|
||||
|
||||
// 显示错误消息
|
||||
function showError(field, message) {
|
||||
field.classList.add('is-invalid');
|
||||
|
||||
const feedback = document.createElement('div');
|
||||
feedback.className = 'invalid-feedback';
|
||||
feedback.innerText = message;
|
||||
|
||||
field.parentNode.appendChild(feedback);
|
||||
}
|
||||
|
||||
// 获取用户统计数据
|
||||
function fetchUserStats() {
|
||||
// 这里使用虚拟数据,实际应用中应当从后端获取
|
||||
// fetch('/api/user/stats')
|
||||
// .then(response => response.json())
|
||||
// .then(data => {
|
||||
// updateUserStats(data);
|
||||
// });
|
||||
|
||||
// 模拟数据
|
||||
setTimeout(() => {
|
||||
const mockData = {
|
||||
borrow: 2,
|
||||
returned: 15,
|
||||
overdue: 0
|
||||
};
|
||||
updateUserStats(mockData);
|
||||
}, 500);
|
||||
}
|
||||
|
||||
// 更新用户统计显示
|
||||
function updateUserStats(data) {
|
||||
const borrowCount = document.getElementById('borrowCount');
|
||||
const returnedCount = document.getElementById('returnedCount');
|
||||
const overdueCount = document.getElementById('overdueCount');
|
||||
|
||||
if (borrowCount) borrowCount.textContent = data.borrow;
|
||||
if (returnedCount) returnedCount.textContent = data.returned;
|
||||
if (overdueCount) overdueCount.textContent = data.overdue;
|
||||
}
|
||||
|
||||
// 获取用户活动记录
|
||||
function fetchUserActivities(type) {
|
||||
const timelineContainer = document.getElementById('activityTimeline');
|
||||
if (!timelineContainer) return;
|
||||
|
||||
// 显示加载中
|
||||
timelineContainer.innerHTML = `
|
||||
<div class="timeline-loading">
|
||||
<div class="spinner-border text-primary" role="status">
|
||||
<span class="sr-only">Loading...</span>
|
||||
</div>
|
||||
<p>加载中...</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// 实际应用中应当从后端获取
|
||||
// fetch(`/api/user/activities?type=${type}`)
|
||||
// .then(response => response.json())
|
||||
// .then(data => {
|
||||
// renderActivityTimeline(data, timelineContainer);
|
||||
// });
|
||||
|
||||
// 模拟数据
|
||||
setTimeout(() => {
|
||||
const mockActivities = [
|
||||
{
|
||||
id: 1,
|
||||
type: 'login',
|
||||
title: '系统登录',
|
||||
details: '成功登录系统',
|
||||
time: '2023-04-28 15:30:22',
|
||||
ip: '192.168.1.1'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
type: 'borrow',
|
||||
title: '借阅图书',
|
||||
details: '借阅《JavaScript高级编程》',
|
||||
time: '2023-04-27 11:45:10',
|
||||
book_id: 101
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
type: 'return',
|
||||
title: '归还图书',
|
||||
details: '归还《Python数据分析》',
|
||||
time: '2023-04-26 09:15:33',
|
||||
book_id: 95
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
type: 'login',
|
||||
title: '系统登录',
|
||||
details: '成功登录系统',
|
||||
time: '2023-04-25 08:22:15',
|
||||
ip: '192.168.1.1'
|
||||
}
|
||||
];
|
||||
|
||||
// 根据筛选条件过滤活动
|
||||
let filteredActivities = mockActivities;
|
||||
if (type !== 'all') {
|
||||
filteredActivities = mockActivities.filter(activity => activity.type === type);
|
||||
}
|
||||
|
||||
renderActivityTimeline(filteredActivities, timelineContainer);
|
||||
}, 800);
|
||||
}
|
||||
|
||||
// 渲染活动时间线
|
||||
function renderActivityTimeline(activities, container) {
|
||||
if (!activities || activities.length === 0) {
|
||||
container.innerHTML = '<div class="text-center p-4">暂无活动记录</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
let timelineHTML = '';
|
||||
|
||||
activities.forEach((activity, index) => {
|
||||
let iconClass = 'fas fa-info';
|
||||
|
||||
if (activity.type === 'login') {
|
||||
iconClass = 'fas fa-sign-in-alt';
|
||||
} else if (activity.type === 'borrow') {
|
||||
iconClass = 'fas fa-book';
|
||||
} else if (activity.type === 'return') {
|
||||
iconClass = 'fas fa-undo';
|
||||
}
|
||||
|
||||
const isLast = index === activities.length - 1;
|
||||
|
||||
timelineHTML += `
|
||||
<div class="timeline-item ${isLast ? 'last' : ''} timeline-type-${activity.type}">
|
||||
<div class="timeline-icon">
|
||||
<i class="${iconClass}"></i>
|
||||
</div>
|
||||
<div class="timeline-content">
|
||||
<div class="timeline-header">
|
||||
<h5 class="timeline-title">${activity.title}</h5>
|
||||
<div class="timeline-time">${activity.time}</div>
|
||||
</div>
|
||||
<div class="timeline-details">
|
||||
${activity.details}
|
||||
${activity.ip ? `<div class="text-muted small">IP: ${activity.ip}</div>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
container.innerHTML = timelineHTML;
|
||||
}
|
||||
});
|
||||
301
app/static/js/user-roles.js
Normal file
301
app/static/js/user-roles.js
Normal file
@ -0,0 +1,301 @@
|
||||
// 角色管理页面交互
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// 获取DOM元素
|
||||
const addRoleBtn = document.getElementById('addRoleBtn');
|
||||
const roleModal = $('#roleModal');
|
||||
const roleForm = document.getElementById('roleForm');
|
||||
const roleIdInput = document.getElementById('roleId');
|
||||
const roleNameInput = document.getElementById('roleName');
|
||||
const roleDescriptionInput = document.getElementById('roleDescription');
|
||||
const saveRoleBtn = document.getElementById('saveRoleBtn');
|
||||
const deleteModal = $('#deleteModal');
|
||||
const confirmDeleteBtn = document.getElementById('confirmDeleteBtn');
|
||||
|
||||
let roleIdToDelete = null;
|
||||
|
||||
// 加载角色用户统计
|
||||
fetchRoleUserCounts();
|
||||
|
||||
// 添加角色按钮点击事件
|
||||
if (addRoleBtn) {
|
||||
addRoleBtn.addEventListener('click', function() {
|
||||
// 重置表单
|
||||
roleIdInput.value = '';
|
||||
roleNameInput.value = '';
|
||||
roleDescriptionInput.value = '';
|
||||
|
||||
// 更新模态框标题
|
||||
document.getElementById('roleModalLabel').textContent = '添加角色';
|
||||
|
||||
// 显示模态框
|
||||
roleModal.modal('show');
|
||||
});
|
||||
}
|
||||
|
||||
// 编辑角色按钮点击事件
|
||||
const editButtons = document.querySelectorAll('.btn-edit-role');
|
||||
editButtons.forEach(button => {
|
||||
button.addEventListener('click', function() {
|
||||
const roleCard = this.closest('.role-card');
|
||||
const roleId = roleCard.getAttribute('data-id');
|
||||
const roleName = roleCard.querySelector('.role-name').textContent;
|
||||
let roleDescription = roleCard.querySelector('.role-description').textContent;
|
||||
|
||||
// 移除"暂无描述"文本
|
||||
if (roleDescription.trim() === '暂无描述') {
|
||||
roleDescription = '';
|
||||
}
|
||||
|
||||
// 填充表单
|
||||
roleIdInput.value = roleId;
|
||||
roleNameInput.value = roleName;
|
||||
roleDescriptionInput.value = roleDescription.trim();
|
||||
|
||||
// 更新模态框标题
|
||||
document.getElementById('roleModalLabel').textContent = '编辑角色';
|
||||
|
||||
// 显示模态框
|
||||
roleModal.modal('show');
|
||||
});
|
||||
});
|
||||
|
||||
// 删除角色按钮点击事件
|
||||
const deleteButtons = document.querySelectorAll('.btn-delete-role');
|
||||
deleteButtons.forEach(button => {
|
||||
button.addEventListener('click', function() {
|
||||
const roleCard = this.closest('.role-card');
|
||||
roleIdToDelete = roleCard.getAttribute('data-id');
|
||||
|
||||
// 显示确认删除模态框
|
||||
deleteModal.modal('show');
|
||||
});
|
||||
});
|
||||
|
||||
// 保存角色按钮点击事件
|
||||
if (saveRoleBtn) {
|
||||
saveRoleBtn.addEventListener('click', function() {
|
||||
if (!roleNameInput.value.trim()) {
|
||||
showAlert('角色名称不能为空', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const roleData = {
|
||||
id: roleIdInput.value || null,
|
||||
role_name: roleNameInput.value.trim(),
|
||||
description: roleDescriptionInput.value.trim() || null
|
||||
};
|
||||
|
||||
saveRole(roleData);
|
||||
});
|
||||
}
|
||||
|
||||
// 确认删除按钮点击事件
|
||||
if (confirmDeleteBtn) {
|
||||
confirmDeleteBtn.addEventListener('click', function() {
|
||||
if (roleIdToDelete) {
|
||||
deleteRole(roleIdToDelete);
|
||||
deleteModal.modal('hide');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 保存角色
|
||||
function saveRole(roleData) {
|
||||
// 显示加载状态
|
||||
saveRoleBtn.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> 保存中...';
|
||||
saveRoleBtn.disabled = true;
|
||||
|
||||
fetch('/user/role/save', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
},
|
||||
body: JSON.stringify(roleData)
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error('网络响应异常');
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
// 恢复按钮状态
|
||||
saveRoleBtn.innerHTML = '保存';
|
||||
saveRoleBtn.disabled = false;
|
||||
|
||||
if (data.success) {
|
||||
// 关闭模态框
|
||||
roleModal.modal('hide');
|
||||
showAlert(data.message, 'success');
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 1500);
|
||||
} else {
|
||||
showAlert(data.message, 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
// 恢复按钮状态
|
||||
saveRoleBtn.innerHTML = '保存';
|
||||
saveRoleBtn.disabled = false;
|
||||
showAlert('保存失败,请稍后重试', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
// 删除角色
|
||||
function deleteRole(roleId) {
|
||||
// 显示加载状态
|
||||
confirmDeleteBtn.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> 删除中...';
|
||||
confirmDeleteBtn.disabled = true;
|
||||
|
||||
fetch(`/user/role/delete/${roleId}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error('网络响应异常');
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
// 恢复按钮状态
|
||||
confirmDeleteBtn.innerHTML = '确认删除';
|
||||
confirmDeleteBtn.disabled = false;
|
||||
|
||||
if (data.success) {
|
||||
showAlert(data.message, 'success');
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 1500);
|
||||
} else {
|
||||
showAlert(data.message, 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
// 恢复按钮状态
|
||||
confirmDeleteBtn.innerHTML = '确认删除';
|
||||
confirmDeleteBtn.disabled = false;
|
||||
showAlert('删除失败,请稍后重试', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
// 获取角色用户数量
|
||||
function fetchRoleUserCounts() {
|
||||
const roleCards = document.querySelectorAll('.role-card');
|
||||
|
||||
roleCards.forEach(card => {
|
||||
const roleId = card.getAttribute('data-id');
|
||||
const countElement = document.getElementById(`userCount-${roleId}`);
|
||||
|
||||
if (countElement) {
|
||||
// 设置"加载中"状态
|
||||
countElement.innerHTML = '<small>加载中...</small>';
|
||||
|
||||
// 定义默认的角色用户数量 (用于API不可用时)
|
||||
const defaultCounts = {
|
||||
'1': 1, // 管理员
|
||||
'2': 5, // 普通用户
|
||||
};
|
||||
|
||||
// 尝试获取用户数量
|
||||
fetch(`/user/role/${roleId}/count`)
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error('API不可用');
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
// 检查返回数据的success属性
|
||||
if (data.success) {
|
||||
countElement.textContent = data.count;
|
||||
} else {
|
||||
throw new Error(data.message || 'API返回错误');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.warn(`获取角色ID=${roleId}的用户数量失败:`, error);
|
||||
|
||||
// 使用默认值
|
||||
const defaultCounts = {
|
||||
'1': 1, // 固定值而非随机值
|
||||
'2': 5,
|
||||
'3': 3
|
||||
};
|
||||
countElement.textContent = defaultCounts[roleId] || 0;
|
||||
|
||||
// 静默失败 - 不向用户显示错误,只在控制台记录
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 显示通知
|
||||
function showAlert(message, type) {
|
||||
// 检查是否已有通知元素
|
||||
let alertBox = document.querySelector('.alert-box');
|
||||
if (!alertBox) {
|
||||
alertBox = document.createElement('div');
|
||||
alertBox.className = 'alert-box';
|
||||
document.body.appendChild(alertBox);
|
||||
}
|
||||
|
||||
// 创建新的通知
|
||||
const alert = document.createElement('div');
|
||||
alert.className = `alert alert-${type === 'success' ? 'success' : 'danger'} fade-in`;
|
||||
alert.innerHTML = message;
|
||||
|
||||
// 添加到通知框中
|
||||
alertBox.appendChild(alert);
|
||||
|
||||
// 自动关闭
|
||||
setTimeout(() => {
|
||||
alert.classList.add('fade-out');
|
||||
setTimeout(() => {
|
||||
alertBox.removeChild(alert);
|
||||
}, 500);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// 添加CSS样式以支持通知动画
|
||||
function addAlertStyles() {
|
||||
if (!document.getElementById('alert-styles')) {
|
||||
const style = document.createElement('style');
|
||||
style.id = 'alert-styles';
|
||||
style.textContent = `
|
||||
.alert-box {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
z-index: 9999;
|
||||
max-width: 350px;
|
||||
}
|
||||
.alert {
|
||||
margin-bottom: 10px;
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
.fade-in {
|
||||
opacity: 1;
|
||||
}
|
||||
.fade-out {
|
||||
opacity: 0;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
}
|
||||
|
||||
// 添加通知样式
|
||||
addAlertStyles();
|
||||
});
|
||||
@ -5,45 +5,157 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>页面未找到 - 图书管理系统</title>
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/main.css') }}">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Poppins', sans-serif;
|
||||
background-color: #fff5f7;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.error-container {
|
||||
text-align: center;
|
||||
padding: 50px 20px;
|
||||
max-width: 650px;
|
||||
padding: 40px;
|
||||
border-radius: 20px;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 10px 30px rgba(252, 162, 193, 0.2);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.error-container::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 6px;
|
||||
background: linear-gradient(to right, #ff8ab3, #f17ab3, #f56eb8);
|
||||
}
|
||||
|
||||
.error-code {
|
||||
font-size: 100px;
|
||||
font-size: 120px;
|
||||
font-weight: bold;
|
||||
color: #4a89dc;
|
||||
margin-bottom: 20px;
|
||||
background: linear-gradient(to right, #ff8ab3, #f17ab3, #f56eb8);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
margin: 0 0 20px 0;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.error-title {
|
||||
font-size: 28px;
|
||||
color: #ff8ab3;
|
||||
margin-bottom: 15px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
font-size: 24px;
|
||||
color: #333;
|
||||
font-size: 18px;
|
||||
color: #7a7a7a;
|
||||
margin-bottom: 30px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.back-button {
|
||||
display: inline-block;
|
||||
padding: 10px 20px;
|
||||
background-color: #4a89dc;
|
||||
padding: 12px 30px;
|
||||
background: linear-gradient(to right, #ff8ab3, #f17ab3);
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
border-radius: 5px;
|
||||
border-radius: 50px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 1px;
|
||||
box-shadow: 0 5px 15px rgba(241, 122, 179, 0.4);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.back-button:hover {
|
||||
background-color: #3b78c4;
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 8px 20px rgba(241, 122, 179, 0.6);
|
||||
}
|
||||
|
||||
.decoration {
|
||||
position: absolute;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 138, 179, 0.1);
|
||||
}
|
||||
|
||||
.decoration-1 {
|
||||
top: -20px;
|
||||
left: -20px;
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
}
|
||||
|
||||
.decoration-2 {
|
||||
bottom: -30px;
|
||||
right: -30px;
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
}
|
||||
|
||||
.decoration-3 {
|
||||
top: 60%;
|
||||
left: -40px;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
.book-icon {
|
||||
margin-bottom: 20px;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.book-icon svg {
|
||||
fill: #ff8ab3;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.error-code {
|
||||
font-size: 100px;
|
||||
}
|
||||
.error-title {
|
||||
font-size: 24px;
|
||||
}
|
||||
.error-container {
|
||||
margin: 0 20px;
|
||||
padding: 30px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="main-container">
|
||||
<div class="error-container">
|
||||
<div class="error-code">404</div>
|
||||
<div class="error-message">页面未找到</div>
|
||||
<p>抱歉,您访问的页面不存在或已被移除。</p>
|
||||
<p style="margin-bottom: 30px;">请检查URL是否正确,或返回首页。</p>
|
||||
<a href="{{ url_for('index') }}" class="back-button">返回首页</a>
|
||||
<div class="error-container">
|
||||
<div class="decoration decoration-1"></div>
|
||||
<div class="decoration decoration-2"></div>
|
||||
<div class="decoration decoration-3"></div>
|
||||
|
||||
<div class="book-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<path d="M19 2H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h13c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm0 18H6V4h13v16z"/>
|
||||
<path d="M9 5h7v2H9zM9 8h7v2H9zM9 11h7v2H9zM9 14h7v2H9z"/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div class="error-code">404</div>
|
||||
<div class="error-title">噢!页面不见了~</div>
|
||||
<div class="error-message">
|
||||
<p>抱歉,您要找的页面似乎藏起来了,或者从未存在过。</p>
|
||||
<p>请检查您输入的网址是否正确,或者回到首页继续浏览吧!</p>
|
||||
</div>
|
||||
<a href="{{ url_for('index') }}" class="back-button">返回首页</a>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -32,10 +32,13 @@
|
||||
<li class="{% if '/announcement' in request.path %}active{% endif %}">
|
||||
<a href="#"><i class="fas fa-bell"></i> 通知公告</a>
|
||||
</li>
|
||||
{% if current_user.role_id == 1 %}
|
||||
{% if current_user.is_authenticated and current_user.role_id == 1 %}
|
||||
<li class="nav-category">管理功能</li>
|
||||
<li class="{% if '/user/manage' in request.path %}active{% endif %}">
|
||||
<a href="#"><i class="fas fa-users"></i> 用户管理</a>
|
||||
<a href="{{ url_for('user.user_list') }}"><i class="fas fa-users"></i> 用户管理</a>
|
||||
</li>
|
||||
<li class="{% if '/user/roles' in request.path %}active{% endif %}">
|
||||
<a href="{{ url_for('user.role_list') }}"><i class="fas fa-user-tag"></i> 角色管理</a>
|
||||
</li>
|
||||
<li class="{% if '/book/list' in request.path %}active{% endif %}">
|
||||
<a href="{{ url_for('book.book_list') }}"><i class="fas fa-layer-group"></i> 图书管理</a>
|
||||
@ -69,6 +72,7 @@
|
||||
<i class="fas fa-bell"></i>
|
||||
<span class="badge">3</span>
|
||||
</div>
|
||||
{% if current_user.is_authenticated %}
|
||||
<div class="user-info">
|
||||
<div class="user-avatar">
|
||||
{{ current_user.username[0] }}
|
||||
@ -78,11 +82,17 @@
|
||||
<span class="user-role">{{ '管理员' if current_user.role_id == 1 else '普通用户' }}</span>
|
||||
</div>
|
||||
<div class="dropdown-menu">
|
||||
<a href="#"><i class="fas fa-user-circle"></i> 个人中心</a>
|
||||
<a href="{{ url_for('user.user_profile') }}"><i class="fas fa-user-circle"></i> 个人中心</a>
|
||||
<a href="#"><i class="fas fa-cog"></i> 设置</a>
|
||||
<a href="{{ url_for('user.logout') }}"><i class="fas fa-sign-out-alt"></i> 退出登录</a>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="user-info">
|
||||
<a href="{{ url_for('user.login') }}" class="login-link">登录</a>
|
||||
<a href="{{ url_for('user.register') }}" class="register-link">注册</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@ -102,16 +112,21 @@
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// 用户菜单下拉
|
||||
const userInfo = document.querySelector('.user-info');
|
||||
userInfo.addEventListener('click', function(e) {
|
||||
userInfo.classList.toggle('active');
|
||||
});
|
||||
if (userInfo) {
|
||||
userInfo.addEventListener('click', function(e) {
|
||||
if (!e.target.classList.contains('login-link') &&
|
||||
!e.target.classList.contains('register-link')) {
|
||||
userInfo.classList.toggle('active');
|
||||
}
|
||||
});
|
||||
|
||||
// 点击其他区域关闭下拉菜单
|
||||
document.addEventListener('click', function(e) {
|
||||
if (!userInfo.contains(e.target)) {
|
||||
userInfo.classList.remove('active');
|
||||
}
|
||||
});
|
||||
// 点击其他区域关闭下拉菜单
|
||||
document.addEventListener('click', function(e) {
|
||||
if (!userInfo.contains(e.target)) {
|
||||
userInfo.classList.remove('active');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@ -73,7 +73,8 @@
|
||||
|
||||
<div class="books-grid">
|
||||
{% for book in books %}
|
||||
<div class="book-card">
|
||||
<!-- 为每个book-card添加data-id属性 -->
|
||||
<div class="book-card" data-id="{{ book.id }}">
|
||||
<div class="book-cover">
|
||||
{% if book.cover_url %}
|
||||
<img src="{{ book.cover_url }}" alt="{{ book.title }}">
|
||||
@ -179,10 +180,31 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- 删除确认模态框 -->
|
||||
<div class="modal fade" id="deleteModal" tabindex="-1" role="dialog" aria-labelledby="deleteModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="deleteModalLabel">确认删除</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
确定要删除《<span id="deleteBookTitle"></span>》吗?此操作不可恢复。
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">取消</button>
|
||||
<button type="button" class="btn btn-danger" id="confirmDelete">确认删除</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="{{ url_for('static', filename='js/book-list.js') }}"></script>
|
||||
{{ super() }}
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
148
app/templates/user/edit.html
Normal file
148
app/templates/user/edit.html
Normal file
@ -0,0 +1,148 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}编辑用户 - 图书管理系统{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/user-edit.css') }}">
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="user-edit-container">
|
||||
<div class="page-header">
|
||||
<h1>编辑用户</h1>
|
||||
<div class="actions">
|
||||
<a href="{{ url_for('user.user_list') }}" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-arrow-left"></i> 返回用户列表
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
<div class="alert alert-{{ category }}">{{ message }}</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<form method="POST" action="{{ url_for('user.user_edit', user_id=user.id) }}" id="userEditForm">
|
||||
<div class="form-row">
|
||||
<!-- 用户基本信息 -->
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="username">用户名</label>
|
||||
<input type="text" class="form-control" id="username" value="{{ user.username }}" readonly>
|
||||
<small class="form-text text-muted">用户名不可修改</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="email">邮箱地址</label>
|
||||
<input type="email" class="form-control" id="email" name="email" value="{{ user.email or '' }}">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="phone">手机号码</label>
|
||||
<input type="text" class="form-control" id="phone" name="phone" value="{{ user.phone or '' }}">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="nickname">昵称</label>
|
||||
<input type="text" class="form-control" id="nickname" name="nickname" value="{{ user.nickname or '' }}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 用户权限和密码 -->
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="role_id">用户角色</label>
|
||||
<select class="form-control" id="role_id" name="role_id">
|
||||
{% for role in roles %}
|
||||
<option value="{{ role.id }}" {% if role.id == user.role_id %}selected{% endif %}>
|
||||
{{ role.role_name }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="status">用户状态</label>
|
||||
<select class="form-control" id="status" name="status">
|
||||
<option value="1" {% if user.status == 1 %}selected{% endif %}>正常</option>
|
||||
<option value="0" {% if user.status == 0 %}selected{% endif %}>禁用</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="password">重置密码</label>
|
||||
<input type="password" class="form-control" id="password" name="password">
|
||||
<small class="form-text text-muted">留空表示不修改密码</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group" id="confirmPasswordGroup" style="display: none;">
|
||||
<label for="confirm_password">确认密码</label>
|
||||
<input type="password" class="form-control" id="confirm_password" name="confirm_password">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 附加信息 -->
|
||||
<div class="form-row">
|
||||
<div class="col-md-12">
|
||||
<div class="user-info-box">
|
||||
<div class="info-item">
|
||||
<span class="info-label">用户ID:</span>
|
||||
<span class="info-value">{{ user.id }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">注册时间:</span>
|
||||
<span class="info-value">{{ user.created_at }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">最后更新:</span>
|
||||
<span class="info-value">{{ user.updated_at }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 提交按钮区域 -->
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fas fa-save"></i> 保存修改
|
||||
</button>
|
||||
<a href="{{ url_for('user.user_list') }}" class="btn btn-secondary">
|
||||
取消
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 操作成功提示模态框 -->
|
||||
<div class="modal fade" id="successModal" tabindex="-1" role="dialog" aria-labelledby="successModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="successModalLabel">操作成功</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
用户信息已成功更新。
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">关闭</button>
|
||||
<a href="{{ url_for('user.user_list') }}" class="btn btn-primary">返回用户列表</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="{{ url_for('static', filename='js/user-edit.js') }}"></script>
|
||||
{% endblock %}
|
||||
190
app/templates/user/list.html
Normal file
190
app/templates/user/list.html
Normal file
@ -0,0 +1,190 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}用户管理 - 图书管理系统{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/user-list.css') }}">
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="user-list-container">
|
||||
<!-- 页面标题 -->
|
||||
<div class="page-header">
|
||||
<h1>用户管理</h1>
|
||||
<div class="actions">
|
||||
<a href="{{ url_for('user.register') }}" class="btn btn-primary">
|
||||
<i class="fas fa-user-plus"></i> 添加用户
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 搜索和过滤区域 -->
|
||||
<div class="search-filter-container">
|
||||
<form method="GET" action="{{ url_for('user.user_list') }}" class="search-filter-form">
|
||||
<div class="form-row">
|
||||
<div class="search-box">
|
||||
<input type="text" name="search" value="{{ search }}" placeholder="搜索用户名/邮箱/昵称/手机" class="form-control">
|
||||
<button type="submit" class="btn btn-search">
|
||||
<i class="fas fa-search"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="filter-box">
|
||||
<select name="status" class="form-control">
|
||||
<option value="">所有状态</option>
|
||||
<option value="1" {% if status == 1 %}selected{% endif %}>正常</option>
|
||||
<option value="0" {% if status == 0 %}selected{% endif %}>禁用</option>
|
||||
</select>
|
||||
|
||||
<select name="role_id" class="form-control">
|
||||
<option value="">所有角色</option>
|
||||
{% for role in roles %}
|
||||
<option value="{{ role.id }}" {% if role_id == role.id %}selected{% endif %}>{{ role.role_name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
||||
<button type="submit" class="btn btn-filter">筛选</button>
|
||||
<a href="{{ url_for('user.user_list') }}" class="btn btn-reset">重置</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- 用户列表表格 -->
|
||||
<div class="table-responsive">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>用户名</th>
|
||||
<th>昵称</th>
|
||||
<th>邮箱</th>
|
||||
<th>手机号</th>
|
||||
<th>角色</th>
|
||||
<th>状态</th>
|
||||
<th>注册时间</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for user in pagination.items %}
|
||||
<tr>
|
||||
<td>{{ user.id }}</td>
|
||||
<td>{{ user.username }}</td>
|
||||
<td>{{ user.nickname or '-' }}</td>
|
||||
<td>{{ user.email or '-' }}</td>
|
||||
<td>{{ user.phone or '-' }}</td>
|
||||
<td>
|
||||
{% for role in roles %}
|
||||
{% if role.id == user.role_id %}
|
||||
{{ role.role_name }}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</td>
|
||||
<td>
|
||||
<span class="status-badge {% if user.status == 1 %}active{% else %}inactive{% endif %}">
|
||||
{{ '正常' if user.status == 1 else '禁用' }}
|
||||
</span>
|
||||
</td>
|
||||
<td>{{ user.created_at }}</td>
|
||||
<td class="actions">
|
||||
<a href="{{ url_for('user.user_edit', user_id=user.id) }}" class="btn btn-sm btn-info" title="编辑">
|
||||
<i class="fas fa-edit"></i>
|
||||
</a>
|
||||
|
||||
{% if user.id != session.get('user_id') %}
|
||||
{% if user.status == 1 %}
|
||||
<button class="btn btn-sm btn-warning toggle-status" data-id="{{ user.id }}" data-status="0" title="禁用">
|
||||
<i class="fas fa-ban"></i>
|
||||
</button>
|
||||
{% else %}
|
||||
<button class="btn btn-sm btn-success toggle-status" data-id="{{ user.id }}" data-status="1" title="启用">
|
||||
<i class="fas fa-check"></i>
|
||||
</button>
|
||||
{% endif %}
|
||||
|
||||
<button class="btn btn-sm btn-danger delete-user" data-id="{{ user.id }}" title="删除">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
{% else %}
|
||||
<span class="text-muted">(当前用户)</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="9" class="text-center">暂无用户数据</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- 分页控件 -->
|
||||
{% if pagination.pages > 1 %}
|
||||
<div class="pagination-container">
|
||||
<ul class="pagination">
|
||||
{% if pagination.has_prev %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="{{ url_for('user.user_list', page=pagination.prev_num, search=search, status=status, role_id=role_id) }}">
|
||||
<i class="fas fa-chevron-left"></i>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% for page in pagination.iter_pages(left_edge=2, left_current=2, right_current=3, right_edge=2) %}
|
||||
{% if page %}
|
||||
{% if page == pagination.page %}
|
||||
<li class="page-item active">
|
||||
<span class="page-link">{{ page }}</span>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="{{ url_for('user.user_list', page=page, search=search, status=status, role_id=role_id) }}">{{ page }}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<li class="page-item disabled">
|
||||
<span class="page-link">...</span>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% if pagination.has_next %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="{{ url_for('user.user_list', page=pagination.next_num, search=search, status=status, role_id=role_id) }}">
|
||||
<i class="fas fa-chevron-right"></i>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- 确认删除模态框 -->
|
||||
<div class="modal fade" id="deleteModal" tabindex="-1" role="dialog" aria-labelledby="deleteModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="deleteModalLabel">确认删除</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
您确定要删除这个用户吗?此操作不可逆。
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">取消</button>
|
||||
<button type="button" class="btn btn-danger" id="confirmDelete">确认删除</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="{{ url_for('static', filename='js/user-list.js') }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
188
app/templates/user/profile.html
Normal file
188
app/templates/user/profile.html
Normal file
@ -0,0 +1,188 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}个人中心 - 图书管理系统{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/user-profile.css') }}">
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="profile-container">
|
||||
<div class="page-header">
|
||||
<h1>个人中心</h1>
|
||||
</div>
|
||||
|
||||
<div class="profile-content">
|
||||
<!-- 左侧用户信息展示 -->
|
||||
<div class="profile-sidebar">
|
||||
<div class="user-avatar-container">
|
||||
<div class="user-avatar large">
|
||||
{{ user.username[0] }}
|
||||
</div>
|
||||
<h3 class="user-name">{{ user.nickname or user.username }}</h3>
|
||||
<p class="user-role">{{ '管理员' if user.role_id == 1 else '普通用户' }}</p>
|
||||
</div>
|
||||
|
||||
<div class="user-stats">
|
||||
<div class="stat-item">
|
||||
<div class="stat-value" id="borrowCount">--</div>
|
||||
<div class="stat-label">借阅中</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-value" id="returnedCount">--</div>
|
||||
<div class="stat-label">已归还</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-value" id="overdueCount">--</div>
|
||||
<div class="stat-label">已逾期</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="account-info">
|
||||
<div class="info-row">
|
||||
<span class="info-label">用户名</span>
|
||||
<span class="info-value">{{ user.username }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">用户ID</span>
|
||||
<span class="info-value">{{ user.id }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">注册时间</span>
|
||||
<span class="info-value">{{ user.created_at }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">最后更新</span>
|
||||
<span class="info-value">{{ user.updated_at }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧内容区域:包含编辑选项卡 -->
|
||||
<div class="profile-main">
|
||||
<!-- 提示消息 -->
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
<div class="alert alert-{{ category }}">{{ message }}</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<!-- 选项卡导航 -->
|
||||
<ul class="nav nav-tabs" id="profileTabs" role="tablist">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" id="basic-tab" data-toggle="tab" href="#basic" role="tab" aria-controls="basic" aria-selected="true">
|
||||
<i class="fas fa-user"></i> 基本信息
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" id="security-tab" data-toggle="tab" href="#security" role="tab" aria-controls="security" aria-selected="false">
|
||||
<i class="fas fa-lock"></i> 安全设置
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" id="activity-tab" data-toggle="tab" href="#activity" role="tab" aria-controls="activity" aria-selected="false">
|
||||
<i class="fas fa-history"></i> 最近活动
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<!-- 选项卡内容 -->
|
||||
<div class="tab-content" id="profileTabsContent">
|
||||
<!-- 基本信息选项卡 -->
|
||||
<div class="tab-pane fade show active" id="basic" role="tabpanel" aria-labelledby="basic-tab">
|
||||
<form method="POST" action="{{ url_for('user.user_profile') }}" id="profileForm">
|
||||
<div class="form-section">
|
||||
<h4>个人信息</h4>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="nickname">昵称</label>
|
||||
<input type="text" class="form-control" id="nickname" name="nickname" value="{{ user.nickname or '' }}" placeholder="请输入您的昵称">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="email">邮箱地址</label>
|
||||
<input type="email" class="form-control" id="email" name="email" value="{{ user.email or '' }}" placeholder="请输入您的邮箱">
|
||||
<small class="form-text text-muted">用于接收系统通知和找回密码</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="phone">手机号码</label>
|
||||
<input type="text" class="form-control" id="phone" name="phone" value="{{ user.phone or '' }}" placeholder="请输入您的手机号">
|
||||
<small class="form-text text-muted">用于接收借阅提醒和系统通知</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary" name="form_type" value="profile">
|
||||
<i class="fas fa-save"></i> 保存修改
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- 安全设置选项卡 -->
|
||||
<div class="tab-pane fade" id="security" role="tabpanel" aria-labelledby="security-tab">
|
||||
<form method="POST" action="{{ url_for('user.user_profile') }}" id="passwordForm">
|
||||
<div class="form-section">
|
||||
<h4>修改密码</h4>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="current_password">当前密码</label>
|
||||
<input type="password" class="form-control" id="current_password" name="current_password" placeholder="请输入当前密码">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="new_password">新密码</label>
|
||||
<input type="password" class="form-control" id="new_password" name="new_password" placeholder="请输入新密码">
|
||||
<small class="form-text text-muted">密码长度至少为6个字符</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="confirm_password">确认新密码</label>
|
||||
<input type="password" class="form-control" id="confirm_password" name="confirm_password" placeholder="请再次输入新密码">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary" name="form_type" value="password">
|
||||
<i class="fas fa-key"></i> 更新密码
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- 最近活动选项卡 -->
|
||||
<div class="tab-pane fade" id="activity" role="tabpanel" aria-labelledby="activity-tab">
|
||||
<div class="activity-header">
|
||||
<h4>最近活动</h4>
|
||||
<div class="activity-filter">
|
||||
<label for="activityFilter">显示:</label>
|
||||
<select id="activityFilter" class="form-control form-control-sm">
|
||||
<option value="all">所有活动</option>
|
||||
<option value="login">登录记录</option>
|
||||
<option value="borrow">借阅活动</option>
|
||||
<option value="return">归还活动</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="activity-timeline" id="activityTimeline">
|
||||
<div class="timeline-loading">
|
||||
<div class="spinner-border text-primary" role="status">
|
||||
<span class="sr-only">Loading...</span>
|
||||
</div>
|
||||
<p>加载中...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="{{ url_for('static', filename='js/user-profile.js') }}"></script>
|
||||
{% endblock %}
|
||||
194
app/templates/user/roles.html
Normal file
194
app/templates/user/roles.html
Normal file
@ -0,0 +1,194 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}角色管理 - 图书管理系统{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/user-roles.css') }}">
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="roles-container">
|
||||
<!-- 页面标题 -->
|
||||
<div class="page-header">
|
||||
<h1>角色管理</h1>
|
||||
<div class="actions">
|
||||
<button class="btn btn-primary" id="addRoleBtn">
|
||||
<i class="fas fa-plus"></i> 添加角色
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 角色列表卡片 -->
|
||||
<div class="role-list">
|
||||
{% for role in roles %}
|
||||
<div class="role-card" data-id="{{ role.id }}">
|
||||
<div class="role-header">
|
||||
<h3 class="role-name">{{ role.role_name }}</h3>
|
||||
<div class="role-actions">
|
||||
<button class="btn btn-sm btn-edit-role" title="编辑角色">
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>
|
||||
{% if role.id not in [1, 2] %}
|
||||
<button class="btn btn-sm btn-delete-role" title="删除角色">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="role-description">
|
||||
{% if role.description %}
|
||||
{{ role.description }}
|
||||
{% else %}
|
||||
<span class="text-muted">暂无描述</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="role-stats">
|
||||
<div class="stat-item">
|
||||
<i class="fas fa-users"></i> <span id="userCount-{{ role.id }}">--</span> 用户
|
||||
</div>
|
||||
{% if role.id == 1 %}
|
||||
<div class="role-badge admin">管理员</div>
|
||||
{% elif role.id == 2 %}
|
||||
<div class="role-badge user">普通用户</div>
|
||||
{% else %}
|
||||
<div class="role-badge custom">自定义</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="no-data-message">
|
||||
<i class="fas fa-users-slash"></i>
|
||||
<p>暂无角色数据</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- 权限描述 -->
|
||||
<div class="permissions-info">
|
||||
<h3>角色权限说明</h3>
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<table class="table permission-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>功能模块</th>
|
||||
<th>管理员</th>
|
||||
<th>普通用户</th>
|
||||
<th>自定义角色</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>图书浏览</td>
|
||||
<td><i class="fas fa-check text-success"></i></td>
|
||||
<td><i class="fas fa-check text-success"></i></td>
|
||||
<td><i class="fas fa-check text-success"></i></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>借阅图书</td>
|
||||
<td><i class="fas fa-check text-success"></i></td>
|
||||
<td><i class="fas fa-check text-success"></i></td>
|
||||
<td><i class="fas fa-check text-success"></i></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>图书管理</td>
|
||||
<td><i class="fas fa-check text-success"></i></td>
|
||||
<td><i class="fas fa-times text-danger"></i></td>
|
||||
<td>可配置</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>用户管理</td>
|
||||
<td><i class="fas fa-check text-success"></i></td>
|
||||
<td><i class="fas fa-times text-danger"></i></td>
|
||||
<td>可配置</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>借阅管理</td>
|
||||
<td><i class="fas fa-check text-success"></i></td>
|
||||
<td><i class="fas fa-times text-danger"></i></td>
|
||||
<td>可配置</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>库存管理</td>
|
||||
<td><i class="fas fa-check text-success"></i></td>
|
||||
<td><i class="fas fa-times text-danger"></i></td>
|
||||
<td>可配置</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>统计分析</td>
|
||||
<td><i class="fas fa-check text-success"></i></td>
|
||||
<td><i class="fas fa-times text-danger"></i></td>
|
||||
<td>可配置</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>系统设置</td>
|
||||
<td><i class="fas fa-check text-success"></i></td>
|
||||
<td><i class="fas fa-times text-danger"></i></td>
|
||||
<td>可配置</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 角色编辑模态框 -->
|
||||
<div class="modal fade" id="roleModal" tabindex="-1" role="dialog" aria-labelledby="roleModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="roleModalLabel">添加角色</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="roleForm">
|
||||
<input type="hidden" id="roleId" value="">
|
||||
|
||||
<div class="form-group">
|
||||
<label for="roleName">角色名称</label>
|
||||
<input type="text" class="form-control" id="roleName" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="roleDescription">角色描述</label>
|
||||
<textarea class="form-control" id="roleDescription" rows="3"></textarea>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">取消</button>
|
||||
<button type="button" class="btn btn-primary" id="saveRoleBtn">保存</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 确认删除模态框 -->
|
||||
<div class="modal fade" id="deleteModal" tabindex="-1" role="dialog" aria-labelledby="deleteModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="deleteModalLabel">确认删除</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
您确定要删除这个角色吗?此操作不可逆。
|
||||
<p class="text-danger mt-3">注意:删除角色将会影响所有使用此角色的用户。</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">取消</button>
|
||||
<button type="button" class="btn btn-danger" id="confirmDeleteBtn">确认删除</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="{{ url_for('static', filename='js/user-roles.js') }}"></script>
|
||||
{% endblock %}
|
||||
6205
code_collection.txt
6205
code_collection.txt
File diff suppressed because it is too large
Load Diff
@ -9,4 +9,5 @@ xlrd==2.0.1
|
||||
email-validator==2.0.0
|
||||
pillow==9.5.0
|
||||
numpy
|
||||
pandas
|
||||
pandas
|
||||
flask-login
|
||||
Loading…
x
Reference in New Issue
Block a user