fix: 基础信息排序问题
This commit is contained in:
@ -114,6 +114,10 @@ class MaterialBaseService:
|
|||||||
"""
|
"""
|
||||||
获取基础信息列表 (带分页、高级筛选和全字段排序)
|
获取基础信息列表 (带分页、高级筛选和全字段排序)
|
||||||
支持库存预警功能(如果用户有 view_warning 权限)
|
支持库存预警功能(如果用户有 view_warning 权限)
|
||||||
|
|
||||||
|
修复说明:
|
||||||
|
1. 使用子查询方案确保聚合列(total_inv)能正确参与预警排序计算
|
||||||
|
2. 高级筛选中针对聚合字段使用 HAVING 而非 WHERE
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# 检查用户是否有查看预警的权限
|
# 检查用户是否有查看预警的权限
|
||||||
@ -144,7 +148,7 @@ class MaterialBaseService:
|
|||||||
func.sum(StockProduct.available_quantity).label('prod_avail')
|
func.sum(StockProduct.available_quantity).label('prod_avail')
|
||||||
).group_by(StockProduct.base_id).subquery()
|
).group_by(StockProduct.base_id).subquery()
|
||||||
|
|
||||||
# 总库存和可用数的 SQL 表达式
|
# 总库存和可用数的 SQL 表达式(用于后续计算)
|
||||||
total_inv = func.coalesce(buy_sub.c.buy_inv, 0) + \
|
total_inv = func.coalesce(buy_sub.c.buy_inv, 0) + \
|
||||||
func.coalesce(semi_sub.c.semi_inv, 0) + \
|
func.coalesce(semi_sub.c.semi_inv, 0) + \
|
||||||
func.coalesce(prod_sub.c.prod_inv, 0)
|
func.coalesce(prod_sub.c.prod_inv, 0)
|
||||||
@ -155,65 +159,83 @@ class MaterialBaseService:
|
|||||||
# 导入预警设置模型
|
# 导入预警设置模型
|
||||||
from app.models.base import MaterialWarningSetting
|
from app.models.base import MaterialWarningSetting
|
||||||
|
|
||||||
# 主查询,关联聚合子查询和预警设置
|
# ============================================================
|
||||||
query = db.session.query(
|
# 【核心修复】使用子查询方案:将聚合结果作为子查询
|
||||||
|
# 这样 total_inv 在外层查询中是一个普通列,可安全参与计算
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
# 内层子查询:基础数据 + 聚合库存
|
||||||
|
inner_sub = db.session.query(
|
||||||
|
MaterialBase.id.label('base_id'),
|
||||||
MaterialBase,
|
MaterialBase,
|
||||||
total_inv.label('total_inv'),
|
total_inv.label('total_inv'),
|
||||||
total_avail.label('total_avail'),
|
total_avail.label('total_avail')
|
||||||
MaterialWarningSetting.is_enabled.label('warning_enabled'),
|
|
||||||
MaterialWarningSetting.yellow_threshold.label('warning_yellow'),
|
|
||||||
MaterialWarningSetting.red_threshold.label('warning_red')
|
|
||||||
).outerjoin(buy_sub, MaterialBase.id == buy_sub.c.base_id) \
|
).outerjoin(buy_sub, MaterialBase.id == buy_sub.c.base_id) \
|
||||||
.outerjoin(semi_sub, MaterialBase.id == semi_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(prod_sub, MaterialBase.id == prod_sub.c.base_id)
|
||||||
.outerjoin(MaterialWarningSetting, MaterialBase.id == MaterialWarningSetting.base_id)
|
|
||||||
|
|
||||||
|
# 关键词精确搜索(需要在子查询层完成)
|
||||||
if filters:
|
if filters:
|
||||||
# 1. 关键词精准搜索(支持指定字段)
|
|
||||||
search_field = filters.get('searchField', 'all')
|
search_field = filters.get('searchField', 'all')
|
||||||
keyword = filters.get('keyword')
|
keyword = filters.get('keyword')
|
||||||
if keyword:
|
if keyword:
|
||||||
kw = f"%{keyword}%"
|
kw = f"%{keyword}%"
|
||||||
if search_field == 'name':
|
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':
|
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':
|
elif search_field == 'spec':
|
||||||
query = query.filter(MaterialBase.spec_model.ilike(kw))
|
inner_sub = inner_sub.filter(MaterialBase.spec_model.ilike(kw))
|
||||||
else: # 'all' 默认全局模糊匹配
|
else:
|
||||||
query = query.filter(or_(
|
inner_sub = inner_sub.filter(or_(
|
||||||
MaterialBase.name.ilike(kw),
|
MaterialBase.name.ilike(kw),
|
||||||
MaterialBase.common_name.ilike(kw),
|
MaterialBase.common_name.ilike(kw),
|
||||||
MaterialBase.spec_model.ilike(kw)
|
MaterialBase.spec_model.ilike(kw)
|
||||||
))
|
))
|
||||||
|
|
||||||
# 2. 精确筛选
|
|
||||||
company = filters.get('company')
|
company = filters.get('company')
|
||||||
if company is not None and 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')
|
category = filters.get('category')
|
||||||
if category is not None and 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')
|
type_val = filters.get('type')
|
||||||
if type_val is not None and type_val != '':
|
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:
|
if filters.get('isEnabled') is not None:
|
||||||
val_str = str(filters['isEnabled']).lower()
|
val_str = str(filters['isEnabled']).lower()
|
||||||
is_active = val_str in ['1', 'true', 'yes', 't']
|
is_active = val_str in ['1', 'true', 'yes', 't']
|
||||||
# 必须使用 filter() 而非 filter_by(),因为 query 是 join 后的复杂查询
|
inner_sub = inner_sub.filter(MaterialBase.is_enabled == is_active)
|
||||||
query = query.filter(MaterialBase.is_enabled == is_active)
|
|
||||||
|
|
||||||
# 【新增】:库存状态筛选 (has_stock)
|
# 库存状态筛选 (has_stock) - 使用子查询列
|
||||||
has_stock = filters.get('has_stock')
|
has_stock = filters.get('has_stock')
|
||||||
if has_stock and str(has_stock).lower() in ['true', '1', 'yes']:
|
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)
|
||||||
|
|
||||||
|
# 将内层子查询具体化
|
||||||
|
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 = []
|
||||||
|
|
||||||
# 3. 高级动态筛选
|
|
||||||
advanced_filters = filters.get('advancedFilters', [])
|
|
||||||
if advanced_filters:
|
if advanced_filters:
|
||||||
allowed_fields = {
|
allowed_fields = {
|
||||||
'companyName': 'company_name',
|
'companyName': 'company_name',
|
||||||
@ -223,10 +245,12 @@ class MaterialBaseService:
|
|||||||
'type': 'material_type',
|
'type': 'material_type',
|
||||||
'spec': 'spec_model',
|
'spec': 'spec_model',
|
||||||
'unit': 'unit',
|
'unit': 'unit',
|
||||||
'inventoryCount': total_inv,
|
'inventoryCount': 'total_inv',
|
||||||
'availableCount': total_avail
|
'availableCount': 'total_avail'
|
||||||
}
|
}
|
||||||
# 字段到权限码的映射
|
# 聚合字段列表(这些需要用 HAVING)
|
||||||
|
aggregate_fields = {'inventoryCount', 'availableCount'}
|
||||||
|
|
||||||
field_permission_map = {
|
field_permission_map = {
|
||||||
'companyName': 'material_list:companyName',
|
'companyName': 'material_list:companyName',
|
||||||
'name': 'material_list:name',
|
'name': 'material_list:name',
|
||||||
@ -238,52 +262,51 @@ class MaterialBaseService:
|
|||||||
'inventoryCount': 'material_list:inventoryCount',
|
'inventoryCount': 'material_list:inventoryCount',
|
||||||
'availableCount': 'material_list:availableCount'
|
'availableCount': 'material_list:availableCount'
|
||||||
}
|
}
|
||||||
filter_conditions = []
|
|
||||||
for condition in advanced_filters:
|
for condition in advanced_filters:
|
||||||
field = condition.get('field')
|
field = condition.get('field')
|
||||||
operator = condition.get('operator')
|
operator = condition.get('operator')
|
||||||
value = condition.get('value')
|
value = condition.get('value')
|
||||||
if not field or not operator or value is None:
|
if not field or not operator or value is None:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
db_field = allowed_fields.get(field)
|
db_field = allowed_fields.get(field)
|
||||||
if not db_field:
|
if not db_field:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# 权限校验
|
# 权限校验
|
||||||
if user_permissions is not None:
|
if user_permissions is not None:
|
||||||
perm_code = field_permission_map.get(field)
|
perm_code = field_permission_map.get(field)
|
||||||
if 'material_list:*' in user_permissions:
|
if 'material_list:*' in user_permissions:
|
||||||
# 超级管理员拥有全部权限
|
|
||||||
pass
|
pass
|
||||||
elif perm_code and perm_code not in user_permissions:
|
elif perm_code and perm_code not in user_permissions:
|
||||||
# 无权限,跳过该条件
|
|
||||||
continue
|
continue
|
||||||
# 对于聚合字段 (inventoryCount, availableCount),需要使用子查询别名
|
|
||||||
if isinstance(db_field, type(total_inv)):
|
# 判断是否为聚合字段
|
||||||
column = db_field
|
is_aggregate = field in aggregate_fields
|
||||||
else:
|
|
||||||
column = getattr(MaterialBase, db_field, None)
|
if is_aggregate:
|
||||||
if column is None:
|
# 聚合字段:使用 HAVING - 通过子查询列进行比较
|
||||||
continue
|
col = inner_sub.c.total_inv if field == 'inventoryCount' else inner_sub.c.total_avail
|
||||||
# 处理操作符
|
|
||||||
# 对于数值型列(聚合字段)只支持 eq, ne, ge, le
|
|
||||||
if isinstance(column, type(total_inv)):
|
|
||||||
# 数值型列
|
|
||||||
try:
|
try:
|
||||||
num_val = float(value)
|
num_val = float(value)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
# 转换失败则跳过该条件
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if operator == 'eq':
|
if operator == 'eq':
|
||||||
filter_conditions.append(column == num_val)
|
having_conditions.append(col == num_val)
|
||||||
elif operator == 'ne':
|
elif operator == 'ne':
|
||||||
filter_conditions.append(column != num_val)
|
having_conditions.append(col != num_val)
|
||||||
elif operator == 'ge':
|
elif operator == 'ge':
|
||||||
filter_conditions.append(column >= num_val)
|
having_conditions.append(col >= num_val)
|
||||||
elif operator == 'le':
|
elif operator == 'le':
|
||||||
filter_conditions.append(column <= num_val)
|
having_conditions.append(col <= num_val)
|
||||||
# 对于 contains 操作符,数值型列不支持,忽略
|
|
||||||
else:
|
else:
|
||||||
# 字符串型列
|
# 非聚合字段:使用 WHERE
|
||||||
|
column = getattr(MaterialBase, db_field, None)
|
||||||
|
if column is None:
|
||||||
|
continue
|
||||||
|
|
||||||
if operator == 'eq':
|
if operator == 'eq':
|
||||||
filter_conditions.append(column == value)
|
filter_conditions.append(column == value)
|
||||||
elif operator == 'ne':
|
elif operator == 'ne':
|
||||||
@ -302,9 +325,15 @@ class MaterialBaseService:
|
|||||||
filter_conditions.append(column <= num_val)
|
filter_conditions.append(column <= num_val)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# 应用 WHERE 条件
|
||||||
if filter_conditions:
|
if filter_conditions:
|
||||||
query = query.filter(and_(*filter_conditions))
|
query = query.filter(and_(*filter_conditions))
|
||||||
|
|
||||||
|
# 应用 HAVING 条件(聚合字段筛选)
|
||||||
|
if having_conditions:
|
||||||
|
query = query.having(and_(*having_conditions))
|
||||||
|
|
||||||
# 排序处理(支持全字段)
|
# 排序处理(支持全字段)
|
||||||
order_by_column = filters.get('orderByColumn', '')
|
order_by_column = filters.get('orderByColumn', '')
|
||||||
is_asc = filters.get('isAsc', None)
|
is_asc = filters.get('isAsc', None)
|
||||||
@ -314,25 +343,31 @@ class MaterialBaseService:
|
|||||||
|
|
||||||
if enable_warning_sort:
|
if enable_warning_sort:
|
||||||
print("====== [DEBUG] 成功进入预警强排逻辑 (SQLA 2.0) ======")
|
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)
|
red_val = cast(MaterialWarningSetting.red_threshold, Numeric)
|
||||||
yellow_val = cast(MaterialWarningSetting.yellow_threshold, Numeric)
|
yellow_val = cast(MaterialWarningSetting.yellow_threshold, Numeric)
|
||||||
inv_val = cast(total_inv, Numeric)
|
|
||||||
|
|
||||||
# 注意:移除了 case 内部的 []
|
# 预警等级计算:红=2, 黄=1, 正常=0
|
||||||
warning_level = case(
|
warning_level = case(
|
||||||
(and_(MaterialWarningSetting.is_enabled.is_(True), inv_val <= red_val), 2),
|
(and_(MaterialWarningSetting.is_enabled.is_(True), inv_val <= red_val), 2),
|
||||||
(and_(MaterialWarningSetting.is_enabled.is_(True), inv_val <= yellow_val), 1),
|
(and_(MaterialWarningSetting.is_enabled.is_(True), inv_val <= yellow_val), 1),
|
||||||
else_=0
|
else_=0
|
||||||
).label('sort_level')
|
).label('sort_level')
|
||||||
|
|
||||||
|
# 红色预警时的缺口(库存与红色阈值的差距)
|
||||||
red_shortage = case(
|
red_shortage = case(
|
||||||
(and_(MaterialWarningSetting.is_enabled.is_(True), inv_val <= red_val), red_val - inv_val),
|
(and_(MaterialWarningSetting.is_enabled.is_(True), inv_val <= red_val), red_val - inv_val),
|
||||||
else_=0
|
else_=0
|
||||||
).label('sort_red')
|
).label('sort_red')
|
||||||
|
|
||||||
|
# 黄色预警时的缺口
|
||||||
yellow_distance = case(
|
yellow_distance = case(
|
||||||
(and_(MaterialWarningSetting.is_enabled.is_(True), inv_val > red_val, inv_val <= yellow_val), inv_val - red_val),
|
(and_(MaterialWarningSetting.is_enabled.is_(True), inv_val > red_val, inv_val <= yellow_val), inv_val - red_val),
|
||||||
else_=999999
|
else_=999999
|
||||||
).label('sort_yellow')
|
).label('sort_yellow')
|
||||||
|
|
||||||
query = query.add_columns(warning_level, red_shortage, yellow_distance)
|
query = query.add_columns(warning_level, red_shortage, yellow_distance)
|
||||||
query = query.order_by(None).order_by(
|
query = query.order_by(None).order_by(
|
||||||
text("sort_level DESC"),
|
text("sort_level DESC"),
|
||||||
@ -341,7 +376,7 @@ class MaterialBaseService:
|
|||||||
MaterialBase.id.desc()
|
MaterialBase.id.desc()
|
||||||
)
|
)
|
||||||
elif order_by_column:
|
elif order_by_column:
|
||||||
# 字段映射
|
# 字段映射 - 使用子查询列进行排序
|
||||||
sort_field_map = {
|
sort_field_map = {
|
||||||
'companyName': MaterialBase.company_name,
|
'companyName': MaterialBase.company_name,
|
||||||
'name': MaterialBase.name,
|
'name': MaterialBase.name,
|
||||||
@ -350,8 +385,8 @@ class MaterialBaseService:
|
|||||||
'type': MaterialBase.material_type,
|
'type': MaterialBase.material_type,
|
||||||
'spec': MaterialBase.spec_model,
|
'spec': MaterialBase.spec_model,
|
||||||
'unit': MaterialBase.unit,
|
'unit': MaterialBase.unit,
|
||||||
'inventoryCount': total_inv,
|
'inventoryCount': inner_sub.c.total_inv,
|
||||||
'availableCount': total_avail
|
'availableCount': inner_sub.c.total_avail
|
||||||
}
|
}
|
||||||
sort_column = sort_field_map.get(order_by_column)
|
sort_column = sort_field_map.get(order_by_column)
|
||||||
if sort_column is not None:
|
if sort_column is not None:
|
||||||
@ -361,7 +396,7 @@ class MaterialBaseService:
|
|||||||
query = query.order_by(sort_column.desc())
|
query = query.order_by(sort_column.desc())
|
||||||
else:
|
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)
|
pagination = query.paginate(page=page, per_page=limit, error_out=False)
|
||||||
|
|||||||
@ -881,8 +881,8 @@ const querySearchType = (queryString: string, cb: any) => {
|
|||||||
|
|
||||||
const getList = () => {
|
const getList = () => {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
// 强制注入预警排序开关(基于权限)
|
// 仅当用户没有进行手动表头排序时才开启预警排序
|
||||||
queryParams.enableWarningSort = userStore.hasPermission('material_list:view_warning');
|
queryParams.enableWarningSort = userStore.hasPermission('material_list:view_warning') && !queryParams.orderByColumn;
|
||||||
// Stringify advancedFilters to JSON string as backend expects
|
// Stringify advancedFilters to JSON string as backend expects
|
||||||
const params = {
|
const params = {
|
||||||
...queryParams,
|
...queryParams,
|
||||||
|
|||||||
@ -376,7 +376,15 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="12">
|
<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>
|
<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>
|
<template #prefix><span class="prefix-tag sn">SN</span></template>
|
||||||
</el-input>
|
</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>
|
<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>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 智能扫码弹窗 -->
|
||||||
|
<SmartScannerDialog v-model="scannerDialogVisible" @confirm="handleScannerConfirm" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -672,6 +683,7 @@ import {getLabelPreview, executePrint} from '@/api/common/print'
|
|||||||
import { getWarehouseTree } from '@/api/common/warehouse'
|
import { getWarehouseTree } from '@/api/common/warehouse'
|
||||||
import WebRtcCamera from '@/components/Camera/WebRtcCamera.vue'
|
import WebRtcCamera from '@/components/Camera/WebRtcCamera.vue'
|
||||||
import WarehouseSelector from '@/components/WarehouseSelector.vue'
|
import WarehouseSelector from '@/components/WarehouseSelector.vue'
|
||||||
|
import SmartScannerDialog from '@/components/SmartScannerDialog.vue'
|
||||||
import { useUserStore } from '@/stores/user'
|
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 currentCameraField = ref<'arrival_photo' | 'inspection_report'>('arrival_photo')
|
||||||
const inspection_report_url = ref('')
|
const inspection_report_url = ref('')
|
||||||
|
|
||||||
|
// 智能扫码弹窗
|
||||||
|
const scannerDialogVisible = ref(false)
|
||||||
|
|
||||||
// 库位级联选择器数据
|
// 库位级联选择器数据
|
||||||
const warehouseOptions = ref<any[]>([])
|
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 = () => {
|
const addCondition = () => {
|
||||||
advancedConditions.value.push({ field: '', operator: '', value: '' })
|
advancedConditions.value.push({ field: '', operator: '', value: '' })
|
||||||
}
|
}
|
||||||
|
|||||||
@ -320,7 +320,15 @@
|
|||||||
<div class="identity-panel">
|
<div class="identity-panel">
|
||||||
<el-row :gutter="24">
|
<el-row :gutter="24">
|
||||||
<el-col :span="12">
|
<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-input v-model="form.serial_number" placeholder="必填: 唯一序列号" clearable><template #prefix><span class="prefix-tag sn">SN</span></template></el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
@ -518,6 +526,8 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 智能扫码弹窗 -->
|
||||||
|
<SmartScannerDialog v-model="scannerDialogVisible" @confirm="handleScannerConfirm" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -540,6 +550,7 @@ import {
|
|||||||
import { uploadFile, deleteFile } from '@/api/inbound/buy'
|
import { uploadFile, deleteFile } from '@/api/inbound/buy'
|
||||||
import WebRtcCamera from '@/components/Camera/WebRtcCamera.vue'
|
import WebRtcCamera from '@/components/Camera/WebRtcCamera.vue'
|
||||||
import WarehouseSelector from '@/components/WarehouseSelector.vue'
|
import WarehouseSelector from '@/components/WarehouseSelector.vue'
|
||||||
|
import SmartScannerDialog from '@/components/SmartScannerDialog.vue'
|
||||||
import { getLabelPreview, executePrint } from '@/api/common/print'
|
import { getLabelPreview, executePrint } from '@/api/common/print'
|
||||||
import { getWarehouseTree } from '@/api/common/warehouse'
|
import { getWarehouseTree } from '@/api/common/warehouse'
|
||||||
import { useUserStore } from '@/stores/user'
|
import { useUserStore } from '@/stores/user'
|
||||||
@ -668,6 +679,9 @@ const currentCameraField = ref<'product_photo' | 'quality_report_link' | 'inspec
|
|||||||
const quality_url = ref('')
|
const quality_url = ref('')
|
||||||
const inspection_url = ref('')
|
const inspection_url = ref('')
|
||||||
|
|
||||||
|
// 智能扫码弹窗
|
||||||
|
const scannerDialogVisible = ref(false)
|
||||||
|
|
||||||
// 库位级联选择器数据
|
// 库位级联选择器数据
|
||||||
const warehouseOptions = ref<any[]>([])
|
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 () => {
|
const submitForm = async () => {
|
||||||
await formRef.value.validate(async (valid: boolean) => {
|
await formRef.value.validate(async (valid: boolean) => {
|
||||||
if(valid) {
|
if(valid) {
|
||||||
|
|||||||
@ -395,7 +395,15 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="12">
|
<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>
|
<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>
|
<template #prefix><span class="prefix-tag sn">SN</span></template>
|
||||||
</el-input>
|
</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>
|
<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>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 智能扫码弹窗 -->
|
||||||
|
<SmartScannerDialog v-model="scannerDialogVisible" @confirm="handleScannerConfirm" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -604,6 +615,7 @@ import {
|
|||||||
import { uploadFile, deleteFile } from '@/api/inbound/buy'
|
import { uploadFile, deleteFile } from '@/api/inbound/buy'
|
||||||
import WebRtcCamera from '@/components/Camera/WebRtcCamera.vue'
|
import WebRtcCamera from '@/components/Camera/WebRtcCamera.vue'
|
||||||
import WarehouseSelector from '@/components/WarehouseSelector.vue'
|
import WarehouseSelector from '@/components/WarehouseSelector.vue'
|
||||||
|
import SmartScannerDialog from '@/components/SmartScannerDialog.vue'
|
||||||
import {getLabelPreview, executePrint} from '@/api/common/print'
|
import {getLabelPreview, executePrint} from '@/api/common/print'
|
||||||
import { getWarehouseTree } from '@/api/common/warehouse'
|
import { getWarehouseTree } from '@/api/common/warehouse'
|
||||||
import { useUserStore } from '@/stores/user'
|
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 currentCameraField = ref<'arrival_photo' | 'quality_report_link'>('arrival_photo')
|
||||||
const quality_report_url = ref('')
|
const quality_report_url = ref('')
|
||||||
|
|
||||||
|
// 智能扫码弹窗
|
||||||
|
const scannerDialogVisible = ref(false)
|
||||||
|
|
||||||
// 库位级联选择器数据
|
// 库位级联选择器数据
|
||||||
const warehouseOptions = ref<any[]>([])
|
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 () => {
|
const submitForm = async () => {
|
||||||
if (!formRef.value) return
|
if (!formRef.value) return
|
||||||
await formRef.value.validate(async (valid: boolean) => {
|
await formRef.value.validate(async (valid: boolean) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user