perf: replace client-side pagination with server-side pagination in stock selection dialog and fix duplicate variable in semi.vue
This commit is contained in:
@ -1,6 +1,5 @@
|
|||||||
from flask import Blueprint, jsonify, request, send_file
|
from flask import Blueprint, jsonify, request, send_file, current_app
|
||||||
from app.extensions import db, beijing_time
|
from app.extensions import db, beijing_time
|
||||||
# ★★★ 修复点:必须引入 datetime,否则下方更新时间时会报错 500 ★★★
|
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from flask_jwt_extended import jwt_required
|
from flask_jwt_extended import jwt_required
|
||||||
from app.utils.decorators import permission_required
|
from app.utils.decorators import permission_required
|
||||||
@ -40,18 +39,6 @@ def _normalize_user_id(user_id):
|
|||||||
return user_id.strip()
|
return user_id.strip()
|
||||||
|
|
||||||
|
|
||||||
# 尝试导入半成品和成品
|
|
||||||
try:
|
|
||||||
from app.models.inbound.semi import StockSemi
|
|
||||||
except ImportError:
|
|
||||||
StockSemi = None
|
|
||||||
|
|
||||||
try:
|
|
||||||
from app.models.inbound.product import StockProduct
|
|
||||||
except ImportError:
|
|
||||||
StockProduct = None
|
|
||||||
|
|
||||||
|
|
||||||
def get_stock_model(source_table):
|
def get_stock_model(source_table):
|
||||||
"""根据source_table获取对应的库存模型"""
|
"""根据source_table获取对应的库存模型"""
|
||||||
if source_table == 'stock_buy':
|
if source_table == 'stock_buy':
|
||||||
@ -151,6 +138,123 @@ def get_all_stock():
|
|||||||
return jsonify({"message": f"查询库存失败: {str(e)}"}), 500
|
return jsonify({"message": f"查询库存失败: {str(e)}"}), 500
|
||||||
|
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# 分页库存查询接口(服务端分页,出库/盘点/借用模块共用)
|
||||||
|
# ==============================================================================
|
||||||
|
@bp.route('/list', methods=['GET'])
|
||||||
|
@jwt_required()
|
||||||
|
def get_stock_list():
|
||||||
|
"""
|
||||||
|
分页获取库存列表(stock_quantity > 0)
|
||||||
|
参数:
|
||||||
|
page - 页码(默认 1)
|
||||||
|
pageSize - 每页条数(默认 20)
|
||||||
|
keyword - 搜索关键字(模糊匹配名称/规格/SKU)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
page = request.args.get('page', 1, type=int)
|
||||||
|
pageSize = request.args.get('pageSize', 20, type=int)
|
||||||
|
keyword = request.args.get('keyword', '', type=str).strip()
|
||||||
|
|
||||||
|
if page < 1:
|
||||||
|
page = 1
|
||||||
|
if pageSize < 1 or pageSize > 200:
|
||||||
|
pageSize = 20
|
||||||
|
|
||||||
|
all_items = []
|
||||||
|
|
||||||
|
# 1. 采购件
|
||||||
|
if StockBuy:
|
||||||
|
q = StockBuy.query.filter(StockBuy.stock_quantity > 0)
|
||||||
|
if keyword:
|
||||||
|
q = q.filter(
|
||||||
|
db.or_(
|
||||||
|
StockBuy.material_name.ilike(f'%{keyword}%'),
|
||||||
|
StockBuy.spec_model.ilike(f'%{keyword}%'),
|
||||||
|
StockBuy.sku.ilike(f'%{keyword}%')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
rows = q.all()
|
||||||
|
for item in rows:
|
||||||
|
d = item.to_dict()
|
||||||
|
d['stock_type'] = 'material'
|
||||||
|
d['type'] = 'material'
|
||||||
|
d['typeLabel'] = '采购件'
|
||||||
|
d['name'] = d.get('material_name', d.get('name', ''))
|
||||||
|
d['standard'] = d.get('spec_model', d.get('standard', ''))
|
||||||
|
d['available_quantity'] = d.get('qty_available', d.get('available_quantity', 0))
|
||||||
|
all_items.append(d)
|
||||||
|
|
||||||
|
# 2. 半成品
|
||||||
|
if StockSemi:
|
||||||
|
try:
|
||||||
|
q = StockSemi.query.filter(StockSemi.stock_quantity > 0)
|
||||||
|
if keyword:
|
||||||
|
q = q.filter(
|
||||||
|
db.or_(
|
||||||
|
StockSemi.material_name.ilike(f'%{keyword}%'),
|
||||||
|
StockSemi.spec_model.ilike(f'%{keyword}%'),
|
||||||
|
StockSemi.sku.ilike(f'%{keyword}%')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
rows = q.all()
|
||||||
|
for item in rows:
|
||||||
|
d = item.to_dict()
|
||||||
|
d['stock_type'] = 'semi'
|
||||||
|
d['type'] = 'semi'
|
||||||
|
d['typeLabel'] = '半成品'
|
||||||
|
d['name'] = d.get('material_name', d.get('name', ''))
|
||||||
|
d['standard'] = d.get('spec_model', d.get('standard', ''))
|
||||||
|
d['available_quantity'] = d.get('qty_available', d.get('available_quantity', 0))
|
||||||
|
all_items.append(d)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 3. 成品
|
||||||
|
if StockProduct:
|
||||||
|
try:
|
||||||
|
q = StockProduct.query.filter(StockProduct.stock_quantity > 0)
|
||||||
|
if keyword:
|
||||||
|
q = q.filter(
|
||||||
|
db.or_(
|
||||||
|
StockProduct.product_name.ilike(f'%{keyword}%'),
|
||||||
|
StockProduct.spec_model.ilike(f'%{keyword}%'),
|
||||||
|
StockProduct.sku.ilike(f'%{keyword}%')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
rows = q.all()
|
||||||
|
for item in rows:
|
||||||
|
d = item.to_dict()
|
||||||
|
d['stock_type'] = 'product'
|
||||||
|
d['type'] = 'product'
|
||||||
|
d['typeLabel'] = '成品'
|
||||||
|
d['name'] = d.get('product_name', d.get('name', ''))
|
||||||
|
d['standard'] = d.get('spec_model', d.get('standard', ''))
|
||||||
|
d['available_quantity'] = d.get('qty_available', d.get('available_quantity', 0))
|
||||||
|
all_items.append(d)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
total = len(all_items)
|
||||||
|
start = (page - 1) * pageSize
|
||||||
|
end = start + pageSize
|
||||||
|
paged = all_items[start:end]
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'msg': '获取成功',
|
||||||
|
'data': {
|
||||||
|
'list': paged,
|
||||||
|
'total': total,
|
||||||
|
'page': page,
|
||||||
|
'pageSize': pageSize
|
||||||
|
}
|
||||||
|
}), 200
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
current_app.logger.error(f"Get Stock List Failed: {str(e)}")
|
||||||
|
return jsonify({'msg': f'获取库存列表失败: {str(e)}'}), 500
|
||||||
|
|
||||||
|
|
||||||
# --- 草稿箱接口 ---
|
# --- 草稿箱接口 ---
|
||||||
|
|
||||||
@bp.route('/draft/list', methods=['GET'])
|
@bp.route('/draft/list', methods=['GET'])
|
||||||
|
|||||||
@ -10,6 +10,15 @@ export function getAllStock() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 分页库存列表(服务端分页,支持关键字搜索)
|
||||||
|
export function getStockList(params: { page?: number; pageSize?: number; keyword?: string }) {
|
||||||
|
return request({
|
||||||
|
url: '/v1/inbound/stock/list',
|
||||||
|
method: 'get',
|
||||||
|
params
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// 打印出库选单
|
// 打印出库选单
|
||||||
// 修改后: 去掉开头的 /api
|
// 修改后: 去掉开头的 /api
|
||||||
export function printSelectionList(items: any[]) {
|
export function printSelectionList(items: any[]) {
|
||||||
|
|||||||
@ -106,7 +106,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<el-table
|
<el-table
|
||||||
ref="manualTableRef"
|
ref="manualTableRef"
|
||||||
:data="paginatedStockData"
|
:data="stockList"
|
||||||
|
v-loading="stockLoading"
|
||||||
height="500"
|
height="500"
|
||||||
border
|
border
|
||||||
row-key="uniqueKey"
|
row-key="uniqueKey"
|
||||||
@ -145,12 +146,12 @@
|
|||||||
style="margin-top: 12px; justify-content: flex-end; display: flex;"
|
style="margin-top: 12px; justify-content: flex-end; display: flex;"
|
||||||
v-model:current-page="stockPage"
|
v-model:current-page="stockPage"
|
||||||
v-model:page-size="stockPageSize"
|
v-model:page-size="stockPageSize"
|
||||||
:total="filteredStockData.length"
|
:total="stockTotal"
|
||||||
:page-sizes="[20, 50, 100, 200]"
|
:page-sizes="[20, 50, 100, 200]"
|
||||||
layout="total, prev, pager, next"
|
layout="total, prev, pager, next"
|
||||||
background
|
background
|
||||||
@size-change="() => { stockPage = 1 }"
|
@size-change="handleStockSizeChange"
|
||||||
@current-change="() => {}"
|
@current-change="handleStockPageChange"
|
||||||
/>
|
/>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<span style="float: left; line-height: 32px; color: #909399;">
|
<span style="float: left; line-height: 32px; color: #909399;">
|
||||||
@ -331,7 +332,7 @@
|
|||||||
import { ref, computed, watch } from 'vue'
|
import { ref, computed, watch } from 'vue'
|
||||||
import { Printer, Search, Plus, Download, List } from '@element-plus/icons-vue'
|
import { Printer, Search, Plus, Download, List } from '@element-plus/icons-vue'
|
||||||
import { ElMessage, ElTable, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElTable, ElMessageBox } from 'element-plus'
|
||||||
import { getAllStock, printSelectionList } from '@/api/inbound/stock'
|
import { getAllStock, getStockList, printSelectionList } from '@/api/inbound/stock'
|
||||||
import { getBomList, getBomDetail } from '@/api/bom'
|
import { getBomList, getBomDetail } from '@/api/bom'
|
||||||
import { useUserStore } from '@/stores/user'
|
import { useUserStore } from '@/stores/user'
|
||||||
|
|
||||||
@ -346,11 +347,14 @@ const exportLoading = ref(false)
|
|||||||
const printLoading = ref(false)
|
const printLoading = ref(false)
|
||||||
|
|
||||||
const allStockData = ref<any[]>([])
|
const allStockData = ref<any[]>([])
|
||||||
const filteredStockData = ref<any[]>([])
|
const stockList = ref<any[]>([]) // 服务端分页数据
|
||||||
const searchKeyword = ref('')
|
const stockTotal = ref(0)
|
||||||
const tempSelection = ref<any[]>([])
|
|
||||||
const stockPage = ref(1)
|
const stockPage = ref(1)
|
||||||
const stockPageSize = ref(20)
|
const stockPageSize = ref(20)
|
||||||
|
const stockLoading = ref(false)
|
||||||
|
const searchKeyword = ref('')
|
||||||
|
const tempSelection = ref<any[]>([])
|
||||||
|
let stockSearchTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
|
|
||||||
// 表格引用
|
// 表格引用
|
||||||
const manualTableRef = ref<InstanceType<typeof ElTable>>()
|
const manualTableRef = ref<InstanceType<typeof ElTable>>()
|
||||||
@ -427,12 +431,6 @@ const shortageList = computed(() => {
|
|||||||
|
|
||||||
const hasShortage = computed(() => shortageList.value.length > 0 && bomSets.value > maxBuildableSets.value)
|
const hasShortage = computed(() => shortageList.value.length > 0 && bomSets.value > maxBuildableSets.value)
|
||||||
|
|
||||||
// ★ 出库选单分页数据(固定 height="500" 时仍需分页减轻渲染压力)
|
|
||||||
const paginatedStockData = computed(() => {
|
|
||||||
const start = (stockPage.value - 1) * stockPageSize.value
|
|
||||||
return filteredStockData.value.slice(start, start + stockPageSize.value)
|
|
||||||
})
|
|
||||||
|
|
||||||
// --- 辅助方法 ---
|
// --- 辅助方法 ---
|
||||||
const getTypeTag = (type: string) => {
|
const getTypeTag = (type: string) => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
@ -445,52 +443,76 @@ const getTypeTag = (type: string) => {
|
|||||||
|
|
||||||
// --- 核心逻辑 1:手动添加库存 ---
|
// --- 核心逻辑 1:手动添加库存 ---
|
||||||
|
|
||||||
|
// 服务端加载库存列表
|
||||||
|
const loadStockList = async () => {
|
||||||
|
stockLoading.value = true
|
||||||
|
try {
|
||||||
|
const res: any = await getStockList({
|
||||||
|
page: stockPage.value,
|
||||||
|
pageSize: stockPageSize.value,
|
||||||
|
keyword: searchKeyword.value.trim()
|
||||||
|
})
|
||||||
|
stockList.value = res.data?.list || []
|
||||||
|
stockTotal.value = res.data?.total || 0
|
||||||
|
} catch (e) {
|
||||||
|
ElMessage.error('加载库存列表失败')
|
||||||
|
} finally {
|
||||||
|
stockLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 手动选库存弹窗:加载服务端分页数据 + BOM 用全量数据
|
||||||
const openManualSelect = async () => {
|
const openManualSelect = async () => {
|
||||||
manualDialogVisible.value = true
|
manualDialogVisible.value = true
|
||||||
stockPage.value = 1
|
stockPage.value = 1
|
||||||
|
searchKeyword.value = ''
|
||||||
|
await loadStockList()
|
||||||
|
|
||||||
|
// 仅在 BOM 关联查询需要时加载全量(一次性缓存)
|
||||||
if (allStockData.value.length === 0) {
|
if (allStockData.value.length === 0) {
|
||||||
try {
|
try {
|
||||||
const res: any = await getAllStock()
|
const res: any = await getAllStock()
|
||||||
const rawMaterials = (res.materials || []).map((i: any) => ({ ...i, type: 'material', typeLabel: '采购件' }))
|
const rawMaterials = (res.materials || []).map((i: any) => ({ ...i, type: 'material', typeLabel: '采购件' }))
|
||||||
const rawSemis = (res.semis || []).map((i: any) => ({ ...i, type: 'semi', typeLabel: '半成品' }))
|
const rawSemis = (res.semis || []).map((i: any) => ({ ...i, type: 'semi', typeLabel: '半成品' }))
|
||||||
const rawProducts = (res.products || []).map((i: any) => ({ ...i, type: 'product', typeLabel: '成品' }))
|
const rawProducts = (res.products || []).map((i: any) => ({ ...i, type: 'product', typeLabel: '成品' }))
|
||||||
|
|
||||||
const list = [...rawMaterials, ...rawSemis, ...rawProducts]
|
const list = [...rawMaterials, ...rawSemis, ...rawProducts]
|
||||||
|
|
||||||
allStockData.value = list.map((i: any) => ({
|
allStockData.value = list.map((i: any) => ({
|
||||||
...i,
|
...i,
|
||||||
name: i.name || i.material_name || i.product_name || '未知名称',
|
name: i.name || i.material_name || i.product_name || '未知名称',
|
||||||
standard: i.standard || i.spec_model || '',
|
standard: i.standard || i.spec_model || '',
|
||||||
// ★ 确保读取库位字段,如果没有则为空
|
|
||||||
warehouse_location: i.warehouse_location || (i.warehouse_loc) || '',
|
warehouse_location: i.warehouse_location || (i.warehouse_loc) || '',
|
||||||
uniqueKey: `${i.type}_${i.id}`,
|
uniqueKey: `${i.type}_${i.id}`,
|
||||||
available_quantity: parseFloat(i.available_quantity) || 0,
|
available_quantity: parseFloat(i.available_quantity) || 0,
|
||||||
export_quantity: 1
|
export_quantity: 1
|
||||||
}))
|
}))
|
||||||
|
|
||||||
filteredStockData.value = allStockData.value
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
ElMessage.error('加载库存数据失败')
|
ElMessage.error('加载全量库存数据失败(BOM 功能可能受影响)')
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
searchKeyword.value = ''
|
|
||||||
allStockData.value.forEach(item => item.export_quantity = 0)
|
allStockData.value.forEach(item => item.export_quantity = 0)
|
||||||
filteredStockData.value = allStockData.value
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 搜索框防抖触发服务端过滤
|
||||||
const filterStock = () => {
|
const filterStock = () => {
|
||||||
|
if (stockSearchTimer) clearTimeout(stockSearchTimer)
|
||||||
|
stockSearchTimer = setTimeout(() => {
|
||||||
stockPage.value = 1
|
stockPage.value = 1
|
||||||
const kw = searchKeyword.value.trim().toLowerCase()
|
loadStockList()
|
||||||
if (!kw) {
|
}, 350)
|
||||||
filteredStockData.value = allStockData.value
|
}
|
||||||
return
|
|
||||||
|
// 分页切换
|
||||||
|
const handleStockPageChange = (page: number) => {
|
||||||
|
stockPage.value = page
|
||||||
|
loadStockList()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleStockSizeChange = (size: number) => {
|
||||||
|
stockPageSize.value = size
|
||||||
|
stockPage.value = 1
|
||||||
|
loadStockList()
|
||||||
}
|
}
|
||||||
filteredStockData.value = allStockData.value.filter(i =>
|
|
||||||
(i.name && i.name.toLowerCase().includes(kw)) ||
|
|
||||||
(i.standard && i.standard.toLowerCase().includes(kw))
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleStockSelection = (val: any[]) => { tempSelection.value = val }
|
const handleStockSelection = (val: any[]) => { tempSelection.value = val }
|
||||||
|
|||||||
Reference in New Issue
Block a user