@ -118,60 +118,63 @@
< el-dialog
v-model = "visible"
: title = "dialogStatus === 'create' ? '新增采购入库' : '编辑入库信息'"
width = "105 0px"
top = "5 vh"
width = "100 0px"
top = "4 vh"
destroy -on -close
:close-on-click-modal = "false"
class = "stylish-dialog"
class = "stylish-dialog compact-layout "
>
< el-form :model = "form" label -width = " 110px " ref = "formRef" :rules = "rules" size = "large" class = "stylish-form " >
< div c lass = "dialog-scroll-container ">
< el-form :model = "form" label -width = " 100px " ref = "formRef" :rules = "rules" size = "default" class = "stylish-form" >
< div class = "form-card basic-card" >
< div class = "card-title" >
< el-icon class = "icon" > < Box / > < / el-icon >
< span > 1. 基础信息 < / span >
< span class = "sub-title" v-if = "dialogStatus === 'create'" > ( 请先搜索选择 物料 ) < / span >
< span class = "sub-title" v-if = "dialogStatus === 'create'" > ( 请先搜索锁定 物料 ) < / span >
< / div >
< div class = "card-content" >
< el-row :gutter = "24" v-if = "dialogStatus === 'create'" style="margin-bottom: 20 px;" >
< el -col :span = "14 " >
< el-row :gutter = "24" v-if = "dialogStatus === 'create'" style="margin-bottom: 15 px;" >
< 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 >
< / 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 >
< div class = "read-only-grid" >
< el-row :gutter = "24 " >
< el-row :gutter = "20 " >
< el-col :span = "8" > < el-form-item label = "名称" > < el-input v-model = "form.material_name" disabled class="is-text-view" / > < / el-form-item > < / el-col >
< el-col :span = "8" > < el-form-item label = "规格型号" > < el-input v-model = "form.spec_model" disabled class="is-text-view" / > < / el-form-item > < / el-col >
< el-col :span = "8" > < el-form-item label = "单位" > < el-input v-model = "form.unit" disabled class="is-text-view" / > < / el-form-item > < / el-col >
@ -189,7 +192,7 @@
< / div >
< div class = "card-content" >
< el-row :gutter = "24 " >
< el-row :gutter = "20 " >
< el-col :span = "6" > < el-form-item label = "编码/SKU" prop = "sku" > < el-input v-model = "form.sku" placeholder="选填" / > < / el-form-item > < / el-col >
< el-col :span = "6" >
< el-form-item label = "入库日期" prop = "in_date" >
@ -202,16 +205,16 @@
< div class = "identity-panel" >
< el-row >
< el-col :span = "24" style = "margin-bottom: 12 px;" >
< el-radio-group v-model = "entryMode" @change="handleEntryModeChange" :disabled="modeLocked" class="custom-radio-group" >
< el-col :span = "24" style = "margin-bottom: 8 px;" >
< el-radio-group v-model = "entryMode" @change="handleEntryModeChange" :disabled="modeLocked" size="small" class="custom-radio-group" >
< el -radio -button label = "batch" > 按批号入库 ( Batch ) < / el-radio-button >
< el-radio-button label = "serial" > 按序列号入库 ( SN ) < / el-radio-button >
< / el-radio-group >
< span v-if = "modeLocked" class="locked-msg" > < el -icon > < Lock / > < / el-icon > 根据历史记录已锁定入库方式 < / span >
< span v-if = "modeLocked" class="locked-msg" > < el -icon > < Lock / > < / el-icon > 历史锁定 < / span >
< / el-col >
< / el-row >
< el-row :gutter = "24 " >
< el-row :gutter = "20 " >
< el-col :span = "12" >
< el-form-item label = "批号" prop = "batch_number" >
< el-input
@ -239,7 +242,7 @@
< / el-row >
< / div >
< el-row :gutter = "24 " style = "margin-top: 15 px;" >
< el-row :gutter = "20 " style = "margin-top: 10 px;" >
< el-col :span = "6" >
< el-form-item label = "入库数量" prop = "in_quantity" >
< el-input-number v-model = "form.in_quantity" :min="1" controls-position="right" style="width:100%" class="strong-input" / >
@ -282,8 +285,23 @@
< div class = "divider-text" > 商务与采购信息 < / div >
< el-row :gutter = "24 " >
< el-col :span = "6" > < el-form-item label = "币种" > < el-input v-model = "form.currency" / > < / el-form-item > < / el-col >
< el-row :gutter = "20 " >
< el-col :span = "6" >
< el-form-item label = "币种" >
< el-autocomplete
v-model = "form.currency"
:fetch-suggestions = "querySearchCurrency"
placeholder = "币种"
style = "width: 100%"
:trigger-on-focus = "true"
>
< template # default = "{ item }" >
< span > { { item . value } } < / span >
< span style = "float:right; color:#999; font-size:12px" > { { item . desc } } < / span >
< / template >
< / el-autocomplete >
< / el-form-item >
< / el-col >
< el-col :span = "6" > < el-form-item label = "汇率" > < el-input-number v-model = "form.exchange_rate" :precision="2" controls-position="right" style="width:100%" /> < / el -form -item > < / el-col >
< el-col :span = "6" >
< el-form-item label = "含税单价" prop = "unit_price" >
@ -297,13 +315,49 @@
< / el-col >
< / el-row >
< el-row :gutter = "24 " >
< el-col :span = "8" > < el-form-item label = "供应商" > < el-input v-model = "form.supplier_name" placeholder="供应商全称" / > < / el-form-item > < / el-col >
< el-col :span = "8" > < el-form-item label = "采购人 " > < el-input v-model = "form.purchaser" / > < / el-form-item > < / el-col >
< el-col :span = "8" > < el-form-item label = "采购邮箱" > < el-input v-model = "form.purchaser_email" / > < / el-form-item > < / el-col >
< el-row :gutter = "20 " >
< el-col :span = "8" >
< el-form-item label = "供应商 " >
< el-autocomplete
v-model = "form.supplier_name"
:fetch-suggestions = "querySearchSupplier"
placeholder = "输入或选择供应商"
style = "width: 100%"
clearable
:trigger-on-focus = "true"
@select ="handleSupplierSelect"
/ >
< / el-form-item >
< / el-col >
< el-col :span = "8" >
< el-form-item label = "采购人" >
< el-autocomplete
v-model = "form.purchaser"
:fetch-suggestions = "querySearchPurchaser"
placeholder = "输入采购人"
style = "width: 100%"
clearable
:trigger-on-focus = "true"
@select ="handlePurchaserSelect"
/ >
< / el-form-item >
< / el-col >
< el-col :span = "8" >
< el-form-item label = "采购邮箱" >
< el-autocomplete
v-model = "form.purchaser_email"
:fetch-suggestions = "querySearchEmail"
placeholder = "输入邮箱"
style = "width: 100%"
clearable
:trigger-on-focus = "true"
@select ="handleEmailSelect"
/ >
< / el-form-item >
< / el-col >
< / el-row >
< el-row :gutter = "24 " >
< el-row :gutter = "20 " >
< el-col :span = "12" > < el-form-item label = "原始链接" > < el-input v-model = "form.source_link" placeholder="http://" / > < / el-form-item > < / el-col >
< el-col :span = "12" > < el-form-item label = "详情链接" > < el-input v-model = "form.detail_link" placeholder="http://" / > < / el-form-item > < / el-col >
< / el-row >
@ -311,6 +365,7 @@
< / div >
< / el-form >
< / div >
< template # footer >
< div class = "dialog-footer" >
@ -392,7 +447,7 @@ const stockColumns = [
const allColumns = [ ... baseColumns , ... stockColumns ]
// 表头持久化
const STORAGE _KEY = 'stock_buy_visible_columns'
const STORAGE _KEY _COLS = 'stock_buy_visible_columns'
const defaultColumns = [
'material_name' , 'category' , 'material_type' , 'spec_model' , 'unit' ,
'inbound_date' , 'serial_number' , 'batch_number' , 'status' , 'inspection_status' ,
@ -401,7 +456,7 @@ const defaultColumns = [
const getSavedColumns = ( ) => {
try {
const saved = localStorage . getItem ( STORAGE _KEY )
const saved = localStorage . getItem ( STORAGE _KEY _COLS )
return saved ? JSON . parse ( saved ) : defaultColumns
} catch ( e ) {
return defaultColumns
@ -411,7 +466,7 @@ const getSavedColumns = () => {
const visibleColumnProps = ref ( getSavedColumns ( ) )
watch ( visibleColumnProps , ( newVal ) => {
localStorage . setItem ( STORAGE _KEY , JSON . stringify ( newVal ) )
localStorage . setItem ( STORAGE _KEY _COLS , JSON . stringify ( newVal ) )
} , { deep : true } )
@ -429,6 +484,163 @@ const form = reactive({
source _link : '' , detail _link : '' , arrival _photo : ''
} )
// ------------------------------------
// 历史记录管理器 (Local Storage)
// ------------------------------------
const HISTORY _KEYS = {
SUPPLIER : 'history_suppliers' ,
PURCHASER : 'history_purchasers' ,
EMAIL : 'history_emails' ,
MATERIAL : 'history_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 ) : [ ]
// 必须保存完整对象,因为下拉框需要显示 name, spec 等
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 ) ) // 表格数据优先( 虽然value一样没区别)
const allList = Array . from ( map . values ( ) )
const results = queryString ? allList . filter ( createFilter ( queryString ) ) : allList
cb ( results )
}
// 1. 供应商
const querySearchSupplier = ( qs : string , cb : any ) => mixedSearch ( qs , 'supplier_name' , HISTORY _KEYS . SUPPLIER , cb )
const handleSupplierSelect = ( item : any ) => saveToHistory ( HISTORY _KEYS . SUPPLIER , item . value )
// 2. 采购人
const querySearchPurchaser = ( qs : string , cb : any ) => mixedSearch ( qs , 'purchaser' , HISTORY _KEYS . PURCHASER , cb )
const handlePurchaserSelect = ( item : any ) => saveToHistory ( HISTORY _KEYS . PURCHASER , item . value )
// 3. 邮箱
const querySearchEmail = ( qs : string , cb : any ) => mixedSearch ( qs , 'purchaser_email' , HISTORY _KEYS . EMAIL , cb )
const handleEmailSelect = ( item : any ) => saveToHistory ( HISTORY _KEYS . EMAIL , item . value )
// 4. 币种 (固定+过滤)
const currencyOptions = [
{ value : 'CNY' , desc : '人民币' } ,
{ value : 'USD' , desc : '美元' } ,
{ value : 'EUR' , desc : '欧元' }
]
const querySearchCurrency = ( queryString : string , cb : any ) => {
const results = queryString ? currencyOptions . filter ( createFilter ( queryString ) ) : currencyOptions
cb ( results )
}
// ------------------------------------
// 物料搜索逻辑 (支持历史回显 + API)
// ------------------------------------
// 下拉框展开时触发
const handleMaterialDropdownVisible = ( visible : boolean ) => {
if ( visible ) {
// 只有当列表为空时( 即没有进行API搜索时) , 才填充历史记录
// 这样不会覆盖用户正在搜的结果
if ( materialOptions . value . length === 0 ) {
materialOptions . value = getMaterialHistory ( )
}
}
}
const handleSearchMaterial = async ( query : string ) => {
if ( query ) {
searchLoading . value = true
try {
const res : any = await searchMaterialBase ( query )
// 给 API 返回的数据加个标记,区分历史
const apiResults = ( res . data || [ ] ) . map ( ( i : any ) => ( { ... i , isHistory : false } ) )
materialOptions . value = apiResults
} finally {
searchLoading . value = false
}
} else {
// 搜索词清空时,恢复历史记录
materialOptions . value = getMaterialHistory ( )
}
}
// 选中物料后 -> 存入历史 -> 填充表单
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
form . unit = item . unit
form . material _type = item . type
checkHistoryAndSetMode ( item . id )
}
}
// ------------------------------------
// 逻辑校验规则
// ------------------------------------
@ -521,32 +733,6 @@ 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
}
} else {
materialOptions . value = [ ]
}
}
const onMaterialSelected = ( val : number ) => {
const item = materialOptions . value . find ( i => i . id === val )
if ( item ) {
form . material _name = item . name
form . spec _model = item . spec
form . category = item . category
form . unit = item . unit
form . material _type = item . type
checkHistoryAndSetMode ( item . id )
}
}
watch ( ( ) => [ form . in _quantity , form . unit _price ] , ( ) => {
form . total _price = Number ( ( form . in _quantity * form . unit _price ) . toFixed ( 4 ) )
} )
@ -568,6 +754,8 @@ const handleCreate = () => {
entryMode . value = 'batch'
form . batch _number = ''
visible . value = true
// 每次打开弹窗时,先清空选项,让下拉时触发“历史加载”
materialOptions . value = [ ]
}
const handleUpdate = ( row : any ) => {
@ -614,6 +802,14 @@ const handleUpdate = (row: any) => {
form . detail _link = row . detail _link
form . arrival _photo = row . arrival _photo
// 编辑模式下,把当前物料塞入选项,防止显示为 ID
materialOptions . value = [ {
id : row . base _id ,
name : row . material _name ,
spec : row . spec _model ,
category : row . category
} ]
visible . value = true
}
@ -635,6 +831,12 @@ const submitForm = async () => {
await updateBuyInbound ( form . id ! , payload )
ElMessage . success ( '更新成功' )
}
// 核心修改:提交成功时,保存供应商等信息到历史记录
saveToHistory ( HISTORY _KEYS . SUPPLIER , form . supplier _name )
saveToHistory ( HISTORY _KEYS . PURCHASER , form . purchaser )
saveToHistory ( HISTORY _KEYS . EMAIL , form . purchaser _email )
await fetchData ( )
visible . value = false
} catch ( e : any ) {
@ -688,7 +890,7 @@ onMounted(() => fetchData())
< style scoped >
/* 全局布局 */
. buy - module {
background : # f5f7fa ; /* 整体背景微灰,突出内容 */
background : # f5f7fa ;
padding : 20 px ;
min - height : 100 vh ;
}
@ -727,21 +929,34 @@ onMounted(() => fetchData())
. avail - num { font - weight : bold ; color : # 67 C23A ; font - size : 15 px ; }
. sum - tag { margin - left : 4 px ; transform : scale ( 0.9 ) ; }
/* 弹窗与表单美化 */
/* 弹窗核心样式调整 */
: deep ( . el - dialog _ _body ) {
padding : 0 ;
overflow : hidden ;
}
/* 内部滚动容器 */
. dialog - scroll - container {
padding : 15 px 20 px ;
max - height : 70 vh ;
overflow - y : auto ;
overflow - x : hidden ;
}
. stylish - form . form - card {
background : # fff ;
border - radius : 8 px ;
border : 1 px solid # e4e7ed ;
margin - bottom : 20 px ;
margin - bottom : 15 px ;
overflow : hidden ;
}
. card - title {
background : # fcfcfc ;
padding : 12 px 20 px ;
padding : 10 px 20 px ;
border - bottom : 1 px solid # ebeef5 ;
font - weight : 600 ;
font - size : 15 px ;
font - size : 14 px ;
color : # 303133 ;
display : flex ;
align - items : center ;
@ -749,10 +964,11 @@ onMounted(() => fetchData())
. card - title . icon { margin - right : 8 px ; font - size : 18 px ; color : # 409 EFF ; }
. card - title . sub - title { font - size : 12 px ; color : # 909399 ; font - weight : normal ; margin - left : 10 px ; }
. card - content { padding : 20 px ; }
. card - content { padding : 15 px 20px ; }
/* 基础信息卡片 (蓝色调,示读) */
/* 基础信息卡片 */
. basic - card { border - left : 4 px solid # 409 EFF ; }
. search - tip { color : # 909399 ; font - size : 12 px ; margin - left : 10 px ; display : flex ; align - items : center ; gap : 4 px ; }
. is - text - view : deep ( . el - input _ _wrapper ) {
box - shadow : none ! important ;
background - color : # f5f7fa ;
@ -760,7 +976,7 @@ onMounted(() => fetchData())
border - radius : 0 ;
padding - left : 0 ;
}
. is - text - view : deep ( . el - input _ _inner ) { color : # 606266 ; font - weight : 500 ; }
. is - text - view : deep ( . el - input _ _inner ) { color : # 606266 ; font - weight : 500 ; font - size : 13 px ; }
/* 入库信息卡片 */
. inbound - card { border - left : 4 px solid # 67 C23A ; }
@ -770,8 +986,8 @@ onMounted(() => fetchData())
background : # fffbf0 ;
border : 1 px dashed # e6a23c ;
border - radius : 6 px ;
padding : 15 px ;
margin - bottom : 20 px ;
padding : 12 px ;
margin - bottom : 15 px ;
}
. custom - radio - group { margin - bottom : 10 px ; }
. locked - msg { font - size : 12 px ; color : # e6a23c ; margin - left : 15 px ; }
@ -784,9 +1000,9 @@ onMounted(() => fetchData())
display : flex ;
align - items : center ;
text - align : center ;
margin : 3 0px 0 20 px ;
margin : 2 0px 0 15 px ;
color : # 909399 ;
font - size : 14 px ;
font - size : 13 px ;
font - weight : 500 ;
}
. divider - text : : before , . divider - text : : after {
@ -798,10 +1014,17 @@ onMounted(() => fetchData())
. divider - text : : after { margin - left : 15 px ; }
/* 底部按钮 */
. dialog - footer { display : flex ; justify - content : flex - end ; gap : 15 px ; margin - top : 20 px ; }
. info - alert { font - size : 12 px ; color : # 909399 ; margin - top : 10 px ; display : flex ; align - items : center ; gap : 5 px ; }
. option - item { display : flex ; justify - content : space - between ; width : 100 % ; }
. dialog - footer {
display : flex ;
justify - content : flex - end ;
gap : 15 px ;
padding : 15 px 20 px ;
background : # fff ;
border - top : 1 px solid # ebeef5 ;
}
. option - item { display : flex ; justify - content : space - between ; width : 100 % ; align - items : center ; }
. opt - name { font - weight : bold ; }
. opt - spec { color : # 8492 a6 ; font - size : 13 px ; }
. opt - spec { color : # 8492 a6 ; font - size : 13 px ; margin - right : 10 px ; }
. total - price - input : deep ( . el - input _ _inner ) { color : # F56C6C ; font - weight : bold ; }
< / style >