python-flask和Vue两种模式初模板
This commit is contained in:
@ -1,7 +1,7 @@
|
|||||||
|
# 文件路径: app/__init__.py
|
||||||
from flask import Flask
|
from flask import Flask
|
||||||
from config import Config
|
from config import Config
|
||||||
from app.extensions import db, ma
|
from app.extensions import db, migrate, cors
|
||||||
|
|
||||||
|
|
||||||
def create_app():
|
def create_app():
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
@ -9,18 +9,15 @@ def create_app():
|
|||||||
|
|
||||||
# 初始化插件
|
# 初始化插件
|
||||||
db.init_app(app)
|
db.init_app(app)
|
||||||
ma.init_app(app)
|
migrate.init_app(app, db)
|
||||||
|
cors.init_app(app) # 允许前端访问
|
||||||
|
|
||||||
# 【新增关键步骤】: 显式导入 models,让 SQLAlchemy 认识所有的表
|
# 注册蓝图 (Blueprints)
|
||||||
# 必须放在 db.init_app 之后,create_all 或 蓝图注册 之前
|
from app.api.v1.stocks import stocks_bp
|
||||||
from app import models
|
app.register_blueprint(stocks_bp, url_prefix='/api/v1/stocks')
|
||||||
|
|
||||||
# 注册路由蓝图
|
# 可以在这里打印一下路由,方便调试
|
||||||
from app.api.v1.stocks import stock_bp
|
print("已注册路由:")
|
||||||
app.register_blueprint(stock_bp, url_prefix='/api/v1')
|
print(app.url_map)
|
||||||
|
|
||||||
# 【可选】如果你没有用 Flask-Migrate,可以用下面这句话自动建表(开发阶段)
|
|
||||||
# with app.app_context():
|
|
||||||
# db.create_all()
|
|
||||||
|
|
||||||
return app
|
return app
|
||||||
@ -1,34 +1,57 @@
|
|||||||
from flask import Blueprint, request, jsonify
|
from flask import Blueprint, request, jsonify
|
||||||
from app.services.stock_service import create_inbound_stock
|
from app.services.stock_service import StockService
|
||||||
from app.schemas.stock_schema import StockBuySchema
|
from app.schemas.stock_schema import stock_buy_schema
|
||||||
|
|
||||||
stock_bp = Blueprint('stocks', __name__)
|
stocks_bp = Blueprint('stocks', __name__)
|
||||||
|
|
||||||
@stock_bp.route('/buy-inbound', methods=['POST'])
|
|
||||||
def buy_inbound():
|
@stocks_bp.route('/inbound', methods=['GET'])
|
||||||
"""
|
def get_inbound_list():
|
||||||
采购入库接口
|
page = request.args.get('page', 1, type=int)
|
||||||
POST /api/v1/buy-inbound
|
limit = request.args.get('pageSize', 10, type=int)
|
||||||
Body: { "material_id": 1, "qty_inbound": 100, "price_unit": 10.5 ... }
|
|
||||||
"""
|
result = StockService.get_list(page, limit)
|
||||||
# 1. 接收 JSON 数据
|
|
||||||
|
return jsonify({
|
||||||
|
'code': 200,
|
||||||
|
'msg': 'success',
|
||||||
|
'data': result
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@stocks_bp.route('/inbound', methods=['POST'])
|
||||||
|
def create_inbound():
|
||||||
json_data = request.get_json()
|
json_data = request.get_json()
|
||||||
if not json_data:
|
|
||||||
return jsonify({"message": "No input data provided"}), 400
|
|
||||||
|
|
||||||
# 2. 数据校验
|
|
||||||
schema = StockBuySchema()
|
|
||||||
try:
|
try:
|
||||||
# 这一步只做校验,不直接生成对象,因为我们要在 Service 里手动处理逻辑
|
# 1. 参数校验
|
||||||
data = schema.load(json_data, partial=True)
|
data = stock_buy_schema.load(json_data)
|
||||||
|
# 2. 调用业务逻辑
|
||||||
|
new_stock = StockService.create_inbound(data)
|
||||||
|
# 3. 返回成功
|
||||||
|
return jsonify({
|
||||||
|
'code': 200,
|
||||||
|
'msg': '入库成功',
|
||||||
|
'data': new_stock.to_dict()
|
||||||
|
}), 201
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({"message": "Validation error", "errors": e.messages}), 422
|
# 捕获 ValueError 或 SQLAlchemyError
|
||||||
|
return jsonify({'code': 400, 'msg': str(e)}), 400
|
||||||
|
|
||||||
# 3. 调用业务逻辑
|
|
||||||
|
@stocks_bp.route('/inbound/<int:id>', methods=['PUT'])
|
||||||
|
def update_inbound(id):
|
||||||
|
json_data = request.get_json()
|
||||||
try:
|
try:
|
||||||
new_stock = create_inbound_stock(data)
|
StockService.update_inbound(id, json_data)
|
||||||
# 4. 返回成功结果
|
return jsonify({'code': 200, 'msg': '更新成功'})
|
||||||
result = schema.dump(new_stock)
|
|
||||||
return jsonify({"message": "Inbound successful", "data": result}), 201
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({"message": "Internal Server Error", "error": str(e)}), 500
|
return jsonify({'code': 400, 'msg': str(e)}), 400
|
||||||
|
|
||||||
|
|
||||||
|
@stocks_bp.route('/inbound/<int:id>', methods=['DELETE'])
|
||||||
|
def delete_inbound(id):
|
||||||
|
try:
|
||||||
|
StockService.delete_inbound(id)
|
||||||
|
return jsonify({'code': 200, 'msg': '删除成功'})
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({'code': 400, 'msg': str(e)}), 400
|
||||||
@ -1,6 +1,8 @@
|
|||||||
|
# 文件路径: app/extensions.py
|
||||||
from flask_sqlalchemy import SQLAlchemy
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
from flask_marshmallow import Marshmallow
|
from flask_migrate import Migrate
|
||||||
|
from flask_cors import CORS # 解决前后端跨域问题
|
||||||
|
|
||||||
# 初始化数据库和序列化工具
|
|
||||||
db = SQLAlchemy()
|
db = SQLAlchemy()
|
||||||
ma = Marshmallow()
|
migrate = Migrate()
|
||||||
|
cors = CORS()
|
||||||
@ -1,11 +1,34 @@
|
|||||||
from app.extensions import db
|
from app.extensions import db
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
class MaterialBase(db.Model):
|
class MaterialBase(db.Model):
|
||||||
__tablename__ = 'material_base'
|
__tablename__ = 'material_base'
|
||||||
|
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
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)
|
sku_code = db.Column(db.String(100), unique=True, nullable=False) # 唯一编码
|
||||||
spec_model = db.Column(db.String(255))
|
name = db.Column(db.String(255), nullable=False) # 名称
|
||||||
unit = db.Column(db.String(50))
|
spec_model = db.Column(db.String(255)) # 规格型号
|
||||||
# 其他字段按需添加,入库时主要是为了外键关联
|
unit = db.Column(db.String(50)) # 单位
|
||||||
|
category = db.Column(db.String(100)) # 分类
|
||||||
|
|
||||||
|
# 审计字段 (自动记录时间)
|
||||||
|
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 时进行进一步过滤
|
||||||
|
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
|
||||||
|
}
|
||||||
@ -6,20 +6,51 @@ class StockBuy(db.Model):
|
|||||||
__tablename__ = 'stock_buy'
|
__tablename__ = 'stock_buy'
|
||||||
|
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
|
||||||
|
# 外键:必须关联一个 MaterialBase 的 ID
|
||||||
material_id = db.Column(db.Integer, db.ForeignKey('material_base.id'), nullable=False)
|
material_id = db.Column(db.Integer, db.ForeignKey('material_base.id'), nullable=False)
|
||||||
inbound_date = db.Column(db.DateTime, default=datetime.now)
|
|
||||||
barcode = db.Column(db.String(100))
|
|
||||||
batch_no = db.Column(db.String(100))
|
|
||||||
|
|
||||||
# 数量相关 (使用 Numeric 对应数据库的 NUMERIC)
|
# 业务数据
|
||||||
qty_inbound = db.Column(db.Numeric(19, 4), default=0)
|
inbound_date = db.Column(db.DateTime, default=datetime.utcnow) # 入库时间
|
||||||
qty_current = db.Column(db.Numeric(19, 4), default=0)
|
batch_no = db.Column(db.String(100)) # 批次号
|
||||||
qty_available = db.Column(db.Numeric(19, 4), default=0)
|
warehouse_loc = db.Column(db.String(100)) # 库位
|
||||||
|
supplier_name = db.Column(db.String(255)) # 供应商
|
||||||
|
|
||||||
price_unit = db.Column(db.Numeric(19, 4), default=0)
|
# 数量与状态
|
||||||
price_total = db.Column(db.Numeric(19, 4), default=0)
|
qty_inbound = db.Column(db.Numeric(19, 4), default=0) # 初始入库量
|
||||||
supplier_name = db.Column(db.String(255))
|
qty_current = db.Column(db.Numeric(19, 4), default=0) # 当前剩余量
|
||||||
warehouse_loc = db.Column(db.String(100))
|
qty_available = db.Column(db.Numeric(19, 4), default=0) # 当前可用量
|
||||||
|
status = db.Column(db.String(50), default='NORMAL')
|
||||||
|
|
||||||
# 建立关联,方便查询物料详情
|
# 财务数据
|
||||||
material = db.relationship('MaterialBase', backref='buy_stocks')
|
price_unit = db.Column(db.Numeric(19, 4), default=0) # 单价
|
||||||
|
price_total = db.Column(db.Numeric(19, 4), default=0) # 总价
|
||||||
|
|
||||||
|
# 建立与 MaterialBase 的双向关系
|
||||||
|
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,
|
||||||
|
'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,
|
||||||
|
|
||||||
|
# 本表信息
|
||||||
|
'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,
|
||||||
|
}
|
||||||
@ -1,14 +1,42 @@
|
|||||||
from app.extensions import ma
|
from marshmallow import Schema, fields, validate, validates_schema, ValidationError
|
||||||
from app.models.stock import StockBuy
|
|
||||||
from marshmallow import fields
|
|
||||||
|
|
||||||
class StockBuySchema(ma.SQLAlchemyAutoSchema):
|
|
||||||
class Meta:
|
|
||||||
model = StockBuy
|
|
||||||
load_instance = True # 反序列化时自动创建模型实例
|
|
||||||
include_fk = True # 包含外键 material_id
|
|
||||||
|
|
||||||
# 必须字段校验
|
class StockBuySchema(Schema):
|
||||||
material_id = fields.Integer(required=True)
|
# 只用于输出的字段
|
||||||
qty_inbound = fields.Decimal(required=True, as_string=True)
|
id = fields.Int(dump_only=True)
|
||||||
price_unit = fields.Decimal(missing=0, as_string=True)
|
|
||||||
|
# --- 输入字段 ---
|
||||||
|
|
||||||
|
# 1. 核心识别字段
|
||||||
|
material_id = fields.Int(missing=None) # 如果是老物料,可能传ID
|
||||||
|
sku_code = fields.Str(required=True, error_messages={"required": "SKU编码是必填项"}) # 必填
|
||||||
|
|
||||||
|
# 2. 新物料自动建档字段 (如果是新SKU,这些需要校验)
|
||||||
|
material_name = fields.Str(missing=None)
|
||||||
|
spec_model = fields.Str(missing=None)
|
||||||
|
unit = fields.Str(missing=None)
|
||||||
|
category = fields.Str(missing=None)
|
||||||
|
|
||||||
|
# 3. 入库业务字段
|
||||||
|
qty_inbound = fields.Float(required=True, validate=validate.Range(min=0.0001, error="入库数量必须大于0"))
|
||||||
|
price_unit = fields.Float(missing=0)
|
||||||
|
|
||||||
|
inbound_date = fields.DateTime(format='%Y-%m-%d %H:%M:%S')
|
||||||
|
batch_no = fields.Str(missing='')
|
||||||
|
warehouse_loc = fields.Str(missing='')
|
||||||
|
supplier_name = fields.Str(missing='')
|
||||||
|
|
||||||
|
@validates_schema
|
||||||
|
def validate_material_logic(self, data, **kwargs):
|
||||||
|
"""
|
||||||
|
自定义校验逻辑:
|
||||||
|
如果用户没传 material_id,说明可能想新建物料。
|
||||||
|
虽然最终是否新建由 Service 层判断数据库决定,
|
||||||
|
但这里可以做一个弱校验:尽量让用户填上名字。
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
# 这里暂时不强制抛出错误,交给 Service 层处理 "SKU不存在且无名字" 的情况
|
||||||
|
|
||||||
|
|
||||||
|
# 实例化 Schema
|
||||||
|
stock_buy_schema = StockBuySchema()
|
||||||
@ -1,39 +1,119 @@
|
|||||||
from app.extensions import db
|
from app.extensions import db
|
||||||
from app.models.stock import StockBuy
|
from app.models.stock import StockBuy
|
||||||
|
from app.models.material import MaterialBase
|
||||||
from sqlalchemy.exc import SQLAlchemyError
|
from sqlalchemy.exc import SQLAlchemyError
|
||||||
|
|
||||||
|
|
||||||
def create_inbound_stock(data):
|
class StockService:
|
||||||
|
@staticmethod
|
||||||
|
def create_inbound(data):
|
||||||
"""
|
"""
|
||||||
处理采购入库逻辑
|
处理入库逻辑:
|
||||||
|
1. 根据 SKU 查找物料。
|
||||||
|
2. 如果没找到,创建新物料 (MaterialBase)。
|
||||||
|
3. 创建入库单 (StockBuy)。
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# 1. 计算总价
|
sku = data.get('sku_code')
|
||||||
|
material_id = data.get('material_id')
|
||||||
|
|
||||||
|
# --- 第一步:确定 material_id ---
|
||||||
|
|
||||||
|
# 如果前端没传 ID,或者传了但我们想二次确认,都通过 SKU 查一遍
|
||||||
|
existing_material = MaterialBase.query.filter_by(sku_code=sku).first()
|
||||||
|
|
||||||
|
if existing_material:
|
||||||
|
# 场景 A: 物料已存在 -> 直接使用其 ID
|
||||||
|
material_id = existing_material.id
|
||||||
|
else:
|
||||||
|
# 场景 B: 物料不存在 -> 自动创建新物料
|
||||||
|
if not data.get('material_name'):
|
||||||
|
raise ValueError(f"SKU [{sku}] 是新物料,必须填写【物料名称】才能入库。")
|
||||||
|
|
||||||
|
new_material = MaterialBase(
|
||||||
|
sku_code=sku,
|
||||||
|
name=data.get('material_name'),
|
||||||
|
spec_model=data.get('spec_model'),
|
||||||
|
unit=data.get('unit'),
|
||||||
|
category=data.get('category')
|
||||||
|
)
|
||||||
|
db.session.add(new_material)
|
||||||
|
db.session.flush() # 关键:将对象刷入暂存区,以获取自增的 ID
|
||||||
|
material_id = new_material.id
|
||||||
|
|
||||||
|
# --- 第二步:创建入库单 ---
|
||||||
|
|
||||||
qty = data.get('qty_inbound')
|
qty = data.get('qty_inbound')
|
||||||
price = data.get('price_unit', 0)
|
price = data.get('price_unit', 0)
|
||||||
total = float(qty) * float(price)
|
|
||||||
|
|
||||||
# 2. 创建库存记录
|
|
||||||
# 注意:入库时,当前库存(current)和可用库存(available)通常等于入库数量
|
|
||||||
new_stock = StockBuy(
|
new_stock = StockBuy(
|
||||||
material_id=data['material_id'],
|
material_id=material_id,
|
||||||
barcode=data.get('barcode'),
|
inbound_date=data.get('inbound_date'),
|
||||||
batch_no=data.get('batch_no'),
|
batch_no=data.get('batch_no'),
|
||||||
qty_inbound=qty,
|
|
||||||
qty_current=qty, # 初始:当前=入库
|
|
||||||
qty_available=qty, # 初始:可用=入库
|
|
||||||
price_unit=price,
|
|
||||||
price_total=total,
|
|
||||||
supplier_name=data.get('supplier_name'),
|
|
||||||
warehouse_loc=data.get('warehouse_loc'),
|
warehouse_loc=data.get('warehouse_loc'),
|
||||||
inbound_date=data.get('inbound_date') # 如果前端没传,Model会默认用当前时间
|
supplier_name=data.get('supplier_name'),
|
||||||
|
|
||||||
|
# 数量逻辑:初始时,当前量 = 可用量 = 入库量
|
||||||
|
qty_inbound=qty,
|
||||||
|
qty_current=qty,
|
||||||
|
qty_available=qty,
|
||||||
|
|
||||||
|
# 财务逻辑
|
||||||
|
price_unit=price,
|
||||||
|
price_total=float(qty) * float(price) if qty else 0
|
||||||
)
|
)
|
||||||
|
|
||||||
db.session.add(new_stock)
|
db.session.add(new_stock)
|
||||||
db.session.commit()
|
db.session.commit() # 统一提交事务
|
||||||
|
|
||||||
return new_stock
|
return new_stock
|
||||||
|
|
||||||
except SQLAlchemyError as e:
|
except SQLAlchemyError as e:
|
||||||
db.session.rollback()
|
db.session.rollback() # 数据库报错回滚
|
||||||
raise e
|
raise e
|
||||||
|
except ValueError as e:
|
||||||
|
db.session.rollback() # 业务报错回滚
|
||||||
|
raise e
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_list(page, per_page):
|
||||||
|
"""获取分页列表"""
|
||||||
|
pagination = StockBuy.query.order_by(StockBuy.inbound_date.desc()).paginate(
|
||||||
|
page=page, per_page=per_page, error_out=False
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
'items': [item.to_dict() for item in pagination.items],
|
||||||
|
'total': pagination.total,
|
||||||
|
'pages': pagination.pages,
|
||||||
|
'current_page': pagination.page
|
||||||
|
}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def update_inbound(stock_id, data):
|
||||||
|
"""更新入库单信息 (通常不允许改物料本身,只改入库相关)"""
|
||||||
|
stock = StockBuy.query.get_or_404(stock_id)
|
||||||
|
|
||||||
|
if 'warehouse_loc' in data: stock.warehouse_loc = data['warehouse_loc']
|
||||||
|
if 'supplier_name' in data: stock.supplier_name = data['supplier_name']
|
||||||
|
if 'batch_no' in data: stock.batch_no = data['batch_no']
|
||||||
|
if 'price_unit' in data: stock.price_unit = data['price_unit']
|
||||||
|
|
||||||
|
# 如果修改了数量,需要级联更新当前库存
|
||||||
|
if 'qty_inbound' in data:
|
||||||
|
old_qty = float(stock.qty_inbound)
|
||||||
|
new_qty = float(data['qty_inbound'])
|
||||||
|
diff = new_qty - old_qty
|
||||||
|
|
||||||
|
stock.qty_inbound = new_qty
|
||||||
|
stock.qty_current = float(stock.qty_current) + diff
|
||||||
|
stock.qty_available = float(stock.qty_available) + diff
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
return stock
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def delete_inbound(stock_id):
|
||||||
|
"""删除入库单"""
|
||||||
|
stock = StockBuy.query.get_or_404(stock_id)
|
||||||
|
db.session.delete(stock)
|
||||||
|
db.session.commit()
|
||||||
@ -5,7 +5,7 @@ class Config:
|
|||||||
# 数据库连接配置
|
# 数据库连接配置
|
||||||
# 请务必将 '你的密码' 替换为你 PostgreSQL 的真实密码
|
# 请务必将 '你的密码' 替换为你 PostgreSQL 的真实密码
|
||||||
# 如果数据库不在本地,请将 localhost 替换为 IP 地址
|
# 如果数据库不在本地,请将 localhost 替换为 IP 地址
|
||||||
SQLALCHEMY_DATABASE_URI = 'postgresql://postgres:1234@localhost:5432/inventory_system'
|
SQLALCHEMY_DATABASE_URI = 'postgresql://test:1234@localhost:5432/inventory_system'
|
||||||
|
|
||||||
# 关闭 SQLAlchemy 的事件追踪,减少内存消耗
|
# 关闭 SQLAlchemy 的事件追踪,减少内存消耗
|
||||||
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
|
# 文件路径: run.py (在项目根目录下,与 config.py 同级)
|
||||||
from app import create_app
|
from app import create_app
|
||||||
|
|
||||||
app = create_app()
|
app = create_app()
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
# debug=True 修改代码后会自动重启
|
||||||
app.run(host='0.0.0.0', port=5000, debug=True)
|
app.run(host='0.0.0.0', port=5000, debug=True)
|
||||||
@ -4,7 +4,7 @@
|
|||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite --host",
|
||||||
"build": "vue-tsc -b && vite build",
|
"build": "vue-tsc -b && vite build",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,30 +1,115 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import HelloWorld from './components/HelloWorld.vue'
|
// 不需要引入组件,由 router-view 控制
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="app-wrapper">
|
||||||
<a href="https://vite.dev" target="_blank">
|
<header class="app-header">
|
||||||
<img src="/vite.svg" class="logo" alt="Vite logo" />
|
<div class="logo-container">
|
||||||
</a>
|
<router-link to="/" class="home-link">
|
||||||
<a href="https://vuejs.org/" target="_blank">
|
<img src="./assets/iris.png" class="logo" alt="Logo" />
|
||||||
<img src="./assets/vue.svg" class="logo vue" alt="Vue logo" />
|
<span class="system-title">库存管理系统</span>
|
||||||
</a>
|
</router-link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="header-right">
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="app-content">
|
||||||
|
<router-view />
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer class="app-footer">
|
||||||
|
<span class="version-tag">
|
||||||
|
<el-icon style="vertical-align: middle; margin-right: 4px"><InfoFilled /></el-icon>
|
||||||
|
当前版本: 1.0 Beta (测试版)
|
||||||
|
</span>
|
||||||
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
<HelloWorld msg="Vite + Vue" />
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style>
|
||||||
|
/* 全局重置 */
|
||||||
|
html, body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
height: 100%;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
#app {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100vh; /* 占满全屏高度 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 顶部栏样式 */
|
||||||
|
.app-header {
|
||||||
|
height: 60px;
|
||||||
|
background-color: #ffffff;
|
||||||
|
border-bottom: 1px solid #dcdfe6;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0 20px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||||
|
flex-shrink: 0; /* 防止被压缩 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-link {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 15px;
|
||||||
|
text-decoration: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
.logo {
|
.logo {
|
||||||
height: 6em;
|
height: 40px;
|
||||||
padding: 1.5em;
|
width: auto;
|
||||||
will-change: filter;
|
|
||||||
transition: filter 300ms;
|
|
||||||
}
|
}
|
||||||
.logo:hover {
|
|
||||||
filter: drop-shadow(0 0 2em #646cffaa);
|
.system-title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #303133;
|
||||||
}
|
}
|
||||||
.logo.vue:hover {
|
|
||||||
filter: drop-shadow(0 0 2em #42b883aa);
|
/* 内容区样式 */
|
||||||
|
.app-content {
|
||||||
|
flex: 1; /* 关键:这会让内容区自动撑开,把 footer 挤到最底下 */
|
||||||
|
overflow: auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 新增:底部栏样式 */
|
||||||
|
.app-footer {
|
||||||
|
height: 30px; /* 固定高度 */
|
||||||
|
background-color: #e9e9eb; /* 稍微深一点的灰色,区分内容区 */
|
||||||
|
border-top: 1px solid #dcdfe6;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center; /* 文字居中 */
|
||||||
|
flex-shrink: 0; /* 防止被压缩 */
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
|
.version-tag {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #e6a23c; /* 使用橙色,表示“测试/警告”意味 */
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
0
inventory-web/src/api/auth.ts
Normal file
0
inventory-web/src/api/auth.ts
Normal file
0
inventory-web/src/api/material.ts
Normal file
0
inventory-web/src/api/material.ts
Normal file
39
inventory-web/src/api/stock.ts
Normal file
39
inventory-web/src/api/stock.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
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'
|
||||||
|
})
|
||||||
|
}
|
||||||
0
inventory-web/src/api/transaction.ts
Normal file
0
inventory-web/src/api/transaction.ts
Normal file
BIN
inventory-web/src/assets/iris.png
Normal file
BIN
inventory-web/src/assets/iris.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 84 KiB |
@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>
|
|
||||||
|
Before Width: | Height: | Size: 496 B |
11
inventory-web/src/layout/index.vue
Normal file
11
inventory-web/src/layout/index.vue
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@ -1,5 +1,29 @@
|
|||||||
import { createApp } from 'vue'
|
import { createApp } from 'vue'
|
||||||
import './style.css'
|
|
||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
|
|
||||||
createApp(App).mount('#app')
|
// 1. 引入路由配置 (确保你已经创建了 src/router/index.ts)
|
||||||
|
import router from './router'
|
||||||
|
|
||||||
|
// 2. 引入 Element Plus (UI组件库)
|
||||||
|
import ElementPlus from 'element-plus'
|
||||||
|
import 'element-plus/dist/index.css'
|
||||||
|
// 引入中文包
|
||||||
|
import zhCn from 'element-plus/es/locale/lang/zh-cn'
|
||||||
|
|
||||||
|
// 3. 引入图标
|
||||||
|
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
|
||||||
|
|
||||||
|
const app = createApp(App)
|
||||||
|
|
||||||
|
// 注册所有图标
|
||||||
|
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
||||||
|
app.component(key, component)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用插件
|
||||||
|
app.use(router)
|
||||||
|
app.use(ElementPlus, {
|
||||||
|
locale: zhCn, // 设置为中文
|
||||||
|
})
|
||||||
|
|
||||||
|
app.mount('#app')
|
||||||
24
inventory-web/src/router/index.ts
Normal file
24
inventory-web/src/router/index.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
|
|
||||||
|
const routes = [
|
||||||
|
// --- 修改点:根路径不再重定向,而是显示 Dashboard 首页 ---
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
name: 'Dashboard',
|
||||||
|
component: () => import('@/views/dashboard/index.vue')
|
||||||
|
},
|
||||||
|
|
||||||
|
// --- 保持原有的入库页路由不变 ---
|
||||||
|
{
|
||||||
|
path: '/stock/inbound',
|
||||||
|
name: 'StockInbound',
|
||||||
|
component: () => import('@/views/stock/inbound.vue')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const router = createRouter({
|
||||||
|
history: createWebHistory(),
|
||||||
|
routes
|
||||||
|
})
|
||||||
|
|
||||||
|
export default router
|
||||||
0
inventory-web/src/router/permission.ts
Normal file
0
inventory-web/src/router/permission.ts
Normal file
0
inventory-web/src/stores/tagsView.ts
Normal file
0
inventory-web/src/stores/tagsView.ts
Normal file
11
inventory-web/src/stores/user.vue
Normal file
11
inventory-web/src/stores/user.vue
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
0
inventory-web/src/utils/format.ts
Normal file
0
inventory-web/src/utils/format.ts
Normal file
47
inventory-web/src/utils/request.ts
Normal file
47
inventory-web/src/utils/request.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import axios from 'axios'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
|
||||||
|
// 1. 创建 axios 实例
|
||||||
|
const service = axios.create({
|
||||||
|
// 这里的 '/api' 配合 vite.config.ts 的 proxy 使用
|
||||||
|
baseURL: '/api/v1',
|
||||||
|
timeout: 5000 // 请求超时时间
|
||||||
|
})
|
||||||
|
|
||||||
|
// 2. 请求拦截器 (可以在这里加 Token)
|
||||||
|
service.interceptors.request.use(
|
||||||
|
(config) => {
|
||||||
|
// 如果以后有登录 token,就在这里加
|
||||||
|
// const token = localStorage.getItem('token')
|
||||||
|
// if (token) {
|
||||||
|
// config.headers['Authorization'] = 'Bearer ' + token
|
||||||
|
// }
|
||||||
|
return config
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
return Promise.reject(error)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// 3. 响应拦截器 (统一处理错误)
|
||||||
|
service.interceptors.response.use(
|
||||||
|
(response) => {
|
||||||
|
const res = response.data
|
||||||
|
// 这里可以根据后端的 code 来判断
|
||||||
|
// 假设你的后端成功返回 code: 200
|
||||||
|
if (res.code && res.code !== 200) {
|
||||||
|
ElMessage.error(res.msg || 'Error')
|
||||||
|
return Promise.reject(new Error(res.msg || 'Error'))
|
||||||
|
} else {
|
||||||
|
return res // 直接返回数据部分
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
console.log('err' + error)
|
||||||
|
ElMessage.error(error.message || '请求失败')
|
||||||
|
return Promise.reject(error)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// 4. 【关键】必须默认导出 service
|
||||||
|
export default service
|
||||||
0
inventory-web/src/utils/validate.ts
Normal file
0
inventory-web/src/utils/validate.ts
Normal file
61
inventory-web/src/views/dashboard/index.vue
Normal file
61
inventory-web/src/views/dashboard/index.vue
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
<template>
|
||||||
|
<div class="dashboard-container">
|
||||||
|
<el-card class="welcome-card">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<span>👋 欢迎回来</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div class="card-body">
|
||||||
|
<h2>欢迎使用 IRIS 库存管理系统</h2>
|
||||||
|
<p>请点击下方按钮进入具体业务模块:</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>
|
||||||
|
|
||||||
|
<el-button size="large" disabled>
|
||||||
|
<el-icon style="margin-right: 5px"><TrendCharts /></el-icon>
|
||||||
|
库存报表 (开发中)
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Box, TrendCharts } from '@element-plus/icons-vue'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.dashboard-container {
|
||||||
|
padding: 20px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-card {
|
||||||
|
width: 600px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-body h2 {
|
||||||
|
color: #303133;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-body p {
|
||||||
|
color: #606266;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
11
inventory-web/src/views/login/index.vue
Normal file
11
inventory-web/src/views/login/index.vue
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
11
inventory-web/src/views/material/list.vue
Normal file
11
inventory-web/src/views/material/list.vue
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
237
inventory-web/src/views/stock/inbound.vue
Normal file
237
inventory-web/src/views/stock/inbound.vue
Normal file
@ -0,0 +1,237 @@
|
|||||||
|
<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>
|
||||||
11
inventory-web/src/views/stock/query.vue
Normal file
11
inventory-web/src/views/stock/query.vue
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
11
inventory-web/src/views/stock/semi.vue
Normal file
11
inventory-web/src/views/stock/semi.vue
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
11
inventory-web/src/views/transaction/borrow.vue
Normal file
11
inventory-web/src/views/transaction/borrow.vue
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
11
inventory-web/src/views/transaction/return.vue
Normal file
11
inventory-web/src/views/transaction/return.vue
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
11
inventory-web/src/views/transaction/scrap.vue
Normal file
11
inventory-web/src/views/transaction/scrap.vue
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@ -1,7 +1,23 @@
|
|||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite'
|
||||||
import vue from '@vitejs/plugin-vue'
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
import { fileURLToPath, URL } from 'node:url'
|
||||||
|
|
||||||
// https://vite.dev/config/
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [vue()],
|
plugins: [vue()],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// --- 新增下面这一段 server 配置 ---
|
||||||
|
server: {
|
||||||
|
proxy: {
|
||||||
|
'/api': {
|
||||||
|
target: 'http://127.0.0.1:5000', // 后端的地址
|
||||||
|
changeOrigin: true,
|
||||||
|
// 如果你的后端路径本身就包含 /api,通常不需要 rewrite
|
||||||
|
// rewrite: (path) => path.replace(/^\/api/, '')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
Reference in New Issue
Block a user