Compare commits

9 Commits

6 changed files with 288 additions and 179 deletions

View File

@ -1,8 +1,10 @@
# 文件路径: app/api/v1/inbound/base.py
from flask import Blueprint, request, jsonify, send_file, g
from app.extensions import db
from app.services.inbound.base_service import MaterialBaseService
from app.utils.decorators import login_required, permission_required, audit_log
from app.models.base import MaterialBase, MaterialWarningSetting
import traceback
import datetime
import json
@ -129,6 +131,12 @@ def get_list():
}
user_permissions = get_current_user_permissions()
# 自动拦截:如果用户有预警查看权限,且当前没有按特定列手动排序,则强制开启预警智能排序
has_warning_perm = 'material_list:view_warning' in user_permissions
if has_warning_perm and not filters.get('orderByColumn'):
filters['enableWarningSort'] = True
result = MaterialBaseService.get_list(page, limit, filters, user_permissions)
# 字段级脱敏
user_permissions = get_current_user_permissions()
@ -348,8 +356,6 @@ def batch_set_warning():
if not isinstance(data, list):
return jsonify({"code": 400, "msg": "请求体必须为数组"})
from app.models.base import MaterialWarningSetting
updated_count = 0
created_count = 0
@ -371,18 +377,21 @@ def batch_set_warning():
# 更新现有记录
if 'isEnabled' in item:
warning.is_enabled = bool(item['isEnabled'])
if 'yellowThreshold' in item:
warning.yellow_threshold = item['yellowThreshold']
if 'redThreshold' in item:
warning.red_threshold = item['redThreshold']
# 安全转换阈值None 默认转为 0
yellow_val = item.get('yellowThreshold')
red_val = item.get('redThreshold')
warning.yellow_threshold = float(yellow_val) if yellow_val is not None else 0
warning.red_threshold = float(red_val) if red_val is not None else 0
updated_count += 1
else:
# 创建新记录
yellow_val = item.get('yellowThreshold')
red_val = item.get('redThreshold')
warning = MaterialWarningSetting(
base_id=base_id,
is_enabled=item.get('isEnabled', False),
yellow_threshold=item.get('yellowThreshold'),
red_threshold=item.get('redThreshold')
yellow_threshold=float(yellow_val) if yellow_val is not None else 0,
red_threshold=float(red_val) if red_val is not None else 0
)
db.session.add(warning)
created_count += 1

View File

