perf: 系统级性能优化与并发安全修复
## 并发安全修复 (4处) - scrap.py: 报废执行添加 SELECT FOR UPDATE 悲观锁,消除 TOCTOU 竞态 - stock.py (adjust_stock): 盘点调整添加 for_update=True 行锁 - outbound_service.py: 低库存预警 SMTP 调用移到 commit 之后,避免长事务 - trans_service.py: execute_dispatch 按 (source_table, id) 排序 items,消除死锁风险 ## N+1 查询优化 (2处) - inventory_task.py: _prefetch_inventory_map 单条 UNION ALL+GROUP BY 替代循环内逐条查询(N*4次→2次) - stock.py (export_stocktake): get_borrowed_qty 批量 GROUP BY 替代逐条 TransBorrow 查询(~18000次→1次) ## BOM 列表性能重构 - bom_service.py: get_bom_list 单条 GROUP BY+string_agg+分页,消除 N+1 循环查询 - bom_service.py: 新增 get_bom_summary (轻量 GROUP BY category+COUNT) - bom.py: 新增 /api/v1/bom/summary 路由,/list 支持 category 过滤 ## Odoo 基础信息懒加载 - base_service.py: 新增 get_odoo_summary (GROUP BY category+COUNT) - base.py: 新增 /api/v1/inbound/base/odoo-summary 路由 - buyOdoo.vue: 懒加载分组架构 (fetchOdooSummary + loadGroupItems) - material_base.ts: 新增 getOdooSummary API ## 前端 Bug 修复 - BomManage.vue: 懒加载分组 (fetchBomSummary + loadGroupItems + collapse) - BomManage.vue: 适配新 API 格式 (res.data.items 替代 res.data) - buyOdoo.vue: 移除 "点击展开加载" 文字 - Selection.vue + borrow/apply/index.vue: openBomSelect 适配新 API 格式
This commit is contained in:
@ -9,6 +9,15 @@ export function getBomList(params?: any) {
|
||||
})
|
||||
}
|
||||
|
||||
// 获取BOM分组摘要(懒加载用,轻量 GROUP BY category + COUNT)
|
||||
export function getBomSummary(params?: { keyword?: string }) {
|
||||
return request({
|
||||
url: '/v1/bom/summary',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
// 获取BOM详情(含库存信息)
|
||||
export function getBomWithStock(bomNo: string) {
|
||||
const trimmed = bomNo.replace(/^\/+|\/+$/g, '');
|
||||
|
||||
@ -94,4 +94,13 @@ export function getMaterialUnitsAPI() {
|
||||
url: '/inbound/base/units',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 10. Odoo 分组摘要(轻量 GROUP BY category + COUNT,不 JOIN 库存表)
|
||||
export function getOdooSummary(params?: { keyword?: string; isEnabled?: boolean }) {
|
||||
return request({
|
||||
url: '/inbound/base/odoo-summary',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
@ -17,30 +17,30 @@
|
||||
<el-button :icon="Search" @click="handleSearch" />
|
||||
</template>
|
||||
</el-input>
|
||||
<el-button @click="activeCategories = bomGroups.map((g: any) => g.category)" size="small" style="margin-right: 6px;">全部展开</el-button>
|
||||
<el-button @click="activeCategories = []" size="small" style="margin-right: 10px;">全部折叠</el-button>
|
||||
<el-button @click="expandAllGroups" size="small" style="margin-right: 6px;">全部展开</el-button>
|
||||
<el-button @click="collapseAllGroups" size="small" style="margin-right: 10px;">全部折叠</el-button>
|
||||
<el-button v-if="userStore.hasPermission('bom_manage:operation')" type="primary" :icon="Plus" @click="handleCreate">新建 BOM</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-skeleton :rows="8" animated v-if="loading && bomGroups.length === 0" />
|
||||
<el-empty v-else-if="!loading && bomGroups.length === 0" description="暂无 BOM 数据" />
|
||||
<el-collapse v-else v-model="activeCategories" class="bom-category-collapse">
|
||||
<el-skeleton :rows="8" animated v-if="loading && groupSummary.length === 0" />
|
||||
<el-empty v-else-if="!loading && groupSummary.length === 0" description="暂无 BOM 数据" />
|
||||
<el-collapse v-else v-model="activeCategories" class="bom-category-collapse" @change="handleCollapseChange">
|
||||
<el-collapse-item
|
||||
v-for="group in bomGroups"
|
||||
v-for="group in groupedData"
|
||||
:key="group.category"
|
||||
:title="group.category + ' (' + group.count + ')'"
|
||||
:name="group.category"
|
||||
>
|
||||
<el-table v-if="activeCategories.includes(group.category)" :data="group.items" border style="width: 100%">
|
||||
<el-table-column v-if="hasColumnPermission('bom_no')" prop="bom_no" label="BOM编号" min-width="180" sortable>
|
||||
<el-table-column v-if="hasColumnPermission('bom_no')" prop="bom_no" label="BOM编号" min-width="180" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<span style="cursor: pointer; color: #409EFF;" @click="handleView(row)">{{ row.bom_no }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="hasColumnPermission('parent_name')" prop="parent_name" label="父件名称" min-width="150" show-overflow-tooltip />
|
||||
<el-table-column v-if="hasColumnPermission('parent_spec')" prop="parent_spec" label="父件规格" min-width="150" show-overflow-tooltip />
|
||||
<el-table-column v-if="hasColumnPermission('parent_name')" prop="parent_name" label="父件名称" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column v-if="hasColumnPermission('parent_spec')" prop="parent_spec" label="父件规格" min-width="180" show-overflow-tooltip />
|
||||
<el-table-column v-if="hasColumnPermission('version')" label="版本" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag>{{ row.version }}</el-tag>
|
||||
@ -52,13 +52,16 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="hasColumnPermission('child_count')" prop="child_count" label="子件数" width="80" align="center" />
|
||||
<el-table-column v-if="userStore.hasPermission('bom_manage:operation')" label="操作" width="200" align="center" fixed="right">
|
||||
<el-table-column v-if="userStore.hasPermission('bom_manage:operation')" label="操作" width="160" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="success" link @click="handleSaveAs(row)">另存为</el-button>
|
||||
<el-button type="danger" link @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div v-if="groupLoadingMap.get(group.category)" style="text-align:center;padding:12px;">
|
||||
<el-icon class="is-loading"><Loading /></el-icon> 加载中...
|
||||
</div>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
</el-card>
|
||||
@ -254,7 +257,7 @@ import { useRoute } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox, FormInstance, FormRules } from 'element-plus'
|
||||
import { Plus, Search, EditPen } from '@element-plus/icons-vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { getBomList, getBomDetail, saveBom, deleteBom, getDraftDetail, saveDraft, publishDraft } from '@/api/bom'
|
||||
import { getBomList, getBomSummary, getBomDetail, saveBom, deleteBom, getDraftDetail, saveDraft, publishDraft } from '@/api/bom'
|
||||
import { searchMaterialBase } from '@/api/inbound/buy'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
|
||||
@ -314,11 +317,27 @@ let originalChildren: ChildRow[] = []
|
||||
let pendingDraftBomNo = ''
|
||||
let pendingDraftVersion = ''
|
||||
|
||||
const bomGroups = ref([]) // 分组结构: [{category, count, items[]}]
|
||||
const activeCategories = ref([]) // 默认全部展开
|
||||
const searchKeyword = ref('')
|
||||
const childSearchKeyword = ref('')
|
||||
|
||||
// ★ 懒加载分组架构
|
||||
interface GroupSummary { category: string; count: number }
|
||||
const groupSummary = ref<GroupSummary[]>([])
|
||||
const groupCache = ref<Map<string, any[]>>(new Map())
|
||||
const groupLoadingMap = ref<Map<string, boolean>>(new Map())
|
||||
const activeCategories = ref<string[]>([])
|
||||
const lastKeyword = ref('')
|
||||
|
||||
const groupedData = computed(() => {
|
||||
if (!groupSummary.value.length) return []
|
||||
return groupSummary.value.map(s => ({
|
||||
category: s.category,
|
||||
count: s.count,
|
||||
items: groupCache.value.get(s.category) ?? [],
|
||||
loaded: groupCache.value.has(s.category)
|
||||
}))
|
||||
})
|
||||
|
||||
const filteredChildren = computed(() => {
|
||||
if (!childSearchKeyword.value) return form.children
|
||||
const kw = childSearchKeyword.value.toLowerCase()
|
||||
@ -330,11 +349,10 @@ const filteredChildren = computed(() => {
|
||||
})
|
||||
|
||||
// 自动搜索:输入后 500ms 防抖触发搜索(无需回车)
|
||||
watch(searchKeyword, (val) => {
|
||||
// 防抖:延迟 500ms 执行,避免频繁请求
|
||||
watch(searchKeyword, () => {
|
||||
clearTimeout((window as any)._bomSearchTimer)
|
||||
;(window as any)._bomSearchTimer = setTimeout(() => {
|
||||
fetchBomList()
|
||||
fetchBomSummary()
|
||||
}, 500)
|
||||
})
|
||||
|
||||
@ -465,7 +483,8 @@ const pureBomNo = computed(() => form.bom_no)
|
||||
|
||||
const versionOptions = computed(() => {
|
||||
const ver = originalVersion || 'V1.0'
|
||||
const allItems = bomGroups.value.flatMap((g: any) => g.items)
|
||||
const allItems: any[] = []
|
||||
groupCache.value.forEach((items) => allItems.push(...items))
|
||||
const occupiedVersions = new Set(
|
||||
allItems.filter((item: any) => item.bom_no === currentBomNo).map((item: any) => item.version)
|
||||
)
|
||||
@ -500,18 +519,61 @@ const rules = reactive<FormRules>({
|
||||
const dialogTitle = ref('新建 BOM')
|
||||
|
||||
const handleSearch = () => {
|
||||
activeCategories.value = [] // 用户主动搜索时重置折叠状态
|
||||
fetchBomList()
|
||||
activeCategories.value = []
|
||||
groupCache.value = new Map()
|
||||
fetchBomSummary()
|
||||
}
|
||||
|
||||
const fetchBomList = async () => {
|
||||
// ★ 挂载+搜索时调用 — 获取分组摘要
|
||||
const fetchBomSummary = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getBomList({ keyword: searchKeyword.value })
|
||||
if (res.code === 200) {
|
||||
bomGroups.value = res.data
|
||||
const params: any = {}
|
||||
if (searchKeyword.value) params.keyword = searchKeyword.value
|
||||
const res: any = await getBomSummary(params)
|
||||
if (res?.code === 200) {
|
||||
groupSummary.value = res.data ?? []
|
||||
if (searchKeyword.value !== lastKeyword.value) {
|
||||
activeCategories.value = []
|
||||
lastKeyword.value = searchKeyword.value
|
||||
}
|
||||
}
|
||||
} finally { loading.value = false }
|
||||
} catch (e) { console.error('获取BOM摘要失败', e) }
|
||||
finally { loading.value = false }
|
||||
}
|
||||
|
||||
// ★ 默认全部展开(用户点击按钮时)
|
||||
const expandAllGroups = () => {
|
||||
activeCategories.value = groupSummary.value.map(g => g.category)
|
||||
groupSummary.value.forEach(g => loadGroupItems(g.category))
|
||||
}
|
||||
const collapseAllGroups = () => { activeCategories.value = [] }
|
||||
|
||||
// ★ 展开分组时懒加载该分类下的 BOM
|
||||
const loadGroupItems = async (category: string) => {
|
||||
if (groupCache.value.has(category)) return
|
||||
if (groupLoadingMap.value.get(category)) return
|
||||
|
||||
groupLoadingMap.value.set(category, true)
|
||||
try {
|
||||
const params: any = {
|
||||
category: category,
|
||||
page: 1,
|
||||
pageSize: 9999 // 单分类内全量加载(分类内数据量可控)
|
||||
}
|
||||
if (searchKeyword.value) params.keyword = searchKeyword.value
|
||||
const res: any = await getBomList(params)
|
||||
if (res?.code === 200) {
|
||||
groupCache.value.set(category, res.data?.items ?? [])
|
||||
}
|
||||
} catch (e) { console.error(`加载BOM分类 [${category}] 失败`, e) }
|
||||
finally { groupLoadingMap.value.set(category, false) }
|
||||
}
|
||||
|
||||
// ★ collapse @change 事件 → 触发懒加载
|
||||
const handleCollapseChange = (val: string | string[]) => {
|
||||
const cats = Array.isArray(val) ? val : (val ? [val] : [])
|
||||
cats.forEach(c => loadGroupItems(c))
|
||||
}
|
||||
|
||||
const onParentChange = (val: number) => {}
|
||||
@ -790,7 +852,7 @@ const handleDelete = (row: BomItem) => {
|
||||
.then(async () => {
|
||||
try {
|
||||
const res = await deleteBom(row.bom_no, row.version)
|
||||
if (res.code === 200) { ElMessage.success('删除成功'); fetchBomList() }
|
||||
if (res.code === 200) { ElMessage.success('删除成功'); groupCache.value = new Map(); fetchBomSummary() }
|
||||
} catch (e) {}
|
||||
})
|
||||
.catch(() => {})
|
||||
@ -872,7 +934,8 @@ const submitForm = async () => {
|
||||
localStorage.removeItem('pending_bom_draft_version')
|
||||
originalDraftHash.value = ''
|
||||
dialogVisible.value = false
|
||||
fetchBomList()
|
||||
groupCache.value = new Map()
|
||||
fetchBomSummary()
|
||||
} else {
|
||||
ElMessage.error(res.msg || '保存失败')
|
||||
}
|
||||
@ -891,22 +954,19 @@ onMounted(() => {
|
||||
|
||||
// 1. 把名称填入背景搜索框,并真正触发一次列表搜索,让背景列表也只显示该物料
|
||||
searchKeyword.value = parentName;
|
||||
fetchBomList();
|
||||
fetchBomSummary();
|
||||
|
||||
// 2. 延迟等待基础渲染后进行查重
|
||||
setTimeout(() => {
|
||||
getBomList({ keyword: parentName }).then((res: any) => {
|
||||
const groups = res.data || [];
|
||||
getBomList({ keyword: parentName, page: 1, pageSize: 50 }).then((res: any) => {
|
||||
// ★ 适配新 API 格式:{ items: [...], total, pages }
|
||||
const flatItems = res.data?.items ?? [];
|
||||
let existingBom = null;
|
||||
|
||||
// ★ 修复点:遍历分组 (groups) 里的 items 来查找正确的 parent_id
|
||||
for (const group of groups) {
|
||||
if (group.items && group.items.length > 0) {
|
||||
const found = group.items.find((b: any) => b.parent_id === parentId);
|
||||
if (found) {
|
||||
existingBom = found;
|
||||
break; // 找到了就跳出循环
|
||||
}
|
||||
for (const bom of flatItems) {
|
||||
if (bom.parent_id === parentId) {
|
||||
existingBom = bom;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@ -930,8 +990,8 @@ onMounted(() => {
|
||||
});
|
||||
}, 300);
|
||||
} else {
|
||||
// 如果不是从其他页面跳转过来的,直接正常加载全部列表
|
||||
fetchBomList();
|
||||
// 如果不是从其他页面跳转过来的,直接正常加载摘要
|
||||
fetchBomSummary();
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@ -708,8 +708,18 @@ const openBomSelect = async () => {
|
||||
selectedBomNo.value = ''
|
||||
currentBomDetail.value = []
|
||||
try {
|
||||
const res = await getBomList({ active_only: true })
|
||||
bomOptions.value = res.data || []
|
||||
// ★ 适配新 API 格式 { items: [...], total, pages } → 前端按 parent_category 分组
|
||||
const res = await getBomList({ active_only: true, pageSize: 9999 })
|
||||
const flatItems = res.data?.items ?? []
|
||||
const groupMap = new Map<string, any[]>()
|
||||
for (const item of flatItems) {
|
||||
const cat = item.parent_category || '未分类'
|
||||
if (!groupMap.has(cat)) groupMap.set(cat, [])
|
||||
groupMap.get(cat)!.push(item)
|
||||
}
|
||||
bomOptions.value = Array.from(groupMap.entries()).map(([category, items]) => ({
|
||||
category, count: items.length, items
|
||||
}))
|
||||
} catch (e) {
|
||||
ElMessage.error('加载 BOM 列表失败')
|
||||
}
|
||||
|
||||
@ -204,7 +204,11 @@
|
||||
</div>
|
||||
|
||||
<div v-loading="loading" class="odoo-view-container">
|
||||
<el-collapse v-model="activeCategories" class="odoo-collapse">
|
||||
<el-collapse
|
||||
v-model="activeCategories"
|
||||
class="odoo-collapse"
|
||||
@change="handleCollapseChange"
|
||||
>
|
||||
<el-collapse-item
|
||||
v-for="group in groupedData"
|
||||
:key="group.category"
|
||||
@ -213,7 +217,8 @@
|
||||
<template #title>
|
||||
<div class="odoo-group-header">
|
||||
<span class="category-name">
|
||||
{{ group.category || '未分类' }} ({{ group.items.length }})
|
||||
{{ group.category || '未分类' }} ({{ group.count }})
|
||||
<el-icon v-if="groupLoadingMap.get(group.category)" class="is-loading" style="margin-left:6px;font-size:14px;"><Loading /></el-icon>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
@ -698,7 +703,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted, nextTick, computed, watch } from 'vue';
|
||||
import { Plus, Document, DocumentCopy, Refresh, Setting, Rank, Camera, Download, Bell, CircleCheck, Files, ZoomIn, Delete, Picture, FolderOpened } from '@element-plus/icons-vue';
|
||||
import { Plus, Document, DocumentCopy, Refresh, Setting, Rank, Camera, Download, Bell, CircleCheck, Files, ZoomIn, Delete, Picture, FolderOpened, Loading } from '@element-plus/icons-vue';
|
||||
import { ElMessage, ElMessageBox, ElLoading } from 'element-plus';
|
||||
import type { FormInstance, FormRules } from 'element-plus';
|
||||
import { useUserStore } from '@/stores/user';
|
||||
@ -716,7 +721,8 @@ import {
|
||||
batchSetWarning,
|
||||
batchSetInspection,
|
||||
markWarningOrdered,
|
||||
getMaterialUnitsAPI
|
||||
getMaterialUnitsAPI,
|
||||
getOdooSummary
|
||||
} from '@/api/material_base';
|
||||
import { uploadFile, deleteFile } from '@/api/common/upload';
|
||||
import { usePasteUpload } from '@/hooks/usePasteUpload';
|
||||
@ -821,11 +827,10 @@ const currentCameraField = ref<'generalImage' | 'generalManual'>('generalImage')
|
||||
|
||||
const originalForm = ref<any>(null);
|
||||
|
||||
// ================= Odoo 分组核心逻辑 =================
|
||||
// 强行关闭分页,使用超大 pageSize
|
||||
// ================= Odoo 分组核心逻辑(懒加载架构 v2) =================
|
||||
const queryParams = reactive<QueryParams>({
|
||||
pageNum: 1,
|
||||
pageSize: 9999,
|
||||
pageSize: 50, // ★ 恢复正常分页大小,不再用 9999
|
||||
keyword: '',
|
||||
searchField: 'all',
|
||||
category: '',
|
||||
@ -838,25 +843,50 @@ const queryParams = reactive<QueryParams>({
|
||||
has_stock: ''
|
||||
});
|
||||
|
||||
// 计算属性:前端内存分组
|
||||
const groupedData = computed(() => {
|
||||
if (!tableData.value || !tableData.value.length) return [];
|
||||
const groupMap = new Map<string, { category: string; items: MaterialBaseVO[] }>();
|
||||
// ★ 新增:Odoo 分组摘要(来自 /odoo-summary API)
|
||||
interface GroupSummary {
|
||||
category: string;
|
||||
count: number;
|
||||
}
|
||||
const groupSummary = ref<GroupSummary[]>([]);
|
||||
|
||||
tableData.value.forEach(item => {
|
||||
const cat = item.category || '未分类';
|
||||
if (!groupMap.has(cat)) {
|
||||
groupMap.set(cat, { category: cat, items: [] });
|
||||
}
|
||||
groupMap.get(cat)!.items.push(item);
|
||||
// ★ 新增:分组数据缓存 Map<category, {items, total}>
|
||||
const groupCache = ref<Map<string, { items: MaterialBaseVO[]; total: number }>>(new Map());
|
||||
|
||||
// ★ 新增:分组加载状态
|
||||
const groupLoadingMap = ref<Map<string, boolean>>(new Map());
|
||||
|
||||
// 当前展开的分类(支持搜索全局过滤时自动全部折叠)
|
||||
const lastKeyword = ref('');
|
||||
|
||||
// 计算属性:基于缓存构建分组数据
|
||||
const groupedData = computed(() => {
|
||||
if (!groupSummary.value.length) return [];
|
||||
|
||||
return groupSummary.value.map(summary => {
|
||||
const cached = groupCache.value.get(summary.category);
|
||||
return {
|
||||
category: summary.category,
|
||||
count: summary.count,
|
||||
items: cached?.items ?? [],
|
||||
total: cached?.total ?? summary.count,
|
||||
loaded: !!cached
|
||||
};
|
||||
});
|
||||
return Array.from(groupMap.values()).sort((a, b) => a.category.localeCompare(b.category));
|
||||
});
|
||||
|
||||
// 折叠面板展开状态
|
||||
const activeCategories = ref<string[]>([]);
|
||||
const expandAllGroups = () => { activeCategories.value = groupedData.value.map(g => g.category); };
|
||||
const collapseAllGroups = () => { activeCategories.value = []; };
|
||||
|
||||
const expandAllGroups = () => {
|
||||
// 展开全部 → 触发所有分组的懒加载
|
||||
activeCategories.value = groupSummary.value.map(g => g.category);
|
||||
groupSummary.value.forEach(g => loadGroupItems(g.category));
|
||||
};
|
||||
|
||||
const collapseAllGroups = () => {
|
||||
activeCategories.value = [];
|
||||
};
|
||||
|
||||
// ================= 跨组表格批量选中处理 =================
|
||||
const isBatchMode = ref(false);
|
||||
@ -1118,27 +1148,78 @@ const querySearchType = (queryString: string, cb: any) => {
|
||||
cb(results.map(item => ({ value: item })));
|
||||
};
|
||||
|
||||
const getList = () => {
|
||||
// ★ 新增:挂载时调用 - 获取分组摘要
|
||||
const fetchOdooSummary = async () => {
|
||||
loading.value = true;
|
||||
queryParams.enableWarningSort = userStore.hasPermission('material_list:view_warning') && !queryParams.orderByColumn;
|
||||
const params = {
|
||||
...queryParams,
|
||||
advancedFilters: JSON.stringify(queryParams.advancedFilters || [])
|
||||
};
|
||||
// 兼容旧版后端:company=ALL 时不传过滤参数
|
||||
if (params.company === 'ALL') {
|
||||
delete params.company
|
||||
}
|
||||
listMaterialBase(params).then((response: any) => {
|
||||
if (response && response.data) {
|
||||
tableData.value = response.data.items;
|
||||
total.value = response.data.total;
|
||||
} else {
|
||||
tableData.value = [];
|
||||
total.value = 0;
|
||||
try {
|
||||
const params: any = {};
|
||||
if (queryParams.keyword) params.keyword = queryParams.keyword;
|
||||
if (queryParams.isEnabled !== undefined) params.isEnabled = queryParams.isEnabled;
|
||||
|
||||
const res: any = await getOdooSummary(params);
|
||||
if (res?.code === 200) {
|
||||
groupSummary.value = res.data ?? [];
|
||||
// 搜索条件变更 → 清除旧缓存,折叠所有分组
|
||||
groupCache.value = new Map();
|
||||
groupLoadingMap.value = new Map();
|
||||
if (queryParams.keyword !== lastKeyword.value) {
|
||||
activeCategories.value = [];
|
||||
lastKeyword.value = queryParams.keyword;
|
||||
}
|
||||
}
|
||||
}).catch((err) => { console.error(err); tableData.value = []; })
|
||||
.finally(() => { loading.value = false; });
|
||||
} catch (err) {
|
||||
console.error('获取 Odoo 摘要失败', err);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// ★ 新增:展开分组时懒加载该分类下的数据
|
||||
const loadGroupItems = async (category: string) => {
|
||||
// 已加载则跳过
|
||||
if (groupCache.value.has(category)) return;
|
||||
// 正在加载中则跳过
|
||||
if (groupLoadingMap.value.get(category)) return;
|
||||
|
||||
groupLoadingMap.value.set(category, true);
|
||||
try {
|
||||
const params: any = {
|
||||
pageNum: 1,
|
||||
pageSize: 9999, // 单分类全量加载(分类内数据量通常可控)
|
||||
category: category,
|
||||
};
|
||||
if (queryParams.keyword) params.keyword = queryParams.keyword;
|
||||
if (queryParams.type) params.type = queryParams.type;
|
||||
if (queryParams.isEnabled !== undefined) params.isEnabled = queryParams.isEnabled;
|
||||
if (queryParams.company && queryParams.company !== 'ALL') params.company = queryParams.company;
|
||||
|
||||
const res: any = await listMaterialBase(params);
|
||||
if (res?.code === 200 && res.data) {
|
||||
groupCache.value.set(category, {
|
||||
items: res.data.items ?? [],
|
||||
total: res.data.total ?? 0
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`加载分类 [${category}] 失败`, err);
|
||||
} finally {
|
||||
groupLoadingMap.value.set(category, false);
|
||||
}
|
||||
};
|
||||
|
||||
// ★ 监听 collapse 展开事件 → 触发懒加载
|
||||
const handleCollapseChange = (val: string | string[]) => {
|
||||
// val 是当前所有展开的分类名数组
|
||||
if (Array.isArray(val)) {
|
||||
val.forEach(cat => loadGroupItems(cat));
|
||||
} else if (val) {
|
||||
loadGroupItems(val);
|
||||
}
|
||||
};
|
||||
|
||||
const getList = () => {
|
||||
// Odoo 页面不再一次性全量加载,改为 fetchOdooSummary
|
||||
fetchOdooSummary();
|
||||
};
|
||||
|
||||
const handleExport = () => {
|
||||
@ -1476,7 +1557,7 @@ watch(
|
||||
|
||||
onMounted(() => {
|
||||
initColumnPermissions();
|
||||
if (!route.query.keyword) getList();
|
||||
if (!route.query.keyword) fetchOdooSummary();
|
||||
getOptionsList(); fetchUnitList();
|
||||
|
||||
if (route.query.edit_id) {
|
||||
|
||||
@ -741,8 +741,18 @@ const openBomSelect = async () => {
|
||||
selectedBomNo.value = ''
|
||||
currentBomDetail.value = []
|
||||
try {
|
||||
const res = await getBomList({ active_only: true })
|
||||
bomOptions.value = res.data || []
|
||||
// ★ 适配新 API 格式 { items: [...], total, pages } → 前端按 parent_category 分组
|
||||
const res = await getBomList({ active_only: true, pageSize: 9999 })
|
||||
const flatItems = res.data?.items ?? []
|
||||
const groupMap = new Map<string, any[]>()
|
||||
for (const item of flatItems) {
|
||||
const cat = item.parent_category || '未分类'
|
||||
if (!groupMap.has(cat)) groupMap.set(cat, [])
|
||||
groupMap.get(cat)!.push(item)
|
||||
}
|
||||
bomOptions.value = Array.from(groupMap.entries()).map(([category, items]) => ({
|
||||
category, count: items.length, items
|
||||
}))
|
||||
} catch (e) {
|
||||
ElMessage.error('加载 BOM 列表失败')
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user