物料-采购件入库页面功能实现
This commit is contained in:
52
docker-compose.yml
Normal file
52
docker-compose.yml
Normal file
@ -0,0 +1,52 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
# --- 数据库服务 ---
|
||||
db:
|
||||
image: postgres:15-alpine
|
||||
container_name: inventory_db
|
||||
restart: always
|
||||
environment:
|
||||
POSTGRES_USER: test
|
||||
POSTGRES_PASSWORD: 1234
|
||||
POSTGRES_DB: inventory_system
|
||||
volumes:
|
||||
# 数据持久化
|
||||
- ./pgdata_docker:/var/lib/postgresql/data
|
||||
ports:
|
||||
- "5434:5432"
|
||||
|
||||
# --- 后端 Flask 服务 ---
|
||||
backend:
|
||||
build:
|
||||
context: ./inventory-backend # 【修改】指向你的新后端目录
|
||||
container_name: inventory_api
|
||||
restart: always
|
||||
ports:
|
||||
- "8000:8000"
|
||||
volumes:
|
||||
- ./inventory-backend:/app # 挂载代码,实现热更新
|
||||
# 加上 --reload 参数,代码变了自动重启
|
||||
command: gunicorn -c gunicorn.conf.py run:app --reload
|
||||
environment:
|
||||
# Host 必须写 'db'
|
||||
DATABASE_URL: postgresql://test:1234@db:5432/inventory_system
|
||||
depends_on:
|
||||
- db
|
||||
|
||||
# --- 前端 Vue+Nginx 服务 ---
|
||||
# --- 前端 Vue 开发服务 ---
|
||||
frontend:
|
||||
build:
|
||||
context: ./inventory-web
|
||||
container_name: inventory_ui
|
||||
restart: always
|
||||
# 【重点1】把本地代码挂载进去,实现“热更新”
|
||||
volumes:
|
||||
- ./inventory-web:/app
|
||||
- /app/node_modules # 排除 node_modules,防止冲突
|
||||
# 【重点2】开发模式端口通常是 5173
|
||||
ports:
|
||||
- "5173:5173"
|
||||
depends_on:
|
||||
- backend
|
||||
6
inventory-backend/.dockerignore
Normal file
6
inventory-backend/.dockerignore
Normal file
@ -0,0 +1,6 @@
|
||||
venv/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
.git/
|
||||
.env
|
||||
pgdata/
|
||||
17
inventory-backend/Dockerfile
Normal file
17
inventory-backend/Dockerfile
Normal file
@ -0,0 +1,17 @@
|
||||
# 【修改】使用与你环境一致的 Python 3.8
|
||||
FROM python:3.8
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# 1. 复制依赖并安装
|
||||
COPY requirements.txt .
|
||||
# 安装依赖 + gunicorn
|
||||
RUN pip install --no-cache-dir -r requirements.txt && \
|
||||
pip install --no-cache-dir gunicorn
|
||||
|
||||
# 2. 复制后端代码
|
||||
COPY . .
|
||||
|
||||
# 3. 启动命令
|
||||
# 假设你的入口文件是 run.py,实例叫 app
|
||||
CMD ["gunicorn", "-w", "4", "-b", "0.0.0.0:8000", "run:app"]
|
||||
0
inventory-backend/__init__.py
Normal file
0
inventory-backend/__init__.py
Normal file
@ -1,4 +1,3 @@
|
||||
# 文件路径: app/__init__.py
|
||||
from flask import Flask
|
||||
from config import Config
|
||||
from app.extensions import db, migrate, cors
|
||||
@ -10,14 +9,30 @@ def create_app():
|
||||
# 初始化插件
|
||||
db.init_app(app)
|
||||
migrate.init_app(app, db)
|
||||
cors.init_app(app) # 允许前端访问
|
||||
# 确保跨域配置正确,允许前端访问
|
||||
cors.init_app(app, resources={r"/api/*": {"origins": "*"}})
|
||||
|
||||
# 注册蓝图 (Blueprints)
|
||||
from app.api.v1.stocks import stocks_bp
|
||||
app.register_blueprint(stocks_bp, url_prefix='/api/v1/stocks')
|
||||
# --- 注册蓝图 ---
|
||||
|
||||
# 可以在这里打印一下路由,方便调试
|
||||
print("已注册路由:")
|
||||
print(app.url_map)
|
||||
# 1. 保持原有的 stocks 模块
|
||||
try:
|
||||
from app.api.v1.stocks import stocks_bp
|
||||
app.register_blueprint(stocks_bp, url_prefix='/api/v1/stocks')
|
||||
except ImportError as e:
|
||||
print(f"⚠️ 警告: 原有 stocks 蓝图导入失败: {e}")
|
||||
|
||||
# 2. 注册新的入库聚合蓝图
|
||||
# 核心:必须先导入,再注册。路径对应 app/api/v1/inbound/__init__.py
|
||||
try:
|
||||
from app.api.v1.inbound import inbound_bp
|
||||
# 最终路径结构:/api/v1/inbound/buy/list
|
||||
app.register_blueprint(inbound_bp, url_prefix='/api/v1/inbound')
|
||||
print("✅ 入库模块蓝图注册成功")
|
||||
except ImportError as e:
|
||||
print(f"❌ 严重错误: 入库模块 inbound 蓝图导入失败: {e}")
|
||||
|
||||
# 打印路由映射,仅在本地调试时建议开启
|
||||
# with app.app_context():
|
||||
# print(app.url_map)
|
||||
|
||||
return app
|
||||
14
inventory-backend/app/api/v1/inbound/__init__.py
Normal file
14
inventory-backend/app/api/v1/inbound/__init__.py
Normal file
@ -0,0 +1,14 @@
|
||||
from flask import Blueprint
|
||||
from .buy import inbound_buy_bp
|
||||
# 后续如果有 semi.py 或 product.py,在这里导入
|
||||
# from .semi import inbound_semi_bp
|
||||
|
||||
# 创建聚合蓝图
|
||||
inbound_bp = Blueprint('inbound', __name__)
|
||||
|
||||
# 挂载子模块。url_prefix 会进行路径拼接
|
||||
# 路径变为:/buy/...
|
||||
inbound_bp.register_blueprint(inbound_buy_bp, url_prefix='/buy')
|
||||
|
||||
# 后续扩展:
|
||||
# inbound_bp.register_blueprint(inbound_semi_bp, url_prefix='/semi')
|
||||
70
inventory-backend/app/api/v1/inbound/buy.py
Normal file
70
inventory-backend/app/api/v1/inbound/buy.py
Normal file
@ -0,0 +1,70 @@
|
||||
from flask import Blueprint, request, jsonify
|
||||
from app.services.inbound.buy_service import BuyInboundService
|
||||
import traceback
|
||||
|
||||
# 定义蓝图
|
||||
inbound_buy_bp = Blueprint('inbound_buy', __name__)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 1. 获取列表 (GET)
|
||||
# ------------------------------------------------------------------
|
||||
@inbound_buy_bp.route('/list', methods=['GET'])
|
||||
def get_list():
|
||||
try:
|
||||
page = request.args.get('page', 1, type=int)
|
||||
limit = request.args.get('pageSize', 15, type=int)
|
||||
|
||||
result = BuyInboundService.get_list(page, limit)
|
||||
return jsonify({
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"data": result
|
||||
})
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
return jsonify({"code": 500, "msg": str(e)}), 500
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 2. 新增入库 (POST)
|
||||
# ------------------------------------------------------------------
|
||||
@inbound_buy_bp.route('/submit', methods=['POST'])
|
||||
def submit():
|
||||
try:
|
||||
data = request.get_json()
|
||||
if not data:
|
||||
return jsonify({"code": 400, "msg": "No data provided"}), 400
|
||||
|
||||
BuyInboundService.handle_inbound(data)
|
||||
return jsonify({"code": 200, "msg": "入库成功"})
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
return jsonify({"code": 500, "msg": str(e)}), 500
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 3. 更新入库 (PUT)
|
||||
# ------------------------------------------------------------------
|
||||
@inbound_buy_bp.route('/<int:id>', methods=['PUT'])
|
||||
def update_buy(id):
|
||||
try:
|
||||
data = request.get_json()
|
||||
BuyInboundService.update_inbound(id, data)
|
||||
return jsonify({"code": 200, "msg": "更新成功"})
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
return jsonify({"code": 500, "msg": str(e)}), 500
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 4. 删除入库 (DELETE)
|
||||
# ------------------------------------------------------------------
|
||||
@inbound_buy_bp.route('/<int:id>', methods=['DELETE'])
|
||||
def delete_buy(id):
|
||||
try:
|
||||
BuyInboundService.delete_inbound(id)
|
||||
return jsonify({"code": 200, "msg": "删除成功"})
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
return jsonify({"code": 500, "msg": str(e)}), 500
|
||||
0
inventory-backend/app/api/v1/inbound/product.py
Normal file
0
inventory-backend/app/api/v1/inbound/product.py
Normal file
0
inventory-backend/app/api/v1/inbound/semi.py
Normal file
0
inventory-backend/app/api/v1/inbound/semi.py
Normal file
@ -1,45 +1,87 @@
|
||||
from flask import Blueprint, request, jsonify
|
||||
from app.services.stock_service import StockService
|
||||
from app.schemas.stock_schema import stock_buy_schema
|
||||
|
||||
# 确保这两个引用路径是存在的,如果报错说明文件没建好
|
||||
try:
|
||||
from app.services.stock_service import StockService
|
||||
from app.schemas.stock_schema import stock_buy_schema
|
||||
except ImportError as e:
|
||||
# 如果服务还没写好,这里会打印错误,防止整个后端起不来
|
||||
print(f"❌ 导入服务出错: {e}")
|
||||
StockService = None
|
||||
stock_buy_schema = None
|
||||
|
||||
stocks_bp = Blueprint('stocks', __name__)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 1. 获取入库列表
|
||||
# URL: /api/v1/stocks/inbound (GET)
|
||||
# ------------------------------------------------------------------
|
||||
@stocks_bp.route('/inbound', methods=['GET'])
|
||||
def get_inbound_list():
|
||||
page = request.args.get('page', 1, type=int)
|
||||
limit = request.args.get('pageSize', 10, type=int)
|
||||
if not StockService:
|
||||
return jsonify({'code': 500, 'msg': '后端服务未初始化'}), 500
|
||||
|
||||
result = StockService.get_list(page, limit)
|
||||
try:
|
||||
page = request.args.get('page', 1, type=int)
|
||||
limit = request.args.get('pageSize', 10, type=int)
|
||||
|
||||
return jsonify({
|
||||
'code': 200,
|
||||
'msg': 'success',
|
||||
'data': result
|
||||
})
|
||||
# 调用 Service 层获取数据
|
||||
result = StockService.get_list(page, limit)
|
||||
|
||||
return jsonify({
|
||||
'code': 200,
|
||||
'msg': 'success',
|
||||
'data': result
|
||||
})
|
||||
except Exception as e:
|
||||
print(f"获取列表报错: {e}")
|
||||
return jsonify({'code': 500, 'msg': '服务器内部错误'}), 500
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 2. 新增入库单
|
||||
# URL: /api/v1/stocks/inbound (POST)
|
||||
# ------------------------------------------------------------------
|
||||
@stocks_bp.route('/inbound', methods=['POST'])
|
||||
def create_inbound():
|
||||
if not StockService:
|
||||
return jsonify({'code': 500, 'msg': '后端服务未初始化'}), 500
|
||||
|
||||
json_data = request.get_json()
|
||||
if not json_data:
|
||||
return jsonify({'code': 400, 'msg': '没有接收到数据'}), 400
|
||||
|
||||
try:
|
||||
# 1. 参数校验
|
||||
# 1. 参数校验 (Marshmallow Schema)
|
||||
data = stock_buy_schema.load(json_data)
|
||||
|
||||
# 2. 调用业务逻辑
|
||||
new_stock = StockService.create_inbound(data)
|
||||
|
||||
# 3. 返回成功
|
||||
# 注意:确保 new_stock 对象有 to_dict() 方法,否则这里会报错
|
||||
return jsonify({
|
||||
'code': 200,
|
||||
'msg': '入库成功',
|
||||
'data': new_stock.to_dict()
|
||||
'data': new_stock.to_dict() if hasattr(new_stock, 'to_dict') else str(new_stock)
|
||||
}), 201
|
||||
|
||||
except Exception as e:
|
||||
# 捕获 ValueError 或 SQLAlchemyError
|
||||
# 捕获校验错误或数据库错误
|
||||
print(f"入库报错: {e}")
|
||||
return jsonify({'code': 400, 'msg': str(e)}), 400
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 3. 更新入库单
|
||||
# URL: /api/v1/stocks/inbound/<id> (PUT)
|
||||
# ------------------------------------------------------------------
|
||||
@stocks_bp.route('/inbound/<int:id>', methods=['PUT'])
|
||||
def update_inbound(id):
|
||||
if not StockService:
|
||||
return jsonify({'code': 500, 'msg': '后端服务未初始化'}), 500
|
||||
|
||||
json_data = request.get_json()
|
||||
try:
|
||||
StockService.update_inbound(id, json_data)
|
||||
@ -48,8 +90,15 @@ def update_inbound(id):
|
||||
return jsonify({'code': 400, 'msg': str(e)}), 400
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 4. 删除入库单
|
||||
# URL: /api/v1/stocks/inbound/<id> (DELETE)
|
||||
# ------------------------------------------------------------------
|
||||
@stocks_bp.route('/inbound/<int:id>', methods=['DELETE'])
|
||||
def delete_inbound(id):
|
||||
if not StockService:
|
||||
return jsonify({'code': 500, 'msg': '后端服务未初始化'}), 500
|
||||
|
||||
try:
|
||||
StockService.delete_inbound(id)
|
||||
return jsonify({'code': 200, 'msg': '删除成功'})
|
||||
|
||||
@ -1,34 +1,32 @@
|
||||
#material.py
|
||||
from app.extensions import db
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class MaterialBase(db.Model):
|
||||
__tablename__ = 'material_base'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
# 核心字段
|
||||
sku_code = db.Column(db.String(100), unique=True, nullable=False) # 唯一编码
|
||||
name = db.Column(db.String(255), nullable=False) # 名称
|
||||
spec_model = db.Column(db.String(255)) # 规格型号
|
||||
unit = db.Column(db.String(50)) # 单位
|
||||
category = db.Column(db.String(100)) # 分类
|
||||
name = db.Column(db.String(255), nullable=False) # 名称
|
||||
category = db.Column(db.String(100)) # 类别
|
||||
material_type = db.Column(db.String(100)) # 类型
|
||||
spec_model = db.Column(db.String(255)) # 规格型号
|
||||
unit = db.Column(db.String(50)) # 计量单位
|
||||
visibility_level = db.Column(db.Integer, default=0) # 信息可见等级
|
||||
manual_link = db.Column(db.Text) # 通用说明书
|
||||
product_image = db.Column(db.Text) # 通用产品图
|
||||
is_enabled = db.Column(db.Boolean, default=True) # 是否启用
|
||||
|
||||
# 审计字段 (自动记录时间)
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
|
||||
# 关联关系 (让 StockBuy 可以反向找到 Material)
|
||||
# 这里的 dynamic 允许在 material.stock_buys 时进行进一步过滤
|
||||
# 【核心关联】
|
||||
# 这里定义了反向关系,lazy='dynamic' 允许我们后续做 count() 查询
|
||||
# cascade='all, delete-orphan' 并不是在这里用的,因为我们是手动控制逻辑
|
||||
stock_buys = db.relationship('StockBuy', back_populates='material', lazy='dynamic')
|
||||
|
||||
def to_dict(self):
|
||||
"""将对象转换为字典,方便接口返回"""
|
||||
return {
|
||||
'id': self.id,
|
||||
'sku_code': self.sku_code,
|
||||
'name': self.name,
|
||||
'spec_model': self.spec_model,
|
||||
'unit': self.unit,
|
||||
'category': self.category,
|
||||
'created_at': self.created_at.strftime('%Y-%m-%d %H:%M:%S') if self.created_at else None
|
||||
'material_type': self.material_type,
|
||||
'spec_model': self.spec_model,
|
||||
'unit': self.unit
|
||||
}
|
||||
@ -1,3 +1,4 @@
|
||||
#stock.py
|
||||
from app.extensions import db
|
||||
from datetime import datetime
|
||||
|
||||
@ -7,50 +8,62 @@ class StockBuy(db.Model):
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
|
||||
# 外键:必须关联一个 MaterialBase 的 ID
|
||||
material_id = db.Column(db.Integer, db.ForeignKey('material_base.id'), nullable=False)
|
||||
# 【核心关联】
|
||||
# 这里明确指定了 base_id 是外键,关联 material_base 表的 id
|
||||
base_id = db.Column(db.Integer, db.ForeignKey('material_base.id'), nullable=False)
|
||||
|
||||
# 业务数据
|
||||
inbound_date = db.Column(db.DateTime, default=datetime.utcnow) # 入库时间
|
||||
batch_no = db.Column(db.String(100)) # 批次号
|
||||
warehouse_loc = db.Column(db.String(100)) # 库位
|
||||
supplier_name = db.Column(db.String(255)) # 供应商
|
||||
sku = db.Column(db.String(100))
|
||||
in_date = db.Column(db.Date)
|
||||
serial_number = db.Column(db.String(100))
|
||||
batch_number = db.Column(db.String(100))
|
||||
|
||||
# 数量与状态
|
||||
qty_inbound = db.Column(db.Numeric(19, 4), default=0) # 初始入库量
|
||||
qty_current = db.Column(db.Numeric(19, 4), default=0) # 当前剩余量
|
||||
qty_available = db.Column(db.Numeric(19, 4), default=0) # 当前可用量
|
||||
status = db.Column(db.String(50), default='NORMAL')
|
||||
# 数量
|
||||
in_quantity = db.Column(db.Numeric(19, 4), default=0)
|
||||
stock_quantity = db.Column(db.Numeric(19, 4), default=0)
|
||||
available_quantity = db.Column(db.Numeric(19, 4), default=0)
|
||||
|
||||
# 财务数据
|
||||
price_unit = db.Column(db.Numeric(19, 4), default=0) # 单价
|
||||
price_total = db.Column(db.Numeric(19, 4), default=0) # 总价
|
||||
# 状态与位置
|
||||
status = db.Column(db.String(50))
|
||||
inspection_status = db.Column(db.String(50))
|
||||
warehouse_location = db.Column(db.String(100))
|
||||
|
||||
# 建立与 MaterialBase 的双向关系
|
||||
# 财务与商务
|
||||
unit_price = db.Column(db.Numeric(19, 4), default=0)
|
||||
total_price = db.Column(db.Numeric(19, 4), default=0)
|
||||
currency = db.Column(db.String(20), default='CNY')
|
||||
exchange_rate = db.Column(db.Numeric(15, 6), default=1.0)
|
||||
supplier_name = db.Column(db.String(255))
|
||||
buyer_name = db.Column(db.String(100))
|
||||
buyer_email = db.Column(db.String(100))
|
||||
original_link = db.Column(db.Text)
|
||||
detail_link = db.Column(db.Text)
|
||||
arrival_photo = db.Column(db.Text)
|
||||
|
||||
# 【核心关联】
|
||||
# 建立对象级别的连接,方便通过 stock.material 访问基础信息
|
||||
material = db.relationship('MaterialBase', back_populates='stock_buys')
|
||||
|
||||
def to_dict(self):
|
||||
"""
|
||||
序列化方法:
|
||||
这里做了一个扁平化处理,把关联的 material 里的 name/sku 直接拿出来,
|
||||
方便前端表格直接显示,不用前端再去拼凑。
|
||||
"""
|
||||
"""序列化"""
|
||||
return {
|
||||
'id': self.id,
|
||||
'material_id': self.material_id,
|
||||
# 从关联对象获取基础信息
|
||||
'sku_code': self.material.sku_code if self.material else None,
|
||||
'base_id': self.base_id, # 前端需要这个ID来判断关联
|
||||
'material_name': self.material.name if self.material else None,
|
||||
'spec_model': self.material.spec_model if self.material else None,
|
||||
'unit': self.material.unit if self.material else None,
|
||||
'category': self.material.category if self.material else None,
|
||||
'unit': self.material.unit if self.material else None,
|
||||
'material_type': self.material.material_type if self.material else None,
|
||||
|
||||
# 本表信息
|
||||
'inbound_date': self.inbound_date.strftime('%Y-%m-%d %H:%M:%S') if self.inbound_date else None,
|
||||
'batch_no': self.batch_no,
|
||||
'warehouse_loc': self.warehouse_loc,
|
||||
'supplier_name': self.supplier_name,
|
||||
'qty_inbound': float(self.qty_inbound) if self.qty_inbound else 0,
|
||||
'price_unit': float(self.price_unit) if self.price_unit else 0,
|
||||
'price_total': float(self.price_total) if self.price_total else 0,
|
||||
'sku': self.sku,
|
||||
'inbound_date': self.in_date.strftime('%Y-%m-%d') if self.in_date else None,
|
||||
'serial_number': self.serial_number,
|
||||
'batch_number': self.batch_number,
|
||||
'qty_inbound': float(self.in_quantity) if self.in_quantity else 0,
|
||||
'qty_stock': float(self.stock_quantity) if self.stock_quantity else 0,
|
||||
'qty_available': float(self.available_quantity) if self.available_quantity else 0,
|
||||
'warehouse_loc': self.warehouse_location,
|
||||
'status': self.status,
|
||||
'price_unit': float(self.unit_price) if self.unit_price else 0,
|
||||
'price_total': float(self.total_price) if self.total_price else 0,
|
||||
'supplier_name': self.supplier_name
|
||||
}
|
||||
0
inventory-backend/app/services/inbound/__init__.py
Normal file
0
inventory-backend/app/services/inbound/__init__.py
Normal file
176
inventory-backend/app/services/inbound/buy_service.py
Normal file
176
inventory-backend/app/services/inbound/buy_service.py
Normal file
@ -0,0 +1,176 @@
|
||||
from app.extensions import db
|
||||
from app.models.material import MaterialBase
|
||||
from app.models.stock import StockBuy
|
||||
from datetime import datetime
|
||||
import traceback
|
||||
|
||||
|
||||
class BuyInboundService:
|
||||
@staticmethod
|
||||
def handle_inbound(data):
|
||||
"""新增入库:自动关联/创建基础信息 + 创建库存记录"""
|
||||
try:
|
||||
# 0. 基础校验
|
||||
if not data.get('spec_model') or not data.get('material_name'):
|
||||
raise ValueError("缺少必要的物料名称或规格型号")
|
||||
|
||||
# 1. 关联逻辑:通过规格型号(spec_model)查找基础库
|
||||
material = MaterialBase.query.filter_by(spec_model=data['spec_model']).first()
|
||||
|
||||
# 如果不存在,则新建 MaterialBase
|
||||
if not material:
|
||||
material = MaterialBase(
|
||||
name=data['material_name'],
|
||||
spec_model=data['spec_model'],
|
||||
category=data.get('category'),
|
||||
material_type='采购件',
|
||||
unit=data.get('unit'),
|
||||
visibility_level=data.get('visibility_level', 0),
|
||||
manual_link=data.get('manual_link'),
|
||||
product_image=data.get('product_image'),
|
||||
is_enabled=True
|
||||
)
|
||||
db.session.add(material)
|
||||
db.session.flush() # 立即执行,拿到 material.id
|
||||
|
||||
# 2. 处理日期 (兼容性处理)
|
||||
in_date_val = None
|
||||
if data.get('in_date'):
|
||||
try:
|
||||
in_date_val = datetime.strptime(data['in_date'], '%Y-%m-%d %H:%M:%S').date()
|
||||
except ValueError:
|
||||
try:
|
||||
in_date_val = datetime.strptime(data['in_date'], '%Y-%m-%d').date()
|
||||
except ValueError:
|
||||
in_date_val = datetime.utcnow().date()
|
||||
|
||||
# 3. 创建 StockBuy
|
||||
new_stock = StockBuy(
|
||||
base_id=material.id,
|
||||
sku=data.get('sku'),
|
||||
in_date=in_date_val,
|
||||
serial_number=data.get('serial_number'),
|
||||
batch_number=data.get('batch_number'),
|
||||
status='在库',
|
||||
inspection_status=data.get('inspection_status'),
|
||||
in_quantity=data.get('in_quantity', 0),
|
||||
stock_quantity=data.get('in_quantity', 0),
|
||||
available_quantity=data.get('in_quantity', 0),
|
||||
warehouse_location=data.get('warehouse_location'),
|
||||
unit_price=data.get('unit_price', 0),
|
||||
total_price=data.get('total_price', 0),
|
||||
currency=data.get('currency', 'CNY'),
|
||||
exchange_rate=data.get('exchange_rate', 1.0),
|
||||
supplier_name=data.get('supplier_name'),
|
||||
buyer_name=data.get('buyer_name'),
|
||||
buyer_email=data.get('buyer_email'),
|
||||
original_link=data.get('original_link'),
|
||||
detail_link=data.get('detail_link'),
|
||||
arrival_photo=data.get('arrival_photo')
|
||||
)
|
||||
|
||||
db.session.add(new_stock)
|
||||
db.session.commit()
|
||||
return new_stock
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
raise e
|
||||
|
||||
@staticmethod
|
||||
def update_inbound(stock_id, data):
|
||||
"""更新入库:支持级联更新基础信息 + 自动重算总价"""
|
||||
try:
|
||||
stock = StockBuy.query.get(stock_id)
|
||||
if not stock:
|
||||
raise ValueError("记录不存在")
|
||||
|
||||
# 1. 更新普通字段
|
||||
if 'serial_number' in data: stock.serial_number = data['serial_number']
|
||||
if 'batch_number' in data: stock.batch_number = data['batch_number']
|
||||
if 'warehouse_location' in data: stock.warehouse_location = data['warehouse_location']
|
||||
if 'supplier_name' in data: stock.supplier_name = data['supplier_name']
|
||||
if 'status' in data: stock.status = data['status']
|
||||
if 'inspection_status' in data: stock.inspection_status = data['inspection_status']
|
||||
if 'arrival_photo' in data: stock.arrival_photo = data['arrival_photo']
|
||||
if 'remark' in data: stock.remark = data['remark']
|
||||
|
||||
# 2. 级联更新基础信息 (MaterialBase)
|
||||
if stock.material:
|
||||
if 'material_name' in data: stock.material.name = data['material_name']
|
||||
if 'category' in data: stock.material.category = data['category']
|
||||
if 'unit' in data: stock.material.unit = data['unit']
|
||||
|
||||
# 3. 核心逻辑:数量与价格联动
|
||||
qty_changed = False
|
||||
price_changed = False
|
||||
|
||||
# (A) 数量变更 -> 更新库存和可用量
|
||||
if 'in_quantity' in data:
|
||||
new_qty = float(data['in_quantity'])
|
||||
old_qty = float(stock.in_quantity)
|
||||
diff = new_qty - old_qty
|
||||
|
||||
if diff != 0:
|
||||
stock.in_quantity = new_qty
|
||||
stock.stock_quantity = float(stock.stock_quantity) + diff
|
||||
stock.available_quantity = float(stock.available_quantity) + diff
|
||||
qty_changed = True
|
||||
|
||||
# (B) 单价变更
|
||||
if 'unit_price' in data:
|
||||
new_price = float(data['unit_price'])
|
||||
if new_price != float(stock.unit_price):
|
||||
stock.unit_price = new_price
|
||||
price_changed = True
|
||||
|
||||
# (C) 重算总价
|
||||
if qty_changed or price_changed:
|
||||
stock.total_price = float(stock.in_quantity) * float(stock.unit_price)
|
||||
|
||||
db.session.commit()
|
||||
return stock
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
raise e
|
||||
|
||||
@staticmethod
|
||||
def delete_inbound(stock_id):
|
||||
"""删除逻辑:孤儿策略(如果MaterialBase无其他引用则一并删除)"""
|
||||
try:
|
||||
stock = StockBuy.query.get(stock_id)
|
||||
if not stock:
|
||||
raise ValueError("记录不存在")
|
||||
|
||||
# 1. 记下 base_id
|
||||
material_id = stock.base_id
|
||||
|
||||
# 2. 删除库存记录
|
||||
db.session.delete(stock)
|
||||
db.session.flush()
|
||||
|
||||
# 3. 检查是否还有残留
|
||||
remaining_count = StockBuy.query.filter_by(base_id=material_id).count()
|
||||
|
||||
if remaining_count == 0:
|
||||
print(f"触发级联删除: MaterialBase ID {material_id} 已无关联,执行清理。")
|
||||
material = MaterialBase.query.get(material_id)
|
||||
if material:
|
||||
db.session.delete(material)
|
||||
|
||||
db.session.commit()
|
||||
return True
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
print(f"删除失败: {e}")
|
||||
raise e
|
||||
|
||||
@staticmethod
|
||||
def get_list(page, limit):
|
||||
try:
|
||||
pagination = StockBuy.query.order_by(StockBuy.id.desc()).paginate(page=page, per_page=limit)
|
||||
items = [item.to_dict() for item in pagination.items]
|
||||
return {"total": pagination.total, "items": items}
|
||||
except Exception as e:
|
||||
print(f"查询列表失败: {e}")
|
||||
return {"total": 0, "items": []}
|
||||
@ -1,14 +1,16 @@
|
||||
import os
|
||||
|
||||
|
||||
class Config:
|
||||
# 数据库连接配置
|
||||
# 请务必将 '你的密码' 替换为你 PostgreSQL 的真实密码
|
||||
# 如果数据库不在本地,请将 localhost 替换为 IP 地址
|
||||
SQLALCHEMY_DATABASE_URI = 'postgresql://test:1234@localhost:5432/inventory_system'
|
||||
# 【核心修改】
|
||||
# 优先读取 Docker 传入的 'DATABASE_URL' 环境变量。
|
||||
# 如果读不到(比如你在非 Docker 环境下本地直接运行),才回退使用 'localhost'。
|
||||
SQLALCHEMY_DATABASE_URI = os.getenv(
|
||||
'DATABASE_URL',
|
||||
'postgresql://test:1234@localhost:5432/inventory_system'
|
||||
)
|
||||
|
||||
# 关闭 SQLAlchemy 的事件追踪,减少内存消耗
|
||||
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
||||
|
||||
# Flask 的密钥,用于 Session 加密等,开发环境随便写一个即可
|
||||
# Flask 的密钥
|
||||
SECRET_KEY = 'dev-secret-key-1234'
|
||||
@ -1,17 +0,0 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
db:
|
||||
image: postgres:15-alpine # 使用轻量级的 Alpine 版本
|
||||
container_name: inventory_db
|
||||
restart: always
|
||||
environment:
|
||||
POSTGRES_USER: test # 自定义用户名
|
||||
POSTGRES_PASSWORD: 1234 # 自定义密码 (开发环境简单点没事)
|
||||
POSTGRES_DB: inventory_system # 默认创建的数据库名
|
||||
ports:
|
||||
- "5432:5432" # 将容器的5432端口映射到 WSL 的5432端口
|
||||
volumes:
|
||||
- ./pgdata:/var/lib/postgresql/data # 【重要】数据持久化!防止重启容器数据丢失
|
||||
|
||||
# 这里以后可以加你的 pgadmin 或者 redis 等其他服务
|
||||
21
inventory-backend/gunicorn.conf.py
Normal file
21
inventory-backend/gunicorn.conf.py
Normal file
@ -0,0 +1,21 @@
|
||||
# inventory-backend/gunicorn.conf.py
|
||||
|
||||
import multiprocessing
|
||||
|
||||
# 原来的写法:根据 CPU 自动算,容易在强机上算太多
|
||||
# workers = multiprocessing.cpu_count() * 2 + 1
|
||||
|
||||
# --- 优化后的写法 ---
|
||||
# 我们设置一个上限:如果是开发环境或为了省资源,最多不超过 8 个
|
||||
# 这样既有并发能力(8个分身足够开发测试用了),又不会撑爆数据库
|
||||
cpu_calc = multiprocessing.cpu_count() * 2 + 1
|
||||
workers = min(cpu_calc, 8)
|
||||
|
||||
# 线程数保持不变
|
||||
threads = 2
|
||||
|
||||
bind = "0.0.0.0:8000"
|
||||
timeout = 120
|
||||
loglevel = 'info'
|
||||
accesslog = '-' # 输出到标准输出(Docker logs 能看到)
|
||||
errorlog = '-'
|
||||
3
inventory-web/.env.development
Normal file
3
inventory-web/.env.development
Normal file
@ -0,0 +1,3 @@
|
||||
# .env.development
|
||||
# 注意:这里必须写你电脑的局域网 IP
|
||||
VITE_API_BASE_URL=http://172.25.16.1:8000/api/v1
|
||||
31
inventory-web/Dockerfile
Normal file
31
inventory-web/Dockerfile
Normal file
@ -0,0 +1,31 @@
|
||||
# ---------------------------------------
|
||||
# 这是开发模式 (Development Mode) 的配置
|
||||
# ---------------------------------------
|
||||
|
||||
# 1. 使用 Node 20 的 Alpine 版本 (轻量级)
|
||||
FROM node:20-alpine
|
||||
|
||||
# 【关键新增】安装 libc6 兼容库
|
||||
# 这一步能解决 90% 的 "Cannot find module ... musl.node" 或二进制文件缺失问题
|
||||
RUN apk add --no-cache libc6-compat
|
||||
|
||||
# 设置工作目录
|
||||
WORKDIR /app
|
||||
|
||||
# 2. 优先复制 package.json 和 lock 文件
|
||||
# 这样如果只改代码不改依赖,Docker 会利用缓存跳过安装步骤,构建更快
|
||||
COPY package*.json ./
|
||||
|
||||
# 3. 安装依赖
|
||||
# 这一步会在容器内部下载适合 Alpine Linux 的依赖包
|
||||
RUN npm install
|
||||
|
||||
# 4. 复制其余源代码
|
||||
COPY . .
|
||||
|
||||
# 5. 暴露端口 (仅作声明,方便查看)
|
||||
EXPOSE 5173
|
||||
|
||||
# 6. 启动开发服务器
|
||||
# 必须加 --host,否则只能在容器内部访问,无法通过浏览器 localhost 访问
|
||||
CMD ["npm", "run", "dev", "--", "--host"]
|
||||
26
inventory-web/nginx.conf
Normal file
26
inventory-web/nginx.conf
Normal file
@ -0,0 +1,26 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
|
||||
gzip on;
|
||||
gzip_min_length 1k;
|
||||
gzip_comp_level 6;
|
||||
gzip_types text/plain application/javascript application/x-javascript text/css application/xml text/javascript;
|
||||
|
||||
# 1. 前端页面
|
||||
location / {
|
||||
root /usr/share/nginx/html;
|
||||
index index.html index.htm;
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
# 2. 后端接口代理
|
||||
location /api {
|
||||
# 'backend' 对应 docker-compose 里的服务名
|
||||
proxy_pass http://backend:8000;
|
||||
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
}
|
||||
}
|
||||
2289
inventory-web/package-lock.json
generated
2289
inventory-web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
// 不需要引入组件,由 router-view 控制
|
||||
// 1. 引入需要的图标组件
|
||||
import { InfoFilled } from '@element-plus/icons-vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -7,8 +8,8 @@
|
||||
<header class="app-header">
|
||||
<div class="logo-container">
|
||||
<router-link to="/" class="home-link">
|
||||
<img src="./assets/iris.png" class="logo" alt="Logo" />
|
||||
<span class="system-title">库存管理系统</span>
|
||||
<img src="@/assets/iris.png" class="logo" alt="Logo" />
|
||||
<span class="system-title">IRIS 库存管理系统</span>
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
@ -30,23 +31,32 @@
|
||||
</template>
|
||||
|
||||
<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;
|
||||
background-color: #f5f7fa; /* 整体背景色 */
|
||||
overflow: hidden; /* 防止最外层出现双滚动条 */
|
||||
}
|
||||
|
||||
#app {
|
||||
height: 100%;
|
||||
}
|
||||
/* --- 全局重置样式 End --- */
|
||||
|
||||
.app-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh; /* 占满全屏高度 */
|
||||
height: 100vh; /* 强制占满视口高度 */
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 顶部栏样式 */
|
||||
@ -59,7 +69,8 @@ html, body {
|
||||
justify-content: space-between;
|
||||
padding: 0 20px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||
flex-shrink: 0; /* 防止被压缩 */
|
||||
flex-shrink: 0; /* 禁止被压缩 */
|
||||
z-index: 1000; /* 确保头部在最上层 */
|
||||
}
|
||||
|
||||
.logo-container {
|
||||
@ -73,43 +84,52 @@ html, body {
|
||||
gap: 15px;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 40px;
|
||||
height: 36px; /* 稍微调整高度适配 */
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.system-title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
/* 内容区样式 */
|
||||
.app-content {
|
||||
flex: 1; /* 关键:这会让内容区自动撑开,把 footer 挤到最底下 */
|
||||
overflow: auto;
|
||||
padding: 20px;
|
||||
flex: 1; /* 自动占据剩余空间 */
|
||||
overflow: hidden; /* 这里设为 hidden,让内部的 Layout 组件去处理滚动 */
|
||||
position: relative;
|
||||
/* 如果您希望整个页面有内边距,可以加 padding;
|
||||
但通常建议 padding 加在具体的业务页面里,保持 Layout 铺满 */
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* 新增:底部栏样式 */
|
||||
/* 底部栏样式 */
|
||||
.app-footer {
|
||||
height: 30px; /* 固定高度 */
|
||||
background-color: #e9e9eb; /* 稍微深一点的灰色,区分内容区 */
|
||||
border-top: 1px solid #dcdfe6;
|
||||
height: 36px;
|
||||
background-color: #f0f2f5;
|
||||
border-top: 1px solid #e4e7ed;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center; /* 文字居中 */
|
||||
flex-shrink: 0; /* 防止被压缩 */
|
||||
justify-content: center;
|
||||
flex-shrink: 0; /* 禁止被压缩 */
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.version-tag {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-weight: 500;
|
||||
color: #e6a23c; /* 使用橙色,表示“测试/警告”意味 */
|
||||
color: #e6a23c; /* 橙色警告色 */
|
||||
background: rgba(230, 162, 60, 0.1); /* 淡橙色背景 */
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
</style>
|
||||
19
inventory-web/src/api/inbound/buy.ts
Normal file
19
inventory-web/src/api/inbound/buy.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
export function getBuyList(params: any) {
|
||||
return request({ url: '/inbound/buy/list', method: 'get', params })
|
||||
}
|
||||
|
||||
export function createBuyInbound(data: any) {
|
||||
return request({ url: '/inbound/buy/submit', method: 'post', data })
|
||||
}
|
||||
|
||||
// 新增:更新接口
|
||||
export function updateBuyInbound(id: number, data: any) {
|
||||
return request({ url: `/inbound/buy/${id}`, method: 'put', data })
|
||||
}
|
||||
|
||||
// 新增:删除接口
|
||||
export function deleteBuyInbound(id: number) {
|
||||
return request({ url: `/inbound/buy/${id}`, method: 'delete' })
|
||||
}
|
||||
0
inventory-web/src/api/inbound/product.ts
Normal file
0
inventory-web/src/api/inbound/product.ts
Normal file
0
inventory-web/src/api/inbound/semi.ts
Normal file
0
inventory-web/src/api/inbound/semi.ts
Normal file
@ -1,39 +0,0 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
// 注意:baseURL 已经是 '/api/v1' 了,所以这里只需要写剩下的部分
|
||||
|
||||
// 获取入库列表
|
||||
// 最终请求: /api/v1 + /stocks/inbound = /api/v1/stocks/inbound
|
||||
export function getInboundList(params: any) {
|
||||
return request({
|
||||
url: '/stocks/inbound', // <--- 修改点:去掉了 /api/v1
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
// 新增入库
|
||||
export function createInbound(data: any) {
|
||||
return request({
|
||||
url: '/stocks/inbound', // <--- 修改点
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// 修改入库
|
||||
export function updateInbound(id: number, data: any) {
|
||||
return request({
|
||||
url: `/stocks/inbound/${id}`, // <--- 修改点
|
||||
method: 'put',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// 删除入库
|
||||
export function deleteInbound(id: number) {
|
||||
return request({
|
||||
url: `/stocks/inbound/${id}`, // <--- 修改点
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
36
inventory-web/src/layout/components/AppMain.vue
Normal file
36
inventory-web/src/layout/components/AppMain.vue
Normal file
@ -0,0 +1,36 @@
|
||||
<template>
|
||||
<section class="app-main">
|
||||
<router-view v-slot="{ Component }">
|
||||
<transition name="fade-transform" mode="out-in">
|
||||
<component :is="Component" />
|
||||
</transition>
|
||||
</router-view>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.app-main {
|
||||
/* 确保占满容器 */
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 简单的页面切换动画 */
|
||||
.fade-transform-leave-active,
|
||||
.fade-transform-enter-active {
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.fade-transform-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateX(-30px);
|
||||
}
|
||||
|
||||
.fade-transform-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(30px);
|
||||
}
|
||||
</style>
|
||||
95
inventory-web/src/layout/components/Sidebar/index.vue
Normal file
95
inventory-web/src/layout/components/Sidebar/index.vue
Normal file
@ -0,0 +1,95 @@
|
||||
<template>
|
||||
<el-menu
|
||||
:default-active="activeMenu"
|
||||
background-color="#304156"
|
||||
text-color="#bfcbd9"
|
||||
active-text-color="#409EFF"
|
||||
:unique-opened="true"
|
||||
router
|
||||
class="el-menu-vertical"
|
||||
>
|
||||
<template v-for="route in menuRoutes" :key="route.path">
|
||||
|
||||
<el-menu-item
|
||||
v-if="!route.children || route.children.length === 1"
|
||||
:index="resolvePath(route)"
|
||||
>
|
||||
<el-icon v-if="getMeta(route).icon">
|
||||
<component :is="getMeta(route).icon" />
|
||||
</el-icon>
|
||||
<span>{{ getMeta(route).title }}</span>
|
||||
</el-menu-item>
|
||||
|
||||
<el-sub-menu v-else :index="route.path">
|
||||
<template #title>
|
||||
<el-icon v-if="route.meta && route.meta.icon">
|
||||
<component :is="route.meta.icon" />
|
||||
</el-icon>
|
||||
<span>{{ route.meta?.title }}</span>
|
||||
</template>
|
||||
|
||||
<el-menu-item
|
||||
v-for="child in route.children"
|
||||
:key="child.path"
|
||||
:index="resolvePath(route, child)"
|
||||
>
|
||||
<template #title>
|
||||
<span>{{ child.meta?.title }}</span>
|
||||
</template>
|
||||
</el-menu-item>
|
||||
</el-sub-menu>
|
||||
|
||||
</template>
|
||||
</el-menu>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
// 1. 获取当前激活的菜单路径
|
||||
const activeMenu = computed(() => {
|
||||
return route.path
|
||||
})
|
||||
|
||||
// 2. 获取需要在菜单中显示的路由(过滤掉 hidden 的路由)
|
||||
const menuRoutes = computed(() => {
|
||||
return router.options.routes.filter((r: any) => !r.meta?.hidden)
|
||||
})
|
||||
|
||||
// 3. 辅助函数:获取 meta 信息
|
||||
const getMeta = (route: any) => {
|
||||
if (route.meta) return route.meta
|
||||
// 如果是 layout 嵌套层(如首页),取第一个子路由的 meta
|
||||
if (route.children && route.children.length > 0) {
|
||||
return route.children[0].meta
|
||||
}
|
||||
return {}
|
||||
}
|
||||
|
||||
// 4. 辅助函数:拼接路径
|
||||
const resolvePath = (parent: any, child?: any) => {
|
||||
// 如果是首页这种 layout 嵌套结构
|
||||
if (!child && parent.children && parent.children.length === 1) {
|
||||
return parent.path === '/' ? '/dashboard' : parent.path + '/' + parent.children[0].path
|
||||
}
|
||||
// 如果是普通子菜单
|
||||
if (child) {
|
||||
return parent.path + '/' + child.path
|
||||
}
|
||||
return parent.path
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.el-menu-vertical {
|
||||
border-right: none; /* 去掉 Element Plus 菜单默认的右边框 */
|
||||
width: 100%;
|
||||
}
|
||||
:deep(.el-menu-item.is-active) {
|
||||
background-color: #263445 !important;
|
||||
}
|
||||
</style>
|
||||
@ -1,11 +1,43 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="layout-wrapper">
|
||||
<Sidebar class="sidebar-container" />
|
||||
|
||||
<div class="main-container">
|
||||
<AppMain />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
<script setup lang="ts">
|
||||
import Sidebar from './components/Sidebar/index.vue'
|
||||
import AppMain from './components/AppMain.vue'
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.layout-wrapper {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%; /* 继承 App.vue 中 app-content 的高度 */
|
||||
overflow: hidden; /* 防止最外层出现滚动条 */
|
||||
}
|
||||
|
||||
.sidebar-container {
|
||||
width: 210px; /* 固定侧边栏宽度 */
|
||||
height: 100%;
|
||||
background-color: #304156; /* 侧边栏背景色 */
|
||||
flex-shrink: 0; /* 防止被挤压 */
|
||||
overflow-y: auto; /* 侧边栏内容过多时允许滚动 */
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.main-container {
|
||||
flex: 1; /* 自动占满右侧剩余空间 */
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-y: auto; /* 关键:页面内容过多时,只在右侧区域滚动 */
|
||||
background-color: #f0f2f5; /* 右侧灰色背景,让白色卡片更明显 */
|
||||
padding: 20px; /* 给内部页面留出边距 */
|
||||
box-sizing: border-box;
|
||||
}
|
||||
</style>
|
||||
@ -1,18 +1,148 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
// 核心修改点:使用 'type' 关键字导入 RouteRecordRaw,或者将其分开导入
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
import Layout from '@/layout/index.vue'
|
||||
|
||||
const routes = [
|
||||
// --- 修改点:根路径不再重定向,而是显示 Dashboard 首页 ---
|
||||
const routes: Array<RouteRecordRaw> = [
|
||||
// 1. 首页 Dashboard
|
||||
{
|
||||
path: '/',
|
||||
name: 'Dashboard',
|
||||
component: () => import('@/views/dashboard/index.vue')
|
||||
component: Layout,
|
||||
redirect: '/dashboard',
|
||||
children: [
|
||||
{
|
||||
path: 'dashboard',
|
||||
name: 'Dashboard',
|
||||
component: () => import('@/views/dashboard/index.vue'),
|
||||
meta: { title: '首页', icon: 'HomeFilled' }
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
// --- 保持原有的入库页路由不变 ---
|
||||
// 2. 基础物料 (对应 views/material/list.vue)
|
||||
{
|
||||
path: '/stock/inbound',
|
||||
name: 'StockInbound',
|
||||
component: () => import('@/views/stock/inbound.vue')
|
||||
path: '/material',
|
||||
component: Layout,
|
||||
redirect: '/material/index',
|
||||
children: [
|
||||
{
|
||||
path: 'index',
|
||||
name: 'MaterialBase',
|
||||
// 基础物料列表
|
||||
component: () => import('@/views/material/list.vue'),
|
||||
meta: { title: '基础物料', icon: 'Box' }
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
// 3. 库存管理 (采购/半成品/成品/权益)
|
||||
{
|
||||
path: '/inventory',
|
||||
component: Layout,
|
||||
meta: { title: '库存管理', icon: 'Shop' },
|
||||
redirect: '/inventory/buy',
|
||||
children: [
|
||||
{
|
||||
path: 'buy',
|
||||
name: 'InventoryBuy',
|
||||
// 采购入库页面
|
||||
component: () => import('@/views/stock/inbound/buy.vue'),
|
||||
meta: { title: '采购件' }
|
||||
},
|
||||
{
|
||||
path: 'semi',
|
||||
name: 'InventorySemi',
|
||||
// 半成品页面
|
||||
component: () => import('@/views/stock/inbound/semi.vue'),
|
||||
meta: { title: '半成品' }
|
||||
},
|
||||
{
|
||||
path: 'product',
|
||||
name: 'InventoryProduct',
|
||||
// 成品页面
|
||||
component: () => import('@/views/stock/inbound/product.vue'),
|
||||
meta: { title: '成品' }
|
||||
},
|
||||
{
|
||||
path: 'service',
|
||||
name: 'InventoryService',
|
||||
// 服务权益页面
|
||||
component: () => import('@/views/stock/inbound/service.vue'),
|
||||
meta: { title: '服务权益' }
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
// 4. 业务操作 (借库/维修/报废)
|
||||
{
|
||||
path: '/operation',
|
||||
component: Layout,
|
||||
meta: { title: '业务操作', icon: 'Operation' },
|
||||
redirect: '/operation/borrow',
|
||||
children: [
|
||||
{
|
||||
path: 'borrow',
|
||||
name: 'OpBorrow',
|
||||
// 借库页面
|
||||
component: () => import('@/views/transaction/borrow.vue'),
|
||||
meta: { title: '借库' }
|
||||
},
|
||||
{
|
||||
path: 'repair',
|
||||
name: 'OpRepair',
|
||||
// 维修页面 (指向 return.vue)
|
||||
component: () => import('@/views/transaction/return.vue'),
|
||||
meta: { title: '维修' }
|
||||
},
|
||||
{
|
||||
path: 'scrap',
|
||||
name: 'OpScrap',
|
||||
// 报废页面
|
||||
component: () => import('@/views/transaction/scrap.vue'),
|
||||
meta: { title: '报废' }
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
/* * 暂时屏蔽 BOM 和 系统管理
|
||||
*/
|
||||
// {
|
||||
// path: '/bom',
|
||||
// component: Layout,
|
||||
// children: [
|
||||
// {
|
||||
// path: 'index',
|
||||
// name: 'BOM',
|
||||
// component: () => import('@/views/bom/index.vue'),
|
||||
// meta: { title: 'BOM管理', icon: 'List' }
|
||||
// }
|
||||
// ]
|
||||
// },
|
||||
// {
|
||||
// path: '/system',
|
||||
// component: Layout,
|
||||
// meta: { title: '系统管理', icon: 'Setting' },
|
||||
// children: [
|
||||
// {
|
||||
// path: 'user',
|
||||
// name: 'UserManage',
|
||||
// component: () => import('@/views/system/user.vue'),
|
||||
// meta: { title: '用户管理', icon: 'User' }
|
||||
// },
|
||||
// {
|
||||
// path: 'log',
|
||||
// name: 'OpLog',
|
||||
// component: () => import('@/views/system/log.vue'),
|
||||
// meta: { title: '操作日志', icon: 'Document' }
|
||||
// }
|
||||
// ]
|
||||
// },
|
||||
|
||||
// 404 路由
|
||||
{
|
||||
path: '/:pathMatch(.*)*',
|
||||
redirect: '/dashboard',
|
||||
meta: { hidden: true }
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@ -3,9 +3,9 @@ import { ElMessage } from 'element-plus'
|
||||
|
||||
// 1. 创建 axios 实例
|
||||
const service = axios.create({
|
||||
// 这里的 '/api' 配合 vite.config.ts 的 proxy 使用
|
||||
baseURL: '/api/v1',
|
||||
timeout: 5000 // 请求超时时间
|
||||
// 【修改这里】不要写死 '/api/v1',改为读取环境变量
|
||||
baseURL: import.meta.env.VITE_API_BASE_URL,
|
||||
timeout: 5000
|
||||
})
|
||||
|
||||
// 2. 请求拦截器 (可以在这里加 Token)
|
||||
|
||||
@ -3,22 +3,36 @@
|
||||
<el-card class="welcome-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>👋 欢迎回来</span>
|
||||
<span class="title">👋 欢迎回来,Admin</span>
|
||||
<el-tag type="success">系统运行正常</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="card-body">
|
||||
<h2>欢迎使用 IRIS 库存管理系统</h2>
|
||||
<p>请点击下方按钮进入具体业务模块:</p>
|
||||
<h2>IRIS 库存管理系统</h2>
|
||||
<p class="subtitle">请选择您要进行的业务操作:</p>
|
||||
|
||||
<div class="action-buttons">
|
||||
<el-button type="primary" size="large" @click="$router.push('/stock/inbound')">
|
||||
<el-icon style="margin-right: 5px"><Box /></el-icon>
|
||||
进入采购入库
|
||||
<el-button type="primary" size="large" @click="handleNav('/inventory/buy')">
|
||||
<el-icon style="margin-right: 5px"><ShoppingCart /></el-icon>
|
||||
采购入库
|
||||
</el-button>
|
||||
|
||||
<el-button size="large" disabled>
|
||||
<el-button type="success" size="large" @click="handleNav('/material/index')">
|
||||
<el-icon style="margin-right: 5px"><Box /></el-icon>
|
||||
基础物料
|
||||
</el-button>
|
||||
|
||||
<el-button type="warning" size="large" @click="handleNav('/operation/borrow')">
|
||||
<el-icon style="margin-right: 5px"><Operation /></el-icon>
|
||||
借库申请
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 20px;">
|
||||
<el-button text bg size="small" disabled>
|
||||
<el-icon style="margin-right: 5px"><TrendCharts /></el-icon>
|
||||
库存报表 (开发中)
|
||||
数据大屏 (开发中)
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
@ -27,35 +41,75 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Box, TrendCharts } from '@element-plus/icons-vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
// 引入需要的图标
|
||||
import { Box, TrendCharts, ShoppingCart, Operation } from '@element-plus/icons-vue'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
// 统一跳转函数
|
||||
const handleNav = (path: string) => {
|
||||
router.push(path)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dashboard-container {
|
||||
padding: 20px;
|
||||
/* 使用 100% 宽度和高度,利用 Flex 居中显示 */
|
||||
height: calc(100vh - 84px); /* 减去顶部导航栏的高度,防止出现双滚动条 */
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 50px;
|
||||
align-items: center;
|
||||
background-color: #f0f2f5; /* 给背景加个淡灰色,突出卡片 */
|
||||
}
|
||||
|
||||
.welcome-card {
|
||||
width: 600px;
|
||||
width: 800px; /*稍微加宽一点 */
|
||||
text-align: center;
|
||||
border-radius: 8px; /* 圆角更好看 */
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.card-body h2 {
|
||||
color: #303133;
|
||||
font-size: 28px;
|
||||
color: #409EFF; /* 使用主题蓝 */
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.card-body p {
|
||||
color: #606266;
|
||||
margin-bottom: 30px;
|
||||
.subtitle {
|
||||
color: #909399;
|
||||
margin-bottom: 40px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 20px;
|
||||
flex-wrap: wrap; /* 防止屏幕过窄时按钮挤压 */
|
||||
}
|
||||
|
||||
/* 给按钮加一点悬浮效果 */
|
||||
.el-button {
|
||||
transition: all 0.3s;
|
||||
}
|
||||
.el-button:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
</style>
|
||||
@ -1,11 +1,38 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<el-card shadow="never">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span class="title">基础物料管理</span>
|
||||
<el-button type="primary">
|
||||
<el-icon style="margin-right: 5px"><Plus /></el-icon>新增物料
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="filter-container">
|
||||
<el-input placeholder="请输入物料名称或编码" style="width: 200px; margin-right: 10px;" />
|
||||
<el-select placeholder="物料类别" style="width: 150px; margin-right: 10px;">
|
||||
<el-option label="电子元器件" value="elec" />
|
||||
<el-option label="结构件" value="struct" />
|
||||
</el-select>
|
||||
<el-button type="primary" plain>搜索</el-button>
|
||||
</div>
|
||||
|
||||
<el-table :data="tableData" border stripe style="width: 100%; margin-top: 20px">
|
||||
<el-table-column prop="code" label="物料编码" width="120" />
|
||||
<el-table-column prop="name" label="物料名称" width="180" />
|
||||
<el-table-column prop="spec" label="规格型号" />
|
||||
<el-table-column prop="unit" label="单位" width="80" />
|
||||
<el-table-column prop="category" label="类别" width="120" />
|
||||
<el-table-column label="操作" width="150">
|
||||
<template #default>
|
||||
<el-button link type="primary" size="small">编辑</el-button>
|
||||
<el-button link type="danger" size="small">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@ -1,237 +0,0 @@
|
||||
<template>
|
||||
<div class="app-container" style="padding: 20px;">
|
||||
<div style="margin-bottom: 20px; display: flex; justify-content: space-between;">
|
||||
<el-button type="primary" :icon="Plus" @click="handleCreate">新增入库</el-button>
|
||||
<el-button :icon="Refresh" @click="fetchData">刷新</el-button>
|
||||
</div>
|
||||
|
||||
<el-table v-loading="loading" :data="tableData" border stripe style="width: 100%">
|
||||
<el-table-column prop="inbound_date" label="入库时间" width="160" />
|
||||
<el-table-column prop="sku_code" label="SKU" width="120" fixed="left" />
|
||||
<el-table-column prop="material_name" label="物料名称" min-width="150" />
|
||||
<el-table-column prop="spec_model" label="规格" width="120" />
|
||||
<el-table-column prop="category" label="分类" width="100" />
|
||||
<el-table-column prop="unit" label="单位" width="60" />
|
||||
|
||||
<el-table-column prop="qty_inbound" label="入库量" width="100">
|
||||
<template #default="{ row }">
|
||||
<span style="font-weight: bold; color: #409EFF">{{ row.qty_inbound }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="price_unit" label="单价" width="100" />
|
||||
<el-table-column prop="price_total" label="总价" width="100" />
|
||||
<el-table-column prop="warehouse_loc" label="库位" width="100" />
|
||||
<el-table-column prop="supplier_name" label="供应商" width="120" />
|
||||
|
||||
<el-table-column label="操作" width="150" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="handleUpdate(row)">编辑</el-button>
|
||||
<el-button link type="danger" @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div style="margin-top: 20px; text-align: right;">
|
||||
<el-pagination
|
||||
v-model:current-page="queryParams.page"
|
||||
v-model:page-size="queryParams.pageSize"
|
||||
:total="total"
|
||||
layout="total, prev, pager, next"
|
||||
@current-change="fetchData"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<el-dialog
|
||||
:title="dialogType === 'create' ? '入库录入 (支持自动建档)' : '编辑入库单'"
|
||||
v-model="dialogVisible"
|
||||
width="650px"
|
||||
>
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
|
||||
|
||||
<div v-if="dialogType === 'create'">
|
||||
<el-divider content-position="left">物料基础信息</el-divider>
|
||||
<el-alert title="输入SKU后,如是新物料,请补全名称和规格;如是旧物料,系统会自动关联。" type="info" :closable="false" style="margin-bottom:15px;" />
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="SKU编码" prop="sku_code">
|
||||
<el-input v-model="form.sku_code" placeholder="唯一识别码" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="物料名称" prop="material_name">
|
||||
<el-input v-model="form.material_name" placeholder="新物料必填" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="规格型号" prop="spec_model">
|
||||
<el-input v-model="form.spec_model" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="分类" prop="category">
|
||||
<el-input v-model="form.category" placeholder="如: 电子料" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="单位" prop="unit">
|
||||
<el-input v-model="form.unit" placeholder="个/包" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
<el-divider content-position="left">入库业务信息</el-divider>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="入库数量" prop="qty_inbound">
|
||||
<el-input-number v-model="form.qty_inbound" :min="0" style="width: 100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="单价" prop="price_unit">
|
||||
<el-input-number v-model="form.price_unit" :min="0" :precision="2" style="width: 100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="库位" prop="warehouse_loc">
|
||||
<el-input v-model="form.warehouse_loc" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="批次号" prop="batch_no">
|
||||
<el-input v-model="form.batch_no" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-form-item label="供应商" prop="supplier_name">
|
||||
<el-input v-model="form.supplier_name" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitting" @click="submitForm">确认提交</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { Plus, Refresh } from '@element-plus/icons-vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { getInboundList, createInbound, updateInbound, deleteInbound } from '@/api/stock'
|
||||
|
||||
const loading = ref(false)
|
||||
const submitting = ref(false)
|
||||
const tableData = ref([])
|
||||
const total = ref(0)
|
||||
const dialogVisible = ref(false)
|
||||
const dialogType = ref<'create' | 'update'>('create')
|
||||
const formRef = ref()
|
||||
|
||||
const queryParams = reactive({ page: 1, pageSize: 10 })
|
||||
|
||||
// 表单对象
|
||||
const form = reactive({
|
||||
id: undefined,
|
||||
sku_code: '',
|
||||
material_name: '',
|
||||
spec_model: '',
|
||||
category: '',
|
||||
unit: '',
|
||||
qty_inbound: 0,
|
||||
price_unit: 0,
|
||||
warehouse_loc: '',
|
||||
batch_no: '',
|
||||
supplier_name: ''
|
||||
})
|
||||
|
||||
const rules = {
|
||||
sku_code: [{ required: true, message: 'SKU不能为空', trigger: 'blur' }],
|
||||
qty_inbound: [{ required: true, message: '数量必须大于0', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
const fetchData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getInboundList(queryParams)
|
||||
tableData.value = res.data.items
|
||||
total.value = res.data.total
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleCreate = () => {
|
||||
dialogType.value = 'create'
|
||||
// 重置表单
|
||||
Object.assign(form, {
|
||||
id: undefined, sku_code: '', material_name: '', spec_model: '', category: '', unit: '',
|
||||
qty_inbound: 1, price_unit: 0, warehouse_loc: '', batch_no: '', supplier_name: ''
|
||||
})
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleUpdate = (row: any) => {
|
||||
dialogType.value = 'update'
|
||||
// 仅允许修改入库相关信息
|
||||
Object.assign(form, row)
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
const submitForm = async () => {
|
||||
if (!formRef.value) return
|
||||
await formRef.value.validate(async (valid: boolean) => {
|
||||
if (valid) {
|
||||
submitting.value = true
|
||||
try {
|
||||
if (dialogType.value === 'create') {
|
||||
await createInbound(form)
|
||||
ElMessage.success('入库成功')
|
||||
} else {
|
||||
// 编辑时只提交入库单ID和可修改字段
|
||||
await updateInbound(form.id!, {
|
||||
qty_inbound: form.qty_inbound,
|
||||
price_unit: form.price_unit,
|
||||
warehouse_loc: form.warehouse_loc,
|
||||
batch_no: form.batch_no,
|
||||
supplier_name: form.supplier_name
|
||||
})
|
||||
ElMessage.success('更新成功')
|
||||
}
|
||||
dialogVisible.value = false
|
||||
fetchData()
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e.response?.data?.msg || '操作失败')
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleDelete = (row: any) => {
|
||||
ElMessageBox.confirm('确认删除该记录?', '警告', { type: 'warning' })
|
||||
.then(async () => {
|
||||
await deleteInbound(row.id)
|
||||
ElMessage.success('已删除')
|
||||
fetchData()
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => fetchData())
|
||||
</script>
|
||||
415
inventory-web/src/views/stock/inbound/buy.vue
Normal file
415
inventory-web/src/views/stock/inbound/buy.vue
Normal file
@ -0,0 +1,415 @@
|
||||
<template>
|
||||
<div class="buy-module">
|
||||
<div class="header-tools">
|
||||
<div class="left-actions">
|
||||
<el-button type="primary" :icon="Plus" @click="handleCreate">全量采购入库登记</el-button>
|
||||
<el-button :icon="Refresh" @click="fetchData">刷新数据</el-button>
|
||||
</div>
|
||||
|
||||
<el-popover placement="bottom" title="显示列配置" :width="400" trigger="click">
|
||||
<template #reference>
|
||||
<el-button :icon="Setting">自定义表格表头</el-button>
|
||||
</template>
|
||||
<el-checkbox-group v-model="visibleColumnProps" class="column-selector">
|
||||
<el-divider content-position="left">基础层字段</el-divider>
|
||||
<el-checkbox v-for="c in baseColumns" :key="c.prop" :value="c.prop">{{ c.label }}</el-checkbox>
|
||||
<el-divider content-position="left">库存/财务层字段</el-divider>
|
||||
<el-checkbox v-for="c in stockColumns" :key="c.prop" :value="c.prop">{{ c.label }}</el-checkbox>
|
||||
</el-checkbox-group>
|
||||
</el-popover>
|
||||
</div>
|
||||
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="tableData"
|
||||
border
|
||||
stripe
|
||||
style="width: 100%"
|
||||
size="small"
|
||||
highlight-current-row
|
||||
>
|
||||
<template v-for="col in allColumns" :key="col.prop">
|
||||
<el-table-column
|
||||
v-if="visibleColumnProps.includes(col.prop)"
|
||||
:prop="col.prop"
|
||||
:label="col.label"
|
||||
:min-width="col.minWidth || '120'"
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<template #default="scope" v-if="col.prop === 'serial_batch'">
|
||||
<span v-if="scope.row.serial_number" style="color: #409EFF; font-weight: bold;">
|
||||
SN: {{ scope.row.serial_number }}
|
||||
</span>
|
||||
<span v-else-if="scope.row.batch_number" style="color: #67C23A; font-weight: bold;">
|
||||
BN: {{ scope.row.batch_number }}
|
||||
</span>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
|
||||
<template #default="scope" v-else-if="col.prop === 'status'">
|
||||
<el-tag size="small" :type="getStatusType(scope.row.status)">
|
||||
{{ scope.row.status }}
|
||||
</el-tag>
|
||||
</template>
|
||||
|
||||
<template #default="scope" v-else-if="['unit_price', 'total_price'].includes(col.prop)">
|
||||
{{ formatMoney(scope.row[col.prop]) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
</template>
|
||||
|
||||
<el-table-column label="操作" width="150" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" size="small" @click="handleUpdate(row)">编辑</el-button>
|
||||
<el-popconfirm title="确定删除该条入库记录吗?" @confirm="handleDelete(row)">
|
||||
<template #reference>
|
||||
<el-button link type="danger" size="small">删除</el-button>
|
||||
</template>
|
||||
</el-popconfirm>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-pagination
|
||||
class="pagination-container"
|
||||
v-model:current-page="queryParams.page"
|
||||
v-model:page-size="queryParams.pageSize"
|
||||
:total="total"
|
||||
:page-sizes="[15, 30, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="fetchData"
|
||||
@current-change="fetchData"
|
||||
/>
|
||||
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
:title="dialogStatus === 'create' ? '新增采购件入库' : '编辑入库信息'"
|
||||
width="950px"
|
||||
top="3vh"
|
||||
destroy-on-close
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<el-form :model="form" label-width="120px" ref="formRef" :rules="rules" size="default">
|
||||
|
||||
<el-divider content-position="left"><b>1. 基础核心层 (Material Base)</b></el-divider>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="8">
|
||||
<el-form-item label="名称" prop="material_name">
|
||||
<el-input v-model="form.material_name" :disabled="dialogStatus === 'update'" placeholder="必填" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="规格型号" prop="spec_model">
|
||||
<el-input v-model="form.spec_model" :disabled="dialogStatus === 'update'" placeholder="必填: 内部货号" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8"><el-form-item label="计量单位" prop="unit"><el-input v-model="form.unit" /></el-form-item></el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="8"><el-form-item label="类别" prop="category"><el-input v-model="form.category" /></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="类型" prop="material_type"><el-input v-model="form.material_type" disabled /></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="可见等级" prop="visibility_level"><el-input-number v-model="form.visibility_level" :min="0" controls-position="right" style="width:100%" /></el-form-item></el-col>
|
||||
</el-row>
|
||||
|
||||
<el-divider content-position="left"><b>2. 实体库存层 (Stock Buy)</b></el-divider>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="8"><el-form-item label="编码/SKU" prop="sku"><el-input v-model="form.sku" placeholder="选填" /></el-form-item></el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="入库日期" prop="in_date">
|
||||
<el-input v-model="form.in_date" disabled placeholder="系统自动生成">
|
||||
<template #suffix><el-icon><Calendar /></el-icon></template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8"><el-form-item label="库位" prop="warehouse_location"><el-input v-model="form.warehouse_location" /></el-form-item></el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20" style="background-color: #fffbf0; border-radius: 4px; padding-top:10px;">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="序列号" prop="serial_number">
|
||||
<el-input v-model="form.serial_number" placeholder="设备SN码" clearable />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="批号" prop="batch_number">
|
||||
<el-input v-model="form.batch_number" placeholder="生产批次号" clearable />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<div style="font-size: 12px; color: #e6a23c; margin-left: 120px; margin-bottom: 10px; line-height: 1;">
|
||||
* 规则:序列号与批号互斥且必填其一 (填写一个会自动清空另一个)
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20" style="margin-top: 10px;">
|
||||
<el-col :span="12"><el-form-item label="到检状态" prop="inspection_status"><el-input v-model="form.inspection_status" /></el-form-item></el-col>
|
||||
<el-col :span="12"><el-form-item label="照片/到货图" prop="arrival_photo"><el-input v-model="form.arrival_photo" /></el-form-item></el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20" style="background-color: #f8fcfd; padding-top: 18px; border-radius: 4px; margin-top: 10px;">
|
||||
<el-col :span="8">
|
||||
<el-form-item label="入库量" prop="in_quantity">
|
||||
<el-input-number
|
||||
v-model="form.in_quantity"
|
||||
:min="1"
|
||||
:step="1"
|
||||
:precision="0"
|
||||
style="width:100%"
|
||||
controls-position="right"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="库存数量" prop="stock_quantity">
|
||||
<el-input-number v-model="form.stock_quantity" disabled style="width:100%" :controls="false" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="可用数量" prop="available_quantity">
|
||||
<el-input-number v-model="form.available_quantity" disabled style="width:100%" :controls="false" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-divider content-position="left"><b>3. 财务与商务信息</b></el-divider>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="8"><el-form-item label="单价" prop="unit_price"><el-input-number v-model="form.unit_price" :precision="4" style="width:100%" controls-position="right" /></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="总价" prop="total_price"><el-input-number v-model="form.total_price" :precision="4" style="width:100%" disabled :controls="false" /></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="供应商" prop="supplier_name"><el-input v-model="form.supplier_name" /></el-form-item></el-col>
|
||||
</el-row>
|
||||
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input v-model="form.remark" type="textarea" :rows="2" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="visible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitting" @click="submitForm">
|
||||
{{ dialogStatus === 'create' ? '确认入库' : '保存修改' }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted, watch } from 'vue'
|
||||
import { Plus, Setting, Refresh, Calendar } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import dayjs from 'dayjs'
|
||||
import { getBuyList, createBuyInbound, updateBuyInbound, deleteBuyInbound } from '@/api/inbound/buy'
|
||||
|
||||
// 状态控制
|
||||
const loading = ref(false)
|
||||
const submitting = ref(false)
|
||||
const visible = ref(false)
|
||||
const dialogStatus = ref<'create' | 'update'>('create')
|
||||
const tableData = ref([])
|
||||
const total = ref(0)
|
||||
const formRef = ref()
|
||||
const queryParams = reactive({ page: 1, pageSize: 15 })
|
||||
|
||||
// --- 1. 列定义 ---
|
||||
const baseColumns = [
|
||||
{ prop: 'material_name', label: '物料名称', minWidth: '150' },
|
||||
{ prop: 'category', label: '类别', minWidth: '100' },
|
||||
{ prop: 'spec_model', label: '规格型号', minWidth: '150' },
|
||||
{ prop: 'unit', label: '单位', minWidth: '70' }
|
||||
]
|
||||
|
||||
const stockColumns = [
|
||||
{ prop: 'sku', label: '编码/SKU', minWidth: '140' },
|
||||
{ prop: 'inbound_date', label: '入库日期', minWidth: '120' },
|
||||
// 组合展示列
|
||||
{ prop: 'serial_batch', label: '序列号/批号', minWidth: '160' },
|
||||
{ prop: 'stock_quantity', label: '库存数', minWidth: '90' },
|
||||
{ prop: 'available_quantity', label: '可用数', minWidth: '90' },
|
||||
{ prop: 'in_quantity', label: '入库量', minWidth: '90' },
|
||||
{ prop: 'unit_price', label: '单价', minWidth: '110' },
|
||||
{ prop: 'total_price', label: '总价', minWidth: '110' },
|
||||
{ prop: 'status', label: '状态', minWidth: '90' },
|
||||
{ prop: 'warehouse_location', label: '库位', minWidth: '100' },
|
||||
{ prop: 'supplier_name', label: '供应商', minWidth: '150' }
|
||||
]
|
||||
|
||||
const allColumns = [...baseColumns, ...stockColumns]
|
||||
|
||||
// --- 2. 默认展示列 ---
|
||||
const visibleColumnProps = ref([
|
||||
'material_name', 'spec_model', 'inbound_date',
|
||||
'serial_batch', 'stock_quantity', 'available_quantity', 'status'
|
||||
])
|
||||
|
||||
// --- 3. 表单对象 ---
|
||||
const form = reactive({
|
||||
id: undefined, // 编辑时使用
|
||||
material_name: '', category: '', spec_model: '', unit: '个',
|
||||
material_type: '采购件', visibility_level: 0,
|
||||
sku: '', in_date: '',
|
||||
serial_number: '', batch_number: '',
|
||||
status: '在库', inspection_status: '未检',
|
||||
in_quantity: 1, stock_quantity: 1, available_quantity: 1,
|
||||
warehouse_location: '', unit_price: 0, total_price: 0,
|
||||
supplier_name: '', arrival_photo: '', remark: ''
|
||||
})
|
||||
|
||||
// --- 4. 校验逻辑 (互斥且必填其一) ---
|
||||
const validateIdentity = (rule: any, value: any, callback: any) => {
|
||||
if (!form.serial_number && !form.batch_number) {
|
||||
callback(new Error('序列号和批号至少填写一项'))
|
||||
} else {
|
||||
// 清除交叉报错
|
||||
if (formRef.value) {
|
||||
if (rule.field === 'serial_number' && form.batch_number) formRef.value.clearValidate('batch_number')
|
||||
if (rule.field === 'batch_number' && form.serial_number) formRef.value.clearValidate('serial_number')
|
||||
}
|
||||
callback()
|
||||
}
|
||||
}
|
||||
|
||||
const rules = {
|
||||
material_name: [{ required: true, message: '必填', trigger: 'blur' }],
|
||||
spec_model: [{ required: true, message: '必填', trigger: 'blur' }],
|
||||
in_quantity: [{ required: true, message: '必填', trigger: 'blur' }],
|
||||
serial_number: [{ validator: validateIdentity, trigger: 'blur' }],
|
||||
batch_number: [{ validator: validateIdentity, trigger: 'blur' }]
|
||||
}
|
||||
|
||||
// --- 5. 监听逻辑 ---
|
||||
watch(() => form.serial_number, (val) => {
|
||||
if (val && form.batch_number) form.batch_number = ''
|
||||
})
|
||||
watch(() => form.batch_number, (val) => {
|
||||
if (val && form.serial_number) form.serial_number = ''
|
||||
})
|
||||
|
||||
watch(() => form.in_quantity, (newVal) => {
|
||||
if (newVal !== undefined) {
|
||||
if (dialogStatus.value === 'create') {
|
||||
form.stock_quantity = newVal
|
||||
form.available_quantity = newVal
|
||||
}
|
||||
form.total_price = Number((newVal * form.unit_price).toFixed(4))
|
||||
}
|
||||
})
|
||||
|
||||
watch(() => form.unit_price, (newVal) => {
|
||||
if (newVal !== undefined) {
|
||||
form.total_price = Number((newVal * form.in_quantity).toFixed(4))
|
||||
}
|
||||
})
|
||||
|
||||
// --- 6. 核心操作 ---
|
||||
|
||||
const fetchData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getBuyList(queryParams)
|
||||
tableData.value = res.data.items || []
|
||||
total.value = res.data.total || 0
|
||||
} finally { loading.value = false }
|
||||
}
|
||||
|
||||
const handleCreate = () => {
|
||||
dialogStatus.value = 'create'
|
||||
resetForm()
|
||||
form.in_date = dayjs().format('YYYY-MM-DD HH:mm:ss')
|
||||
visible.value = true
|
||||
}
|
||||
|
||||
// 核心修改:手动映射后端数据到前端表单
|
||||
const handleUpdate = (row: any) => {
|
||||
dialogStatus.value = 'update'
|
||||
|
||||
// 先重置表单防止残留
|
||||
resetForm()
|
||||
|
||||
// 1. 基础字段拷贝
|
||||
Object.assign(form, row)
|
||||
|
||||
// 2. 修正字段映射 (后端Key -> 前端Form Key)
|
||||
form.id = row.id
|
||||
form.in_quantity = Number(row.qty_inbound) || 1
|
||||
form.stock_quantity = Number(row.qty_inbound) || 1 // 这里假设库存没变,或者应由后端传回stock_quantity
|
||||
form.available_quantity = Number(row.qty_available) || 1
|
||||
form.unit_price = Number(row.price_unit) || 0
|
||||
form.warehouse_location = row.warehouse_loc || ''
|
||||
form.serial_number = row.serial_number || ''
|
||||
form.batch_number = row.batch_number || ''
|
||||
form.status = row.status || '在库'
|
||||
|
||||
// 3. 计算总价
|
||||
form.total_price = Number((form.in_quantity * form.unit_price).toFixed(2))
|
||||
|
||||
// 4. 补充日期
|
||||
if (!form.in_date) form.in_date = dayjs().format('YYYY-MM-DD HH:mm:ss')
|
||||
|
||||
visible.value = true
|
||||
}
|
||||
|
||||
const handleDelete = async (row: any) => {
|
||||
try {
|
||||
await deleteBuyInbound(row.id)
|
||||
ElMessage.success('删除成功')
|
||||
fetchData()
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e.message || '删除失败')
|
||||
}
|
||||
}
|
||||
|
||||
const submitForm = async () => {
|
||||
if (!formRef.value) return
|
||||
await formRef.value.validate(async (valid: boolean) => {
|
||||
if (valid) {
|
||||
submitting.value = true
|
||||
try {
|
||||
if (dialogStatus.value === 'create') {
|
||||
await createBuyInbound(form)
|
||||
ElMessage.success('入库成功')
|
||||
} else {
|
||||
await updateBuyInbound(form.id!, form)
|
||||
ElMessage.success('更新成功')
|
||||
}
|
||||
visible.value = false
|
||||
fetchData()
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e.message || '提交失败')
|
||||
} finally { submitting.value = false }
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const resetForm = () => {
|
||||
Object.assign(form, {
|
||||
id: undefined,
|
||||
material_name: '', category: '', spec_model: '', unit: '个',
|
||||
material_type: '采购件', visibility_level: 0,
|
||||
sku: '', in_date: '',
|
||||
serial_number: '', batch_number: '',
|
||||
status: '在库', inspection_status: '未检',
|
||||
in_quantity: 1, stock_quantity: 1, available_quantity: 1,
|
||||
warehouse_location: '', unit_price: 0, total_price: 0,
|
||||
supplier_name: '', arrival_photo: '', remark: ''
|
||||
})
|
||||
}
|
||||
|
||||
const getStatusType = (status: string) => {
|
||||
return status === '在库' ? 'success' : 'info'
|
||||
}
|
||||
|
||||
const formatMoney = (val: number) => {
|
||||
return val ? `¥ ${Number(val).toFixed(2)}` : '-'
|
||||
}
|
||||
|
||||
onMounted(() => fetchData())
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.buy-module { background: #fff; border-radius: 8px; }
|
||||
.header-tools { display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; }
|
||||
.column-selector { display: flex; flex-direction: column; padding: 10px; max-height: 400px; overflow-y: auto; }
|
||||
.pagination-container { margin-top: 20px; display: flex; justify-content: flex-end; }
|
||||
:deep(.el-divider--horizontal) { margin: 15px 0 15px 0; }
|
||||
</style>
|
||||
27
inventory-web/src/views/stock/inbound/index.vue
Normal file
27
inventory-web/src/views/stock/inbound/index.vue
Normal file
@ -0,0 +1,27 @@
|
||||
<template>
|
||||
<div class="inbound-container" style="padding: 20px;">
|
||||
<el-tabs v-model="activeModule" type="border-card">
|
||||
<el-tab-pane label="物料采购入库" name="buy">
|
||||
<BuyInbound v-if="activeModule === 'buy'" />
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="半成品入库" name="semi">
|
||||
<SemiInbound v-if="activeModule === 'semi'" />
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="成品入库" name="product">
|
||||
<ProductInbound v-if="activeModule === 'product'" />
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
// 因为在同一个文件夹下,直接用 ./ 即可
|
||||
import BuyInbound from './buy.vue'
|
||||
import SemiInbound from './semi.vue'
|
||||
import ProductInbound from './product.vue'
|
||||
|
||||
const activeModule = ref('buy')
|
||||
</script>
|
||||
11
inventory-web/src/views/stock/inbound/product.vue
Normal file
11
inventory-web/src/views/stock/inbound/product.vue
Normal file
@ -0,0 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
11
inventory-web/src/views/stock/inbound/semi.vue
Normal file
11
inventory-web/src/views/stock/inbound/semi.vue
Normal file
@ -0,0 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
1
inventory-web/src/views/stock/inbound/service.vue
Normal file
1
inventory-web/src/views/stock/inbound/service.vue
Normal file
@ -0,0 +1 @@
|
||||
<template><div style="padding:20px;"><h2>服务权益管理</h2></div></template>
|
||||
@ -1,11 +1 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
<template><div style="padding:20px;"><h2>借库申请</h2></div></template>
|
||||
@ -1,11 +1 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
<template><div style="padding:20px;"><h2>维修登记</h2></div></template>
|
||||
@ -1,11 +1 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
<template><div style="padding:20px;"><h2>报废处理</h2></div></template>
|
||||
@ -9,14 +9,21 @@ export default defineConfig({
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||
}
|
||||
},
|
||||
// --- 新增下面这一段 server 配置 ---
|
||||
server: {
|
||||
// 【关键修改1】必须设置为 0.0.0.0,否则容器外无法访问
|
||||
host: '0.0.0.0',
|
||||
// 【关键修改2】显式指定端口,与 docker-compose 映射保持一致
|
||||
port: 5173,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://127.0.0.1:5000', // 后端的地址
|
||||
// 【关键修改3】
|
||||
// 1. 'backend' 是 docker-compose.yml 里的服务名
|
||||
// 2. 端口改为 8000 (Gunicorn 配置的端口)
|
||||
target: 'http://backend:8000',
|
||||
changeOrigin: true,
|
||||
// 如果你的后端路径本身就包含 /api,通常不需要 rewrite
|
||||
// rewrite: (path) => path.replace(/^\/api/, '')
|
||||
// 注意:如果你的 Flask 路由代码里没有写 /api 前缀(例如 @app.route('/login')),
|
||||
// 那么你需要取消下面这行的注释,把 /api 去掉,否则后端会收到 /api/login 报 404
|
||||
rewrite: (path) => path.replace(/^\/api/, '')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user