采购人走用户的表
This commit is contained in:
@ -29,7 +29,7 @@ def search_base():
|
|||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# 1. 获取列表 (修改:支持状态筛选)
|
# 1. 获取列表
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
@inbound_buy_bp.route('/list', methods=['GET'])
|
@inbound_buy_bp.route('/list', methods=['GET'])
|
||||||
def get_list():
|
def get_list():
|
||||||
@ -38,7 +38,7 @@ def get_list():
|
|||||||
limit = request.args.get('pageSize', 15, type=int)
|
limit = request.args.get('pageSize', 15, type=int)
|
||||||
keyword = request.args.get('keyword', '')
|
keyword = request.args.get('keyword', '')
|
||||||
|
|
||||||
# 获取状态列表参数,前端传参格式: statuses=在库,借库
|
# 获取状态列表参数
|
||||||
statuses_str = request.args.get('statuses', '')
|
statuses_str = request.args.get('statuses', '')
|
||||||
statuses = statuses_str.split(',') if statuses_str else []
|
statuses = statuses_str.split(',') if statuses_str else []
|
||||||
|
|
||||||
@ -126,10 +126,22 @@ def get_supplier_suggestions():
|
|||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# 7. 系统用户建议
|
# 7. 系统用户建议 (采购人)
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
@inbound_buy_bp.route('/suggestions/users', methods=['GET'])
|
@inbound_buy_bp.route('/suggestions/users', methods=['GET'])
|
||||||
def get_user_suggestions():
|
def get_user_suggestions():
|
||||||
keyword = request.args.get('keyword', '')
|
keyword = request.args.get('keyword', '')
|
||||||
data = BuyInboundService.search_system_users(keyword)
|
data = BuyInboundService.search_system_users(keyword)
|
||||||
return jsonify({"code": 200, "msg": "success", "data": data})
|
return jsonify({"code": 200, "msg": "success", "data": data})
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 8. [新增] 链接建议
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
@inbound_buy_bp.route('/suggestions/links', methods=['GET'])
|
||||||
|
def get_link_suggestions():
|
||||||
|
base_id = request.args.get('base_id', type=int)
|
||||||
|
link_type = request.args.get('type', 'original') # original or detail
|
||||||
|
if not base_id:
|
||||||
|
return jsonify({"code": 400, "msg": "base_id required"}), 400
|
||||||
|
data = BuyInboundService.get_history_links(base_id, link_type)
|
||||||
|
return jsonify({"code": 200, "msg": "success", "data": data})
|
||||||
@ -17,19 +17,14 @@ import json
|
|||||||
class BuyInboundService:
|
class BuyInboundService:
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# 0. 辅助:唯一性校验 (核心修复)
|
# 0. 辅助:唯一性校验
|
||||||
# ============================================================
|
# ============================================================
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _check_unique(base_id, serial_number, batch_number, exclude_id=None):
|
def _check_unique(base_id, serial_number, batch_number, exclude_id=None):
|
||||||
"""
|
"""
|
||||||
校验序列号和批号的唯一性逻辑
|
校验序列号和批号的唯一性逻辑
|
||||||
:param base_id: 当前物料的基础ID
|
|
||||||
:param serial_number: 序列号
|
|
||||||
:param batch_number: 批号
|
|
||||||
:param exclude_id: 排除的ID (用于编辑模式)
|
|
||||||
"""
|
"""
|
||||||
# 1. 序列号 (SN) 全局唯一校验
|
# 1. 序列号 (SN) 全局唯一校验
|
||||||
# 解释: 不同规格的物料通常也不应该有相同的SN,防止扫码混淆
|
|
||||||
if serial_number:
|
if serial_number:
|
||||||
query = StockBuy.query.filter(StockBuy.serial_number == serial_number)
|
query = StockBuy.query.filter(StockBuy.serial_number == serial_number)
|
||||||
if exclude_id:
|
if exclude_id:
|
||||||
@ -37,12 +32,10 @@ class BuyInboundService:
|
|||||||
|
|
||||||
exists = query.first()
|
exists = query.first()
|
||||||
if exists:
|
if exists:
|
||||||
# [修改] 获取占用该SN的物料名称 (material -> base)
|
|
||||||
occupied_name = exists.base.name if exists.base else "未知物料"
|
occupied_name = exists.base.name if exists.base else "未知物料"
|
||||||
raise ValueError(f"序列号【{serial_number}】已存在!被物料 [{occupied_name}] 占用,请核查。")
|
raise ValueError(f"序列号【{serial_number}】已存在!被物料 [{occupied_name}] 占用,请核查。")
|
||||||
|
|
||||||
# 2. 批号 (BN) 同物料唯一校验
|
# 2. 批号 (BN) 同物料唯一校验
|
||||||
# 解释: 不同规格的物料可以有相同的批号(如都有 001 批次),但同一个物料不能重复建单
|
|
||||||
if batch_number and base_id:
|
if batch_number and base_id:
|
||||||
query = StockBuy.query.filter(
|
query = StockBuy.query.filter(
|
||||||
StockBuy.base_id == base_id,
|
StockBuy.base_id == base_id,
|
||||||
@ -60,7 +53,7 @@ class BuyInboundService:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def search_base_material(keyword):
|
def search_base_material(keyword):
|
||||||
try:
|
try:
|
||||||
# [核心修改] 只查询已启用的物料,防止选择已禁用的历史物料
|
# 只查询已启用的物料
|
||||||
query = MaterialBase.query.filter(MaterialBase.is_enabled == True)
|
query = MaterialBase.query.filter(MaterialBase.is_enabled == True)
|
||||||
|
|
||||||
if keyword:
|
if keyword:
|
||||||
@ -68,7 +61,7 @@ class BuyInboundService:
|
|||||||
or_(
|
or_(
|
||||||
MaterialBase.name.ilike(f'%{keyword}%'),
|
MaterialBase.name.ilike(f'%{keyword}%'),
|
||||||
MaterialBase.spec_model.ilike(f'%{keyword}%'),
|
MaterialBase.spec_model.ilike(f'%{keyword}%'),
|
||||||
MaterialBase.pinyin.ilike(f'%{keyword}%') # 假设有拼音搜索
|
MaterialBase.pinyin.ilike(f'%{keyword}%')
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
query = query.order_by(MaterialBase.id.desc()).limit(20)
|
query = query.order_by(MaterialBase.id.desc()).limit(20)
|
||||||
@ -77,7 +70,7 @@ class BuyInboundService:
|
|||||||
results.append({
|
results.append({
|
||||||
'id': item.id,
|
'id': item.id,
|
||||||
'name': item.name,
|
'name': item.name,
|
||||||
'spec': item.spec_model, # 确保这里字段对应正确
|
'spec': item.spec_model,
|
||||||
'category': item.category,
|
'category': item.category,
|
||||||
'unit': item.unit,
|
'unit': item.unit,
|
||||||
'type': item.material_type,
|
'type': item.material_type,
|
||||||
@ -102,18 +95,16 @@ class BuyInboundService:
|
|||||||
if not material:
|
if not material:
|
||||||
raise ValueError("所选物料不存在")
|
raise ValueError("所选物料不存在")
|
||||||
|
|
||||||
# [核心修改] 后端二次校验:如果物料已停用,禁止入库
|
|
||||||
if not material.is_enabled:
|
if not material.is_enabled:
|
||||||
raise ValueError(f"物料【{material.name}】已停用,无法办理新入库。")
|
raise ValueError(f"物料【{material.name}】已停用,无法办理新入库。")
|
||||||
|
|
||||||
# --- [修复点] 执行唯一性校验 ---
|
|
||||||
BuyInboundService._check_unique(
|
BuyInboundService._check_unique(
|
||||||
base_id=base_id,
|
base_id=base_id,
|
||||||
serial_number=data.get('serial_number'),
|
serial_number=data.get('serial_number'),
|
||||||
batch_number=data.get('batch_number')
|
batch_number=data.get('batch_number')
|
||||||
)
|
)
|
||||||
|
|
||||||
# 时间处理 (强制北京时间)
|
# 时间处理
|
||||||
beijing_tz = timezone(timedelta(hours=8))
|
beijing_tz = timezone(timedelta(hours=8))
|
||||||
current_time = datetime.now(beijing_tz).replace(tzinfo=None)
|
current_time = datetime.now(beijing_tz).replace(tzinfo=None)
|
||||||
in_date_val = current_time
|
in_date_val = current_time
|
||||||
@ -162,7 +153,7 @@ class BuyInboundService:
|
|||||||
batch_number=data.get('batch_number'),
|
batch_number=data.get('batch_number'),
|
||||||
status=data.get('status', '在库'),
|
status=data.get('status', '在库'),
|
||||||
in_quantity=in_qty,
|
in_quantity=in_qty,
|
||||||
stock_quantity=in_qty, # 初始库存等于入库数
|
stock_quantity=in_qty,
|
||||||
available_quantity=in_qty,
|
available_quantity=in_qty,
|
||||||
inspection_status=data.get('inspection_status', '未检'),
|
inspection_status=data.get('inspection_status', '未检'),
|
||||||
warehouse_location=data.get('warehouse_location'),
|
warehouse_location=data.get('warehouse_location'),
|
||||||
@ -195,8 +186,6 @@ class BuyInboundService:
|
|||||||
if not stock:
|
if not stock:
|
||||||
raise ValueError("记录不存在")
|
raise ValueError("记录不存在")
|
||||||
|
|
||||||
# --- [修复点] 编辑时也要校验唯一性 (排除自身ID) ---
|
|
||||||
# 如果修改了物料(base_id),或者修改了SN/BN,都需要校验
|
|
||||||
new_base_id = data.get('base_id', stock.base_id)
|
new_base_id = data.get('base_id', stock.base_id)
|
||||||
new_sn = data.get('serial_number', stock.serial_number)
|
new_sn = data.get('serial_number', stock.serial_number)
|
||||||
new_bn = data.get('batch_number', stock.batch_number)
|
new_bn = data.get('batch_number', stock.batch_number)
|
||||||
@ -315,7 +304,6 @@ class BuyInboundService:
|
|||||||
d = {
|
d = {
|
||||||
'id': item.id,
|
'id': item.id,
|
||||||
'base_id': item.base_id,
|
'base_id': item.base_id,
|
||||||
# [核心修改] 确保这里从关联的 .base 获取信息
|
|
||||||
'material_name': item.base.name if item.base else '',
|
'material_name': item.base.name if item.base else '',
|
||||||
'spec_model': item.base.spec_model if item.base else '',
|
'spec_model': item.base.spec_model if item.base else '',
|
||||||
'category': item.base.category if item.base else '',
|
'category': item.base.category if item.base else '',
|
||||||
@ -354,7 +342,7 @@ class BuyInboundService:
|
|||||||
return {"total": 0, "items": []}
|
return {"total": 0, "items": []}
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# 6. 供应商历史查询
|
# 6. 供应商历史查询 (根据 base_id)
|
||||||
# ============================================================
|
# ============================================================
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_history_suppliers(base_id):
|
def get_history_suppliers(base_id):
|
||||||
@ -362,7 +350,8 @@ class BuyInboundService:
|
|||||||
try:
|
try:
|
||||||
query = db.session.query(StockBuy.supplier_name).filter(
|
query = db.session.query(StockBuy.supplier_name).filter(
|
||||||
StockBuy.base_id == base_id,
|
StockBuy.base_id == base_id,
|
||||||
StockBuy.supplier_name.isnot(None)
|
StockBuy.supplier_name.isnot(None),
|
||||||
|
StockBuy.supplier_name != ''
|
||||||
).distinct().order_by(StockBuy.supplier_name)
|
).distinct().order_by(StockBuy.supplier_name)
|
||||||
suppliers = [row[0] for row in query.all()]
|
suppliers = [row[0] for row in query.all()]
|
||||||
return suppliers
|
return suppliers
|
||||||
@ -370,7 +359,7 @@ class BuyInboundService:
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# 7. 系统用户搜索
|
# 7. 系统用户搜索 (全局)
|
||||||
# ============================================================
|
# ============================================================
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def search_system_users(keyword):
|
def search_system_users(keyword):
|
||||||
@ -394,3 +383,22 @@ class BuyInboundService:
|
|||||||
return users
|
return users
|
||||||
except Exception:
|
except Exception:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# 8. [新增] 链接建议 (根据 base_id)
|
||||||
|
# ============================================================
|
||||||
|
@staticmethod
|
||||||
|
def get_history_links(base_id, link_type='original'):
|
||||||
|
"""查询该物料的历史链接,方便复用"""
|
||||||
|
try:
|
||||||
|
target_col = StockBuy.original_link if link_type == 'original' else StockBuy.detail_link
|
||||||
|
query = db.session.query(target_col).filter(
|
||||||
|
StockBuy.base_id == base_id,
|
||||||
|
target_col.isnot(None),
|
||||||
|
target_col != ''
|
||||||
|
).distinct().limit(10)
|
||||||
|
|
||||||
|
links = [row[0] for row in query.all()]
|
||||||
|
return links
|
||||||
|
except Exception:
|
||||||
|
return []
|
||||||
@ -47,17 +47,17 @@ export function searchMaterialBase(keyword: string) {
|
|||||||
// 6. 文件上传 (用于图片/拍照)
|
// 6. 文件上传 (用于图片/拍照)
|
||||||
export function uploadFile(data: FormData) {
|
export function uploadFile(data: FormData) {
|
||||||
return request({
|
return request({
|
||||||
url: '/common/upload', // 对应后端 /api/v1/common/upload
|
url: '/common/upload',
|
||||||
method: 'post',
|
method: 'post',
|
||||||
data,
|
data,
|
||||||
headers: { 'Content-Type': 'multipart/form-data' }
|
headers: { 'Content-Type': 'multipart/form-data' }
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 7. [新增] 文件删除
|
// 7. 文件删除
|
||||||
export function deleteFile(filename: string) {
|
export function deleteFile(filename: string) {
|
||||||
return request({
|
return request({
|
||||||
url: `/common/files/${filename}`, // 对应后端 /api/v1/common/files/<filename>
|
url: `/common/files/${filename}`,
|
||||||
method: 'delete'
|
method: 'delete'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -71,7 +71,7 @@ export function getSupplierSuggestions(params: any) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 9. 用户建议
|
// 9. 用户建议 (采购人)
|
||||||
export function getUserSuggestions(params: any) {
|
export function getUserSuggestions(params: any) {
|
||||||
return request({
|
return request({
|
||||||
url: '/inbound/buy/suggestions/users',
|
url: '/inbound/buy/suggestions/users',
|
||||||
@ -79,3 +79,12 @@ export function getUserSuggestions(params: any) {
|
|||||||
params
|
params
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 10. [新增] 链接建议
|
||||||
|
export function getLinkSuggestions(params: any) {
|
||||||
|
return request({
|
||||||
|
url: '/inbound/buy/suggestions/links',
|
||||||
|
method: 'get',
|
||||||
|
params
|
||||||
|
})
|
||||||
|
}
|
||||||
@ -318,7 +318,15 @@
|
|||||||
<el-col :span="24">
|
<el-col :span="24">
|
||||||
<el-form-item label="到货图片" prop="arrival_photo">
|
<el-form-item label="到货图片" prop="arrival_photo">
|
||||||
<div class="upload-container">
|
<div class="upload-container">
|
||||||
<el-upload v-model:file-list="arrivalFileList" action="#" list-type="picture-card" multiple :http-request="(opts) => customUpload(opts, 'arrival_photo')" :on-preview="handlePreviewPicture" :on-remove="(file) => handleRemoveImage(file, 'arrival_photo')" :before-upload="beforeAvatarUpload">
|
<el-upload
|
||||||
|
v-model:file-list="arrivalFileList"
|
||||||
|
action="#"
|
||||||
|
list-type="picture-card"
|
||||||
|
multiple
|
||||||
|
:http-request="(opts) => customUpload(opts, 'arrival_photo')"
|
||||||
|
:on-preview="handlePreviewPicture"
|
||||||
|
:on-remove="(file) => handleRemoveImage(file, 'arrival_photo')"
|
||||||
|
:before-upload="beforeAvatarUpload">
|
||||||
<el-icon><Plus /></el-icon>
|
<el-icon><Plus /></el-icon>
|
||||||
</el-upload>
|
</el-upload>
|
||||||
<div class="camera-card" @click="triggerCamera('arrival_photo')"><el-icon><Camera /></el-icon><span class="text">拍照</span></div>
|
<div class="camera-card" @click="triggerCamera('arrival_photo')"><el-icon><Camera /></el-icon><span class="text">拍照</span></div>
|
||||||
@ -329,7 +337,15 @@
|
|||||||
<el-col :span="24">
|
<el-col :span="24">
|
||||||
<el-form-item label="检测报告" prop="inspection_report">
|
<el-form-item label="检测报告" prop="inspection_report">
|
||||||
<div class="upload-container">
|
<div class="upload-container">
|
||||||
<el-upload v-model:file-list="reportFileList" action="#" list-type="picture-card" multiple :http-request="(opts) => customUpload(opts, 'inspection_report')" :on-preview="handlePreviewPicture" :on-remove="(file) => handleRemoveImage(file, 'inspection_report')" :before-upload="beforeAvatarUpload">
|
<el-upload
|
||||||
|
v-model:file-list="reportFileList"
|
||||||
|
action="#"
|
||||||
|
list-type="picture-card"
|
||||||
|
multiple
|
||||||
|
:http-request="(opts) => customUpload(opts, 'inspection_report')"
|
||||||
|
:on-preview="handlePreviewPicture"
|
||||||
|
:on-remove="(file) => handleRemoveImage(file, 'inspection_report')"
|
||||||
|
:before-upload="beforeAvatarUpload">
|
||||||
<el-icon><Plus /></el-icon>
|
<el-icon><Plus /></el-icon>
|
||||||
</el-upload>
|
</el-upload>
|
||||||
<div class="camera-card" @click="triggerCamera('inspection_report')"><el-icon><Camera /></el-icon><span class="text">拍照</span></div>
|
<div class="camera-card" @click="triggerCamera('inspection_report')"><el-icon><Camera /></el-icon><span class="text">拍照</span></div>
|
||||||
@ -354,13 +370,76 @@
|
|||||||
<el-col :span="6"><el-form-item label="总价"><el-input-number v-model="form.total_price" :precision="2" disabled :controls="false" style="width:100%" class="total-price-input"/></el-form-item></el-col>
|
<el-col :span="6"><el-form-item label="总价"><el-input-number v-model="form.total_price" :precision="2" disabled :controls="false" style="width:100%" class="total-price-input"/></el-form-item></el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
<el-row :gutter="20">
|
<el-row :gutter="20">
|
||||||
<el-col :span="8"><el-form-item label="供应商"><el-autocomplete v-model="form.supplier_name" :fetch-suggestions="querySearchSupplier" placeholder="输入或选择供应商" style="width: 100%" clearable :trigger-on-focus="true" @select="handleSupplierSelect"/></el-form-item></el-col>
|
<el-col :span="8">
|
||||||
<el-col :span="8"><el-form-item label="采购人"><el-autocomplete v-model="form.purchaser" :fetch-suggestions="querySearchPurchaser" placeholder="输入采购人" style="width: 100%" clearable :trigger-on-focus="true" @select="handlePurchaserSelect"/></el-form-item></el-col>
|
<el-form-item label="供应商">
|
||||||
<el-col :span="8"><el-form-item label="采购邮箱"><el-autocomplete v-model="form.purchaser_email" :fetch-suggestions="querySearchEmail" placeholder="输入邮箱" style="width: 100%" clearable :trigger-on-focus="true" @select="handleEmailSelect"/></el-form-item></el-col>
|
<el-autocomplete
|
||||||
|
v-model="form.supplier_name"
|
||||||
|
:fetch-suggestions="querySearchSupplier"
|
||||||
|
placeholder="输入或选择供应商"
|
||||||
|
style="width: 100%"
|
||||||
|
clearable
|
||||||
|
:trigger-on-focus="true"
|
||||||
|
@select="handleSupplierSelect"
|
||||||
|
>
|
||||||
|
<template #default="{ item }">
|
||||||
|
<div style="font-weight: 500">{{ item.value }}</div>
|
||||||
|
</template>
|
||||||
|
</el-autocomplete>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="8">
|
||||||
|
<el-form-item label="采购人">
|
||||||
|
<el-autocomplete
|
||||||
|
v-model="form.purchaser"
|
||||||
|
:fetch-suggestions="querySearchPurchaser"
|
||||||
|
placeholder="输入采购人"
|
||||||
|
style="width: 100%"
|
||||||
|
clearable
|
||||||
|
:trigger-on-focus="true"
|
||||||
|
@select="handlePurchaserSelect"
|
||||||
|
>
|
||||||
|
<template #default="{ item }">
|
||||||
|
<span>{{ item.value }}</span>
|
||||||
|
<span v-if="item.email" style="float: right; color: #999; font-size: 12px; margin-left:10px">{{ item.email }}</span>
|
||||||
|
</template>
|
||||||
|
</el-autocomplete>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="8">
|
||||||
|
<el-form-item label="采购邮箱">
|
||||||
|
<el-input v-model="form.purchaser_email" placeholder="自动填充或手动输入" clearable />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
<el-row :gutter="20">
|
<el-row :gutter="20">
|
||||||
<el-col :span="12"><el-form-item label="原始链接"><el-input v-model="form.source_link" placeholder="http://"/></el-form-item></el-col>
|
<el-col :span="12">
|
||||||
<el-col :span="12"><el-form-item label="详情链接"><el-input v-model="form.detail_link" placeholder="http://"/></el-form-item></el-col>
|
<el-form-item label="原始链接">
|
||||||
|
<el-autocomplete
|
||||||
|
v-model="form.source_link"
|
||||||
|
:fetch-suggestions="(qs, cb) => querySearchLinks(qs, cb, 'original')"
|
||||||
|
placeholder="http://"
|
||||||
|
style="width: 100%"
|
||||||
|
clearable
|
||||||
|
:trigger-on-focus="true"
|
||||||
|
>
|
||||||
|
<template #default="{ item }"><div style="font-size: 12px; line-height: 1.2; padding: 4px 0;">{{ item.value }}</div></template>
|
||||||
|
</el-autocomplete>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="详情链接">
|
||||||
|
<el-autocomplete
|
||||||
|
v-model="form.detail_link"
|
||||||
|
:fetch-suggestions="(qs, cb) => querySearchLinks(qs, cb, 'detail')"
|
||||||
|
placeholder="http://"
|
||||||
|
style="width: 100%"
|
||||||
|
clearable
|
||||||
|
:trigger-on-focus="true"
|
||||||
|
>
|
||||||
|
<template #default="{ item }"><div style="font-size: 12px; line-height: 1.2; padding: 4px 0;">{{ item.value }}</div></template>
|
||||||
|
</el-autocomplete>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -402,7 +481,6 @@
|
|||||||
<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, Camera, Delete, Picture} from '@element-plus/icons-vue'
|
import {Plus, Setting, Refresh, Search, Lock, Box, House, InfoFilled, Link, Printer, Camera, Delete, Picture} from '@element-plus/icons-vue'
|
||||||
// 修改:引入 ElLoading
|
|
||||||
import {ElMessage, ElMessageBox, ElLoading} from 'element-plus'
|
import {ElMessage, ElMessageBox, ElLoading} from 'element-plus'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import {
|
import {
|
||||||
@ -412,11 +490,17 @@ import {
|
|||||||
deleteBuyInbound,
|
deleteBuyInbound,
|
||||||
searchMaterialBase,
|
searchMaterialBase,
|
||||||
uploadFile,
|
uploadFile,
|
||||||
deleteFile
|
deleteFile,
|
||||||
|
getSupplierSuggestions,
|
||||||
|
getUserSuggestions,
|
||||||
|
getLinkSuggestions
|
||||||
} from '@/api/inbound/buy'
|
} from '@/api/inbound/buy'
|
||||||
import {getLabelPreview, executePrint} from '@/api/common/print'
|
import {getLabelPreview, executePrint} from '@/api/common/print'
|
||||||
import WebRtcCamera from '@/components/Camera/WebRtcCamera.vue'
|
import WebRtcCamera from '@/components/Camera/WebRtcCamera.vue'
|
||||||
|
|
||||||
|
// 获取环境变量中的 API Base URL,用于图片拼接
|
||||||
|
const apiBaseUrl = import.meta.env.VITE_APP_BASE_API || ''
|
||||||
|
|
||||||
// ------------------------------------
|
// ------------------------------------
|
||||||
// 状态与变量
|
// 状态与变量
|
||||||
// ------------------------------------
|
// ------------------------------------
|
||||||
@ -510,9 +594,14 @@ const form = reactive({
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
// 供应商建议 API
|
// ------------------------------------
|
||||||
|
// 建议/Autocomplete 逻辑
|
||||||
|
// ------------------------------------
|
||||||
|
|
||||||
|
// 1. 供应商建议 (基于 base_id)
|
||||||
const fetchSupplierSuggestions = async (query: string, cb: any) => {
|
const fetchSupplierSuggestions = async (query: string, cb: any) => {
|
||||||
if (!form.base_id) {
|
if (!form.base_id) {
|
||||||
|
// 如果没有选物料,不给建议,或者可以给空
|
||||||
cb([])
|
cb([])
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -529,12 +618,17 @@ const fetchSupplierSuggestions = async (query: string, cb: any) => {
|
|||||||
cb([])
|
cb([])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const querySearchSupplier = (qs: string, cb: any) => fetchSupplierSuggestions(qs, cb)
|
||||||
|
const handleSupplierSelect = (item: any) => {
|
||||||
|
form.supplier_name = item.value
|
||||||
|
}
|
||||||
|
|
||||||
// 用户建议 API
|
// 2. 采购人建议 (全局搜索 + 系统用户)
|
||||||
const fetchUserSuggestions = async (query: string, cb: any) => {
|
const fetchUserSuggestions = async (query: string, cb: any) => {
|
||||||
try {
|
try {
|
||||||
const res: any = await getUserSuggestions({ keyword: query })
|
const res: any = await getUserSuggestions({ keyword: query })
|
||||||
if (res.code === 200) {
|
if (res.code === 200) {
|
||||||
|
// 假设后端返回 [{value: '张三', email: 'zhangsan@xxx.com'}, ...]
|
||||||
const users = res.data.map((user: any) => ({ value: user.value, email: user.email }))
|
const users = res.data.map((user: any) => ({ value: user.value, email: user.email }))
|
||||||
cb(users)
|
cb(users)
|
||||||
} else {
|
} else {
|
||||||
@ -544,24 +638,31 @@ const fetchUserSuggestions = async (query: string, cb: any) => {
|
|||||||
cb([])
|
cb([])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const querySearchSupplier = (qs: string, cb: any) => fetchSupplierSuggestions(qs, cb)
|
|
||||||
const handleSupplierSelect = (item: any) => {
|
|
||||||
form.supplier_name = item.value
|
|
||||||
}
|
|
||||||
const querySearchPurchaser = (qs: string, cb: any) => fetchUserSuggestions(qs, cb)
|
const querySearchPurchaser = (qs: string, cb: any) => fetchUserSuggestions(qs, cb)
|
||||||
const handlePurchaserSelect = (item: any) => {
|
const handlePurchaserSelect = (item: any) => {
|
||||||
form.purchaser = item.value
|
form.purchaser = item.value
|
||||||
form.purchaser_email = item.email || ''
|
// 核心:选中采购人时,自动填入邮箱
|
||||||
|
if (item.email) {
|
||||||
|
form.purchaser_email = item.email
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const querySearchEmail = (qs: string, cb: any) => fetchUserSuggestions(qs, (users: any[]) => {
|
|
||||||
const emailUsers = users.filter(u => u.email).map(u => ({ value: u.email }))
|
// 3. 链接建议 (基于 base_id)
|
||||||
const filtered = qs ? emailUsers.filter((item: any) => item.value.toLowerCase().includes(qs.toLowerCase())) : emailUsers
|
const fetchLinkSuggestions = async (query: string, cb: any, type: 'original' | 'detail') => {
|
||||||
cb(filtered)
|
if (!form.base_id) { cb([]); return }
|
||||||
})
|
try {
|
||||||
const handleEmailSelect = (item: any) => {
|
const res: any = await getLinkSuggestions({ base_id: form.base_id, type })
|
||||||
form.purchaser_email = item.value
|
if (res.code === 200) {
|
||||||
|
// 后端返回 ['http://...', 'http://...']
|
||||||
|
const links = res.data.map((link: string) => ({ value: link }))
|
||||||
|
const filtered = query ? links.filter((item:any) => item.value.toLowerCase().includes(query.toLowerCase())) : links
|
||||||
|
cb(filtered)
|
||||||
|
} else { cb([]) }
|
||||||
|
} catch(e) { cb([]) }
|
||||||
}
|
}
|
||||||
|
const querySearchLinks = (qs: string, cb: any, type: 'original' | 'detail') => fetchLinkSuggestions(qs, cb, type)
|
||||||
|
|
||||||
|
// 4. 币种
|
||||||
const currencyOptions = [{value: 'CNY', desc: '人民币'}, {value: 'USD', desc: '美元'}, {value: 'EUR', desc: '欧元'}]
|
const currencyOptions = [{value: 'CNY', desc: '人民币'}, {value: 'USD', desc: '美元'}, {value: 'EUR', desc: '欧元'}]
|
||||||
const querySearchCurrency = (queryString: string, cb: any) => {
|
const querySearchCurrency = (queryString: string, cb: any) => {
|
||||||
const filtered = queryString ? currencyOptions.filter(item => item.value.toLowerCase().includes(queryString.toLowerCase()) || item.desc.toLowerCase().includes(queryString.toLowerCase())) : currencyOptions
|
const filtered = queryString ? currencyOptions.filter(item => item.value.toLowerCase().includes(queryString.toLowerCase()) || item.desc.toLowerCase().includes(queryString.toLowerCase())) : currencyOptions
|
||||||
@ -585,6 +686,10 @@ const onMaterialSelected = (val: number) => {
|
|||||||
form.category = item.category
|
form.category = item.category
|
||||||
form.unit = item.unit
|
form.unit = item.unit
|
||||||
form.material_type = item.type
|
form.material_type = item.type
|
||||||
|
// 切换物料后,清空跟物料相关的供应商、链接,因为它们不再适用新物料
|
||||||
|
// form.supplier_name = '' // 可选:是否清空
|
||||||
|
// form.source_link = ''
|
||||||
|
// form.detail_link = ''
|
||||||
checkHistoryAndSetMode(item.id)
|
checkHistoryAndSetMode(item.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -702,6 +807,7 @@ const handleUpdate = (row: any) => {
|
|||||||
source_link: row.source_link, detail_link: row.detail_link,
|
source_link: row.source_link, detail_link: row.detail_link,
|
||||||
arrival_photo: row.arrival_photo || [], inspection_report: row.inspection_report || []
|
arrival_photo: row.arrival_photo || [], inspection_report: row.inspection_report || []
|
||||||
})
|
})
|
||||||
|
// 核心:回显图片时,使用 getImageUrl 补全路径
|
||||||
arrivalFileList.value = form.arrival_photo.map(url => ({ name: url.split('/').pop(), url: getImageUrl(url) }))
|
arrivalFileList.value = form.arrival_photo.map(url => ({ name: url.split('/').pop(), url: getImageUrl(url) }))
|
||||||
const reports = form.inspection_report || []
|
const reports = form.inspection_report || []
|
||||||
const reportImgs = reports.filter(r => !isExternalLink(r))
|
const reportImgs = reports.filter(r => !isExternalLink(r))
|
||||||
@ -749,16 +855,34 @@ const submitForm = async () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 其他辅助函数保持不变
|
// ------------------------------------
|
||||||
const getImageUrl = (url: string) => { return !url ? '' : (url.startsWith('http') ? url : url) }
|
// 图片/文件处理 (核心修复)
|
||||||
|
// ------------------------------------
|
||||||
|
|
||||||
|
// 1. 路径补全:如果是http开头则直接用,否则拼接 apiBaseUrl
|
||||||
|
const getImageUrl = (url: string) => {
|
||||||
|
if (!url) return ''
|
||||||
|
if (url.startsWith('http') || url.startsWith('https') || url.startsWith('blob:')) {
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
// 拼接 API 基础路径,例如 http://localhost:5000 + /static/files/xxx.jpg
|
||||||
|
// 注意处理斜杠,防止双斜杠
|
||||||
|
const baseUrl = apiBaseUrl.endsWith('/') ? apiBaseUrl.slice(0, -1) : apiBaseUrl
|
||||||
|
const path = url.startsWith('/') ? url : '/' + url
|
||||||
|
return baseUrl + path
|
||||||
|
}
|
||||||
|
|
||||||
const isExternalLink = (str: string) => { return str && (str.startsWith('http://') || str.startsWith('https://')) && !str.includes('/api/v1/common/files') }
|
const isExternalLink = (str: string) => { return str && (str.startsWith('http://') || str.startsWith('https://')) && !str.includes('/api/v1/common/files') }
|
||||||
const getImagesOnly = (list: string[]) => { return !list ? [] : list.filter(item => !isExternalLink(item)) }
|
const getImagesOnly = (list: string[]) => { return !list ? [] : list.filter(item => !isExternalLink(item)) }
|
||||||
const hasExternalLink = (list: string[]) => { return !list ? false : list.some(item => isExternalLink(item)) }
|
const hasExternalLink = (list: string[]) => { return !list ? false : list.some(item => isExternalLink(item)) }
|
||||||
|
|
||||||
const beforeAvatarUpload = (rawFile: any) => {
|
const beforeAvatarUpload = (rawFile: any) => {
|
||||||
if (rawFile.type !== 'image/jpeg' && rawFile.type !== 'image/png') { ElMessage.error('仅支持 JPG/PNG'); return false }
|
if (rawFile.type !== 'image/jpeg' && rawFile.type !== 'image/png') { ElMessage.error('仅支持 JPG/PNG'); return false }
|
||||||
if (rawFile.size / 1024 / 1024 > 5) { ElMessage.error('图片不能超过 5MB'); return false }
|
if (rawFile.size / 1024 / 1024 > 5) { ElMessage.error('图片不能超过 5MB'); return false }
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 2. 自定义上传 (修复回显)
|
||||||
const customUpload = async (options: any, targetField: 'arrival_photo' | 'inspection_report') => {
|
const customUpload = async (options: any, targetField: 'arrival_photo' | 'inspection_report') => {
|
||||||
const { file, onSuccess, onError } = options
|
const { file, onSuccess, onError } = options
|
||||||
const formData = new FormData()
|
const formData = new FormData()
|
||||||
@ -766,38 +890,64 @@ const customUpload = async (options: any, targetField: 'arrival_photo' | 'inspec
|
|||||||
try {
|
try {
|
||||||
const res: any = await uploadFile(formData)
|
const res: any = await uploadFile(formData)
|
||||||
if (res.code === 200) {
|
if (res.code === 200) {
|
||||||
const newUrl = res.data.url
|
const newUrl = res.data.url // 后端返回的相对路径
|
||||||
|
|
||||||
|
// 1. 存入表单数据
|
||||||
form[targetField].push(newUrl)
|
form[targetField].push(newUrl)
|
||||||
|
|
||||||
|
// 2. 核心修复:显式更新 fileList 以确保缩略图显示
|
||||||
|
// 需要拼接完整路径用于展示
|
||||||
|
const fullUrl = getImageUrl(newUrl)
|
||||||
|
const fileObj = { name: file.name, url: fullUrl, status: 'success', uid: file.uid }
|
||||||
|
|
||||||
|
if (targetField === 'arrival_photo') {
|
||||||
|
// 替换或追加
|
||||||
|
const idx = arrivalFileList.value.findIndex(f => f.uid === file.uid)
|
||||||
|
if (idx > -1) arrivalFileList.value[idx] = fileObj
|
||||||
|
else arrivalFileList.value.push(fileObj)
|
||||||
|
} else {
|
||||||
|
const idx = reportFileList.value.findIndex(f => f.uid === file.uid)
|
||||||
|
if (idx > -1) reportFileList.value[idx] = fileObj
|
||||||
|
else reportFileList.value.push(fileObj)
|
||||||
|
}
|
||||||
|
|
||||||
ElMessage.success('上传成功')
|
ElMessage.success('上传成功')
|
||||||
onSuccess(res)
|
onSuccess(res)
|
||||||
} else { ElMessage.error(res.msg || '上传失败'); onError(new Error(res.msg)) }
|
} else {
|
||||||
} catch (e) { ElMessage.error('网络错误'); onError(e) }
|
ElMessage.error(res.msg || '上传失败')
|
||||||
|
onError(new Error(res.msg))
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
ElMessage.error('网络错误')
|
||||||
|
onError(e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleRemoveImage = async (uploadFile: any, targetField: 'arrival_photo' | 'inspection_report') => {
|
const handleRemoveImage = async (uploadFile: any, targetField: 'arrival_photo' | 'inspection_report') => {
|
||||||
try {
|
try {
|
||||||
const urlToRemove = form[targetField].find(u => getImageUrl(u) === uploadFile.url) || uploadFile.url
|
// 这里需要反向查找,因为 uploadFile.url 可能是带域名的完整路径,而 form 里存的是相对路径
|
||||||
|
// 简单比对末尾文件名
|
||||||
|
const filename = uploadFile.url.split('/').pop()
|
||||||
|
const urlToRemove = form[targetField].find(u => u.endsWith(filename)) || uploadFile.url
|
||||||
|
|
||||||
form[targetField] = form[targetField].filter(u => u !== urlToRemove)
|
form[targetField] = form[targetField].filter(u => u !== urlToRemove)
|
||||||
if (!isExternalLink(urlToRemove)) { const filename = urlToRemove.split('/').pop(); if (filename) await deleteFile(filename) }
|
if (!isExternalLink(urlToRemove)) {
|
||||||
|
if (filename) await deleteFile(filename)
|
||||||
|
}
|
||||||
ElMessage.success('已删除')
|
ElMessage.success('已删除')
|
||||||
} catch (e) { console.error(e) }
|
} catch (e) { console.error(e) }
|
||||||
}
|
}
|
||||||
const handlePreviewPicture = (uploadFile: any) => { dialogImageUrl.value = uploadFile.url!; dialogVisibleImage.value = true }
|
const handlePreviewPicture = (uploadFile: any) => { dialogImageUrl.value = uploadFile.url!; dialogVisibleImage.value = true }
|
||||||
|
|
||||||
const triggerCamera = (field: 'arrival_photo' | 'inspection_report') => {
|
const triggerCamera = (field: 'arrival_photo' | 'inspection_report') => {
|
||||||
currentCameraField.value = field;
|
currentCameraField.value = field;
|
||||||
cameraDialogVisible.value = true;
|
cameraDialogVisible.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------
|
|
||||||
// 【修复核心】:处理拍照上传
|
|
||||||
// ----------------------------------------------------
|
|
||||||
const handleCameraConfirm = async (file: File) => {
|
const handleCameraConfirm = async (file: File) => {
|
||||||
console.log('✅ 父组件收到照片:', file.name, file.size)
|
|
||||||
|
|
||||||
if (!beforeAvatarUpload(file)) {
|
if (!beforeAvatarUpload(file)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 修复点:使用 ElLoading.service 替代报错的 ElMessage.loading
|
|
||||||
const loadingInstance = ElLoading.service({
|
const loadingInstance = ElLoading.service({
|
||||||
lock: true,
|
lock: true,
|
||||||
text: '照片上传中,请稍候...',
|
text: '照片上传中,请稍候...',
|
||||||
@ -807,35 +957,32 @@ const handleCameraConfirm = async (file: File) => {
|
|||||||
try {
|
try {
|
||||||
const formData = new FormData()
|
const formData = new FormData()
|
||||||
formData.append('file', file)
|
formData.append('file', file)
|
||||||
|
|
||||||
console.log('🚀 开始上传...')
|
|
||||||
const res: any = await uploadFile(formData)
|
const res: any = await uploadFile(formData)
|
||||||
console.log('📡 上传结果:', res)
|
|
||||||
|
|
||||||
if (res.code === 200) {
|
if (res.code === 200) {
|
||||||
const newUrl = res.data.url
|
const newUrl = res.data.url
|
||||||
const field = currentCameraField.value // 'arrival_photo' 或 'inspection_report'
|
const field = currentCameraField.value
|
||||||
|
|
||||||
// 更新表单数据
|
// 更新表单
|
||||||
form[field].push(newUrl)
|
form[field].push(newUrl)
|
||||||
|
|
||||||
// 更新文件展示列表
|
// 更新文件列表 (使用 getImageUrl 补全显示)
|
||||||
|
const fileObj = { name: file.name, url: getImageUrl(newUrl) }
|
||||||
if (field === 'arrival_photo') {
|
if (field === 'arrival_photo') {
|
||||||
arrivalFileList.value.push({ name: newUrl.split('/').pop(), url: getImageUrl(newUrl) })
|
arrivalFileList.value.push(fileObj)
|
||||||
} else if (field === 'inspection_report') {
|
} else if (field === 'inspection_report') {
|
||||||
reportFileList.value.push({ name: newUrl.split('/').pop(), url: getImageUrl(newUrl) })
|
reportFileList.value.push(fileObj)
|
||||||
}
|
}
|
||||||
|
|
||||||
ElMessage.success('拍照上传成功')
|
ElMessage.success('拍照上传成功')
|
||||||
cameraDialogVisible.value = false // 成功才关闭弹窗
|
cameraDialogVisible.value = false
|
||||||
} else {
|
} else {
|
||||||
ElMessage.error(res.msg || '上传失败')
|
ElMessage.error(res.msg || '上传失败')
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error('上传异常:', e)
|
|
||||||
ElMessage.error('网络错误,上传失败')
|
ElMessage.error('网络错误,上传失败')
|
||||||
} finally {
|
} finally {
|
||||||
loadingInstance.close() // 关闭加载状态
|
loadingInstance.close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user