三个基础入库页面修改新增弹窗内容展示,下拉框以及弹窗屏幕大小自适应性

This commit is contained in:
dxc
2026-01-30 12:58:19 +08:00
parent 0009fe3121
commit a1133aac94
2 changed files with 317 additions and 34 deletions

View File

@ -71,15 +71,37 @@
<div class="card-title"><el-icon class="icon"><Box /></el-icon><span>1. 基础信息</span></div>
<div class="card-content">
<el-row :gutter="24" v-if="dialogStatus === 'create'" style="margin-bottom: 20px;">
<el-col :span="14">
<el-col :span="10">
<el-form-item label="物料搜索" prop="base_id">
<el-select v-model="form.base_id" filterable remote reserve-keyword placeholder="搜名称/规格..." :remote-method="handleSearchMaterial" :loading="searchLoading" style="width: 100%" @change="onMaterialSelected">
<el-select
v-model="form.base_id"
filterable
remote
reserve-keyword
placeholder="搜名称/规格..."
:remote-method="handleSearchMaterial"
@visible-change="handleMaterialDropdownVisible"
:loading="searchLoading"
style="width: 100%"
@change="onMaterialSelected"
default-first-option
>
<el-option v-for="item in materialOptions" :key="item.id" :label="item.name" :value="item.id">
<div class="option-item"><span class="opt-name">{{ item.name }}</span><span class="opt-spec">{{ item.spec }}</span></div>
<div class="option-item">
<span class="opt-name">{{ item.name }}</span>
<span class="opt-spec">{{ item.spec }}</span>
<el-tag v-if="item.isHistory" size="small" type="info" effect="plain">历史</el-tag>
<el-tag v-else size="small" type="success" effect="plain">系统</el-tag>
</div>
</el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="14" style="display: flex; align-items: center;">
<span class="search-tip">
<el-icon><InfoFilled /></el-icon> 未输入时展示最新物料输入关键词进行精确搜索
</span>
</el-col>
</el-row>
<div class="read-only-grid">
<el-row :gutter="24">
@ -140,7 +162,19 @@
</el-row>
<el-row :gutter="24">
<el-col :span="8"><el-form-item label="订单号"><el-input v-model="form.order_id" placeholder="关联销售订单" /></el-form-item></el-col>
<el-col :span="8"><el-form-item label="负责人"><el-input v-model="form.production_manager" /></el-form-item></el-col>
<el-col :span="8">
<el-form-item label="负责人">
<el-autocomplete
v-model="form.production_manager"
:fetch-suggestions="querySearchManager"
placeholder="输入或选择负责人"
style="width: 100%"
clearable
:trigger-on-focus="true"
@select="handleManagerSelect"
/>
</el-form-item>
</el-col>
<el-col :span="8"><el-form-item label="产品定价"><el-input-number v-model="form.sale_price" :precision="2" style="width:100%"><template #prefix>¥</template></el-input-number></el-form-item></el-col>
</el-row>
@ -172,7 +206,7 @@
<script setup lang="ts">
import { ref, reactive, onMounted, watch } from 'vue'
import { Plus, Setting, Refresh, Search, Box, House, Link } from '@element-plus/icons-vue'
import { Plus, Setting, Refresh, Search, Box, House, Link, InfoFilled } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import dayjs from 'dayjs'
// 引用更新后的 product.ts
@ -228,6 +262,87 @@ const rules = {
in_quantity: [{ required: true, message: '必填', trigger: 'blur' }]
}
// ------------------------------------
// 历史记录管理器 (Local Storage)
// ------------------------------------
const HISTORY_KEYS = {
PRODUCTION_MANAGER: 'history_product_managers',
MATERIAL: 'history_product_materials'
}
// 保存历史 (String 类型)
const saveToHistory = (key: string, value: string) => {
if (!value) return
try {
const existing = localStorage.getItem(key)
let list = existing ? JSON.parse(existing) : []
list = list.filter((i: string) => i !== value)
list.unshift(value)
if (list.length > 20) list = list.slice(0, 20)
localStorage.setItem(key, JSON.stringify(list))
} catch (e) { console.error('save history failed', e) }
}
// 获取历史 (String 类型)
const getHistoryList = (key: string): any[] => {
try {
const existing = localStorage.getItem(key)
const list = existing ? JSON.parse(existing) : []
return list.map((v: string) => ({ value: v }))
} catch (e) { return [] }
}
// 保存物料历史 (Object 类型)
const saveMaterialHistory = (item: any) => {
if (!item || !item.id) return
const key = HISTORY_KEYS.MATERIAL
try {
const existing = localStorage.getItem(key)
let list = existing ? JSON.parse(existing) : []
list = list.filter((i: any) => i.id !== item.id)
list.unshift({ ...item, isHistory: true })
if (list.length > 10) list = list.slice(0, 10)
localStorage.setItem(key, JSON.stringify(list))
} catch (e) {}
}
const getMaterialHistory = () => {
try {
const existing = localStorage.getItem(HISTORY_KEYS.MATERIAL)
return existing ? JSON.parse(existing) : []
} catch (e) { return [] }
}
// ------------------------------------
// Autocomplete 建议逻辑 (混合模式)
// ------------------------------------
const createFilter = (queryString: string) => {
return (item: any) => {
return (item.value.toLowerCase().indexOf(queryString.toLowerCase()) === 0)
}
}
const getTableDataUnique = (field: string) => {
const uniqueItems = Array.from(new Set(tableData.value.map((i: any) => i[field]).filter(Boolean)))
return uniqueItems.map(i => ({ value: i }))
}
const mixedSearch = (queryString: string, tableField: string, storageKey: string, cb: any) => {
const tableList = getTableDataUnique(tableField)
const historyList = getHistoryList(storageKey)
const map = new Map()
historyList.forEach(i => map.set(i.value, i))
tableList.forEach(i => map.set(i.value, i))
const allList = Array.from(map.values())
const results = queryString ? allList.filter(createFilter(queryString)) : allList
cb(results)
}
// 1. 负责人
const querySearchManager = (qs: string, cb: any) => mixedSearch(qs, 'production_manager', HISTORY_KEYS.PRODUCTION_MANAGER, cb)
const handleManagerSelect = (item: any) => saveToHistory(HISTORY_KEYS.PRODUCTION_MANAGER, item.value)
const fetchData = async () => {
loading.value = true
try {
@ -237,19 +352,37 @@ const fetchData = async () => {
} finally { loading.value = false }
}
const handleSearchMaterial = async (query: string) => {
if (query) {
searchLoading.value = true
try {
const res: any = await searchMaterialBase(query)
materialOptions.value = res.data || []
} finally { searchLoading.value = false }
// ------------------------------------
// 物料搜索逻辑 (优化)
// ------------------------------------
const handleMaterialDropdownVisible = (visible: boolean) => {
if (visible) {
if (materialOptions.value.length === 0) {
handleSearchMaterial('')
}
}
}
const handleSearchMaterial = async (query: string) => {
searchLoading.value = true
try {
const res: any = await searchMaterialBase(query)
const apiResults = (res.data || []).map((i: any) => ({ ...i, isHistory: false }))
if (!query) {
const history = getMaterialHistory()
const historyIds = new Set(history.map((h: any) => h.id))
const filteredApi = apiResults.filter((apiItem: any) => !historyIds.has(apiItem.id))
materialOptions.value = [...history, ...filteredApi]
} else {
materialOptions.value = apiResults
}
} finally { searchLoading.value = false }
}
const onMaterialSelected = (val: number) => {
const item = materialOptions.value.find(i => i.id === val)
if (item) {
saveMaterialHistory(item)
form.material_name = item.name
form.spec_model = item.spec
form.material_type = item.type
@ -261,6 +394,7 @@ const handleCreate = () => {
resetForm()
form.in_date = dayjs().format('YYYY-MM-DD')
visible.value = true
materialOptions.value = []
}
const handleUpdate = (row: any) => {
@ -272,6 +406,13 @@ const handleUpdate = (row: any) => {
} else {
form.production_time_range = []
}
// 编辑模式下填充当前物料
materialOptions.value = [{
id: row.base_id,
name: row.material_name,
spec: row.spec_model,
isHistory: false
}]
visible.value = true
}
@ -286,6 +427,10 @@ const submitForm = async () => {
}
if(dialogStatus.value === 'create') await createProductInbound(payload)
else await updateProductInbound(form.id!, payload)
// 保存历史
saveToHistory(HISTORY_KEYS.PRODUCTION_MANAGER, form.production_manager)
ElMessage.success('操作成功')
visible.value = false
fetchData()
@ -334,7 +479,9 @@ onMounted(() => fetchData())
.production-card { border-left: 4px solid #E6A23C; }
.identity-panel { background: #fffbf0; border: 1px dashed #e6a23c; padding: 15px; margin: 10px 0; border-radius: 6px; }
.prefix-tag.sn { color: #409EFF; background: #ecf5ff; padding: 0 5px; font-weight: bold; }
.option-item { display: flex; justify-content: space-between; width: 100%; }
.opt-spec { color: #8492a6; font-size: 12px; }
.option-item { display: flex; justify-content: space-between; width: 100%; align-items: center; }
.opt-name { font-weight: bold; }
.opt-spec { color: #8492a6; font-size: 12px; margin-right: 10px; }
.is-text-view :deep(.el-input__wrapper) { box-shadow: none !important; background: #f5f7fa; border-bottom: 1px solid #dcdfe6; }
.search-tip { color: #909399; font-size: 12px; margin-left: 10px; display: flex; align-items: center; gap: 4px; }
</style>

View File

@ -141,38 +141,42 @@
<div class="card-content">
<el-row :gutter="24" v-if="dialogStatus === 'create'" style="margin-bottom: 20px;">
<el-col :span="14">
<el-col :span="10">
<el-form-item label="物料搜索" prop="base_id" class="highlight-label">
<el-select
v-model="form.base_id"
filterable
remote
reserve-keyword
placeholder="输入名称 / 规格型号进行模糊搜索..."
placeholder="输入名称或规格..."
:remote-method="handleSearchMaterial"
@visible-change="handleMaterialDropdownVisible"
:loading="searchLoading"
style="width: 100%"
@change="onMaterialSelected"
size="large"
default-first-option
>
<el-option
v-for="item in materialOptions"
:key="item.id"
:label="item.name + ' [' + item.spec + ']'"
:label="item.name"
:value="item.id"
>
<div class="option-item">
<span class="opt-name">{{ item.name }}</span>
<span class="opt-spec">{{ item.spec }}</span>
<el-tag v-if="item.isHistory" size="small" type="info" effect="plain">历史</el-tag>
<el-tag v-else size="small" type="success" effect="plain">系统</el-tag>
</div>
</el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="10">
<div class="info-alert">
<el-icon><InfoFilled /></el-icon> 仅展示状态为启用的基础物料
</div>
<el-col :span="14" style="display: flex; align-items: center;">
<span class="search-tip">
<el-icon><InfoFilled /></el-icon> 未输入时展示最新物料输入关键词进行精确搜索
</span>
</el-col>
</el-row>
@ -303,7 +307,19 @@
</el-row>
<el-row :gutter="24">
<el-col :span="8"><el-form-item label="生产负责人"><el-input v-model="form.production_manager" /></el-form-item></el-col>
<el-col :span="8">
<el-form-item label="生产负责人">
<el-autocomplete
v-model="form.production_manager"
:fetch-suggestions="querySearchManager"
placeholder="输入或选择负责人"
style="width: 100%"
clearable
:trigger-on-focus="true"
@select="handleManagerSelect"
/>
</el-form-item>
</el-col>
<el-col :span="16">
<el-form-item label="生产时间">
<el-date-picker
@ -481,6 +497,93 @@ const form = reactive({
detail_link: ''
})
// ------------------------------------
// 历史记录管理器 (Local Storage)
// ------------------------------------
const HISTORY_KEYS = {
PRODUCTION_MANAGER: 'history_production_managers',
MATERIAL: 'history_semi_materials'
}
// 保存历史 (String 类型)
const saveToHistory = (key: string, value: string) => {
if (!value) return
try {
const existing = localStorage.getItem(key)
let list = existing ? JSON.parse(existing) : []
// 移除旧的,添加到前面
list = list.filter((i: string) => i !== value)
list.unshift(value)
if (list.length > 20) list = list.slice(0, 20) // 最多存20条
localStorage.setItem(key, JSON.stringify(list))
} catch (e) { console.error('save history failed', e) }
}
// 获取历史 (String 类型)
const getHistoryList = (key: string): any[] => {
try {
const existing = localStorage.getItem(key)
const list = existing ? JSON.parse(existing) : []
return list.map((v: string) => ({ value: v }))
} catch (e) { return [] }
}
// 保存物料历史 (Object 类型)
const saveMaterialHistory = (item: any) => {
if (!item || !item.id) return
const key = HISTORY_KEYS.MATERIAL
try {
const existing = localStorage.getItem(key)
let list = existing ? JSON.parse(existing) : []
list = list.filter((i: any) => i.id !== item.id)
list.unshift({ ...item, isHistory: true })
if (list.length > 10) list = list.slice(0, 10)
localStorage.setItem(key, JSON.stringify(list))
} catch (e) {}
}
const getMaterialHistory = () => {
try {
const existing = localStorage.getItem(HISTORY_KEYS.MATERIAL)
return existing ? JSON.parse(existing) : []
} catch (e) { return [] }
}
// ------------------------------------
// Autocomplete 建议逻辑 (混合模式:历史+当前表格)
// ------------------------------------
const createFilter = (queryString: string) => {
return (item: any) => {
return (item.value.toLowerCase().indexOf(queryString.toLowerCase()) === 0)
}
}
// 辅助函数:从当前表格提取
const getTableDataUnique = (field: string) => {
const uniqueItems = Array.from(new Set(tableData.value.map((i: any) => i[field]).filter(Boolean)))
return uniqueItems.map(i => ({ value: i }))
}
// 通用查询: 历史记录 + 当前页面数据
const mixedSearch = (queryString: string, tableField: string, storageKey: string, cb: any) => {
const tableList = getTableDataUnique(tableField)
const historyList = getHistoryList(storageKey)
// 合并去重
const map = new Map()
historyList.forEach(i => map.set(i.value, i))
tableList.forEach(i => map.set(i.value, i))
const allList = Array.from(map.values())
const results = queryString ? allList.filter(createFilter(queryString)) : allList
cb(results)
}
// 1. 生产负责人
const querySearchManager = (qs: string, cb: any) => mixedSearch(qs, 'production_manager', HISTORY_KEYS.PRODUCTION_MANAGER, cb)
const handleManagerSelect = (item: any) => saveToHistory(HISTORY_KEYS.PRODUCTION_MANAGER, item.value)
// ------------------------------------
// 逻辑校验规则
// ------------------------------------
@ -573,23 +676,41 @@ const handleEntryModeChange = (val: string) => {
}
}
const handleSearchMaterial = async (query: string) => {
if (query) {
searchLoading.value = true
try {
const res: any = await searchMaterialBase(query)
materialOptions.value = res.data || []
} finally {
searchLoading.value = false
// ------------------------------------
// 物料搜索逻辑 (优化:支持空查询加载默认值)
// ------------------------------------
const handleMaterialDropdownVisible = (visible: boolean) => {
if (visible) {
if (materialOptions.value.length === 0) {
handleSearchMaterial('')
}
} else {
materialOptions.value = []
}
}
const handleSearchMaterial = async (query: string) => {
searchLoading.value = true
try {
const res: any = await searchMaterialBase(query)
const apiResults = (res.data || []).map((i: any) => ({ ...i, isHistory: false }))
if (!query) {
const history = getMaterialHistory()
const historyIds = new Set(history.map((h: any) => h.id))
const filteredApi = apiResults.filter((apiItem: any) => !historyIds.has(apiItem.id))
materialOptions.value = [...history, ...filteredApi]
} else {
materialOptions.value = apiResults
}
} finally {
searchLoading.value = false
}
}
const onMaterialSelected = (val: number) => {
const item = materialOptions.value.find(i => i.id === val)
if (item) {
saveMaterialHistory(item)
form.material_name = item.name
form.spec_model = item.spec
form.category = item.category
@ -621,6 +742,8 @@ const handleCreate = () => {
entryMode.value = 'batch'
form.batch_number = ''
visible.value = true
// 每次打开弹窗时,先清空选项,让下拉时触发“历史加载”
materialOptions.value = []
}
const handleUpdate = (row: any) => {
@ -673,6 +796,14 @@ const handleUpdate = (row: any) => {
form.quality_report_link = row.quality_report_link
form.detail_link = row.detail_link
// 编辑模式下,把当前物料塞入选项,防止显示为 ID
materialOptions.value = [{
id: row.base_id,
name: row.material_name,
spec: row.spec_model,
category: row.category
}]
visible.value = true
}
@ -700,6 +831,10 @@ const submitForm = async () => {
await updateSemiInbound(form.id!, payload)
ElMessage.success('更新成功')
}
// 保存生产负责人信息到历史记录
saveToHistory(HISTORY_KEYS.PRODUCTION_MANAGER, form.production_manager)
await fetchData()
visible.value = false
} catch (e: any) {
@ -824,6 +959,7 @@ onMounted(() => fetchData())
/* 基础信息卡片 (蓝色调,示读) */
.basic-card { border-left: 4px solid #409EFF; }
.search-tip { color: #909399; font-size: 12px; margin-left: 10px; display: flex; align-items: center; gap: 4px; }
.is-text-view :deep(.el-input__wrapper) {
box-shadow: none !important;
background-color: #f5f7fa;
@ -876,8 +1012,8 @@ onMounted(() => fetchData())
/* 底部按钮 */
.dialog-footer { display: flex; justify-content: flex-end; gap: 15px; margin-top: 20px; }
.info-alert { font-size: 12px; color: #909399; margin-top: 10px; display: flex; align-items: center; gap: 5px; }
.option-item { display: flex; justify-content: space-between; width: 100%; }
.option-item { display: flex; justify-content: space-between; width: 100%; align-items: center;}
.opt-name { font-weight: bold; }
.opt-spec { color: #8492a6; font-size: 13px; }
.opt-spec { color: #8492a6; font-size: 13px; margin-right: 10px; }
.total-price-input :deep(.el-input__inner) { color: #F56C6C; font-weight: bold; }
</style>