修改登录退出逻辑
This commit is contained in:
@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
from flask import Flask
|
from flask import Flask
|
||||||
from config import Config
|
from config import Config
|
||||||
# 【修改】增加 jwt 引入
|
|
||||||
from app.extensions import db, migrate, cors, jwt
|
from app.extensions import db, migrate, cors, jwt
|
||||||
import os
|
import os
|
||||||
|
|
||||||
@ -16,36 +15,44 @@ def create_app():
|
|||||||
# =========================================================
|
# =========================================================
|
||||||
db.init_app(app)
|
db.init_app(app)
|
||||||
migrate.init_app(app, db)
|
migrate.init_app(app, db)
|
||||||
|
jwt.init_app(app) # 初始化 JWT
|
||||||
|
|
||||||
# 【新增】初始化 JWT (用于 Token 认证)
|
# 允许所有 /api/ 开头的请求跨域,支持 credentials
|
||||||
jwt.init_app(app)
|
cors.init_app(app, resources={r"/*": {"origins": "*"}}, supports_credentials=True)
|
||||||
|
|
||||||
# 确保跨域配置
|
|
||||||
# 允许 /api/ 开头的请求跨域
|
|
||||||
cors.init_app(app, resources={r"/*": {"origins": "*"}})
|
|
||||||
|
|
||||||
# =========================================================
|
# =========================================================
|
||||||
# 2. 注册蓝图 (Blueprints)
|
# 2. 注册蓝图 (Blueprints)
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
# 注意:为了解决前端请求不带 /v1 导致的 404 错误,
|
||||||
|
# 下面的模块都采用了 "双重注册" 策略:
|
||||||
|
# 1. 标准地址: /api/v1/...
|
||||||
|
# 2. 兼容地址: /api/... (name 参数必须不同)
|
||||||
# =========================================================
|
# =========================================================
|
||||||
|
|
||||||
# -----------------------------------------------------
|
# -----------------------------------------------------
|
||||||
# 2.0 [新增] 注册权限与认证模块 (Auth) - 最关键修复
|
# 2.0 注册权限与认证模块 (Auth)
|
||||||
# -----------------------------------------------------
|
# -----------------------------------------------------
|
||||||
try:
|
try:
|
||||||
from app.api.v1.auth import auth_bp
|
from app.api.v1.auth import auth_bp
|
||||||
# 前端请求地址: /api/v1/auth/login
|
# 标准
|
||||||
app.register_blueprint(auth_bp, url_prefix='/api/v1/auth')
|
app.register_blueprint(auth_bp, url_prefix='/api/v1/auth')
|
||||||
print("✅ Auth (System & Login) 模块注册成功")
|
# 兼容 (防止前端忘记写 v1)
|
||||||
|
app.register_blueprint(auth_bp, url_prefix='/api/auth', name='auth_legacy')
|
||||||
|
print("✅ Auth 模块注册成功")
|
||||||
except ImportError as e:
|
except ImportError as e:
|
||||||
print(f"❌ 错误: Auth 模块导入失败: {e}")
|
print(f"❌ 错误: Auth 模块导入失败: {e}")
|
||||||
|
|
||||||
# -----------------------------------------------------
|
# -----------------------------------------------------
|
||||||
# 2.1 注册入库聚合模块 (Inbound)
|
# 2.1 注册入库聚合模块 (Inbound) - 【核心修复点】
|
||||||
# -----------------------------------------------------
|
# -----------------------------------------------------
|
||||||
try:
|
try:
|
||||||
from app.api.v1.inbound import inbound_bp
|
from app.api.v1.inbound import inbound_bp
|
||||||
|
# 标准: /api/v1/inbound/base/list
|
||||||
app.register_blueprint(inbound_bp, url_prefix='/api/v1/inbound')
|
app.register_blueprint(inbound_bp, url_prefix='/api/v1/inbound')
|
||||||
print("✅ Inbound (Buy, Semi, Product, Base) 模块注册成功")
|
|
||||||
|
# 兼容: /api/inbound/base/list (修复前端 404)
|
||||||
|
app.register_blueprint(inbound_bp, url_prefix='/api/inbound', name='inbound_legacy')
|
||||||
|
print("✅ Inbound 模块注册成功 (已启用兼容模式: /api/inbound)")
|
||||||
except ImportError as e:
|
except ImportError as e:
|
||||||
print(f"❌ 错误: Inbound 模块导入失败: {e}")
|
print(f"❌ 错误: Inbound 模块导入失败: {e}")
|
||||||
|
|
||||||
@ -55,7 +62,8 @@ def create_app():
|
|||||||
try:
|
try:
|
||||||
from app.api.v1.common.print import print_bp
|
from app.api.v1.common.print import print_bp
|
||||||
app.register_blueprint(print_bp, url_prefix='/api/v1/common/print')
|
app.register_blueprint(print_bp, url_prefix='/api/v1/common/print')
|
||||||
print("✅ Print (Label Printing) 模块注册成功")
|
app.register_blueprint(print_bp, url_prefix='/api/common/print', name='print_legacy')
|
||||||
|
print("✅ Print 模块注册成功")
|
||||||
except ImportError as e:
|
except ImportError as e:
|
||||||
print(f"❌ 错误: Print 模块导入失败: {e}")
|
print(f"❌ 错误: Print 模块导入失败: {e}")
|
||||||
|
|
||||||
@ -64,51 +72,44 @@ def create_app():
|
|||||||
# -----------------------------------------------------
|
# -----------------------------------------------------
|
||||||
try:
|
try:
|
||||||
from app.api.v1.common.upload import upload_bp
|
from app.api.v1.common.upload import upload_bp
|
||||||
# 注册方式 1: 标准路径
|
|
||||||
app.register_blueprint(upload_bp, url_prefix='/api/v1/common')
|
app.register_blueprint(upload_bp, url_prefix='/api/v1/common')
|
||||||
# 注册方式 2: 兼容路径 (防止反向代理剥离 /api)
|
app.register_blueprint(upload_bp, url_prefix='/api/common', name='upload_legacy')
|
||||||
app.register_blueprint(upload_bp, url_prefix='/v1/common', name='upload_fallback')
|
print("✅ Upload 模块注册成功")
|
||||||
print("✅ Upload (File Storage) 模块注册成功 (双路径兼容模式)")
|
|
||||||
except ImportError as e:
|
except ImportError as e:
|
||||||
print(f"❌ 错误: Upload 模块导入失败: {e}")
|
print(f"❌ 错误: Upload 模块导入失败: {e}")
|
||||||
|
|
||||||
# -----------------------------------------------------
|
# -----------------------------------------------------
|
||||||
# 2.4 [新增] 注册业务操作模块 (Transactions)
|
# 2.4 注册业务操作模块 (Transactions)
|
||||||
# -----------------------------------------------------
|
# -----------------------------------------------------
|
||||||
try:
|
try:
|
||||||
# 对应 borrow, return, scrap 等操作
|
|
||||||
from app.api.v1.transactions import trans_bp
|
from app.api.v1.transactions import trans_bp
|
||||||
app.register_blueprint(trans_bp, url_prefix='/api/v1/trans')
|
app.register_blueprint(trans_bp, url_prefix='/api/v1/trans')
|
||||||
print("✅ Transactions (Borrow, Return, Scrap) 模块注册成功")
|
app.register_blueprint(trans_bp, url_prefix='/api/trans', name='trans_legacy')
|
||||||
|
print("✅ Transactions 模块注册成功")
|
||||||
except ImportError as e:
|
except ImportError as e:
|
||||||
# 如果文件还没写好,这里会报错,但不影响主程序启动
|
# 允许模块不存在时不崩溃
|
||||||
print(f"⚠️ 警告: Transaction 模块导入失败 (如果是新建项目可忽略): {e}")
|
print(f"⚠️ 提示: Transaction 模块尚未创建或导入失败: {e}")
|
||||||
|
|
||||||
# =========================================================
|
# =========================================================
|
||||||
# 3. 预加载数据模型 (解决 relationship 找不到模型的问题)
|
# 3. 预加载数据模型
|
||||||
# =========================================================
|
# =========================================================
|
||||||
with app.app_context():
|
with app.app_context():
|
||||||
try:
|
try:
|
||||||
# 1. 基础物料
|
# 基础与库存模型
|
||||||
from app.models.base import MaterialBase
|
from app.models.base import MaterialBase
|
||||||
# 2. 采购入库
|
|
||||||
from app.models.inbound.buy import StockBuy
|
from app.models.inbound.buy import StockBuy
|
||||||
# 3. 半成品入库
|
|
||||||
from app.models.inbound.semi import StockSemi
|
from app.models.inbound.semi import StockSemi
|
||||||
# 4. 成品入库
|
|
||||||
from app.models.inbound.product import StockProduct
|
from app.models.inbound.product import StockProduct
|
||||||
|
|
||||||
# 【新增】5. 系统用户 (关键:确保创建 user 表)
|
# 系统与业务模型
|
||||||
from app.models.system import SysUser, SysLog
|
from app.models.system import SysUser, SysLog
|
||||||
|
|
||||||
# 【新增】6. 业务流水
|
|
||||||
from app.models.transaction import TransBorrow, TransRepair, TransScrap
|
from app.models.transaction import TransBorrow, TransRepair, TransScrap
|
||||||
|
|
||||||
# 开发环境自动建表 (根据之前的对话,强烈建议在容器第一次启动时开启或手动调用)
|
# 首次运行时可取消注释自动建表 (但在生产环境建议使用 flask db upgrade)
|
||||||
# db.create_all()
|
# db.create_all()
|
||||||
|
|
||||||
except ImportError as e:
|
except ImportError as e:
|
||||||
print(f"⚠️ 模型预加载失败: {e}")
|
print(f"⚠️ 模型预加载部分失败 (检查是否缺少文件): {e}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"⚠️ 模型预加载发生未知错误: {e}")
|
print(f"⚠️ 模型预加载发生未知错误: {e}")
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
# app/api/v1/auth.py
|
# app/api/v1/auth.py
|
||||||
from flask import Blueprint, request, jsonify
|
from flask import Blueprint, request, jsonify, current_app
|
||||||
from flask_jwt_extended import jwt_required, get_jwt
|
from flask_jwt_extended import jwt_required, get_jwt
|
||||||
from app.services.auth_service import AuthService
|
from app.services.auth_service import AuthService
|
||||||
|
|
||||||
@ -10,13 +10,38 @@ auth_bp = Blueprint('auth', __name__)
|
|||||||
def login():
|
def login():
|
||||||
try:
|
try:
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
|
if not data:
|
||||||
|
return jsonify({'msg': '无效的请求数据'}), 400
|
||||||
|
|
||||||
if not data.get('username') or not data.get('password'):
|
if not data.get('username') or not data.get('password'):
|
||||||
return jsonify({'msg': '请输入用户名和密码'}), 400
|
return jsonify({'msg': '请输入用户名和密码'}), 400
|
||||||
|
|
||||||
|
# 调用 Service 层逻辑
|
||||||
result = AuthService.login(data)
|
result = AuthService.login(data)
|
||||||
return jsonify({'msg': '登录成功', 'data': result}), 200
|
|
||||||
|
# [关键修复]
|
||||||
|
# 前端 store 代码写的是: token.value = res.data.access_token
|
||||||
|
# 所以我们这里不能把 access_token 包裹在 data 字段里,
|
||||||
|
# 而是应该直接合并返回,或者让前端去 data.data 里面取。
|
||||||
|
# 为了不改前端,我们这里做解构返回:
|
||||||
|
|
||||||
|
response_data = {
|
||||||
|
'msg': '登录成功',
|
||||||
|
'access_token': result.get('access_token'),
|
||||||
|
'user': result.get('user')
|
||||||
|
}
|
||||||
|
|
||||||
|
return jsonify(response_data), 200
|
||||||
|
|
||||||
|
except ValueError as ve:
|
||||||
|
# 捕获已知的业务错误(如密码错误、用户不存在)
|
||||||
|
return jsonify({'msg': str(ve)}), 401
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({'msg': str(e)}), 401
|
# [关键修复] 打印详细报错到控制台,方便排查 500 错误
|
||||||
|
# (例如数据库连接失败、表不存在等)
|
||||||
|
current_app.logger.error(f"Login Failed Error: {str(e)}")
|
||||||
|
# 生产环境不建议直接把 error 返回给前端,但调试阶段很有用
|
||||||
|
return jsonify({'msg': f'服务器内部错误: {str(e)}'}), 500
|
||||||
|
|
||||||
|
|
||||||
# 新增:创建用户 (替代了原来的注册)
|
# 新增:创建用户 (替代了原来的注册)
|
||||||
@ -30,8 +55,13 @@ def create_user():
|
|||||||
claims = get_jwt()
|
claims = get_jwt()
|
||||||
operator_role = claims.get('role')
|
operator_role = claims.get('role')
|
||||||
|
|
||||||
|
# 增加一个简单的权限判断(可选)
|
||||||
|
if operator_role not in ['super_admin', 'supervisor']:
|
||||||
|
return jsonify({'msg': '权限不足,无法创建用户'}), 403
|
||||||
|
|
||||||
result = AuthService.create_user(data, operator_role)
|
result = AuthService.create_user(data, operator_role)
|
||||||
return jsonify({'msg': '用户创建成功', 'data': result}), 201
|
return jsonify({'msg': '用户创建成功', 'data': result}), 201
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# 这里虽然返回 400,但实际可能包含 403 的含义,具体看前端处理
|
current_app.logger.error(f"User Create Failed: {str(e)}")
|
||||||
return jsonify({'msg': str(e)}), 400
|
return jsonify({'msg': str(e)}), 400
|
||||||
@ -1,8 +1,12 @@
|
|||||||
from flask import Blueprint, jsonify
|
from flask import Blueprint, jsonify
|
||||||
|
|
||||||
# 定义蓝图,名字叫 'transactions'
|
# 定义蓝图
|
||||||
|
# 注意:这个变量名 trans_bp 必须与 app/__init__.py 中注册时引用的名字一致
|
||||||
trans_bp = Blueprint('transactions', __name__)
|
trans_bp = Blueprint('transactions', __name__)
|
||||||
|
|
||||||
@trans_bp.route('/test', methods=['GET'])
|
@trans_bp.route('/test', methods=['GET'])
|
||||||
def test_transaction():
|
def test_transaction():
|
||||||
return jsonify({"message": "Transaction module is working"})
|
"""
|
||||||
|
测试接口:用于验证 Transaction 模块是否加载成功
|
||||||
|
"""
|
||||||
|
return jsonify({"message": "Transaction module is working", "status": "success"})
|
||||||
@ -10,7 +10,8 @@ class MaterialBase(db.Model):
|
|||||||
|
|
||||||
# 1. 基础字段
|
# 1. 基础字段
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
name = db.Column(db.String(255), nullable=False, comment='基础信息名称')
|
name = db.Column(db.String(255), nullable=False, comment='名称')
|
||||||
|
common_name = db.Column(db.String(255), comment='俗名') # ✅ 新增字段
|
||||||
category = db.Column(db.String(100), comment='类别')
|
category = db.Column(db.String(100), comment='类别')
|
||||||
material_type = db.Column(db.String(100), comment='类型')
|
material_type = db.Column(db.String(100), comment='类型')
|
||||||
spec_model = db.Column(db.String(255), comment='规格型号')
|
spec_model = db.Column(db.String(255), comment='规格型号')
|
||||||
@ -46,6 +47,7 @@ class MaterialBase(db.Model):
|
|||||||
return {
|
return {
|
||||||
'id': self.id,
|
'id': self.id,
|
||||||
'name': self.name,
|
'name': self.name,
|
||||||
|
'commonName': self.common_name, # ✅ 序列化新增字段
|
||||||
'category': self.category,
|
'category': self.category,
|
||||||
'type': self.material_type, # 前端字段映射
|
'type': self.material_type, # 前端字段映射
|
||||||
'spec': self.spec_model, # 前端字段映射
|
'spec': self.spec_model, # 前端字段映射
|
||||||
|
|||||||
@ -1,66 +1,96 @@
|
|||||||
from app.extensions import db
|
from app.extensions import db
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
# 1. 借用表
|
# 1. 借用表
|
||||||
class TransBorrow(db.Model):
|
class TransBorrow(db.Model):
|
||||||
__tablename__ = 'trans_borrow'
|
__tablename__ = 'trans_borrow'
|
||||||
|
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
sku = db.Column(db.String(100))
|
sku = db.Column(db.String(100), index=True) # 加索引优化查询
|
||||||
source_table = db.Column(db.String(50))
|
source_table = db.Column(db.String(50))
|
||||||
stock_id = db.Column(db.Integer)
|
stock_id = db.Column(db.Integer)
|
||||||
quantity = db.Column(db.Numeric(19, 4))
|
quantity = db.Column(db.Numeric(19, 4))
|
||||||
|
|
||||||
borrow_time = db.Column(db.DateTime, default=datetime.now)
|
borrow_time = db.Column(db.DateTime, default=datetime.now)
|
||||||
expected_return_time = db.Column(db.DateTime)
|
expected_return_time = db.Column(db.DateTime)
|
||||||
|
|
||||||
borrower_name = db.Column(db.String(100))
|
borrower_name = db.Column(db.String(100))
|
||||||
actual_return_time = db.Column(db.DateTime)
|
actual_return_time = db.Column(db.DateTime)
|
||||||
approver_name = db.Column(db.String(100))
|
approver_name = db.Column(db.String(100))
|
||||||
status = db.Column(db.String(20))
|
|
||||||
|
# 状态:borrowed(借出), returned(已还), overdue(逾期)
|
||||||
|
status = db.Column(db.String(20), default='borrowed')
|
||||||
|
|
||||||
def to_dict(self):
|
def to_dict(self):
|
||||||
return {
|
return {
|
||||||
'id': self.id,
|
'id': self.id,
|
||||||
'sku': self.sku,
|
'sku': self.sku,
|
||||||
|
'quantity': float(self.quantity) if self.quantity else 0,
|
||||||
'borrower_name': self.borrower_name,
|
'borrower_name': self.borrower_name,
|
||||||
|
'borrow_time': self.borrow_time.strftime('%Y-%m-%d %H:%M:%S') if self.borrow_time else None,
|
||||||
'status': self.status
|
'status': self.status
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
# 2. 维修表
|
# 2. 维修表
|
||||||
class TransRepair(db.Model):
|
class TransRepair(db.Model):
|
||||||
__tablename__ = 'trans_repair'
|
__tablename__ = 'trans_repair'
|
||||||
|
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
sku = db.Column(db.String(100))
|
sku = db.Column(db.String(100), index=True)
|
||||||
source_table = db.Column(db.String(50))
|
source_table = db.Column(db.String(50))
|
||||||
stock_id = db.Column(db.Integer)
|
stock_id = db.Column(db.Integer)
|
||||||
|
|
||||||
arrival_date = db.Column(db.Date)
|
arrival_date = db.Column(db.Date)
|
||||||
expected_repair_time = db.Column(db.String(100))
|
expected_repair_time = db.Column(db.String(100))
|
||||||
shipping_date = db.Column(db.Date)
|
shipping_date = db.Column(db.Date)
|
||||||
|
|
||||||
is_self_made = db.Column(db.Boolean, default=False)
|
is_self_made = db.Column(db.Boolean, default=False)
|
||||||
related_product_id = db.Column(db.Integer)
|
related_product_id = db.Column(db.Integer)
|
||||||
related_contract_id = db.Column(db.String(100))
|
related_contract_id = db.Column(db.String(100))
|
||||||
|
|
||||||
repair_manager = db.Column(db.String(100))
|
repair_manager = db.Column(db.String(100))
|
||||||
fault_description = db.Column(db.Text)
|
fault_description = db.Column(db.Text)
|
||||||
repair_result = db.Column(db.Text)
|
repair_result = db.Column(db.Text)
|
||||||
|
|
||||||
cost_price = db.Column(db.Numeric(19, 4))
|
cost_price = db.Column(db.Numeric(19, 4))
|
||||||
sale_price = db.Column(db.Numeric(19, 4))
|
sale_price = db.Column(db.Numeric(19, 4))
|
||||||
|
|
||||||
def to_dict(self):
|
def to_dict(self):
|
||||||
return {'id': self.id, 'sku': self.sku, 'status': 'repaired' if self.repair_result else 'pending'}
|
return {
|
||||||
|
'id': self.id,
|
||||||
|
'sku': self.sku,
|
||||||
|
'status': 'repaired' if self.repair_result else 'pending',
|
||||||
|
'manager': self.repair_manager
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
# 3. 报废表
|
# 3. 报废表
|
||||||
class TransScrap(db.Model):
|
class TransScrap(db.Model):
|
||||||
__tablename__ = 'trans_scrap'
|
__tablename__ = 'trans_scrap'
|
||||||
|
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
sku = db.Column(db.String(100))
|
sku = db.Column(db.String(100), index=True)
|
||||||
source_table = db.Column(db.String(50))
|
source_table = db.Column(db.String(50))
|
||||||
stock_id = db.Column(db.Integer)
|
stock_id = db.Column(db.Integer)
|
||||||
quantity = db.Column(db.Numeric(19, 4))
|
quantity = db.Column(db.Numeric(19, 4))
|
||||||
|
|
||||||
reason = db.Column(db.Text)
|
reason = db.Column(db.Text)
|
||||||
operator_name = db.Column(db.String(100))
|
operator_name = db.Column(db.String(100))
|
||||||
operation_time = db.Column(db.DateTime, default=datetime.now)
|
operation_time = db.Column(db.DateTime, default=datetime.now)
|
||||||
|
|
||||||
approver_name = db.Column(db.String(100))
|
approver_name = db.Column(db.String(100))
|
||||||
approval_status = db.Column(db.String(20))
|
approval_status = db.Column(db.String(20), default='pending') # pending, approved, rejected
|
||||||
|
|
||||||
cost_at_scrap = db.Column(db.Numeric(19, 4))
|
cost_at_scrap = db.Column(db.Numeric(19, 4))
|
||||||
total_loss = db.Column(db.Numeric(19, 4))
|
total_loss = db.Column(db.Numeric(19, 4))
|
||||||
|
|
||||||
def to_dict(self):
|
def to_dict(self):
|
||||||
return {'id': self.id, 'sku': self.sku, 'total_loss': float(self.total_loss) if self.total_loss else 0}
|
return {
|
||||||
|
'id': self.id,
|
||||||
|
'sku': self.sku,
|
||||||
|
'quantity': float(self.quantity) if self.quantity else 0,
|
||||||
|
'total_loss': float(self.total_loss) if self.total_loss else 0,
|
||||||
|
'reason': self.reason
|
||||||
|
}
|
||||||
@ -2,12 +2,8 @@
|
|||||||
|
|
||||||
from app.extensions import db
|
from app.extensions import db
|
||||||
from app.models.base import MaterialBase
|
from app.models.base import MaterialBase
|
||||||
|
from app.models.inbound.buy import StockBuy
|
||||||
# ==============================================================================
|
from app.models.inbound.semi import StockSemi
|
||||||
# ✅ 正确的引用方式
|
|
||||||
# ==============================================================================
|
|
||||||
from app.models.inbound.buy import StockBuy # 引用采购库存模型
|
|
||||||
from app.models.inbound.semi import StockSemi # 引用半成品库存模型
|
|
||||||
from sqlalchemy import or_
|
from sqlalchemy import or_
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
@ -28,11 +24,12 @@ class MaterialBaseService:
|
|||||||
if not keyword:
|
if not keyword:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
# 搜索名称或规格型号,且必须是启用的
|
# ✅ 搜索范围增加 common_name (俗名)
|
||||||
query = MaterialBase.query.filter(
|
query = MaterialBase.query.filter(
|
||||||
MaterialBase.is_enabled == True,
|
MaterialBase.is_enabled == True,
|
||||||
or_(
|
or_(
|
||||||
MaterialBase.name.ilike(f'%{keyword}%'),
|
MaterialBase.name.ilike(f'%{keyword}%'),
|
||||||
|
MaterialBase.common_name.ilike(f'%{keyword}%'),
|
||||||
MaterialBase.spec_model.ilike(f'%{keyword}%')
|
MaterialBase.spec_model.ilike(f'%{keyword}%')
|
||||||
)
|
)
|
||||||
).limit(20)
|
).limit(20)
|
||||||
@ -42,6 +39,7 @@ class MaterialBaseService:
|
|||||||
results.append({
|
results.append({
|
||||||
'id': item.id,
|
'id': item.id,
|
||||||
'name': item.name,
|
'name': item.name,
|
||||||
|
'commonName': item.common_name, # ✅ 返回俗名
|
||||||
'spec': item.spec_model,
|
'spec': item.spec_model,
|
||||||
'category': item.category,
|
'category': item.category,
|
||||||
'unit': item.unit,
|
'unit': item.unit,
|
||||||
@ -62,12 +60,14 @@ class MaterialBaseService:
|
|||||||
query = MaterialBase.query
|
query = MaterialBase.query
|
||||||
|
|
||||||
if filters:
|
if filters:
|
||||||
# 1. 关键词模糊搜索 (名称 或 规格型号)
|
# 1. 关键词模糊搜索 (名称 或 俗名 或 规格型号)
|
||||||
if filters.get('keyword'):
|
if filters.get('keyword'):
|
||||||
kw = f"%{filters['keyword']}%"
|
kw = f"%{filters['keyword']}%"
|
||||||
|
# ✅ 增加俗名搜索
|
||||||
query = query.filter(or_(
|
query = query.filter(or_(
|
||||||
MaterialBase.name.like(kw),
|
MaterialBase.name.ilike(kw),
|
||||||
MaterialBase.spec_model.like(kw)
|
MaterialBase.common_name.ilike(kw),
|
||||||
|
MaterialBase.spec_model.ilike(kw)
|
||||||
))
|
))
|
||||||
|
|
||||||
# 2. 精确筛选
|
# 2. 精确筛选
|
||||||
@ -101,6 +101,7 @@ class MaterialBaseService:
|
|||||||
raise ValueError("名称和规格型号不能为空")
|
raise ValueError("名称和规格型号不能为空")
|
||||||
|
|
||||||
# 1. 查重 (名称+规格型号 唯一)
|
# 1. 查重 (名称+规格型号 唯一)
|
||||||
|
# 注意:俗名不参与唯一性校验,允许重复或为空
|
||||||
exist = MaterialBase.query.filter_by(
|
exist = MaterialBase.query.filter_by(
|
||||||
name=data['name'],
|
name=data['name'],
|
||||||
spec_model=data['spec']
|
spec_model=data['spec']
|
||||||
@ -111,6 +112,7 @@ class MaterialBaseService:
|
|||||||
# 2. 创建对象
|
# 2. 创建对象
|
||||||
new_material = MaterialBase(
|
new_material = MaterialBase(
|
||||||
name=data['name'],
|
name=data['name'],
|
||||||
|
common_name=data.get('commonName'), # ✅ 读取俗名
|
||||||
spec_model=data['spec'],
|
spec_model=data['spec'],
|
||||||
category=data.get('category'),
|
category=data.get('category'),
|
||||||
material_type=data.get('type'),
|
material_type=data.get('type'),
|
||||||
@ -139,6 +141,7 @@ class MaterialBaseService:
|
|||||||
|
|
||||||
# 更新字段
|
# 更新字段
|
||||||
if 'name' in data: material.name = data['name']
|
if 'name' in data: material.name = data['name']
|
||||||
|
if 'commonName' in data: material.common_name = data['commonName'] # ✅ 更新俗名
|
||||||
if 'spec' in data: material.spec_model = data['spec']
|
if 'spec' in data: material.spec_model = data['spec']
|
||||||
if 'category' in data: material.category = data['category']
|
if 'category' in data: material.category = data['category']
|
||||||
if 'type' in data: material.material_type = data['type']
|
if 'type' in data: material.material_type = data['type']
|
||||||
@ -161,7 +164,6 @@ class MaterialBaseService:
|
|||||||
def delete_material(m_id):
|
def delete_material(m_id):
|
||||||
"""
|
"""
|
||||||
删除基础信息 (带依赖检查)
|
删除基础信息 (带依赖检查)
|
||||||
✅ 已升级:同时检查采购库(Buy)和半成品库(Semi)
|
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
material = MaterialBase.query.get(m_id)
|
material = MaterialBase.query.get(m_id)
|
||||||
|
|||||||
@ -1,14 +1,27 @@
|
|||||||
# inventory-backend/run.py
|
# inventory-backend/run.py
|
||||||
from app import create_app
|
from app import create_app
|
||||||
|
|
||||||
# 【关键】这一行必须在最外层,不能放在 if __name__ ... 里面!
|
# Gunicorn 或 uWSGI 会寻找名为 'app' 的实例
|
||||||
# Gunicorn 会导入这个变量
|
|
||||||
app = create_app()
|
app = create_app()
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
# 这里是开发调试用的,Docker/Gunicorn 不会执行这里
|
# =================================================
|
||||||
print("\n====== 当前所有注册路由 ======")
|
# 路由打印调试 (启动时会在控制台列出所有 URL)
|
||||||
for rule in app.url_map.iter_rules():
|
# 这一步能帮你确认 /api/inbound/base/list 是否存在
|
||||||
print(f"{rule} -> {rule.endpoint}")
|
# =================================================
|
||||||
|
print("\n====== 当前生效的路由映射 ======")
|
||||||
|
try:
|
||||||
|
# 按 URL 排序打印,方便查找
|
||||||
|
sorted_rules = sorted(app.url_map.iter_rules(), key=lambda x: str(x))
|
||||||
|
for rule in sorted_rules:
|
||||||
|
# 过滤掉一些系统自带的 static 路由,只显示 API
|
||||||
|
if 'api' in str(rule):
|
||||||
|
methods = ','.join(rule.methods - {'OPTIONS', 'HEAD'})
|
||||||
|
print(f"{str(rule):<50} | {methods:<10} | {rule.endpoint}")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
print("==============================\n")
|
print("==============================\n")
|
||||||
|
|
||||||
|
# 启动开发服务器
|
||||||
|
# 端口设置为 5000 (Flask 默认) 或 8000,请确保与前端 Vite 代理一致
|
||||||
app.run(host='0.0.0.0', port=8000, debug=True)
|
app.run(host='0.0.0.0', port=8000, debug=True)
|
||||||
@ -1,6 +1,43 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
// 1. 引入需要的图标组件
|
import { useRouter } from 'vue-router'
|
||||||
import { InfoFilled } from '@element-plus/icons-vue'
|
import { ElMessageBox, ElMessage } from 'element-plus'
|
||||||
|
import { InfoFilled, SwitchButton, UserFilled } from '@element-plus/icons-vue'
|
||||||
|
import { useUserStore } from '@/stores/user'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const userStore = useUserStore()
|
||||||
|
|
||||||
|
// --- 退出登录逻辑 Start ---
|
||||||
|
const handleLogout = () => {
|
||||||
|
ElMessageBox.confirm(
|
||||||
|
'确定要退出系统吗?',
|
||||||
|
'提示',
|
||||||
|
{
|
||||||
|
confirmButtonText: '确定退出',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.then(async () => {
|
||||||
|
// 1. 调用 Store 的 logout 清除状态
|
||||||
|
userStore.logout()
|
||||||
|
|
||||||
|
// 2. 提示消息
|
||||||
|
ElMessage({
|
||||||
|
type: 'success',
|
||||||
|
message: '已安全退出',
|
||||||
|
})
|
||||||
|
|
||||||
|
// 3. [关键修改] 强制跳转回登录页
|
||||||
|
// 使用 replace,这样用户点浏览器“返回”按钮不会又回到系统里
|
||||||
|
// 此时 store.token 已为空,路由守卫会放行 /login
|
||||||
|
await router.replace('/login')
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
// 取消操作
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// --- 退出登录逻辑 End ---
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -14,6 +51,22 @@ import { InfoFilled } from '@element-plus/icons-vue'
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="header-right">
|
<div class="header-right">
|
||||||
|
<div class="user-profile">
|
||||||
|
<el-avatar :size="32" :icon="UserFilled" class="user-avatar" />
|
||||||
|
<span class="user-name">{{ userStore.username || '管理员' }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-divider direction="vertical" />
|
||||||
|
|
||||||
|
<el-button
|
||||||
|
type="danger"
|
||||||
|
link
|
||||||
|
@click="handleLogout"
|
||||||
|
class="logout-btn"
|
||||||
|
>
|
||||||
|
<el-icon style="margin-right: 4px; font-size: 16px"><SwitchButton /></el-icon>
|
||||||
|
退出
|
||||||
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@ -31,35 +84,16 @@ import { InfoFilled } from '@element-plus/icons-vue'
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
/* 注意:App.vue 中的 style 标签通常不加 scoped,
|
/* 保持原有的样式,不需要改动 */
|
||||||
或者将全局样式(html, body)单独放在一个 style 标签中,
|
|
||||||
以确保 html, body 的高度设置能生效
|
|
||||||
*/
|
|
||||||
|
|
||||||
/* --- 全局重置样式 Start --- */
|
|
||||||
html, body {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
||||||
background-color: #f5f7fa; /* 整体背景色 */
|
|
||||||
overflow: hidden; /* 防止最外层出现双滚动条 */
|
|
||||||
}
|
|
||||||
|
|
||||||
#app {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
/* --- 全局重置样式 End --- */
|
|
||||||
|
|
||||||
.app-wrapper {
|
.app-wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: 100vh; /* 强制占满视口高度 */
|
height: 100vh;
|
||||||
|
width: 100vw;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
background-color: #f5f7fa;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 顶部栏样式 */
|
|
||||||
.app-header {
|
.app-header {
|
||||||
height: 60px;
|
height: 60px;
|
||||||
background-color: #ffffff;
|
background-color: #ffffff;
|
||||||
@ -68,56 +102,88 @@ html, body {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: 0 20px;
|
padding: 0 20px;
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
|
||||||
flex-shrink: 0; /* 禁止被压缩 */
|
flex-shrink: 0;
|
||||||
z-index: 1000; /* 确保头部在最上层 */
|
z-index: 1000;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo-container {
|
.logo-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.home-link {
|
.home-link {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 15px;
|
gap: 12px;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
height: 100%;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo {
|
.logo {
|
||||||
height: 36px; /* 稍微调整高度适配 */
|
height: 32px;
|
||||||
width: auto;
|
width: auto;
|
||||||
|
object-fit: contain;
|
||||||
}
|
}
|
||||||
|
|
||||||
.system-title {
|
.system-title {
|
||||||
font-size: 20px;
|
font-size: 18px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #303133;
|
color: #303133;
|
||||||
letter-spacing: 1px;
|
letter-spacing: 0.5px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-profile {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-avatar {
|
||||||
|
background-color: #409eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-name {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #606266;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logout-btn {
|
||||||
|
font-weight: 400;
|
||||||
|
padding: 4px 8px;
|
||||||
|
}
|
||||||
|
.logout-btn:hover {
|
||||||
|
color: #f56c6c !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 内容区样式 */
|
|
||||||
.app-content {
|
.app-content {
|
||||||
flex: 1; /* 自动占据剩余空间 */
|
flex: 1;
|
||||||
overflow: hidden; /* 这里设为 hidden,让内部的 Layout 组件去处理滚动 */
|
min-height: 0;
|
||||||
|
width: 100%;
|
||||||
position: relative;
|
position: relative;
|
||||||
/* 如果您希望整个页面有内边距,可以加 padding;
|
overflow: hidden;
|
||||||
但通常建议 padding 加在具体的业务页面里,保持 Layout 铺满 */
|
|
||||||
padding: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 底部栏样式 */
|
|
||||||
.app-footer {
|
.app-footer {
|
||||||
height: 36px;
|
height: 30px;
|
||||||
background-color: #f0f2f5;
|
background-color: #f0f2f5;
|
||||||
border-top: 1px solid #e4e7ed;
|
border-top: 1px solid #e4e7ed;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
flex-shrink: 0; /* 禁止被压缩 */
|
flex-shrink: 0;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #909399;
|
color: #909399;
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
@ -126,10 +192,5 @@ html, body {
|
|||||||
.version-tag {
|
.version-tag {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
font-weight: 500;
|
|
||||||
color: #e6a23c; /* 橙色警告色 */
|
|
||||||
background: rgba(230, 162, 60, 0.1); /* 淡橙色背景 */
|
|
||||||
padding: 2px 8px;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@ -1,19 +1,18 @@
|
|||||||
import { createRouter, createWebHistory } from 'vue-router'
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
// 使用 'type' 关键字导入 RouteRecordRaw
|
|
||||||
import type { RouteRecordRaw } from 'vue-router'
|
import type { RouteRecordRaw } from 'vue-router'
|
||||||
import Layout from '@/layout/index.vue'
|
import Layout from '@/layout/index.vue'
|
||||||
import { useUserStore } from '@/stores/user' // [新增] 引入 Store 用于权限判断
|
import { useUserStore } from '@/stores/user'
|
||||||
|
|
||||||
const routes: Array<RouteRecordRaw> = [
|
const routes: Array<RouteRecordRaw> = [
|
||||||
// [新增] 登录页 (不需要 Layout)
|
// 1. 登录页
|
||||||
{
|
{
|
||||||
path: '/login',
|
path: '/login',
|
||||||
name: 'Login',
|
name: 'Login',
|
||||||
component: () => import('@/views/login/index.vue'),
|
component: () => import('@/views/login/index.vue'),
|
||||||
meta: { hidden: true } // 不在侧边栏显示
|
meta: { hidden: true }
|
||||||
},
|
},
|
||||||
|
|
||||||
// 1. 首页 Dashboard
|
// 2. 首页 Dashboard
|
||||||
{
|
{
|
||||||
path: '/',
|
path: '/',
|
||||||
component: Layout,
|
component: Layout,
|
||||||
@ -28,7 +27,7 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
// 2. 基础信息 (对应 views/material/list.vue)
|
// 3. 基础信息
|
||||||
{
|
{
|
||||||
path: '/material',
|
path: '/material',
|
||||||
component: Layout,
|
component: Layout,
|
||||||
@ -37,14 +36,13 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
{
|
{
|
||||||
path: 'index',
|
path: 'index',
|
||||||
name: 'MaterialBase',
|
name: 'MaterialBase',
|
||||||
// 基础信息列表
|
|
||||||
component: () => import('@/views/material/list.vue'),
|
component: () => import('@/views/material/list.vue'),
|
||||||
meta: { title: '基础信息', icon: 'Box' }
|
meta: { title: '基础信息', icon: 'Box' }
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
// 3. 库存管理 (采购/半成品/成品/权益)
|
// 4. 库存管理
|
||||||
{
|
{
|
||||||
path: '/inventory',
|
path: '/inventory',
|
||||||
component: Layout,
|
component: Layout,
|
||||||
@ -54,35 +52,31 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
{
|
{
|
||||||
path: 'buy',
|
path: 'buy',
|
||||||
name: 'InventoryBuy',
|
name: 'InventoryBuy',
|
||||||
// 采购入库页面
|
|
||||||
component: () => import('@/views/stock/inbound/buy.vue'),
|
component: () => import('@/views/stock/inbound/buy.vue'),
|
||||||
meta: { title: '采购件' }
|
meta: { title: '采购件' }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'semi',
|
path: 'semi',
|
||||||
name: 'InventorySemi',
|
name: 'InventorySemi',
|
||||||
// 半成品页面
|
|
||||||
component: () => import('@/views/stock/inbound/semi.vue'),
|
component: () => import('@/views/stock/inbound/semi.vue'),
|
||||||
meta: { title: '半成品' }
|
meta: { title: '半成品' }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'product',
|
path: 'product',
|
||||||
name: 'InventoryProduct',
|
name: 'InventoryProduct',
|
||||||
// 成品页面
|
|
||||||
component: () => import('@/views/stock/inbound/product.vue'),
|
component: () => import('@/views/stock/inbound/product.vue'),
|
||||||
meta: { title: '成品' }
|
meta: { title: '成品' }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'service',
|
path: 'service',
|
||||||
name: 'InventoryService',
|
name: 'InventoryService',
|
||||||
// 服务权益页面
|
|
||||||
component: () => import('@/views/stock/inbound/service.vue'),
|
component: () => import('@/views/stock/inbound/service.vue'),
|
||||||
meta: { title: '服务权益' }
|
meta: { title: '服务权益' }
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
// 4. 业务操作 (借库/维修/报废)
|
// 5. 业务操作
|
||||||
{
|
{
|
||||||
path: '/operation',
|
path: '/operation',
|
||||||
component: Layout,
|
component: Layout,
|
||||||
@ -92,70 +86,43 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
{
|
{
|
||||||
path: 'borrow',
|
path: 'borrow',
|
||||||
name: 'OpBorrow',
|
name: 'OpBorrow',
|
||||||
// 借库页面
|
|
||||||
component: () => import('@/views/transaction/borrow.vue'),
|
component: () => import('@/views/transaction/borrow.vue'),
|
||||||
meta: { title: '借库' }
|
meta: { title: '借库' }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'repair',
|
path: 'repair',
|
||||||
name: 'OpRepair',
|
name: 'OpRepair',
|
||||||
// 维修页面 (指向 return.vue)
|
|
||||||
component: () => import('@/views/transaction/return.vue'),
|
component: () => import('@/views/transaction/return.vue'),
|
||||||
meta: { title: '维修' }
|
meta: { title: '维修' }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'scrap',
|
path: 'scrap',
|
||||||
name: 'OpScrap',
|
name: 'OpScrap',
|
||||||
// 报废页面
|
|
||||||
component: () => import('@/views/transaction/scrap.vue'),
|
component: () => import('@/views/transaction/scrap.vue'),
|
||||||
meta: { title: '报废' }
|
meta: { title: '报废' }
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
// 5. [修改] 系统管理 (权限控制 + 用户创建)
|
// 6. 系统管理
|
||||||
{
|
{
|
||||||
path: '/system',
|
path: '/system',
|
||||||
component: Layout,
|
component: Layout,
|
||||||
meta: {
|
meta: {
|
||||||
title: '系统管理',
|
title: '系统管理',
|
||||||
icon: 'Setting',
|
icon: 'Setting',
|
||||||
// 只有超级管理员和主管能看到此菜单
|
|
||||||
roles: ['super_admin', 'supervisor']
|
roles: ['super_admin', 'supervisor']
|
||||||
},
|
},
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: 'user-create',
|
path: 'user-create',
|
||||||
name: 'UserCreate',
|
name: 'UserCreate',
|
||||||
// 指向我们之前创建的新增用户页面
|
|
||||||
component: () => import('@/views/system/UserCreate.vue'),
|
component: () => import('@/views/system/UserCreate.vue'),
|
||||||
meta: { title: '账号开通', icon: 'User' }
|
meta: { title: '账号开通', icon: 'User' }
|
||||||
},
|
}
|
||||||
// 原有的日志页面保留 (如果文件存在)
|
|
||||||
// {
|
|
||||||
// path: 'log',
|
|
||||||
// name: 'OpLog',
|
|
||||||
// component: () => import('@/views/system/log.vue'),
|
|
||||||
// meta: { title: '操作日志', icon: 'Document' }
|
|
||||||
// }
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
/* * 暂时屏蔽 BOM
|
|
||||||
*/
|
|
||||||
// {
|
|
||||||
// path: '/bom',
|
|
||||||
// component: Layout,
|
|
||||||
// children: [
|
|
||||||
// {
|
|
||||||
// path: 'index',
|
|
||||||
// name: 'BOM',
|
|
||||||
// component: () => import('@/views/bom/index.vue'),
|
|
||||||
// meta: { title: 'BOM管理', icon: 'List' }
|
|
||||||
// }
|
|
||||||
// ]
|
|
||||||
// },
|
|
||||||
|
|
||||||
// 404 路由
|
// 404 路由
|
||||||
{
|
{
|
||||||
path: '/:pathMatch(.*)*',
|
path: '/:pathMatch(.*)*',
|
||||||
@ -170,37 +137,45 @@ const router = createRouter({
|
|||||||
})
|
})
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// [新增] 全局路由守卫:处理登录拦截与权限验证
|
// [关键修改] 全局路由守卫
|
||||||
// ==========================================
|
// ==========================================
|
||||||
router.beforeEach((to, from, next) => {
|
router.beforeEach((to, from, next) => {
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
const token = userStore.token
|
|
||||||
const userRole = userStore.role
|
|
||||||
|
|
||||||
// 1. 白名单:如果是去登录页,直接放行
|
// 1. 实时获取 Token (优先取 store,防止 store 未初始化取 localStorage)
|
||||||
|
const token = userStore.token || localStorage.getItem('token')
|
||||||
|
const userRole = userStore.role || localStorage.getItem('role') || 'user'
|
||||||
|
|
||||||
|
// 2. 如果要去的是登录页
|
||||||
if (to.path === '/login') {
|
if (to.path === '/login') {
|
||||||
next()
|
// 如果有 Token,说明已登录,踢回首页 (防止重复登录)
|
||||||
|
if (token) {
|
||||||
|
next('/')
|
||||||
|
} else {
|
||||||
|
// 没有 Token,允许访问登录页
|
||||||
|
next()
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 无 Token:强制跳转登录页
|
// 3. 如果去的不是登录页,但没有 Token
|
||||||
if (!token) {
|
if (!token) {
|
||||||
next('/login')
|
// 强制重定向到登录页
|
||||||
|
// 使用 replace 防止用户点击浏览器“返回”按钮时进入死循环
|
||||||
|
next({ path: '/login', replace: true })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 权限判断:检查 meta.roles
|
// 4. 权限判断 (已有 Token)
|
||||||
if (to.meta.roles && Array.isArray(to.meta.roles)) {
|
if (to.meta.roles && Array.isArray(to.meta.roles)) {
|
||||||
// 如果当前用户角色在允许列表中,放行
|
|
||||||
if (to.meta.roles.includes(userRole)) {
|
if (to.meta.roles.includes(userRole)) {
|
||||||
next()
|
next()
|
||||||
} else {
|
} else {
|
||||||
// 权限不足,重定向到首页或 403 页面 (这里简单跳回 dashboard)
|
// 权限不足,跳回首页
|
||||||
// 可以在这里触发一个 Element Plus 的 Message 提示
|
|
||||||
next('/dashboard')
|
next('/dashboard')
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 没有定义权限要求的页面,默认放行
|
// 无特殊权限要求,放行
|
||||||
next()
|
next()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@ -3,44 +3,85 @@ import { login } from '@/api/auth'
|
|||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
|
||||||
export const useUserStore = defineStore('user', () => {
|
export const useUserStore = defineStore('user', () => {
|
||||||
|
// 1. State: 初始化时优先从 localStorage 获取,防止刷新丢失
|
||||||
const token = ref(localStorage.getItem('token') || '')
|
const token = ref(localStorage.getItem('token') || '')
|
||||||
const role = ref(localStorage.getItem('role') || '') // 持久化角色
|
const role = ref(localStorage.getItem('role') || '')
|
||||||
const username = ref(localStorage.getItem('username') || '')
|
const username = ref(localStorage.getItem('username') || '')
|
||||||
|
|
||||||
|
// 2. Actions
|
||||||
|
// 登录逻辑
|
||||||
const handleLogin = async (loginForm: any) => {
|
const handleLogin = async (loginForm: any) => {
|
||||||
try {
|
try {
|
||||||
const res = await login(loginForm)
|
const res = await login(loginForm)
|
||||||
// res.data 结构: { access_token, user: { role, username, ... } }
|
|
||||||
const data = res.data
|
|
||||||
|
|
||||||
|
// [调试日志] 查看实际返回的数据结构 (调试完成后可删除)
|
||||||
|
console.log('Login API Response:', res)
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// [关键修复] 兼容 Axios 拦截器的不同处理方式
|
||||||
|
// 如果拦截器已经返回了 response.data,那么 res 本身就是数据对象
|
||||||
|
// 如果拦截器返回的是原始 response,那么数据在 res.data 中
|
||||||
|
// ============================================================
|
||||||
|
const data = res.data || res
|
||||||
|
|
||||||
|
// 安全检查:确保 data 存在且包含 access_token
|
||||||
|
if (!data || !data.access_token) {
|
||||||
|
console.error('Login Error: 响应数据中缺少 access_token', data)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新 Pinia 状态 (内存)
|
||||||
token.value = data.access_token
|
token.value = data.access_token
|
||||||
role.value = data.user.role
|
|
||||||
username.value = data.user.username
|
|
||||||
|
|
||||||
// 持久化存储 (简单处理,生产环境建议加密或仅存Token)
|
// 处理用户信息 (确保后端返回结构中有 user 字段)
|
||||||
|
if (data.user) {
|
||||||
|
role.value = data.user.role || 'user' // 默认给个 user 角色防止空
|
||||||
|
username.value = data.user.username || '用户'
|
||||||
|
|
||||||
|
// 持久化存储用户信息
|
||||||
|
localStorage.setItem('role', role.value)
|
||||||
|
localStorage.setItem('username', username.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 持久化存储 Token
|
||||||
localStorage.setItem('token', data.access_token)
|
localStorage.setItem('token', data.access_token)
|
||||||
localStorage.setItem('role', data.user.role)
|
|
||||||
localStorage.setItem('username', data.user.username)
|
|
||||||
|
|
||||||
return true
|
return true
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error('Login failed:', error)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 退出逻辑
|
||||||
const logout = () => {
|
const logout = () => {
|
||||||
|
// 1. 清空 Pinia 状态 (内存)
|
||||||
token.value = ''
|
token.value = ''
|
||||||
role.value = ''
|
role.value = ''
|
||||||
username.value = ''
|
username.value = ''
|
||||||
localStorage.clear()
|
|
||||||
window.location.reload()
|
// 2. 清空 LocalStorage (硬盘)
|
||||||
|
// 建议使用 removeItem 而不是 clear,避免误删该域名下其他非登录数据
|
||||||
|
localStorage.removeItem('token')
|
||||||
|
localStorage.removeItem('role')
|
||||||
|
localStorage.removeItem('username')
|
||||||
|
|
||||||
|
// 注意:这里不再执行 window.location.reload()
|
||||||
|
// 而是把跳转控制权交给调用者 (如 App.vue 中的 router.push)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 辅助函数:判断当前用户是否拥有某些角色
|
// 3. Getters / Helpers
|
||||||
|
// 判断当前用户是否拥有某些角色
|
||||||
const hasRole = (roles: string[]) => {
|
const hasRole = (roles: string[]) => {
|
||||||
return roles.includes(role.value)
|
return roles.includes(role.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
return { token, role, username, handleLogin, logout, hasRole }
|
return {
|
||||||
|
token,
|
||||||
|
role,
|
||||||
|
username,
|
||||||
|
handleLogin,
|
||||||
|
logout,
|
||||||
|
hasRole
|
||||||
|
}
|
||||||
})
|
})
|
||||||
@ -1,18 +1,31 @@
|
|||||||
|
/* inventory-web/src/style.css */
|
||||||
|
|
||||||
|
/* 1. 保留原有的字体定义,确保文字清晰好看 */
|
||||||
:root {
|
:root {
|
||||||
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
|
|
||||||
|
/* 颜色方案配置 */
|
||||||
color-scheme: light dark;
|
color-scheme: light dark;
|
||||||
color: rgba(255, 255, 255, 0.87);
|
color: rgba(255, 255, 255, 0.87);
|
||||||
background-color: #242424;
|
|
||||||
|
|
||||||
|
/* 字体渲染优化 */
|
||||||
font-synthesis: none;
|
font-synthesis: none;
|
||||||
text-rendering: optimizeLegibility;
|
text-rendering: optimizeLegibility;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 2. 针对亮色模式的颜色适配 (保留) */
|
||||||
|
@media (prefers-color-scheme: light) {
|
||||||
|
:root {
|
||||||
|
color: #213547;
|
||||||
|
background-color: #ffffff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 3. 链接的基本样式 (保留,但通常 RouterLink 会覆盖) */
|
||||||
a {
|
a {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: #646cff;
|
color: #646cff;
|
||||||
@ -22,58 +35,44 @@ a:hover {
|
|||||||
color: #535bf2;
|
color: #535bf2;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
/* -------------------------------------------------
|
||||||
|
【重要修改区域】
|
||||||
|
下面的代码是为了修复“无法铺满全屏”的问题
|
||||||
|
-------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* 4. 全局盒模型修复:防止 padding 撑大元素 */
|
||||||
|
*, *::before, *::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 5. 重置 body 和 html */
|
||||||
|
html, body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
display: flex;
|
padding: 0;
|
||||||
place-items: center;
|
width: 100%;
|
||||||
min-width: 320px;
|
height: 100%; /* 强制高度占满 */
|
||||||
min-height: 100vh;
|
|
||||||
}
|
/* !!! 删除了原有的 display: flex; place-items: center;
|
||||||
|
这是导致你页面缩在中间的罪魁祸首
|
||||||
h1 {
|
*/
|
||||||
font-size: 3.2em;
|
display: block;
|
||||||
line-height: 1.1;
|
|
||||||
}
|
overflow: hidden; /* 防止最外层出现双滚动条 */
|
||||||
|
|
||||||
button {
|
|
||||||
border-radius: 8px;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
padding: 0.6em 1.2em;
|
|
||||||
font-size: 1em;
|
|
||||||
font-weight: 500;
|
|
||||||
font-family: inherit;
|
|
||||||
background-color: #1a1a1a;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: border-color 0.25s;
|
|
||||||
}
|
|
||||||
button:hover {
|
|
||||||
border-color: #646cff;
|
|
||||||
}
|
|
||||||
button:focus,
|
|
||||||
button:focus-visible {
|
|
||||||
outline: 4px auto -webkit-focus-ring-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card {
|
|
||||||
padding: 2em;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 6. 重置 #app 挂载点 */
|
||||||
#app {
|
#app {
|
||||||
max-width: 1280px;
|
/* !!! 删除了 max-width: 1280px; padding: 2rem; text-align: center;
|
||||||
margin: 0 auto;
|
这是导致你页面两边留白、无法全屏的原因
|
||||||
padding: 2rem;
|
*/
|
||||||
text-align: center;
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: light) {
|
/* 注意:原文件中关于 button, .card 的样式已被删除,
|
||||||
:root {
|
因为你的项目中引入了 Element Plus,
|
||||||
color: #213547;
|
保留原生 button 样式会和 Element Plus 组件产生冲突。
|
||||||
background-color: #ffffff;
|
*/
|
||||||
}
|
|
||||||
a:hover {
|
|
||||||
color: #747bff;
|
|
||||||
}
|
|
||||||
button {
|
|
||||||
background-color: #f9f9f9;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -5,7 +5,7 @@
|
|||||||
<div class="filter-container">
|
<div class="filter-container">
|
||||||
<el-input
|
<el-input
|
||||||
v-model="queryParams.keyword"
|
v-model="queryParams.keyword"
|
||||||
placeholder="请输入名称或规格 (支持模糊搜索)"
|
placeholder="请输入名称、俗名或规格"
|
||||||
style="width: 240px; margin-right: 10px;"
|
style="width: 240px; margin-right: 10px;"
|
||||||
clearable
|
clearable
|
||||||
@input="handleInputSearch"
|
@input="handleInputSearch"
|
||||||
@ -82,6 +82,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<el-checkbox v-model="columns.id.visible" label="ID" />
|
<el-checkbox v-model="columns.id.visible" label="ID" />
|
||||||
<el-checkbox v-model="columns.name.visible" label="名称" />
|
<el-checkbox v-model="columns.name.visible" label="名称" />
|
||||||
|
<el-checkbox v-model="columns.commonName.visible" label="俗名" />
|
||||||
<el-checkbox v-model="columns.category.visible" label="类别" />
|
<el-checkbox v-model="columns.category.visible" label="类别" />
|
||||||
<el-checkbox v-model="columns.type.visible" label="类型" />
|
<el-checkbox v-model="columns.type.visible" label="类型" />
|
||||||
<el-checkbox v-model="columns.spec.visible" label="规格型号" />
|
<el-checkbox v-model="columns.spec.visible" label="规格型号" />
|
||||||
@ -103,7 +104,15 @@
|
|||||||
style="width: 100%; margin-top: 15px"
|
style="width: 100%; margin-top: 15px"
|
||||||
>
|
>
|
||||||
<el-table-column v-if="columns.id.visible" prop="id" label="ID" min-width="80" align="center" fixed="left" />
|
<el-table-column v-if="columns.id.visible" prop="id" label="ID" min-width="80" align="center" fixed="left" />
|
||||||
<el-table-column v-if="columns.name.visible" prop="name" label="基础信息名称" min-width="180" show-overflow-tooltip />
|
<el-table-column v-if="columns.name.visible" prop="name" label="名称" min-width="160" show-overflow-tooltip />
|
||||||
|
|
||||||
|
<el-table-column v-if="columns.commonName.visible" prop="commonName" label="俗名" min-width="140" show-overflow-tooltip>
|
||||||
|
<template #default="scope">
|
||||||
|
<span v-if="scope.row.commonName">{{ scope.row.commonName }}</span>
|
||||||
|
<span v-else style="color: #ccc;">-</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
<el-table-column v-if="columns.category.visible" prop="category" label="类别" min-width="120" align="center" show-overflow-tooltip>
|
<el-table-column v-if="columns.category.visible" prop="category" label="类别" min-width="120" align="center" show-overflow-tooltip>
|
||||||
<template #default="scope">{{ scope.row.category || '-' }}</template>
|
<template #default="scope">{{ scope.row.category || '-' }}</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
@ -162,9 +171,18 @@
|
|||||||
>
|
>
|
||||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="110px">
|
<el-form ref="formRef" :model="form" :rules="rules" label-width="110px">
|
||||||
|
|
||||||
<el-form-item label="名称" prop="name">
|
<el-row>
|
||||||
<el-input v-model="form.name" placeholder="请输入基础信息名称" />
|
<el-col :span="12">
|
||||||
</el-form-item>
|
<el-form-item label="名称" prop="name">
|
||||||
|
<el-input v-model="form.name" placeholder="标准名称" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="俗名" prop="commonName">
|
||||||
|
<el-input v-model="form.commonName" placeholder="日常叫法/别名" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
<el-row>
|
<el-row>
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
@ -254,6 +272,7 @@ import {
|
|||||||
interface MaterialBaseVO {
|
interface MaterialBaseVO {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
|
commonName?: string; // ✅ 新增类型定义
|
||||||
category: string;
|
category: string;
|
||||||
type: string;
|
type: string;
|
||||||
spec: string;
|
spec: string;
|
||||||
@ -284,6 +303,7 @@ const tableSize = ref<'large' | 'default' | 'small'>('large');
|
|||||||
const columns = reactive({
|
const columns = reactive({
|
||||||
id: { visible: true },
|
id: { visible: true },
|
||||||
name: { visible: true },
|
name: { visible: true },
|
||||||
|
commonName: { visible: true }, // ✅ 新增列控制
|
||||||
category: { visible: true },
|
category: { visible: true },
|
||||||
type: { visible: true },
|
type: { visible: true },
|
||||||
spec: { visible: true },
|
spec: { visible: true },
|
||||||
@ -316,6 +336,7 @@ const formRef = ref<FormInstance>();
|
|||||||
const initForm = {
|
const initForm = {
|
||||||
id: undefined,
|
id: undefined,
|
||||||
name: '',
|
name: '',
|
||||||
|
commonName: '', // ✅ 初始化新增字段
|
||||||
category: '',
|
category: '',
|
||||||
type: '',
|
type: '',
|
||||||
spec: '',
|
spec: '',
|
||||||
@ -350,13 +371,10 @@ const extractDynamicOptions = (items: MaterialBaseVO[]) => {
|
|||||||
typeOptions.value = Array.from(newTypes);
|
typeOptions.value = Array.from(newTypes);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 【核心新增】Autocomplete 的建议查询方法
|
|
||||||
// 格式化数据以适配 el-autocomplete 的回调参数格式 [{ value: 'abc' }]
|
|
||||||
const querySearchCategory = (queryString: string, cb: any) => {
|
const querySearchCategory = (queryString: string, cb: any) => {
|
||||||
const results = queryString
|
const results = queryString
|
||||||
? categoryOptions.value.filter(item => item.toLowerCase().includes(queryString.toLowerCase()))
|
? categoryOptions.value.filter(item => item.toLowerCase().includes(queryString.toLowerCase()))
|
||||||
: categoryOptions.value;
|
: categoryOptions.value;
|
||||||
// el-autocomplete 默认只展示 value 属性
|
|
||||||
const formattedResults = results.map(item => ({ value: item }));
|
const formattedResults = results.map(item => ({ value: item }));
|
||||||
cb(formattedResults);
|
cb(formattedResults);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user