Compare commits

3 Commits

4 changed files with 346 additions and 142 deletions

8
.qwen/settings.json Normal file
View File

@ -0,0 +1,8 @@
{
"permissions": {
"allow": [
"Bash(git add *)",
"Bash(git commit *)"
]
}
}

0
deploy_full.sh Executable file → Normal file
View File

View File

@ -1,4 +1,5 @@
from flask import Blueprint, request, jsonify, current_app from flask import Blueprint, request, jsonify, current_app
from sqlalchemy import or_
from app.services.bom_service import BomService from app.services.bom_service import BomService
from app.models.base import MaterialBase from app.models.base import MaterialBase
from app.models.bom import BomTable from app.models.bom import BomTable
@ -312,12 +313,36 @@ def save_bom_legacy():
@jwt_required() @jwt_required()
@permission_required('bom_manage') @permission_required('bom_manage')
def get_material_base_list(): def get_material_base_list():
"""获取所有基础物料列表,用于前端下拉框""" """获取基础物料列表,支持分页和关键字搜索,用于前端下拉框"""
try: try:
materials = MaterialBase.query.filter_by(is_enabled=True).order_by(MaterialBase.id.desc()).all() # 获取分页和搜索参数
data = [item.to_dict() for item in materials] page = int(request.args.get('page', 1))
# 字段级脱敏 (如果需要,但此接口通常用于下拉选择,可能不需要脱敏) limit = int(request.args.get('limit', 20))
# 保持原样 keyword = request.args.get('keyword', '').strip()
# 构建查询条件
query = MaterialBase.query.filter_by(is_enabled=True)
# 添加关键字模糊搜索
if keyword:
query = query.filter(
or_(
MaterialBase.name.ilike(f"%{keyword}%"),
MaterialBase.spec_model.ilike(f"%{keyword}%")
)
)
# 执行分页查询
pagination = query.order_by(MaterialBase.id.desc()).paginate(
page=page, per_page=limit, error_out=False
)
# 构建返回数据
data = {
'list': [item.to_dict() for item in pagination.items],
'total': pagination.total
}
return jsonify({ return jsonify({
'code': 200, 'code': 200,
'msg': 'success', 'msg': 'success',

View File

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