@ -1,12 +1,12 @@
# 文件路径: app/services/inbound/base_service.py
from app.extensions import db
from app.models.base import MaterialBase
from app.models.base import MaterialBase, MaterialWarningSetting
from app.models.inbound.buy import StockBuy
from app.models.inbound.semi import StockSemi
from app.models.inbound.product import StockProduct
# from app.models.inbound.service import StockService
from sqlalchemy import or_, and_, func
from sqlalchemy import or_, and_, func, case, desc, asc, cast, Numeric, text
import traceback
import json
import io
@ -114,6 +114,10 @@ class MaterialBaseService:
"""
获取基础信息列表 (带分页、高级筛选和全字段排序)
支持库存预警功能(如果用户有 view_warning 权限)
修复说明:
1. 使用子查询方案确保聚合列(total_inv)能正确参与预警排序计算
2. 高级筛选中针对聚合字段使用 HAVING 而非 WHERE
"""
try:
# 检查用户是否有查看预警的权限
@ -144,7 +148,7 @@ class MaterialBaseService:
func.sum(StockProduct.available_quantity).label('prod_avail')
).group_by(StockProduct.base_id).subquery()
# 总库存和可用数的 SQL 表达式
# 总库存和可用数的 SQL 表达式(用于后续计算)
total_inv = func.coalesce(buy_sub.c.buy_inv, 0) + \
func.coalesce(semi_sub.c.semi_inv, 0) + \
func.coalesce(prod_sub.c.prod_inv, 0)
@ -155,218 +159,224 @@ class MaterialBaseService:
# 导入预警设置模型
from app.models.base import MaterialWarningSetting
# 主查询,关联聚合子查询和预警设置
query = db.session.query(
# ============================================================
# 【核心修复】使用子查询方案:将聚合结果作为子查询
# 这样 total_inv 在外层查询中是一个普通列,可安全参与计算
# ============================================================
# 内层子查询:基础数据 + 聚合库存
inner_sub = db.session.query(
MaterialBase.id.label('base_id'),
MaterialBase,
total_inv.label('total_inv'),
total_avail.label('total_avail'),
MaterialWarningSetting.is_enabled.label('warning_enabled'),
MaterialWarningSetting.yellow_threshold.label('warning_yellow'),
MaterialWarningSetting.red_threshold.label('warning_red')
total_avail.label('total_avail')
).outerjoin(buy_sub, MaterialBase.id == buy_sub.c.base_id) \
.outerjoin(semi_sub, MaterialBase.id == semi_sub.c.base_id) \
.outerjoin(prod_sub, MaterialBase.id == prod_sub.c.base_id) \
.outerjoin(MaterialWarningSetting, MaterialBase.id == MaterialWarningSetting.base_id)
.outerjoin(prod_sub, MaterialBase.id == prod_sub.c.base_id)
# 关键词精确搜索(需要在子查询层完成)
if filters:
# 1. 关键词精准搜索(支持指定字段)
search_field = filters.get('searchField', 'all')
keyword = filters.get('keyword')
if keyword:
kw = f"%{keyword}%"
if search_field == 'name':
query = query.filter(MaterialBase.name.ilike(kw))
inner_sub = inner_sub.filter(MaterialBase.name.ilike(kw))
elif search_field == 'common_name':
query = query.filter(MaterialBase.common_name.ilike(kw))
inner_sub = inner_sub.filter(MaterialBase.common_name.ilike(kw))
elif search_field == 'spec':
query = query.filter(MaterialBase.spec_model.ilike(kw))
else: # 'all' 默认全局模糊匹配
query = query.filter(or_(
inner_sub = inner_sub.filter(MaterialBase.spec_model.ilike(kw))
else:
inner_sub = inner_sub.filter(or_(
MaterialBase.name.ilike(kw),
MaterialBase.common_name.ilike(kw),
MaterialBase.spec_model.ilike(kw)
))
# 2. 精确筛选
company = filters.get('company')
if company is not None and company != '':
query = query.filter(MaterialBase.company_name.ilike(company.strip()))
inner_sub = inner_sub.filter(MaterialBase.company_name.ilike(company.strip()))
category = filters.get('category')
if category is not None and category != '':
query = query.filter(MaterialBase.category.ilike(category.strip()))
inner_sub = inner_sub.filter(MaterialBase.category.ilike(category.strip()))
type_val = filters.get('type')
if type_val is not None and type_val != '':
query = query.filter(MaterialBase.material_type.ilike(type_val.strip()))
inner_sub = inner_sub.filter(MaterialBase.material_type.ilike(type_val.strip()))
# 【核心修改】:增强布尔值解析
if filters.get('isEnabled') is not None:
val_str = str(filters['isEnabled']).lower()
is_active = val_str in ['1', 'true', 'yes', 't']
# 必须使用 filter() 而非 filter_by(),因为 query 是 join 后的复杂查询
query = query.filter(MaterialBase.is_enabled == is_active)
inner_sub = inner_sub.filter(MaterialBase.is_enabled == is_active)
# 【新增】:库存状态筛选 (has_stock)
# 库存状态筛选 (has_stock) - 使用子查询列
has_stock = filters.get('has_stock')
if has_stock and str(has_stock).lower() in ['true', '1', 'yes']:
query = query.filter(total_inv > 0)
inner_sub = inner_sub.filter(total_inv > 0)
# 3. 高级动态筛选
advanced_filters = filters.get('advancedFilters', [])
if advanced_filters:
allowed_fields = {
'companyName': 'company_name',
'name': 'name',
'commonName': 'common_name',
'category': 'category',
'type': 'material_type',
'spec': 'spec_model',
'unit': 'unit',
'inventoryCount': total_inv,
'availableCount': total_avail
}
# 字段到权限码的映射
field_permission_map = {
'companyName': 'material_list:companyName',
'name': 'material_list:name',
'commonName': 'material_list:commonName',
'category': 'material_list:category',
'type': 'material_list:type',
'spec': 'material_list:spec',
'unit': 'material_list:unit',
'inventoryCount': 'material_list:inventoryCount',
'availableCount': 'material_list:availableCount'
}
filter_conditions = []
for condition in advanced_filters:
field = condition.get('field')
operator = condition.get('operator')
value = condition.get('value')
if not field or not operator or value is None:
# 将内层子查询具体化
inner_sub = inner_sub.subquery()
# 外层查询:关联预警设置,并在外层处理排序和高级筛选
query = db.session.query(
MaterialBase,
inner_sub.c.total_inv,
inner_sub.c.total_avail,
MaterialWarningSetting.is_enabled.label('warning_enabled'),
MaterialWarningSetting.yellow_threshold.label('warning_yellow'),
MaterialWarningSetting.red_threshold.label('warning_red')
).outerjoin(inner_sub, MaterialBase.id == inner_sub.c.base_id) \
.outerjoin(MaterialWarningSetting, MaterialBase.id == MaterialWarningSetting.base_id)
# ============================================================
# 【修复3】高级筛选聚合字段使用 HAVING非聚合字段使用 WHERE
# ============================================================
advanced_filters = filters.get('advancedFilters', []) if filters else []
having_conditions = []
filter_conditions = []
if advanced_filters:
allowed_fields = {
'companyName': 'company_name',
'name': 'name',
'commonName': 'common_name',
'category': 'category',
'type': 'material_type',
'spec': 'spec_model',
'unit': 'unit',
'inventoryCount': 'total_inv',
'availableCount': 'total_avail'
}
# 聚合字段列表(这些需要用 HAVING
aggregate_fields = {'inventoryCount', 'availableCount'}
field_permission_map = {
'companyName': 'material_list:companyName',
'name': 'material_list:name',
'commonName': 'material_list:commonName',
'category': 'material_list:category',
'type': 'material_list:type',
'spec': 'material_list:spec',
'unit': 'material_list:unit',
'inventoryCount': 'material_list:inventoryCount',
'availableCount': 'material_list:availableCount'
}
for condition in advanced_filters:
field = condition.get('field')
operator = condition.get('operator')
value = condition.get('value')
if not field or not operator or value is None:
continue
db_field = allowed_fields.get(field)
if not db_field:
continue
# 权限校验
if user_permissions is not None:
perm_code = field_permission_map.get(field)
if 'material_list:*' in user_permissions:
pass
elif perm_code and perm_code not in user_permissions:
continue
db_field = allowed_fields.get(field)
if not db_field:
# 判断是否为聚合字段
is_aggregate = field in aggregate_fields
if is_aggregate:
# 聚合字段:使用 HAVING - 通过子查询列进行比较
col = inner_sub.c.total_inv if field == 'inventoryCount' else inner_sub.c.total_avail
try:
num_val = float(value)
except ValueError:
continue
# 权限校验
if user_permissions is not None:
perm_code = field_permission_map.get(field)
if 'material_list:*' in user_permissions:
# 超级管理员拥有全部权限
pass
elif perm_code and perm_code not in user_permissions:
# 无权限,跳过该条件
continue
# 对于聚合字段 (inventoryCount, availableCount),需要使用子查询别名
if isinstance(db_field, type(total_inv)):
column = db_field
else:
column = getattr(MaterialBase, db_field, None)
if operator == 'eq':
having_conditions.append(col == num_val)
elif operator == 'ne':
having_conditions.append(col != num_val)
elif operator == 'ge':
having_conditions.append(col >= num_val)
elif operator == 'le':
having_conditions.append(col <= num_val)
else:
# 非聚合字段:使用 WHERE
column = getattr(MaterialBase, db_field, None)
if column is None:
continue
# 处理操作符
# 对于数值型列(聚合字段)只支持 eq, ne, ge, le
if isinstance(column, type(total_inv)):
# 数值型列
if operator == 'eq':
filter_conditions.append(column == value)
elif operator == 'ne':
filter_conditions.append(column != value)
elif operator == 'contains':
filter_conditions.append(column.ilike(f'%{value}%'))
elif operator == 'ge':
try:
num_val = float(value)
except ValueError:
# 转换失败则跳过该条件
continue
if operator == 'eq':
filter_conditions.append(column == num_val)
elif operator == 'ne':
filter_conditions.append(column != num_val)
elif operator == 'ge':
filter_conditions.append(column >= num_val)
elif operator == 'le':
except ValueError:
continue
elif operator == 'le':
try:
num_val = float(value)
filter_conditions.append(column <= num_val)
# 对于 contains 操作符,数值型列不支持,忽略
else:
# 字符串型列
if operator == 'eq':
filter_conditions.append(column == value)
elif operator == 'ne':
filter_conditions.append(column != value)
elif operator == 'contains':
filter_conditions.append(column.ilike(f'%{value}%'))
elif operator == 'ge':
try:
num_val = float(value)
filter_conditions.append(column >= num_val)
except ValueError:
continue
elif operator == 'le':
try:
num_val = float(value)
filter_conditions.append(column <= num_val)
except ValueError:
continue
if filter_conditions:
query = query.filter(and_(*filter_conditions))
except ValueError:
continue
# 应用 WHERE 条件
if filter_conditions:
query = query.filter(and_(*filter_conditions))
# 应用 HAVING 条件(聚合字段筛选)
if having_conditions:
query = query.having(and_(*having_conditions))
# 排序处理(支持全字段)
order_by_column = filters.get('orderByColumn', '')
is_asc = filters.get('isAsc', None)
# 检查是否启用了预警智能排序
enable_warning_sort = has_warning_permission and filters.get('enableWarningSort', False)
# 信任前端传递的预警排序开关(前端已基于权限注入)
enable_warning_sort = filters.get('enableWarningSort', False)
if enable_warning_sort:
# 预警智能排序:先按预警状态排序,再按缺口/余量排序
# 使用 CASE 表达式计算预警状态
# 状态: 2=红(库存<=red), 1=黄(red<库存<=yellow), 0=正常/未开启
from sqlalchemy import case
print("====== [DEBUG] 成功进入预警强排逻辑 (SQLA 2.0) ======")
# 【核心修复】使用子查询列进行预警排序计算
# total_inv 现在是 inner_sub.c.total_inv可以安全参与 case 计算
inv_val = inner_sub.c.total_inv
red_val = cast(MaterialWarningSetting.red_threshold, Numeric)
yellow_val = cast(MaterialWarningSetting.yellow_threshold, Numeric)
# 计算预警状态
warning_status = case(
(
(MaterialWarningSetting.is_enabled == True) &
(total_inv <= MaterialWarningSetting.red_threshold),
2 # 红色预警
),
(
(MaterialWarningSetting.is_enabled == True) &
(total_inv > MaterialWarningSetting.red_threshold) &
(total_inv <= MaterialWarningSetting.yellow_threshold),
1 # 黄色预警
),
else_=0 # 正常或未开启
).label('warning_status')
# 红色组内部:按缺口降序 (red_threshold - inventory)
red_gap = case(
(
(MaterialWarningSetting.is_enabled == True) &
(total_inv <= MaterialWarningSetting.red_threshold),
MaterialWarningSetting.red_threshold - total_inv
),
# 预警等级计算:红=2, 黄=1, 正常=0
warning_level = case(
(and_(MaterialWarningSetting.is_enabled.is_(True), inv_val <= red_val), 2),
(and_(MaterialWarningSetting.is_enabled.is_(True), inv_val <= yellow_val), 1),
else_=0
).label('red_gap')
).label('sort_level')
# 黄色组内部:按余量升序 (inventory - red_threshold),离红线越近越靠前
yellow_gap = case(
(
(MaterialWarningSetting.is_enabled == True) &
(total_inv > MaterialWarningSetting.red_threshold) &
(total_inv <= MaterialWarningSetting.yellow_threshold),
total_inv - MaterialWarningSetting.red_threshold
),
else_=999999 # 正常物料放在最后
).label('yellow_gap')
# 红色预警时的缺口(库存与红色阈值的差距)
red_shortage = case(
(and_(MaterialWarningSetting.is_enabled.is_(True), inv_val <= red_val), red_val - inv_val),
else_=0
).label('sort_red')
# 添加排序字段到查询
query = query.add_columns(warning_status, red_gap, yellow_gap)
# 黄色预警时的缺口
yellow_distance = case(
(and_(MaterialWarningSetting.is_enabled.is_(True), inv_val > red_val, inv_val <= yellow_val), inv_val - red_val),
else_=999999
).label('sort_yellow')
# 强制排序规则:预警状态降序 -> 红色缺口降序 -> 黄色余量升序 -> 规格型号升序
query = query.order_by(
warning_status.desc(),
red_gap.desc(),
yellow_gap.asc(),
MaterialBase.spec_model.asc()
query = query.add_columns(warning_level, red_shortage, yellow_distance)
query = query.order_by(None).order_by(
text("sort_level DESC"),
text("sort_red DESC"),
text("sort_yellow ASC"),
MaterialBase.id.desc()
)
elif order_by_column:
# 字段映射
# 字段映射 - 使用子查询列进行排序
sort_field_map = {
'companyName': MaterialBase.company_name,
'name': MaterialBase.name,
@ -375,8 +385,8 @@ class MaterialBaseService:
'type': MaterialBase.material_type,
'spec': MaterialBase.spec_model,
'unit': MaterialBase.unit,
'inventoryCount': total_inv,
'availableCount': total_avail
'inventoryCount': inner_sub.c.total_inv,
'availableCount': inner_sub.c.total_avail
}
sort_column = sort_field_map.get(order_by_column)
if sort_column is not None:
@ -386,7 +396,7 @@ class MaterialBaseService:
query = query.order_by(sort_column.desc())
else:
# 默认排序:优先按总库存数降序,当库存相同时,再按规格型号升序
query = query.order_by(total_inv.desc(), MaterialBase.spec_model.asc())
query = query.order_by(inner_sub.c.total_inv.desc(), MaterialBase.spec_model.asc())
# 分页
pagination = query.paginate(page=page, per_page=limit, error_out=False)

View File

@ -580,6 +580,7 @@ interface QueryParams {
isAsc: string | undefined;
advancedFilters?: any[];
has_stock?: string;
enableWarningSort?: boolean;
}
interface CascaderOption {
@ -880,6 +881,8 @@ const querySearchType = (queryString: string, cb: any) => {
const getList = () => {
loading.value = true;
// 仅当用户没有进行手动表头排序时才开启预警排序
queryParams.enableWarningSort = userStore.hasPermission('material_list:view_warning') && !queryParams.orderByColumn;
// Stringify advancedFilters to JSON string as backend expects
const params = {
...queryParams,
@ -1204,13 +1207,23 @@ const submitWarning = async () => {
await warningFormRef.value.validate();
// 安全转换数值null/undefined 默认转为 0
const yellow = Number(warningForm.yellowThreshold) || 0;
const red = Number(warningForm.redThreshold) || 0;
// 逻辑校验:启用预警时,黄色阈值必须大于红色阈值
if (warningForm.isEnabled && yellow !== 0 && red !== 0 && yellow <= red) {
ElMessage.warning('黄色阈值必须大于红色阈值');
return;
}
warningLoading.value = true;
try {
const data = warningDialog.selectedIds.map(baseId => ({
baseId,
isEnabled: warningForm.isEnabled,
redThreshold: warningForm.redThreshold,
yellowThreshold: warningForm.yellowThreshold
redThreshold: red,
yellowThreshold: yellow
}));
await batchSetWarning(data);

View File

@ -376,7 +376,15 @@
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="序列号" prop="serial_number">
<el-form-item prop="serial_number">
<template #label>
<div class="flex items-center justify-between w-full">
<span>序列号</span>
<el-link type="primary" :underline="false" @click="openScanner" title="开启摄像头智能扫码" :disabled="entryMode === 'batch'">
<el-icon><Camera /></el-icon> 智能扫码
</el-link>
</div>
</template>
<el-input v-model="form.serial_number" :placeholder="entryMode === 'serial' ? '请扫描 SN...' : '不可用'" :disabled="entryMode === 'batch'" clearable>
<template #prefix><span class="prefix-tag sn">SN</span></template>
</el-input>
@ -646,6 +654,9 @@
<div class="dialog-footer"><el-button @click="printVisible = false">取消</el-button><el-button type="primary" :loading="printing" @click="confirmPrint"><el-icon><Printer/></el-icon>确认打印</el-button></div>
</template>
</el-dialog>
<!-- 智能扫码弹窗 -->
<SmartScannerDialog v-model="scannerDialogVisible" @confirm="handleScannerConfirm" />
</div>
</template>
@ -672,6 +683,7 @@ import {getLabelPreview, executePrint} from '@/api/common/print'
import { getWarehouseTree } from '@/api/common/warehouse'
import WebRtcCamera from '@/components/Camera/WebRtcCamera.vue'
import WarehouseSelector from '@/components/WarehouseSelector.vue'
import SmartScannerDialog from '@/components/SmartScannerDialog.vue'
import { useUserStore } from '@/stores/user'
// ------------------------------------
@ -813,6 +825,9 @@ const cameraRef = ref<InstanceType<typeof WebRtcCamera> | null>(null)
const currentCameraField = ref<'arrival_photo' | 'inspection_report'>('arrival_photo')
const inspection_report_url = ref('')
// 智能扫码弹窗
const scannerDialogVisible = ref(false)
// 库位级联选择器数据
const warehouseOptions = ref<any[]>([])
@ -1488,6 +1503,17 @@ const handleCameraConfirm = async (file: File) => {
}
}
// 智能扫码
const openScanner = () => {
scannerDialogVisible.value = true
}
const handleScannerConfirm = (result: string) => {
form.serial_number = result
scannerDialogVisible.value = false
ElMessage.success('序列号已提取')
}
const addCondition = () => {
advancedConditions.value.push({ field: '', operator: '', value: '' })
}

View File

@ -320,7 +320,15 @@
<div class="identity-panel">
<el-row :gutter="24">
<el-col :span="12">
<el-form-item label="序列号(SN)" prop="serial_number">
<el-form-item prop="serial_number">
<template #label>
<div class="flex items-center justify-between w-full">
<span>序列号(SN)</span>
<el-link type="primary" :underline="false" @click="openScanner" title="开启摄像头智能扫码">
<el-icon><Camera /></el-icon> 智能扫码
</el-link>
</div>
</template>
<el-input v-model="form.serial_number" placeholder="必填: 唯一序列号" clearable><template #prefix><span class="prefix-tag sn">SN</span></template></el-input>
</el-form-item>
</el-col>
@ -518,6 +526,8 @@
</template>
</el-dialog>
<!-- 智能扫码弹窗 -->
<SmartScannerDialog v-model="scannerDialogVisible" @confirm="handleScannerConfirm" />
</div>
</template>
@ -540,6 +550,7 @@ import {
import { uploadFile, deleteFile } from '@/api/inbound/buy'
import WebRtcCamera from '@/components/Camera/WebRtcCamera.vue'
import WarehouseSelector from '@/components/WarehouseSelector.vue'
import SmartScannerDialog from '@/components/SmartScannerDialog.vue'
import { getLabelPreview, executePrint } from '@/api/common/print'
import { getWarehouseTree } from '@/api/common/warehouse'
import { useUserStore } from '@/stores/user'
@ -668,6 +679,9 @@ const currentCameraField = ref<'product_photo' | 'quality_report_link' | 'inspec
const quality_url = ref('')
const inspection_url = ref('')
// 智能扫码弹窗
const scannerDialogVisible = ref(false)
// 库位级联选择器数据
const warehouseOptions = ref<any[]>([])
@ -1187,6 +1201,17 @@ const handleCameraConfirm = async (file: File) => {
}
};
// 智能扫码
const openScanner = () => {
scannerDialogVisible.value = true
}
const handleScannerConfirm = (result: string) => {
form.serial_number = result
scannerDialogVisible.value = false
ElMessage.success('序列号已提取')
}
const submitForm = async () => {
await formRef.value.validate(async (valid: boolean) => {
if(valid) {

View File

@ -395,7 +395,15 @@
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="序列号" prop="serial_number">
<el-form-item prop="serial_number">
<template #label>
<div class="flex items-center justify-between w-full">
<span>序列号</span>
<el-link type="primary" :underline="false" @click="openScanner" title="开启摄像头智能扫码" :disabled="entryMode === 'batch'">
<el-icon><Camera /></el-icon> 智能扫码
</el-link>
</div>
</template>
<el-input v-model="form.serial_number" :placeholder="entryMode === 'serial' ? '请扫描 SN...' : '不可用'" :disabled="entryMode === 'batch'" clearable>
<template #prefix><span class="prefix-tag sn">SN</span></template>
</el-input>
@ -582,6 +590,9 @@
<div class="dialog-footer"><el-button @click="printVisible = false">取消</el-button><el-button type="primary" :loading="printing" @click="confirmPrint"><el-icon><Printer/></el-icon>确认打印</el-button></div>
</template>
</el-dialog>
<!-- 智能扫码弹窗 -->
<SmartScannerDialog v-model="scannerDialogVisible" @confirm="handleScannerConfirm" />
</div>
</template>
@ -604,6 +615,7 @@ import {
import { uploadFile, deleteFile } from '@/api/inbound/buy'
import WebRtcCamera from '@/components/Camera/WebRtcCamera.vue'
import WarehouseSelector from '@/components/WarehouseSelector.vue'
import SmartScannerDialog from '@/components/SmartScannerDialog.vue'
import {getLabelPreview, executePrint} from '@/api/common/print'
import { getWarehouseTree } from '@/api/common/warehouse'
import { useUserStore } from '@/stores/user'
@ -731,6 +743,9 @@ const cameraRef = ref<InstanceType<typeof WebRtcCamera> | null>(null)
const currentCameraField = ref<'arrival_photo' | 'quality_report_link'>('arrival_photo')
const quality_report_url = ref('')
// 智能扫码弹窗
const scannerDialogVisible = ref(false)
// 库位级联选择器数据
const warehouseOptions = ref<any[]>([])
@ -1309,6 +1324,17 @@ const handleCameraConfirm = async (file: File) => {
}
};
// 智能扫码
const openScanner = () => {
scannerDialogVisible.value = true
}
const handleScannerConfirm = (result: string) => {
form.serial_number = result
scannerDialogVisible.value = false
ElMessage.success('序列号已提取')
}
const submitForm = async () => {
if (!formRef.value) return
await formRef.value.validate(async (valid: boolean) => {