Compare commits
3 Commits
5fe645dc0b
...
f738b3b3af
| Author | SHA1 | Date | |
|---|---|---|---|
| f738b3b3af | |||
| 321ef8c7bd | |||
| b12a91a763 |
8
.qwen/settings.json
Normal file
8
.qwen/settings.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"Bash(git add *)",
|
||||||
|
"Bash(git commit *)"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
0
deploy_full.sh
Executable file → Normal file
0
deploy_full.sh
Executable file → Normal 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',
|
||||||
|
|||||||
@ -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>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user