refactor(bom): optimize material select with remote search and pagination lazy loading
This commit is contained in:
0
deploy_full.sh
Executable file → Normal file
0
deploy_full.sh
Executable file → Normal file
@ -55,18 +55,23 @@
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="16">
|
||||
<el-form-item label="父件 (成品)" prop="parent_id" v-if="hasFormFieldPermission('parent_id')">
|
||||
<!-- ====== 改造:父件下拉 - 远程搜索 + 懒加载 ====== -->
|
||||
<el-select
|
||||
v-model="form.parent_id"
|
||||
placeholder="请搜索并选择父件"
|
||||
filterable
|
||||
remote
|
||||
reserve-keyword
|
||||
:remote-method="(q: string) => handleRemoteSearch(q, 'parent')"
|
||||
:loading="selectLoading"
|
||||
style="width: 100%"
|
||||
:disabled="isEditMode"
|
||||
class="beautified-select"
|
||||
:filter-method="filterMaterial"
|
||||
@change="onParentChange"
|
||||
@visible-change="(visible: boolean) => handleVisibleChange(visible, 'parent')"
|
||||
v-loadmore="loadMoreParent"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in materialOptions"
|
||||
v-for="item in parentOptions"
|
||||
:key="item.id"
|
||||
:label="`${item.name} (${item.spec})`"
|
||||
:value="item.id"
|
||||
@ -101,7 +106,6 @@
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<!-- 任务1:另存为模式显示版本选项,新建/编辑模式显示输入框 -->
|
||||
<el-form-item label="版本号" prop="version" v-if="hasFormFieldPermission('version')">
|
||||
<template v-if="isSaveAsMode">
|
||||
<el-radio-group v-model="form.versionUpgradeType" @change="onVersionUpgradeTypeChange">
|
||||
@ -121,7 +125,6 @@
|
||||
<span style="font-weight: normal; font-size: 12px; color: #909399; margin-left: 10px;">(已选 {{ filteredChildren.length }} 条)</span>
|
||||
</div>
|
||||
|
||||
<!-- 任务2:子件列表本地模糊搜索 -->
|
||||
<el-input
|
||||
v-model="childSearchKeyword"
|
||||
placeholder="请输入子件名称或规格型号搜索"
|
||||
@ -133,16 +136,22 @@
|
||||
<el-table :data="filteredChildren" border style="width: 100%; margin-bottom: 15px" max-height="300">
|
||||
<el-table-column label="子件物料" min-width="280" v-if="hasFormFieldPermission('child_id')">
|
||||
<template #default="{ row, $index }">
|
||||
<!-- ====== 改造:子件下拉 - 远程搜索 + 懒加载 ====== -->
|
||||
<el-select
|
||||
v-model="row.child_id"
|
||||
placeholder="请搜索原料"
|
||||
filterable
|
||||
remote
|
||||
reserve-keyword
|
||||
:remote-method="(q: string) => handleRemoteSearch(q, 'child', $index)"
|
||||
:loading="selectLoading"
|
||||
style="width: 100%"
|
||||
:filter-method="filterMaterial"
|
||||
@change="(val) => onChildChange(val, $index)"
|
||||
:loading-text="`正在加载第 ${childQueryParams.page} 页...`"
|
||||
@visible-change="(visible: boolean) => handleVisibleChange(visible, 'child', $index)"
|
||||
v-loadmore="(el: HTMLElement) => loadMoreChild(el, $index)"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in materialOptions"
|
||||
v-for="item in getChildOptions($index)"
|
||||
:key="item.id"
|
||||
:label="`${item.name} (${item.spec})`"
|
||||
:value="item.id"
|
||||
@ -158,7 +167,6 @@
|
||||
|
||||
<el-table-column label="用量" width="140" v-if="hasFormFieldPermission('dosage')">
|
||||
<template #default="{ row }">
|
||||
<!-- 任务2:整数精度,去掉调节按钮 -->
|
||||
<el-input-number v-model="row.dosage" :min="0" :precision="0" style="width: 100%" :controls="false" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
@ -192,14 +200,66 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted, computed } from 'vue'
|
||||
import { ref, reactive, onMounted, computed, nextTick } from 'vue'
|
||||
import { ElMessage, ElMessageBox, FormInstance, FormRules } from 'element-plus'
|
||||
import { Plus, Search } from '@element-plus/icons-vue'
|
||||
import { getBomList, getBomDetail, saveBom, deleteBom } from '@/api/bom'
|
||||
import { getMaterialBaseList } from '@/api/inbound/stock'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
|
||||
// ============================================================
|
||||
// v-loadmore 自定义指令(局部注册)
|
||||
// 用于监听 Element Plus 下拉面板的滚动触底事件
|
||||
// ============================================================
|
||||
const vLoadmore = {
|
||||
mounted(el: HTMLElement, binding: any) {
|
||||
// 找到 .el-select-dropdown__wrap(滚动容器)
|
||||
const targetSelector = '.el-select-dropdown:not(.is-hidden) .el-select-dropdown__wrap'
|
||||
|
||||
const scrollHandler = () => {
|
||||
const scrollContainer = el.querySelector(targetSelector) as HTMLElement | null
|
||||
if (!scrollContainer) return
|
||||
|
||||
const { scrollTop, scrollHeight, clientHeight } = scrollContainer
|
||||
const distanceToBottom = scrollHeight - scrollTop - clientHeight
|
||||
|
||||
// 距离底部 10px 以内时触发加载更多
|
||||
if (distanceToBottom <= 10) {
|
||||
binding.value(el)
|
||||
}
|
||||
}
|
||||
|
||||
// 使用 MutationObserver 监听 DOM 变化(Element Plus 下拉弹窗是动态创建的)
|
||||
const observer = new MutationObserver(() => {
|
||||
const dropdown = el.querySelector('.el-select-dropdown:not(.is-hidden)') as HTMLElement | null
|
||||
if (dropdown) {
|
||||
const wrap = dropdown.querySelector('.el-select-dropdown__wrap') as HTMLElement | null
|
||||
if (wrap) {
|
||||
wrap.removeEventListener('scroll', scrollHandler)
|
||||
wrap.addEventListener('scroll', scrollHandler)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
observer.observe(el, {
|
||||
childList: true,
|
||||
subtree: true
|
||||
})
|
||||
|
||||
// 存储清理函数
|
||||
;(el as any)._loadmoreObserver = observer
|
||||
},
|
||||
unmounted(el: HTMLElement) {
|
||||
const observer = (el as any)._loadmoreObserver
|
||||
if (observer) {
|
||||
observer.disconnect()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 类型定义
|
||||
// ============================================================
|
||||
interface BomItem {
|
||||
bom_no: string
|
||||
parent_id: number
|
||||
@ -224,25 +284,197 @@ const loading = ref(false)
|
||||
const dialogVisible = ref(false)
|
||||
const saving = ref(false)
|
||||
const isEditMode = ref(false)
|
||||
const isSaveAsMode = ref(false) // 任务1:标记是否为另存为模式
|
||||
let originalVersion = '' // 保存原始版本号用于计算升级选项
|
||||
let currentBomNo = '' // 保存当前操作的 BOM 编号用于计算版本避让
|
||||
let originalChildren: ChildRow[] = [] // 保存原始子件数据用于本地查重
|
||||
const isSaveAsMode = ref(false)
|
||||
let originalVersion = ''
|
||||
let currentBomNo = ''
|
||||
let originalChildren: ChildRow[] = []
|
||||
|
||||
const bomList = ref<BomItem[]>([])
|
||||
const materialOptions = ref<MaterialBase[]>([])
|
||||
const searchKeyword = ref('')
|
||||
const childSearchKeyword = ref('') // 任务2:子件列表搜索关键字
|
||||
const childSearchKeyword = ref('')
|
||||
|
||||
// 任务2:子件列表本地模糊搜索 - 计算属性
|
||||
// ============================================================
|
||||
// 【改造】分页 + 远程搜索相关状态
|
||||
// ============================================================
|
||||
const PAGE_SIZE = 20
|
||||
|
||||
// 父件下拉
|
||||
const parentOptions = ref<MaterialBase[]>([])
|
||||
const parentQueryParams = reactive({ page: 1, limit: PAGE_SIZE, keyword: '' })
|
||||
const parentHasMore = ref(true)
|
||||
|
||||
// 子件下拉(每行独立状态)
|
||||
interface ChildDropdownState {
|
||||
options: MaterialBase[]
|
||||
queryParams: { page: number; limit: number; keyword: string }
|
||||
hasMore: boolean
|
||||
}
|
||||
const childDropdownStates = ref<Map<number, ChildDropdownState>>(new Map())
|
||||
|
||||
// 子件下拉专用的查询参数(用于 loading-text 显示)
|
||||
const childQueryParams = reactive({ page: 1 })
|
||||
|
||||
// 加载状态(父件和子件共用一个 loading,避免闪烁)
|
||||
const selectLoading = ref(false)
|
||||
|
||||
const getChildOptions = (index: number): MaterialBase[] => {
|
||||
return childDropdownStates.value.get(index)?.options ?? []
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 【改造】获取物料列表(分页版本)
|
||||
// ============================================================
|
||||
const fetchMaterialOptions = async (
|
||||
type: 'parent' | 'child',
|
||||
index?: number,
|
||||
isLoadMore = false
|
||||
) => {
|
||||
// 子件行需要 index
|
||||
if (type === 'child' && index === undefined) return
|
||||
|
||||
const params = type === 'parent'
|
||||
? parentQueryParams
|
||||
: childDropdownStates.value.get(index!)?.queryParams
|
||||
|
||||
if (!params) return
|
||||
|
||||
selectLoading.value = true
|
||||
|
||||
try {
|
||||
const res = await getMaterialBaseList({
|
||||
page: params.page,
|
||||
limit: params.limit,
|
||||
keyword: params.keyword
|
||||
})
|
||||
|
||||
if (res.code === 200) {
|
||||
const list: MaterialBase[] = res.data?.list ?? res.data ?? []
|
||||
const total = res.data?.total ?? list.length
|
||||
|
||||
if (type === 'parent') {
|
||||
if (isLoadMore) {
|
||||
// 去重追加
|
||||
const existingIds = new Set(parentOptions.value.map(m => m.id))
|
||||
const newItems = list.filter(m => !existingIds.has(m.id))
|
||||
parentOptions.value.push(...newItems)
|
||||
} else {
|
||||
parentOptions.value = list
|
||||
}
|
||||
// 判断是否还有更多数据
|
||||
parentHasMore.value = parentOptions.value.length < total
|
||||
} else {
|
||||
const state = childDropdownStates.value.get(index!)
|
||||
if (!state) return
|
||||
|
||||
if (isLoadMore) {
|
||||
const existingIds = new Set(state.options.map(m => m.id))
|
||||
const newItems = list.filter(m => !existingIds.has(m.id))
|
||||
state.options.push(...newItems)
|
||||
} else {
|
||||
state.options = list
|
||||
}
|
||||
state.hasMore = state.options.length < total
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// 错误已由全局拦截器统一处理
|
||||
} finally {
|
||||
selectLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 【改造】远程搜索处理函数
|
||||
// ============================================================
|
||||
const handleRemoteSearch = (
|
||||
query: string,
|
||||
type: 'parent' | 'child',
|
||||
index?: number
|
||||
) => {
|
||||
if (type === 'parent') {
|
||||
parentQueryParams.keyword = query
|
||||
parentQueryParams.page = 1
|
||||
parentHasMore.value = true
|
||||
fetchMaterialOptions('parent')
|
||||
} else if (type === 'child' && index !== undefined) {
|
||||
const state = childDropdownStates.value.get(index)
|
||||
if (!state) return
|
||||
state.queryParams.keyword = query
|
||||
state.queryParams.page = 1
|
||||
state.hasMore = true
|
||||
fetchMaterialOptions('child', index)
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 【改造】下拉框展开/收起处理(重置分页 + 预加载第一页)
|
||||
// ============================================================
|
||||
const handleVisibleChange = (visible: boolean, type: 'parent' | 'child', index?: number) => {
|
||||
if (!visible) return
|
||||
|
||||
if (type === 'parent') {
|
||||
parentQueryParams.page = 1
|
||||
parentQueryParams.keyword = ''
|
||||
parentHasMore.value = true
|
||||
fetchMaterialOptions('parent')
|
||||
} else if (type === 'child' && index !== undefined) {
|
||||
// 确保该行下拉状态已初始化
|
||||
if (!childDropdownStates.value.has(index)) {
|
||||
childDropdownStates.value.set(index, {
|
||||
options: [],
|
||||
queryParams: { page: 1, limit: PAGE_SIZE, keyword: '' },
|
||||
hasMore: true
|
||||
})
|
||||
}
|
||||
const state = childDropdownStates.value.get(index)!
|
||||
state.queryParams.page = 1
|
||||
state.queryParams.keyword = ''
|
||||
state.hasMore = true
|
||||
fetchMaterialOptions('child', index)
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 【改造】滚动触底加载更多
|
||||
// ============================================================
|
||||
const loadMoreParent = () => {
|
||||
if (selectLoading.value || !parentHasMore.value) return
|
||||
parentQueryParams.page++
|
||||
fetchMaterialOptions('parent', undefined, true)
|
||||
}
|
||||
|
||||
const loadMoreChild = (_el: HTMLElement, index: number) => {
|
||||
const state = childDropdownStates.value.get(index)
|
||||
if (!state) return
|
||||
if (selectLoading.value || !state.hasMore) return
|
||||
state.queryParams.page++
|
||||
fetchMaterialOptions('child', index, true)
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 【改造】初始化子件行下拉状态
|
||||
// ============================================================
|
||||
const initChildDropdownState = (index: number) => {
|
||||
if (!childDropdownStates.value.has(index)) {
|
||||
childDropdownStates.value.set(index, {
|
||||
options: [],
|
||||
queryParams: { page: 1, limit: PAGE_SIZE, keyword: '' },
|
||||
hasMore: true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 原有逻辑保留
|
||||
// ============================================================
|
||||
const filteredChildren = computed(() => {
|
||||
if (!childSearchKeyword.value) {
|
||||
return form.children
|
||||
}
|
||||
const kw = childSearchKeyword.value.toLowerCase()
|
||||
return form.children.filter(child => {
|
||||
// 获取子件物料的名称和规格
|
||||
const material = materialOptions.value.find(m => m.id === child.child_id)
|
||||
const state = childDropdownStates.value.get(form.children.indexOf(child))
|
||||
const material = state?.options.find(m => m.id === child.child_id)
|
||||
if (!material) return false
|
||||
const name = (material.name || '').toLowerCase()
|
||||
const spec = (material.spec || '').toLowerCase()
|
||||
@ -250,17 +482,6 @@ const filteredChildren = computed(() => {
|
||||
})
|
||||
})
|
||||
|
||||
// 任务3:自定义过滤方法 - 同时匹配名称和规格
|
||||
const filterMaterial = (val: string) => {
|
||||
if (!val) return true
|
||||
const kw = val.toLowerCase()
|
||||
return (item: MaterialBase) => {
|
||||
const name = (item.name || '').toLowerCase()
|
||||
const spec = (item.spec || '').toLowerCase()
|
||||
return name.includes(kw) || spec.includes(kw)
|
||||
}
|
||||
}
|
||||
|
||||
// 列与权限Code的映射关系(数据库中的code)
|
||||
const permissionMap: Record<string, string> = {
|
||||
bom_no: 'bom_manage:bom_no',
|
||||
@ -269,94 +490,62 @@ const permissionMap: Record<string, string> = {
|
||||
version: 'bom_manage:version',
|
||||
status: 'bom_manage:status',
|
||||
child_count: 'bom_manage:child_count',
|
||||
// 表单字段
|
||||
parent_id: 'bom_manage:parent_id',
|
||||
is_enabled: 'bom_manage:status',
|
||||
bom_no: 'bom_manage:bom_no',
|
||||
child_id: 'bom_manage:child_id',
|
||||
dosage: 'bom_manage:dosage',
|
||||
remark: 'bom_manage:remark',
|
||||
}
|
||||
|
||||
// 检查列权限
|
||||
const hasColumnPermission = (prop: string) => {
|
||||
if (userStore.role === 'SUPER_ADMIN' || userStore.username === 'IRIS') {
|
||||
return true
|
||||
}
|
||||
if (userStore.role === 'SUPER_ADMIN' || userStore.username === 'IRIS') return true
|
||||
const code = permissionMap[prop]
|
||||
return code ? userStore.hasPermission(code) : false
|
||||
}
|
||||
|
||||
// 检查表单字段权限
|
||||
const hasFormFieldPermission = (fieldName: string) => {
|
||||
if (userStore.role === 'SUPER_ADMIN' || userStore.username === 'IRIS') {
|
||||
return true
|
||||
}
|
||||
if (userStore.role === 'SUPER_ADMIN' || userStore.username === 'IRIS') return true
|
||||
const code = permissionMap[fieldName]
|
||||
return code ? userStore.hasPermission(code) : false
|
||||
}
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
const form = reactive({
|
||||
bom_no: '', // 纯净的 BOM 编号(从父件规格提取)
|
||||
remark: '', // BOM 备注信息
|
||||
bom_no: '',
|
||||
remark: '',
|
||||
parent_id: null as number | null,
|
||||
version: 'V1.0',
|
||||
versionUpgradeType: 'minor' as 'minor' | 'major', // 任务1:另存为时的版本升级类型
|
||||
versionUpgradeType: 'minor' as 'minor' | 'major',
|
||||
is_enabled: true,
|
||||
children: [] as ChildRow[]
|
||||
})
|
||||
|
||||
// 纯净 BOM 编号(直接从表单获取,无拼接逻辑)
|
||||
const pureBomNo = computed(() => form.bom_no)
|
||||
|
||||
// 任务1:根据原版本号计算升级选项(智能避让已占用版本)
|
||||
const versionOptions = computed(() => {
|
||||
const ver = originalVersion || 'V1.0'
|
||||
|
||||
// 获取当前 BOM 编号下所有已占用的版本号集合
|
||||
const occupiedVersions = new Set(
|
||||
bomList.value
|
||||
.filter(item => item.bom_no === currentBomNo)
|
||||
.map(item => item.version)
|
||||
bomList.value.filter(item => item.bom_no === currentBomNo).map(item => item.version)
|
||||
)
|
||||
|
||||
// 智能递增避让函数
|
||||
const getNextMinor = (baseMajor: number, baseMinor: number): string => {
|
||||
let minor = baseMinor + 1
|
||||
let candidate = `V${baseMajor}.${minor}`
|
||||
while (occupiedVersions.has(candidate)) {
|
||||
minor++
|
||||
candidate = `V${baseMajor}.${minor}`
|
||||
}
|
||||
while (occupiedVersions.has(candidate)) { minor++; candidate = `V${baseMajor}.${minor}` }
|
||||
return candidate
|
||||
}
|
||||
|
||||
const getNextMajor = (baseMajor: number): string => {
|
||||
let major = baseMajor + 1
|
||||
let candidate = `V${major}.0`
|
||||
while (occupiedVersions.has(candidate)) {
|
||||
major++
|
||||
candidate = `V${major}.0`
|
||||
}
|
||||
while (occupiedVersions.has(candidate)) { major++; candidate = `V${major}.0` }
|
||||
return candidate
|
||||
}
|
||||
|
||||
// 解析版本号格式 Vx.y
|
||||
const match = ver.match(/^V(\d+)\.(\d+)$/)
|
||||
if (match) {
|
||||
const major = parseInt(match[1])
|
||||
const minor = parseInt(match[2])
|
||||
return {
|
||||
minor: getNextMinor(major, minor),
|
||||
major: getNextMajor(major)
|
||||
}
|
||||
return { minor: getNextMinor(parseInt(match[1]), parseInt(match[2])), major: getNextMajor(parseInt(match[1])) }
|
||||
}
|
||||
// 无法解析时返回默认选项(带避让)
|
||||
return { minor: getNextMinor(1, 0), major: getNextMajor(1) }
|
||||
})
|
||||
|
||||
// 任务1:版本升级类型变更时更新版本号
|
||||
const onVersionUpgradeTypeChange = (type: 'minor' | 'major') => {
|
||||
form.version = versionOptions.value[type]
|
||||
}
|
||||
@ -373,42 +562,23 @@ const fetchBomList = async () => {
|
||||
try {
|
||||
const res = await getBomList({ keyword: searchKeyword.value })
|
||||
if (res.code === 200) bomList.value = res.data
|
||||
} catch (error) {
|
||||
// 错误已由全局拦截器统一处理
|
||||
}
|
||||
finally { loading.value = false }
|
||||
} finally { loading.value = false }
|
||||
}
|
||||
|
||||
const fetchMaterialOptions = async () => {
|
||||
try {
|
||||
const res = await getMaterialBaseList()
|
||||
if (res.code === 200) materialOptions.value = res.data
|
||||
} catch (error) {
|
||||
// 错误已由全局拦截器统一处理
|
||||
}
|
||||
}
|
||||
|
||||
// 监听父件变化,自动设置纯净的 BOM 编号
|
||||
const onParentChange = (val: number) => {
|
||||
const selected = materialOptions.value.find(m => m.id === val)
|
||||
const selected = parentOptions.value.find(m => m.id === val)
|
||||
if (selected && selected.spec) {
|
||||
// 安全提取:取 / 前面的部分作为纯净 BOM 编号
|
||||
const rawSpec = selected.spec || ''
|
||||
form.bom_no = rawSpec.split('/')[0].trim()
|
||||
form.bom_no = selected.spec.split('/')[0].trim()
|
||||
} else {
|
||||
form.bom_no = ''
|
||||
}
|
||||
}
|
||||
|
||||
// 任务2:子件物料选中后自动设置用量为1,并检测重复
|
||||
const onChildChange = (val: number | null, index: number) => {
|
||||
if (val !== null) {
|
||||
// 任务1:检测重复 - 检查该物料是否已在其他行存在
|
||||
const existingIndex = form.children.findIndex((child, idx) => idx !== index && child.child_id === val)
|
||||
if (existingIndex !== -1) {
|
||||
const material = materialOptions.value.find(m => m.id === val)
|
||||
ElMessage.warning(`该物料已在第 ${existingIndex + 1} 行存在,请合并用量或删除重复项`)
|
||||
// 清空当前选择
|
||||
form.children[index].child_id = null
|
||||
form.children[index].dosage = 0
|
||||
return
|
||||
@ -421,15 +591,15 @@ const handleCreate = () => {
|
||||
resetForm()
|
||||
dialogTitle.value = '新建 BOM'
|
||||
isEditMode.value = false
|
||||
isSaveAsMode.value = false // 确保新建模式
|
||||
isSaveAsMode.value = false
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleEdit = async (row: BomItem) => {
|
||||
await loadDetail(row.bom_no, row.version)
|
||||
dialogTitle.value = '编辑 BOM'
|
||||
isEditMode.value = true // 编辑时不允许修改编号前缀/后缀
|
||||
isSaveAsMode.value = false // 确保编辑模式
|
||||
isEditMode.value = true
|
||||
isSaveAsMode.value = false
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
@ -437,14 +607,10 @@ const handleSaveAs = async (row: BomItem) => {
|
||||
await loadDetail(row.bom_no, row.version)
|
||||
dialogTitle.value = '另存为新版/变体'
|
||||
isEditMode.value = true
|
||||
isSaveAsMode.value = true // 任务1:进入另存为模式,显示版本选项
|
||||
|
||||
// 保存原始版本号和 BOM 编号用于计算升级选项
|
||||
isSaveAsMode.value = true
|
||||
originalVersion = form.version
|
||||
currentBomNo = row.bom_no // 保存当前操作的 BOM 编号
|
||||
// 保存原始子件数据用于本地查重
|
||||
currentBomNo = row.bom_no
|
||||
originalChildren = JSON.parse(JSON.stringify(form.children))
|
||||
// 默认选中次版本升级
|
||||
form.versionUpgradeType = 'minor'
|
||||
form.version = versionOptions.value.minor
|
||||
dialogVisible.value = true
|
||||
@ -463,20 +629,16 @@ const loadDetail = async (bomNo: string, version: string) => {
|
||||
dosage: child.dosage,
|
||||
remark: child.remark || ''
|
||||
}))
|
||||
|
||||
// 解析编号:安全提取纯净 BOM 编号
|
||||
// 为每个子件行初始化下拉状态
|
||||
form.children.forEach((_, idx) => initChildDropdownState(idx))
|
||||
if (data.parent_spec) {
|
||||
form.bom_no = (data.parent_spec || '').split('/')[0].trim()
|
||||
} else {
|
||||
// 无父件规格时直接使用 bom_no
|
||||
form.bom_no = bomNo.split('/')[0].trim()
|
||||
}
|
||||
// 加载备注信息
|
||||
form.remark = data.remark || ''
|
||||
}
|
||||
} catch (e) {
|
||||
// 错误已由全局拦截器统一处理
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
const handleDelete = (row: BomItem) => {
|
||||
@ -484,10 +646,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('删除成功'); fetchBomList() }
|
||||
} catch (e) {}
|
||||
})
|
||||
.catch(() => {})
|
||||
@ -501,15 +660,42 @@ const resetForm = () => {
|
||||
form.versionUpgradeType = 'minor'
|
||||
form.is_enabled = true
|
||||
form.children = []
|
||||
isSaveAsMode.value = false // 任务1:重置另存为模式
|
||||
isSaveAsMode.value = false
|
||||
originalVersion = ''
|
||||
currentBomNo = '' // 重置当前 BOM 编号
|
||||
childSearchKeyword.value = '' // 任务2:重置搜索关键字
|
||||
currentBomNo = ''
|
||||
childSearchKeyword.value = ''
|
||||
// 重置子件下拉状态
|
||||
childDropdownStates.value.clear()
|
||||
// 重置父件下拉状态
|
||||
parentOptions.value = []
|
||||
parentQueryParams.page = 1
|
||||
parentQueryParams.keyword = ''
|
||||
parentHasMore.value = true
|
||||
if (formRef.value) formRef.value.resetFields()
|
||||
}
|
||||
|
||||
const addChild = () => form.children.push({ child_id: null, dosage: 0, remark: '' })
|
||||
const removeChild = (idx: number) => form.children.splice(idx, 1)
|
||||
const addChild = () => {
|
||||
const idx = form.children.length
|
||||
form.children.push({ child_id: null, dosage: 0, remark: '' })
|
||||
initChildDropdownState(idx)
|
||||
}
|
||||
|
||||
const removeChild = (idx: number) => {
|
||||
form.children.splice(idx, 1)
|
||||
// 清理该行下拉状态(需要重新索引后续行的状态)
|
||||
rebuildChildDropdownStates()
|
||||
}
|
||||
|
||||
// 重建子件下拉状态索引(删除行后需要重新编号)
|
||||
const rebuildChildDropdownStates = () => {
|
||||
const newMap = new Map<number, ChildDropdownState>()
|
||||
form.children.forEach((_, idx) => {
|
||||
if (childDropdownStates.value.has(idx)) {
|
||||
newMap.set(idx, childDropdownStates.value.get(idx)!)
|
||||
}
|
||||
})
|
||||
childDropdownStates.value = newMap
|
||||
}
|
||||
|
||||
const submitForm = async () => {
|
||||
if (!formRef.value) return
|
||||
@ -518,23 +704,16 @@ const submitForm = async () => {
|
||||
if (!pureBomNo.value) return ElMessage.warning('BOM编号不能为空')
|
||||
if (form.children.length === 0) return ElMessage.warning('请至少添加一个子件')
|
||||
|
||||
// 任务1:提交前校验重复子件
|
||||
const childIds = form.children.map(c => c.child_id).filter(id => id !== null)
|
||||
const uniqueIds = new Set(childIds)
|
||||
if (childIds.length !== uniqueIds.size) {
|
||||
return ElMessage.warning('子件列表中存在重复物料,请合并用量或删除重复项')
|
||||
}
|
||||
|
||||
// ===== 另存为本地查重:检查是否修改过 =====
|
||||
if (isSaveAsMode.value && originalChildren.length > 0) {
|
||||
// 将当前 children 和原始 children 转换为 set 进行比较
|
||||
const currentSet = new Set(form.children.map(c => `${c.child_id}-${c.dosage}`))
|
||||
const originalSet = new Set(originalChildren.map(c => `${c.child_id}-${c.dosage}`))
|
||||
|
||||
// 比较两个集合是否完全相同
|
||||
const isIdentical = currentSet.size === originalSet.size &&
|
||||
[...currentSet].every(item => originalSet.has(item))
|
||||
|
||||
const isIdentical = currentSet.size === originalSet.size && [...currentSet].every(item => originalSet.has(item))
|
||||
if (isIdentical) {
|
||||
return ElMessage.warning('您未修改任何子件,与原版本内容一致,请修改后再保存')
|
||||
}
|
||||
@ -552,22 +731,14 @@ const submitForm = async () => {
|
||||
saving.value = true
|
||||
try {
|
||||
const res = await saveBom(payload)
|
||||
if (res.code === 200) {
|
||||
ElMessage.success('保存成功')
|
||||
dialogVisible.value = false
|
||||
fetchBomList()
|
||||
} else { ElMessage.error(res.msg || '保存失败') }
|
||||
} catch (e: any) {
|
||||
// 全局拦截器已处理错误提示,此处不再重复弹窗
|
||||
// 如需特殊处理,可在此处添加,但不要调用 ElMessage
|
||||
}
|
||||
finally { saving.value = false }
|
||||
if (res.code === 200) { ElMessage.success('保存成功'); dialogVisible.value = false; fetchBomList() }
|
||||
else ElMessage.error(res.msg || '保存失败')
|
||||
} finally { saving.value = false }
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchBomList()
|
||||
fetchMaterialOptions()
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user