采购件图像上传初实现
This commit is contained in:
@ -19,14 +19,15 @@ services:
|
|||||||
# --- 后端 Flask 服务 ---
|
# --- 后端 Flask 服务 ---
|
||||||
backend:
|
backend:
|
||||||
build:
|
build:
|
||||||
context: ./inventory-backend # 【修改】指向你的新后端目录
|
context: ./inventory-backend # 指向你的新后端目录
|
||||||
container_name: inventory_api
|
container_name: inventory_api
|
||||||
restart: always
|
restart: always
|
||||||
ports:
|
ports:
|
||||||
- "8000:8000"
|
- "8000:8000"
|
||||||
volumes:
|
volumes:
|
||||||
- ./inventory-backend:/app # 挂载代码,实现热更新
|
- ./inventory-backend:/app # 挂载代码,实现热更新
|
||||||
# 加上 --reload 参数,代码变了自动重启
|
# 【核心修改】显式挂载 uploads 目录,确保图片持久化且宿主机可见
|
||||||
|
- ./inventory-backend/uploads:/app/uploads
|
||||||
command: gunicorn -c gunicorn.conf.py run:app --reload
|
command: gunicorn -c gunicorn.conf.py run:app --reload
|
||||||
environment:
|
environment:
|
||||||
# Host 必须写 'db'
|
# Host 必须写 'db'
|
||||||
@ -34,18 +35,17 @@ services:
|
|||||||
depends_on:
|
depends_on:
|
||||||
- db
|
- db
|
||||||
|
|
||||||
# --- 前端 Vue+Nginx 服务 ---
|
# --- 前端 Vue 开发服务 ---
|
||||||
# --- 前端 Vue 开发服务 ---
|
|
||||||
frontend:
|
frontend:
|
||||||
build:
|
build:
|
||||||
context: ./inventory-web
|
context: ./inventory-web
|
||||||
container_name: inventory_ui
|
container_name: inventory_ui
|
||||||
restart: always
|
restart: always
|
||||||
# 【重点1】把本地代码挂载进去,实现“热更新”
|
# 把本地代码挂载进去,实现“热更新”
|
||||||
volumes:
|
volumes:
|
||||||
- ./inventory-web:/app
|
- ./inventory-web:/app
|
||||||
- /app/node_modules # 排除 node_modules,防止冲突
|
- /app/node_modules # 排除 node_modules,防止冲突
|
||||||
# 【重点2】开发模式端口通常是 5173
|
# 开发模式端口通常是 5173
|
||||||
ports:
|
ports:
|
||||||
- "5173:5173"
|
- "5173:5173"
|
||||||
depends_on:
|
depends_on:
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
from flask import Flask
|
from flask import Flask
|
||||||
from config import Config
|
from config import Config
|
||||||
from app.extensions import db, migrate, cors
|
from app.extensions import db, migrate, cors
|
||||||
|
import os
|
||||||
|
|
||||||
def create_app():
|
def create_app():
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
@ -14,7 +14,8 @@ def create_app():
|
|||||||
migrate.init_app(app, db)
|
migrate.init_app(app, db)
|
||||||
|
|
||||||
# 确保跨域配置
|
# 确保跨域配置
|
||||||
cors.init_app(app, resources={r"/api/*": {"origins": "*"}})
|
# 允许 /api/ 开头的请求跨域
|
||||||
|
cors.init_app(app, resources={r"/*": {"origins": "*"}}) # 放宽跨域限制,防止图片访问被拦截
|
||||||
|
|
||||||
# =========================================================
|
# =========================================================
|
||||||
# 2. 注册蓝图 (Blueprints)
|
# 2. 注册蓝图 (Blueprints)
|
||||||
@ -25,12 +26,9 @@ def create_app():
|
|||||||
# -----------------------------------------------------
|
# -----------------------------------------------------
|
||||||
try:
|
try:
|
||||||
# 指向聚合文件: app/api/v1/inbound/__init__.py
|
# 指向聚合文件: app/api/v1/inbound/__init__.py
|
||||||
# 该文件里应该包含了 buy, semi, base, product 的聚合逻辑
|
|
||||||
from app.api.v1.inbound import inbound_bp
|
from app.api.v1.inbound import inbound_bp
|
||||||
|
|
||||||
# 注册父蓝图,路由前缀为 /api/v1/inbound
|
# 注册父蓝图,路由前缀为 /api/v1/inbound
|
||||||
# 最终路由效果:
|
|
||||||
# /api/v1/inbound + /buy/list -> /api/v1/inbound/buy/list
|
|
||||||
app.register_blueprint(inbound_bp, url_prefix='/api/v1/inbound')
|
app.register_blueprint(inbound_bp, url_prefix='/api/v1/inbound')
|
||||||
|
|
||||||
print("✅ Inbound (Buy, Semi, Product, Base) 模块注册成功")
|
print("✅ Inbound (Buy, Semi, Product, Base) 模块注册成功")
|
||||||
@ -39,14 +37,12 @@ def create_app():
|
|||||||
print(f"❌ 错误: Inbound 模块导入失败: {e}")
|
print(f"❌ 错误: Inbound 模块导入失败: {e}")
|
||||||
|
|
||||||
# -----------------------------------------------------
|
# -----------------------------------------------------
|
||||||
# 2.2 注册通用打印模块 (Common Print) - [新增]
|
# 2.2 注册通用打印模块 (Common Print)
|
||||||
# -----------------------------------------------------
|
# -----------------------------------------------------
|
||||||
try:
|
try:
|
||||||
from app.api.v1.common.print import print_bp
|
from app.api.v1.common.print import print_bp
|
||||||
|
|
||||||
# 注册打印蓝图
|
# 注册打印蓝图
|
||||||
# 前端请求地址: /common/print/preview
|
|
||||||
# 配合 baseURL=/api/v1,最终对应后端: /api/v1/common/print/preview
|
|
||||||
app.register_blueprint(print_bp, url_prefix='/api/v1/common/print')
|
app.register_blueprint(print_bp, url_prefix='/api/v1/common/print')
|
||||||
|
|
||||||
print("✅ Print (Label Printing) 模块注册成功")
|
print("✅ Print (Label Printing) 模块注册成功")
|
||||||
@ -54,6 +50,25 @@ def create_app():
|
|||||||
except ImportError as e:
|
except ImportError as e:
|
||||||
print(f"❌ 错误: Print 模块导入失败: {e}")
|
print(f"❌ 错误: Print 模块导入失败: {e}")
|
||||||
|
|
||||||
|
# -----------------------------------------------------
|
||||||
|
# 2.3 [新增] 注册通用上传模块 (Common Upload)
|
||||||
|
# -----------------------------------------------------
|
||||||
|
try:
|
||||||
|
from app.api.v1.common.upload import upload_bp
|
||||||
|
|
||||||
|
# 【核心修改】注册方式 1: 标准路径 (对应 /api/v1/common/files/xxx)
|
||||||
|
app.register_blueprint(upload_bp, url_prefix='/api/v1/common')
|
||||||
|
|
||||||
|
# 【核心修改】注册方式 2: 兼容路径 (对应 /v1/common/files/xxx)
|
||||||
|
# 解决部分代理服务器剥离 /api 前缀导致的 404 问题
|
||||||
|
# name='upload_fallback' 防止蓝图名称冲突
|
||||||
|
app.register_blueprint(upload_bp, url_prefix='/v1/common', name='upload_fallback')
|
||||||
|
|
||||||
|
print("✅ Upload (File Storage) 模块注册成功 (双路径兼容模式)")
|
||||||
|
|
||||||
|
except ImportError as e:
|
||||||
|
print(f"❌ 错误: Upload 模块导入失败: {e}")
|
||||||
|
|
||||||
# =========================================================
|
# =========================================================
|
||||||
# 3. 预加载数据模型 (解决 relationship 找不到模型的问题)
|
# 3. 预加载数据模型 (解决 relationship 找不到模型的问题)
|
||||||
# =========================================================
|
# =========================================================
|
||||||
|
|||||||
132
inventory-backend/app/api/v1/common/upload.py
Normal file
132
inventory-backend/app/api/v1/common/upload.py
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
# 文件路径: inventory-backend/app/api/v1/common/upload.py
|
||||||
|
|
||||||
|
import os
|
||||||
|
import uuid
|
||||||
|
from flask import Blueprint, request, jsonify, send_from_directory
|
||||||
|
|
||||||
|
# 定义蓝图
|
||||||
|
upload_bp = Blueprint('upload', __name__)
|
||||||
|
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# 配置上传路径 (核心修改:确保路径绝对准确)
|
||||||
|
# =========================================================
|
||||||
|
# 向上寻找直到找到 inventory-backend 目录,或者默认为当前文件的上级目录的...上级
|
||||||
|
# 这种方式比数 dirname 层级更稳健
|
||||||
|
|
||||||
|
def get_project_root():
|
||||||
|
"""获取项目根目录 inventory-backend"""
|
||||||
|
current_path = os.path.abspath(__file__)
|
||||||
|
# 循环向上查找,直到找到名为 inventory-backend 的目录
|
||||||
|
# 如果你的根目录名字不是 inventory-backend,请修改这里的判断逻辑
|
||||||
|
# 或者直接使用相对路径回退 5 层: api/v1/common -> app -> inventory-backend
|
||||||
|
base = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(current_path)))))
|
||||||
|
return base
|
||||||
|
|
||||||
|
|
||||||
|
BASE_DIR = get_project_root()
|
||||||
|
UPLOAD_FOLDER = os.path.join(BASE_DIR, 'uploads')
|
||||||
|
|
||||||
|
# 允许上传的文件后缀
|
||||||
|
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp'}
|
||||||
|
|
||||||
|
|
||||||
|
def allowed_file(filename):
|
||||||
|
return '.' in filename and \
|
||||||
|
filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_upload_folder_exists():
|
||||||
|
if not os.path.exists(UPLOAD_FOLDER):
|
||||||
|
try:
|
||||||
|
os.makedirs(UPLOAD_FOLDER)
|
||||||
|
print(f"✅ [Upload] 目录创建成功: {UPLOAD_FOLDER}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ [Upload] 目录创建失败: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 1. 文件上传接口
|
||||||
|
# URL: /api/v1/common/upload (POST)
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
@upload_bp.route('/upload', methods=['POST'])
|
||||||
|
def upload_file():
|
||||||
|
ensure_upload_folder_exists()
|
||||||
|
|
||||||
|
if 'file' not in request.files:
|
||||||
|
return jsonify({"code": 400, "msg": "未找到文件部分"}), 400
|
||||||
|
|
||||||
|
file = request.files['file']
|
||||||
|
|
||||||
|
if file.filename == '':
|
||||||
|
return jsonify({"code": 400, "msg": "未选择文件"}), 400
|
||||||
|
|
||||||
|
if file and allowed_file(file.filename):
|
||||||
|
try:
|
||||||
|
ext = file.filename.rsplit('.', 1)[1].lower()
|
||||||
|
new_filename = f"{uuid.uuid4().hex}.{ext}"
|
||||||
|
|
||||||
|
save_path = os.path.join(UPLOAD_FOLDER, new_filename)
|
||||||
|
file.save(save_path)
|
||||||
|
|
||||||
|
print(f"💾 [Upload] 文件已保存: {save_path}")
|
||||||
|
|
||||||
|
# 生成访问 URL
|
||||||
|
# 这里的路径必须与 __init__.py 中注册的 url_prefix + 路由匹配
|
||||||
|
file_url = f"/api/v1/common/files/{new_filename}"
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
"code": 200,
|
||||||
|
"msg": "上传成功",
|
||||||
|
"data": {
|
||||||
|
"url": file_url,
|
||||||
|
"filename": new_filename
|
||||||
|
}
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ [Upload] 保存异常: {e}")
|
||||||
|
return jsonify({"code": 500, "msg": "文件保存失败"}), 500
|
||||||
|
|
||||||
|
return jsonify({"code": 400, "msg": "不支持的文件格式"}), 400
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 2. 静态文件访问接口 (回显)
|
||||||
|
# URL: /api/v1/common/files/<filename>
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
@upload_bp.route('/files/<filename>')
|
||||||
|
def uploaded_file(filename):
|
||||||
|
# 打印日志帮助调试 404 问题
|
||||||
|
full_path = os.path.join(UPLOAD_FOLDER, filename)
|
||||||
|
if not os.path.exists(full_path):
|
||||||
|
print(f"❌ [File Access] 文件未找到: {full_path}")
|
||||||
|
return jsonify({"code": 404, "msg": "文件不存在"}), 404
|
||||||
|
|
||||||
|
return send_from_directory(UPLOAD_FOLDER, filename)
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 3. 文件删除接口 (同步删除物理文件)
|
||||||
|
# URL: /api/v1/common/files/<filename> (DELETE)
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
@upload_bp.route('/files/<filename>', methods=['DELETE'])
|
||||||
|
def delete_file(filename):
|
||||||
|
try:
|
||||||
|
# 安全处理文件名
|
||||||
|
safe_filename = os.path.basename(filename)
|
||||||
|
file_path = os.path.join(UPLOAD_FOLDER, safe_filename)
|
||||||
|
|
||||||
|
print(f"🗑️ [Delete] 尝试删除文件: {file_path}")
|
||||||
|
|
||||||
|
if os.path.exists(file_path):
|
||||||
|
os.remove(file_path)
|
||||||
|
print(f"✅ [Delete] 文件删除成功")
|
||||||
|
return jsonify({"code": 200, "msg": "文件已删除"})
|
||||||
|
else:
|
||||||
|
print(f"⚠️ [Delete] 文件不存在,无需删除")
|
||||||
|
# 即使文件不存在也返回成功,保证前端流程继续
|
||||||
|
return jsonify({"code": 200, "msg": "文件不存在或已删除"})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ [Delete] 删除异常: {e}")
|
||||||
|
return jsonify({"code": 500, "msg": f"删除失败: {str(e)}"}), 500
|
||||||
@ -36,13 +36,16 @@ class StockBuy(db.Model):
|
|||||||
exchange_rate = db.Column(db.Numeric(15, 6), default=1.0)
|
exchange_rate = db.Column(db.Numeric(15, 6), default=1.0)
|
||||||
|
|
||||||
supplier_name = db.Column(db.String(255))
|
supplier_name = db.Column(db.String(255))
|
||||||
buyer_name = db.Column(db.String(100)) # 对应 SQL: buyer_name
|
buyer_name = db.Column(db.String(100))
|
||||||
buyer_email = db.Column(db.String(100)) # 对应 SQL: buyer_email
|
buyer_email = db.Column(db.String(100))
|
||||||
original_link = db.Column(db.Text) # 对应 SQL: original_link
|
original_link = db.Column(db.Text)
|
||||||
detail_link = db.Column(db.Text)
|
detail_link = db.Column(db.Text)
|
||||||
arrival_photo = db.Column(db.Text)
|
arrival_photo = db.Column(db.Text)
|
||||||
|
|
||||||
# [新增] 全局打印流水号 (用于跨表连续编号,对应 Sequence: global_print_seq)
|
# [新增] 检测报告图片路径
|
||||||
|
inspection_report = db.Column(db.Text)
|
||||||
|
|
||||||
|
# 全局打印流水号
|
||||||
global_print_id = db.Column(db.Integer)
|
global_print_id = db.Column(db.Integer)
|
||||||
|
|
||||||
# 关系定义
|
# 关系定义
|
||||||
@ -86,7 +89,9 @@ class StockBuy(db.Model):
|
|||||||
'detail_link': self.detail_link,
|
'detail_link': self.detail_link,
|
||||||
'arrival_photo': self.arrival_photo,
|
'arrival_photo': self.arrival_photo,
|
||||||
|
|
||||||
# [新增] 返回全局打印ID及其格式化字符串
|
# [新增] 返回检测报告字段
|
||||||
|
'inspection_report': self.inspection_report,
|
||||||
|
|
||||||
'global_print_id': self.global_print_id,
|
'global_print_id': self.global_print_id,
|
||||||
'global_print_id_str': f"{self.global_print_id:010d}" if self.global_print_id else ""
|
'global_print_id_str': f"{self.global_print_id:010d}" if self.global_print_id else ""
|
||||||
}
|
}
|
||||||
@ -82,9 +82,8 @@ class BuyInboundService:
|
|||||||
generated_sku = str(next_global_id).zfill(10)
|
generated_sku = str(next_global_id).zfill(10)
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# 3. 条码逻辑处理 (核心修改)
|
# 3. 条码逻辑处理
|
||||||
# 如果前端没传条码(barcode),则默认使用系统生成的 SKU 作为条码
|
# 如果前端没传条码(barcode),则默认使用系统生成的 SKU 作为条码
|
||||||
# 这样保证了条码生成依据是 "自动填写的 SKU"
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
final_barcode = data.get('barcode')
|
final_barcode = data.get('barcode')
|
||||||
if not final_barcode:
|
if not final_barcode:
|
||||||
@ -93,8 +92,8 @@ class BuyInboundService:
|
|||||||
new_stock = StockBuy(
|
new_stock = StockBuy(
|
||||||
base_id=material.id,
|
base_id=material.id,
|
||||||
global_print_id=next_global_id,
|
global_print_id=next_global_id,
|
||||||
sku=generated_sku, # 自动生成的SKU
|
sku=generated_sku,
|
||||||
barcode=final_barcode, # 如果未输入,则存入SKU值
|
barcode=final_barcode,
|
||||||
|
|
||||||
in_date=in_date_val,
|
in_date=in_date_val,
|
||||||
serial_number=data.get('serial_number'),
|
serial_number=data.get('serial_number'),
|
||||||
@ -114,13 +113,15 @@ class BuyInboundService:
|
|||||||
buyer_email=data.get('purchaser_email'),
|
buyer_email=data.get('purchaser_email'),
|
||||||
original_link=data.get('source_link'),
|
original_link=data.get('source_link'),
|
||||||
detail_link=data.get('detail_link'),
|
detail_link=data.get('detail_link'),
|
||||||
arrival_photo=data.get('arrival_photo')
|
arrival_photo=data.get('arrival_photo'),
|
||||||
|
|
||||||
|
# [新增] 保存检测报告字段
|
||||||
|
inspection_report=data.get('inspection_report')
|
||||||
)
|
)
|
||||||
|
|
||||||
db.session.add(new_stock)
|
db.session.add(new_stock)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
# 返回创建的对象实例
|
|
||||||
return new_stock
|
return new_stock
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -151,7 +152,10 @@ class BuyInboundService:
|
|||||||
'exchange_rate': 'exchange_rate',
|
'exchange_rate': 'exchange_rate',
|
||||||
'purchaser': 'buyer_name',
|
'purchaser': 'buyer_name',
|
||||||
'purchaser_email': 'buyer_email',
|
'purchaser_email': 'buyer_email',
|
||||||
'source_link': 'original_link'
|
'source_link': 'original_link',
|
||||||
|
|
||||||
|
# [新增] 允许更新检测报告
|
||||||
|
'inspection_report': 'inspection_report'
|
||||||
}
|
}
|
||||||
|
|
||||||
for frontend_key, db_attr in field_mapping.items():
|
for frontend_key, db_attr in field_mapping.items():
|
||||||
@ -207,7 +211,6 @@ class BuyInboundService:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def get_list(page, limit, keyword=None):
|
def get_list(page, limit, keyword=None):
|
||||||
try:
|
try:
|
||||||
# 1. 查询分页数据
|
|
||||||
query = db.session.query(StockBuy).outerjoin(MaterialBase, StockBuy.base_id == MaterialBase.id)
|
query = db.session.query(StockBuy).outerjoin(MaterialBase, StockBuy.base_id == MaterialBase.id)
|
||||||
|
|
||||||
if keyword:
|
if keyword:
|
||||||
@ -223,9 +226,6 @@ class BuyInboundService:
|
|||||||
|
|
||||||
pagination = query.order_by(StockBuy.id.desc()).paginate(page=page, per_page=limit, error_out=False)
|
pagination = query.order_by(StockBuy.id.desc()).paginate(page=page, per_page=limit, error_out=False)
|
||||||
|
|
||||||
# ---------------------------------------------------------------------
|
|
||||||
# 计算总库存 (聚合)
|
|
||||||
# ---------------------------------------------------------------------
|
|
||||||
current_items = pagination.items
|
current_items = pagination.items
|
||||||
base_ids = list(set([item.base_id for item in current_items if item.base_id]))
|
base_ids = list(set([item.base_id for item in current_items if item.base_id]))
|
||||||
|
|
||||||
@ -289,6 +289,9 @@ class BuyInboundService:
|
|||||||
'detail_link': item.detail_link,
|
'detail_link': item.detail_link,
|
||||||
'arrival_photo': item.arrival_photo,
|
'arrival_photo': item.arrival_photo,
|
||||||
|
|
||||||
|
# [新增] 返回检测报告
|
||||||
|
'inspection_report': item.inspection_report,
|
||||||
|
|
||||||
'global_print_id': item.global_print_id,
|
'global_print_id': item.global_print_id,
|
||||||
'global_print_id_str': f"{item.global_print_id:08d}" if item.global_print_id else ""
|
'global_print_id_str': f"{item.global_print_id:08d}" if item.global_print_id else ""
|
||||||
}
|
}
|
||||||
|
|||||||
@ -42,4 +42,22 @@ export function searchMaterialBase(keyword: string) {
|
|||||||
method: 'get',
|
method: 'get',
|
||||||
params: { keyword }
|
params: { keyword }
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. 文件上传 (用于图片/拍照)
|
||||||
|
export function uploadFile(data: FormData) {
|
||||||
|
return request({
|
||||||
|
url: '/common/upload', // 对应后端 /api/v1/common/upload
|
||||||
|
method: 'post',
|
||||||
|
data,
|
||||||
|
headers: { 'Content-Type': 'multipart/form-data' }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. [新增] 文件删除
|
||||||
|
export function deleteFile(filename: string) {
|
||||||
|
return request({
|
||||||
|
url: `/common/files/${filename}`, // 对应后端 /api/v1/common/files/<filename>
|
||||||
|
method: 'delete'
|
||||||
|
})
|
||||||
}
|
}
|
||||||
@ -83,6 +83,25 @@
|
|||||||
</el-tag>
|
</el-tag>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<template #default="scope" v-else-if="['arrival_photo', 'inspection_report'].includes(col.prop)">
|
||||||
|
<div v-if="scope.row[col.prop]" style="display: flex; align-items: center; justify-content: center;">
|
||||||
|
<el-image
|
||||||
|
style="width: 40px; height: 40px; border-radius: 4px; border: 1px solid #dcdfe6; cursor: zoom-in;"
|
||||||
|
:src="getImageUrl(scope.row[col.prop])"
|
||||||
|
:preview-src-list="[getImageUrl(scope.row[col.prop])]"
|
||||||
|
preview-teleported
|
||||||
|
fit="cover"
|
||||||
|
>
|
||||||
|
<template #error>
|
||||||
|
<div style="display: flex; justify-content: center; align-items: center; width: 100%; height: 100%; background: #f5f7fa; color: #909399;">
|
||||||
|
<el-icon><Picture /></el-icon>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-image>
|
||||||
|
</div>
|
||||||
|
<span v-else class="text-placeholder">-</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
<template #default="scope" v-else-if="col.prop.includes('link')">
|
<template #default="scope" v-else-if="col.prop.includes('link')">
|
||||||
<el-link v-if="scope.row[col.prop]" type="primary" :href="scope.row[col.prop]" target="_blank"
|
<el-link v-if="scope.row[col.prop]" type="primary" :href="scope.row[col.prop]" target="_blank"
|
||||||
:underline="false">
|
:underline="false">
|
||||||
@ -338,9 +357,59 @@
|
|||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<el-row :gutter="20">
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<el-form-item label="到货图片" prop="arrival_photo">
|
<el-form-item label="到货图片" prop="arrival_photo">
|
||||||
<el-input v-model="form.arrival_photo" placeholder="输入图片 URL"/>
|
<div style="display: flex; align-items: flex-start; gap: 10px;">
|
||||||
|
|
||||||
|
<div v-if="form.arrival_photo" class="preview-wrapper">
|
||||||
|
<img :src="getImageUrl(form.arrival_photo)" class="avatar" />
|
||||||
|
<div class="delete-overlay">
|
||||||
|
<el-button type="danger" :icon="Delete" circle @click="handleRemoveImage('arrival_photo')" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-upload
|
||||||
|
v-else
|
||||||
|
class="avatar-uploader"
|
||||||
|
action="#"
|
||||||
|
:http-request="(opts) => customUpload(opts, 'arrival_photo')"
|
||||||
|
:show-file-list="false"
|
||||||
|
:before-upload="beforeAvatarUpload"
|
||||||
|
>
|
||||||
|
<el-icon class="avatar-uploader-icon"><Plus /></el-icon>
|
||||||
|
</el-upload>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<el-input v-model="form.arrival_photo" placeholder="上传后生成" style="margin-top: 5px;" size="small" readonly/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="检测报告" prop="inspection_report">
|
||||||
|
<div style="display: flex; align-items: flex-start; gap: 10px;">
|
||||||
|
|
||||||
|
<div v-if="form.inspection_report" class="preview-wrapper">
|
||||||
|
<img :src="getImageUrl(form.inspection_report)" class="avatar" />
|
||||||
|
<div class="delete-overlay">
|
||||||
|
<el-button type="danger" :icon="Delete" circle @click="handleRemoveImage('inspection_report')" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-upload
|
||||||
|
v-else
|
||||||
|
class="avatar-uploader"
|
||||||
|
action="#"
|
||||||
|
:http-request="(opts) => customUpload(opts, 'inspection_report')"
|
||||||
|
:show-file-list="false"
|
||||||
|
:before-upload="beforeAvatarUpload"
|
||||||
|
>
|
||||||
|
<el-icon class="avatar-uploader-icon"><Plus /></el-icon>
|
||||||
|
</el-upload>
|
||||||
|
</div>
|
||||||
|
<el-input v-model="form.inspection_report" placeholder="上传后生成" style="margin-top: 5px;" size="small" readonly/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
@ -490,15 +559,17 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {ref, reactive, onMounted, watch} from 'vue'
|
import {ref, reactive, onMounted, watch} from 'vue'
|
||||||
import {Plus, Setting, Refresh, Search, Lock, Box, House, InfoFilled, Link, Printer} from '@element-plus/icons-vue'
|
import {Plus, Setting, Refresh, Search, Lock, Box, House, InfoFilled, Link, Printer, Camera, Delete, Picture} from '@element-plus/icons-vue'
|
||||||
import {ElMessage} from 'element-plus'
|
import {ElMessage, ElMessageBox} from 'element-plus'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import {
|
import {
|
||||||
getBuyList,
|
getBuyList,
|
||||||
createBuyInbound,
|
createBuyInbound,
|
||||||
updateBuyInbound,
|
updateBuyInbound,
|
||||||
deleteBuyInbound,
|
deleteBuyInbound,
|
||||||
searchMaterialBase
|
searchMaterialBase,
|
||||||
|
uploadFile,
|
||||||
|
deleteFile
|
||||||
} from '@/api/inbound/buy'
|
} from '@/api/inbound/buy'
|
||||||
import {getLabelPreview, executePrint} from '@/api/common/print'
|
import {getLabelPreview, executePrint} from '@/api/common/print'
|
||||||
|
|
||||||
@ -526,6 +597,9 @@ const currentPrintData = ref<any>({})
|
|||||||
const entryMode = ref('batch')
|
const entryMode = ref('batch')
|
||||||
const modeLocked = ref(false)
|
const modeLocked = ref(false)
|
||||||
|
|
||||||
|
// 拍照/上传相关
|
||||||
|
const cameraInputRef = ref<HTMLInputElement | null>(null)
|
||||||
|
|
||||||
// 列定义
|
// 列定义
|
||||||
const baseColumns = [
|
const baseColumns = [
|
||||||
{prop: 'material_name', label: '名称'},
|
{prop: 'material_name', label: '名称'},
|
||||||
@ -558,17 +632,18 @@ const stockColumns = [
|
|||||||
{prop: 'purchaser_email', label: '邮箱', minWidth: '150'},
|
{prop: 'purchaser_email', label: '邮箱', minWidth: '150'},
|
||||||
{prop: 'source_link', label: '采购链接', minWidth: '100'},
|
{prop: 'source_link', label: '采购链接', minWidth: '100'},
|
||||||
{prop: 'detail_link', label: '详情链接', minWidth: '100'},
|
{prop: 'detail_link', label: '详情链接', minWidth: '100'},
|
||||||
{prop: 'arrival_photo', label: '到货图', minWidth: '100'}
|
{prop: 'arrival_photo', label: '到货图', minWidth: '100'},
|
||||||
|
{prop: 'inspection_report', label: '检测报告', minWidth: '100'} // 新增列
|
||||||
]
|
]
|
||||||
|
|
||||||
const allColumns = [...baseColumns, ...stockColumns]
|
const allColumns = [...baseColumns, ...stockColumns]
|
||||||
|
|
||||||
// 表头持久化
|
|
||||||
const STORAGE_KEY_COLS = 'stock_buy_visible_columns'
|
const STORAGE_KEY_COLS = 'stock_buy_visible_columns'
|
||||||
|
// 确保 arrival_photo 和 inspection_report 在默认列中
|
||||||
const defaultColumns = [
|
const defaultColumns = [
|
||||||
'material_name', 'category', 'material_type', 'spec_model', 'unit',
|
'material_name', 'category', 'material_type', 'spec_model', 'unit',
|
||||||
'inbound_date', 'serial_number', 'batch_number', 'status', 'inspection_status',
|
'inbound_date', 'serial_number', 'batch_number', 'status', 'inspection_status',
|
||||||
'unit_price', 'total_price', 'supplier_name', 'purchaser', 'qty_stock', 'qty_available'
|
'unit_price', 'total_price', 'supplier_name', 'purchaser', 'qty_stock', 'qty_available', 'arrival_photo', 'inspection_report'
|
||||||
]
|
]
|
||||||
|
|
||||||
const getSavedColumns = () => {
|
const getSavedColumns = () => {
|
||||||
@ -598,7 +673,9 @@ const form = reactive({
|
|||||||
warehouse_location: '',
|
warehouse_location: '',
|
||||||
unit_price: 0, total_price: 0, currency: 'CNY', exchange_rate: 1.00,
|
unit_price: 0, total_price: 0, currency: 'CNY', exchange_rate: 1.00,
|
||||||
supplier_name: '', purchaser: '', purchaser_email: '',
|
supplier_name: '', purchaser: '', purchaser_email: '',
|
||||||
source_link: '', detail_link: '', arrival_photo: ''
|
source_link: '', detail_link: '',
|
||||||
|
arrival_photo: '',
|
||||||
|
inspection_report: '' // 新增字段
|
||||||
})
|
})
|
||||||
|
|
||||||
// ------------------------------------
|
// ------------------------------------
|
||||||
@ -611,23 +688,20 @@ const HISTORY_KEYS = {
|
|||||||
MATERIAL: 'history_materials'
|
MATERIAL: 'history_materials'
|
||||||
}
|
}
|
||||||
|
|
||||||
// 保存历史 (String 类型)
|
|
||||||
const saveToHistory = (key: string, value: string) => {
|
const saveToHistory = (key: string, value: string) => {
|
||||||
if (!value) return
|
if (!value) return
|
||||||
try {
|
try {
|
||||||
const existing = localStorage.getItem(key)
|
const existing = localStorage.getItem(key)
|
||||||
let list = existing ? JSON.parse(existing) : []
|
let list = existing ? JSON.parse(existing) : []
|
||||||
// 移除旧的,添加到前面
|
|
||||||
list = list.filter((i: string) => i !== value)
|
list = list.filter((i: string) => i !== value)
|
||||||
list.unshift(value)
|
list.unshift(value)
|
||||||
if (list.length > 20) list = list.slice(0, 20) // 最多存20条
|
if (list.length > 20) list = list.slice(0, 20)
|
||||||
localStorage.setItem(key, JSON.stringify(list))
|
localStorage.setItem(key, JSON.stringify(list))
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('save history failed', e)
|
console.error('save history failed', e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取历史 (String 类型)
|
|
||||||
const getHistoryList = (key: string): any[] => {
|
const getHistoryList = (key: string): any[] => {
|
||||||
try {
|
try {
|
||||||
const existing = localStorage.getItem(key)
|
const existing = localStorage.getItem(key)
|
||||||
@ -638,7 +712,6 @@ const getHistoryList = (key: string): any[] => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 保存物料历史 (Object 类型)
|
|
||||||
const saveMaterialHistory = (item: any) => {
|
const saveMaterialHistory = (item: any) => {
|
||||||
if (!item || !item.id) return
|
if (!item || !item.id) return
|
||||||
const key = HISTORY_KEYS.MATERIAL
|
const key = HISTORY_KEYS.MATERIAL
|
||||||
@ -664,7 +737,7 @@ const getMaterialHistory = () => {
|
|||||||
|
|
||||||
|
|
||||||
// ------------------------------------
|
// ------------------------------------
|
||||||
// Autocomplete 建议逻辑 (混合模式:历史+当前表格)
|
// Autocomplete & Search Logic
|
||||||
// ------------------------------------
|
// ------------------------------------
|
||||||
const createFilter = (queryString: string) => {
|
const createFilter = (queryString: string) => {
|
||||||
return (item: any) => {
|
return (item: any) => {
|
||||||
@ -672,40 +745,31 @@ const createFilter = (queryString: string) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 辅助函数:从当前表格提取
|
|
||||||
const getTableDataUnique = (field: string) => {
|
const getTableDataUnique = (field: string) => {
|
||||||
const uniqueItems = Array.from(new Set(tableData.value.map((i: any) => i[field]).filter(Boolean)))
|
const uniqueItems = Array.from(new Set(tableData.value.map((i: any) => i[field]).filter(Boolean)))
|
||||||
return uniqueItems.map(i => ({value: i}))
|
return uniqueItems.map(i => ({value: i}))
|
||||||
}
|
}
|
||||||
|
|
||||||
// 通用查询: 历史记录 + 当前页面数据
|
|
||||||
const mixedSearch = (queryString: string, tableField: string, storageKey: string, cb: any) => {
|
const mixedSearch = (queryString: string, tableField: string, storageKey: string, cb: any) => {
|
||||||
const tableList = getTableDataUnique(tableField)
|
const tableList = getTableDataUnique(tableField)
|
||||||
const historyList = getHistoryList(storageKey)
|
const historyList = getHistoryList(storageKey)
|
||||||
|
|
||||||
// 合并去重
|
|
||||||
const map = new Map()
|
const map = new Map()
|
||||||
historyList.forEach(i => map.set(i.value, i))
|
historyList.forEach(i => map.set(i.value, i))
|
||||||
tableList.forEach(i => map.set(i.value, i))
|
tableList.forEach(i => map.set(i.value, i))
|
||||||
|
|
||||||
const allList = Array.from(map.values())
|
const allList = Array.from(map.values())
|
||||||
const results = queryString ? allList.filter(createFilter(queryString)) : allList
|
const results = queryString ? allList.filter(createFilter(queryString)) : allList
|
||||||
cb(results)
|
cb(results)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1. 供应商
|
|
||||||
const querySearchSupplier = (qs: string, cb: any) => mixedSearch(qs, 'supplier_name', HISTORY_KEYS.SUPPLIER, cb)
|
const querySearchSupplier = (qs: string, cb: any) => mixedSearch(qs, 'supplier_name', HISTORY_KEYS.SUPPLIER, cb)
|
||||||
const handleSupplierSelect = (item: any) => saveToHistory(HISTORY_KEYS.SUPPLIER, item.value)
|
const handleSupplierSelect = (item: any) => saveToHistory(HISTORY_KEYS.SUPPLIER, item.value)
|
||||||
|
|
||||||
// 2. 采购人
|
|
||||||
const querySearchPurchaser = (qs: string, cb: any) => mixedSearch(qs, 'purchaser', HISTORY_KEYS.PURCHASER, cb)
|
const querySearchPurchaser = (qs: string, cb: any) => mixedSearch(qs, 'purchaser', HISTORY_KEYS.PURCHASER, cb)
|
||||||
const handlePurchaserSelect = (item: any) => saveToHistory(HISTORY_KEYS.PURCHASER, item.value)
|
const handlePurchaserSelect = (item: any) => saveToHistory(HISTORY_KEYS.PURCHASER, item.value)
|
||||||
|
|
||||||
// 3. 邮箱
|
|
||||||
const querySearchEmail = (qs: string, cb: any) => mixedSearch(qs, 'purchaser_email', HISTORY_KEYS.EMAIL, cb)
|
const querySearchEmail = (qs: string, cb: any) => mixedSearch(qs, 'purchaser_email', HISTORY_KEYS.EMAIL, cb)
|
||||||
const handleEmailSelect = (item: any) => saveToHistory(HISTORY_KEYS.EMAIL, item.value)
|
const handleEmailSelect = (item: any) => saveToHistory(HISTORY_KEYS.EMAIL, item.value)
|
||||||
|
|
||||||
// 4. 币种 (固定+过滤)
|
|
||||||
const currencyOptions = [
|
const currencyOptions = [
|
||||||
{value: 'CNY', desc: '人民币'},
|
{value: 'CNY', desc: '人民币'},
|
||||||
{value: 'USD', desc: '美元'},
|
{value: 'USD', desc: '美元'},
|
||||||
@ -717,9 +781,8 @@ const querySearchCurrency = (queryString: string, cb: any) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------
|
// ------------------------------------
|
||||||
// 物料搜索逻辑 (优化:支持空查询加载默认值)
|
// Material Search Logic
|
||||||
// ------------------------------------
|
// ------------------------------------
|
||||||
|
|
||||||
const handleMaterialDropdownVisible = (visible: boolean) => {
|
const handleMaterialDropdownVisible = (visible: boolean) => {
|
||||||
if (visible) {
|
if (visible) {
|
||||||
if (materialOptions.value.length === 0) {
|
if (materialOptions.value.length === 0) {
|
||||||
@ -751,7 +814,6 @@ const onMaterialSelected = (val: number) => {
|
|||||||
const item = materialOptions.value.find(i => i.id === val)
|
const item = materialOptions.value.find(i => i.id === val)
|
||||||
if (item) {
|
if (item) {
|
||||||
saveMaterialHistory(item)
|
saveMaterialHistory(item)
|
||||||
|
|
||||||
form.material_name = item.name
|
form.material_name = item.name
|
||||||
form.spec_model = item.spec
|
form.spec_model = item.spec
|
||||||
form.category = item.category
|
form.category = item.category
|
||||||
@ -762,7 +824,7 @@ const onMaterialSelected = (val: number) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------
|
// ------------------------------------
|
||||||
// 逻辑校验规则
|
// Validation Logic
|
||||||
// ------------------------------------
|
// ------------------------------------
|
||||||
const validateUnique = (rule: any, value: string, callback: any) => {
|
const validateUnique = (rule: any, value: string, callback: any) => {
|
||||||
if (!value) return callback()
|
if (!value) return callback()
|
||||||
@ -797,7 +859,7 @@ const rules = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------
|
// ------------------------------------
|
||||||
// 核心逻辑函数
|
// Business Logic: Batch/SN Mode
|
||||||
// ------------------------------------
|
// ------------------------------------
|
||||||
const checkHistoryAndSetMode = async (baseId: number) => {
|
const checkHistoryAndSetMode = async (baseId: number) => {
|
||||||
try {
|
try {
|
||||||
@ -884,17 +946,36 @@ const handleUpdate = (row: any) => {
|
|||||||
resetForm()
|
resetForm()
|
||||||
modeLocked.value = true
|
modeLocked.value = true
|
||||||
|
|
||||||
form.id = row.id
|
// 映射所有字段,包括新增的 inspection_report
|
||||||
form.base_id = row.base_id
|
Object.assign(form, {
|
||||||
form.material_name = row.material_name
|
id: row.id,
|
||||||
form.spec_model = row.spec_model
|
base_id: row.base_id,
|
||||||
form.category = row.category
|
material_name: row.material_name,
|
||||||
form.unit = row.unit
|
spec_model: row.spec_model,
|
||||||
form.material_type = row.material_type
|
category: row.category,
|
||||||
form.sku = row.sku
|
unit: row.unit,
|
||||||
form.barcode = row.barcode
|
material_type: row.material_type,
|
||||||
form.in_date = row.inbound_date
|
sku: row.sku,
|
||||||
form.warehouse_location = row.warehouse_loc
|
barcode: row.barcode,
|
||||||
|
in_date: row.inbound_date,
|
||||||
|
warehouse_location: row.warehouse_loc,
|
||||||
|
status: row.status,
|
||||||
|
inspection_status: row.inspection_status,
|
||||||
|
in_quantity: Number(row.qty_inbound),
|
||||||
|
stock_quantity: Number(row.qty_stock),
|
||||||
|
available_quantity: Number(row.qty_available),
|
||||||
|
unit_price: Number(row.unit_price),
|
||||||
|
total_price: Number(row.total_price),
|
||||||
|
currency: row.currency,
|
||||||
|
exchange_rate: Number(row.exchange_rate),
|
||||||
|
supplier_name: row.supplier_name,
|
||||||
|
purchaser: row.purchaser,
|
||||||
|
purchaser_email: row.purchaser_email,
|
||||||
|
source_link: row.source_link,
|
||||||
|
detail_link: row.detail_link,
|
||||||
|
arrival_photo: row.arrival_photo,
|
||||||
|
inspection_report: row.inspection_report // 映射新字段
|
||||||
|
})
|
||||||
|
|
||||||
if (row.serial_number) {
|
if (row.serial_number) {
|
||||||
entryMode.value = 'serial'
|
entryMode.value = 'serial'
|
||||||
@ -906,23 +987,6 @@ const handleUpdate = (row: any) => {
|
|||||||
form.serial_number = ''
|
form.serial_number = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
form.status = row.status
|
|
||||||
form.inspection_status = row.inspection_status
|
|
||||||
form.in_quantity = Number(row.qty_inbound) || 0
|
|
||||||
form.stock_quantity = Number(row.qty_stock) || 0
|
|
||||||
form.available_quantity = Number(row.qty_available) || 0
|
|
||||||
|
|
||||||
form.unit_price = Number(row.unit_price) || 0
|
|
||||||
form.total_price = Number(row.total_price) || 0
|
|
||||||
form.currency = row.currency
|
|
||||||
form.exchange_rate = Number(row.exchange_rate)
|
|
||||||
form.supplier_name = row.supplier_name
|
|
||||||
form.purchaser = row.purchaser
|
|
||||||
form.purchaser_email = row.purchaser_email
|
|
||||||
form.source_link = row.source_link
|
|
||||||
form.detail_link = row.detail_link
|
|
||||||
form.arrival_photo = row.arrival_photo
|
|
||||||
|
|
||||||
materialOptions.value = [{
|
materialOptions.value = [{
|
||||||
id: row.base_id,
|
id: row.base_id,
|
||||||
name: row.material_name,
|
name: row.material_name,
|
||||||
@ -934,7 +998,122 @@ const handleUpdate = (row: any) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------
|
// ------------------------------------
|
||||||
// 提交逻辑 (新增自动打印逻辑)
|
// 图片上传、拍照、删除逻辑 (通用化)
|
||||||
|
// ------------------------------------
|
||||||
|
// 1. 获取图片URL辅助函数
|
||||||
|
const getImageUrl = (url: string) => {
|
||||||
|
if (!url) return ''
|
||||||
|
if (url.startsWith('http')) return url
|
||||||
|
// 如果是相对路径,直接返回 (假设后端代理已配置好)
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 校验
|
||||||
|
const beforeAvatarUpload = (rawFile: any) => {
|
||||||
|
if (rawFile.type !== 'image/jpeg' && rawFile.type !== 'image/png') {
|
||||||
|
ElMessage.error('图片必须是 JPG 或 PNG 格式!')
|
||||||
|
return false
|
||||||
|
} else if (rawFile.size / 1024 / 1024 > 5) {
|
||||||
|
ElMessage.error('图片大小不能超过 5MB!')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 自定义上传 (支持不同字段)
|
||||||
|
const customUpload = async (options: any, targetField: keyof typeof form) => {
|
||||||
|
const { file, onSuccess, onError } = options
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append('file', file)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res: any = await uploadFile(formData)
|
||||||
|
if (res.code === 200) {
|
||||||
|
// @ts-ignore
|
||||||
|
form[targetField] = res.data.url
|
||||||
|
ElMessage.success('上传成功')
|
||||||
|
onSuccess(res)
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res.msg || '上传失败')
|
||||||
|
onError(new Error(res.msg))
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
ElMessage.error('网络错误')
|
||||||
|
onError(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 拍照触发 (逻辑保留,UI已移除)
|
||||||
|
const triggerCamera = () => {
|
||||||
|
if (cameraInputRef.value) {
|
||||||
|
cameraInputRef.value.click()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 处理拍照文件 (逻辑保留,UI已移除)
|
||||||
|
const handleCameraFile = async (event: Event) => {
|
||||||
|
const input = event.target as HTMLInputElement
|
||||||
|
if (input.files && input.files[0]) {
|
||||||
|
const file = input.files[0]
|
||||||
|
|
||||||
|
if (!beforeAvatarUpload(file)) {
|
||||||
|
input.value = ''
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append('file', file)
|
||||||
|
const loadingMsg = ElMessage.loading({ message: '正在上传...', duration: 0 })
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res: any = await uploadFile(formData)
|
||||||
|
if (res.code === 200) {
|
||||||
|
form.arrival_photo = res.data.url // 默认拍给到货图,如果需要给检测报告需扩展逻辑
|
||||||
|
ElMessage.success('拍照上传成功')
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res.msg || '上传失败')
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
ElMessage.error('网络错误,上传失败')
|
||||||
|
} finally {
|
||||||
|
loadingMsg.close()
|
||||||
|
input.value = '' // 清空以便下次触发
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. 删除图片 (带物理删除,支持指定字段)
|
||||||
|
const handleRemoveImage = (targetField: keyof typeof form) => {
|
||||||
|
ElMessageBox.confirm(
|
||||||
|
'确定要删除当前图片吗?',
|
||||||
|
'提示',
|
||||||
|
{ confirmButtonText: '删除', cancelButtonText: '取消', type: 'warning' }
|
||||||
|
).then(async () => {
|
||||||
|
try {
|
||||||
|
// @ts-ignore
|
||||||
|
const url = form[targetField]
|
||||||
|
if (url) {
|
||||||
|
// 解析文件名: /api/v1/common/files/xxxx.jpg -> xxxx.jpg
|
||||||
|
const filename = url.split('/').pop()
|
||||||
|
if (filename) {
|
||||||
|
// 调用后端删除文件
|
||||||
|
await deleteFile(filename)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 清空前端引用
|
||||||
|
// @ts-ignore
|
||||||
|
form[targetField] = ''
|
||||||
|
ElMessage.success('图片已删除')
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
ElMessage.error('删除失败')
|
||||||
|
}
|
||||||
|
}).catch(() => {})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ------------------------------------
|
||||||
|
// 提交逻辑
|
||||||
// ------------------------------------
|
// ------------------------------------
|
||||||
const submitForm = async () => {
|
const submitForm = async () => {
|
||||||
if (!formRef.value) return
|
if (!formRef.value) return
|
||||||
@ -947,8 +1126,7 @@ const submitForm = async () => {
|
|||||||
const res: any = await createBuyInbound(form)
|
const res: any = await createBuyInbound(form)
|
||||||
ElMessage.success('入库成功')
|
ElMessage.success('入库成功')
|
||||||
|
|
||||||
// 2. 自动打印 (使用返回的完整数据)
|
// 2. 自动打印
|
||||||
// res.data 包含了 newly created stock item, with generated ID and SKU
|
|
||||||
const newItem = res.data
|
const newItem = res.data
|
||||||
if (newItem) {
|
if (newItem) {
|
||||||
ElMessage.info('正在发送打印指令...')
|
ElMessage.info('正在发送打印指令...')
|
||||||
@ -1013,7 +1191,7 @@ const handlePrint = async (row: any) => {
|
|||||||
warehouse_loc: row.warehouse_loc,
|
warehouse_loc: row.warehouse_loc,
|
||||||
serial_number: row.serial_number,
|
serial_number: row.serial_number,
|
||||||
batch_number: row.batch_number,
|
batch_number: row.batch_number,
|
||||||
sku: row.sku // 【重要】显式增加 SKU 字段传递给后端
|
sku: row.sku
|
||||||
}
|
}
|
||||||
currentPrintData.value = printData
|
currentPrintData.value = printData
|
||||||
|
|
||||||
@ -1052,7 +1230,9 @@ const resetForm = () => {
|
|||||||
warehouse_location: '',
|
warehouse_location: '',
|
||||||
unit_price: 0, total_price: 0, currency: 'CNY', exchange_rate: 1.00,
|
unit_price: 0, total_price: 0, currency: 'CNY', exchange_rate: 1.00,
|
||||||
supplier_name: '', purchaser: '', purchaser_email: '',
|
supplier_name: '', purchaser: '', purchaser_email: '',
|
||||||
source_link: '', detail_link: '', arrival_photo: ''
|
source_link: '', detail_link: '',
|
||||||
|
arrival_photo: '',
|
||||||
|
inspection_report: '' // 重置新增字段
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1339,4 +1519,70 @@ onMounted(() => fetchData())
|
|||||||
.empty-preview {
|
.empty-preview {
|
||||||
color: #909399;
|
color: #909399;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 上传相关样式 */
|
||||||
|
.avatar-uploader .avatar {
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
display: block;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 删除遮罩层 */
|
||||||
|
.preview-wrapper {
|
||||||
|
position: relative;
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-wrapper:hover .delete-overlay {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-uploader :deep(.el-upload) {
|
||||||
|
border: 1px dashed var(--el-border-color);
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: var(--el-transition-duration-fast);
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-uploader :deep(.el-upload:hover) {
|
||||||
|
border-color: var(--el-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-uploader-icon {
|
||||||
|
font-size: 28px;
|
||||||
|
color: #8c939d;
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 隐藏拍照按钮容器 */
|
||||||
|
.camera-trigger {
|
||||||
|
display: none; /* 根据需求隐藏,如需恢复改为 flex */
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
Reference in New Issue
Block a user