Files
KCGL/inventory-web/src/views/stock/inbound/semi.vue

1139 lines
41 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="semi-module">
<div class="header-tools">
<div class="left-tools">
<el-input
v-model="queryParams.keyword"
placeholder="🔍 搜索物料 / 批号 / SN / 工单号 / BOM编号..."
class="search-input"
clearable
@clear="fetchData"
@keyup.enter="fetchData"
>
<template #append>
<el-button :icon="Search" @click="fetchData" />
</template>
</el-input>
</div>
<div class="right-tools">
<el-button type="primary" :icon="Plus" @click="handleCreate" class="action-btn">半成品入库登记</el-button>
<el-button :icon="Refresh" @click="fetchData" class="action-btn">刷新</el-button>
<el-popover placement="bottom-end" title="列配置" :width="500" trigger="click">
<template #reference>
<el-button :icon="Setting" class="action-btn">表头</el-button>
</template>
<el-checkbox-group v-model="visibleColumnProps" class="column-selector">
<div class="col-group-title">基础信息</div>
<el-row :gutter="10">
<el-col :span="12" v-for="c in baseColumns" :key="c.prop"><el-checkbox :label="c.prop">{{ c.label }}</el-checkbox></el-col>
</el-row>
<div class="col-group-title" style="margin-top:10px">生产与库存</div>
<el-row :gutter="10">
<el-col :span="12" v-for="c in stockColumns" :key="c.prop"><el-checkbox :label="c.prop">{{ c.label }}</el-checkbox></el-col>
</el-row>
</el-checkbox-group>
</el-popover>
</div>
</div>
<el-table
v-loading="loading"
:data="tableData"
border
stripe
style="width: 100%"
class="modern-table"
highlight-current-row
header-cell-class-name="table-header-gray"
>
<template v-for="col in allColumns" :key="col.prop">
<el-table-column
v-if="visibleColumnProps.includes(col.prop)"
:prop="col.prop"
:label="col.label"
:min-width="col.minWidth || '140'"
show-overflow-tooltip
>
<template #default="scope" v-if="['serial_number', 'batch_number'].includes(col.prop)">
<span v-if="scope.row[col.prop]" :class="col.prop === 'serial_number' ? 'tag-sn' : 'tag-bn'">
{{ scope.row[col.prop] }}
</span>
<span v-else class="text-placeholder">-</span>
</template>
<template #default="scope" v-else-if="col.prop === 'qty_stock'">
<span class="stock-num">{{ scope.row.sum_stock }}</span>
<el-tag size="small" type="info" effect="plain" class="sum-tag"></el-tag>
</template>
<template #default="scope" v-else-if="col.prop === 'qty_available'">
<span class="avail-num">{{ scope.row.sum_available }}</span>
<el-tag size="small" type="info" effect="plain" class="sum-tag"></el-tag>
</template>
<template #default="scope" v-else-if="col.prop === 'status'">
<el-tag :type="getStatusType(scope.row.status)" effect="light" round>
{{ scope.row.status }}
</el-tag>
</template>
<template #default="scope" v-else-if="col.prop === 'quality_status'">
<el-tag :type="getQualityType(scope.row.quality_status)" effect="dark" size="small">
{{ scope.row.quality_status }}
</el-tag>
</template>
<template #default="scope" v-else-if="['quality_report_link', 'detail_link'].includes(col.prop)">
<el-link v-if="scope.row[col.prop]" type="primary" :href="scope.row[col.prop]" target="_blank" :underline="false">
<el-icon><Link /></el-icon> 查看
</el-link>
</template>
<template #default="scope" v-else-if="['raw_material_cost', 'manual_cost'].includes(col.prop)">
<span class="money-text">{{ formatMoney(scope.row[col.prop]) }}</span>
</template>
</el-table-column>
</template>
<el-table-column label="操作" width="220" fixed="right" align="center">
<template #default="{ row }">
<el-button link type="warning" size="default" @click="handlePrint(row)">
<el-icon><Printer /></el-icon> 打印
</el-button>
<el-button link type="primary" size="default" @click="handleUpdate(row)">编辑</el-button>
<el-popconfirm title="确定删除该条记录吗?不可恢复。" @confirm="handleDelete(row)" width="220">
<template #reference>
<el-button link type="danger" size="default">删除</el-button>
</template>
</el-popconfirm>
</template>
</el-table-column>
</el-table>
<el-pagination
class="pagination-bar"
v-model:current-page="queryParams.page"
v-model:page-size="queryParams.pageSize"
:total="total"
:page-sizes="[15, 30, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
background
@size-change="fetchData"
@current-change="fetchData"
/>
<el-dialog
v-model="visible"
:title="dialogStatus === 'create' ? '新增半成品入库' : '编辑半成品信息'"
width="1050px"
top="5vh"
destroy-on-close
:close-on-click-modal="false"
class="stylish-dialog"
>
<el-form :model="form" label-width="110px" ref="formRef" :rules="rules" size="large" 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>
</div>
<div class="card-content">
<el-row :gutter="24" v-if="dialogStatus === 'create'" style="margin-bottom: 20px;">
<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="输入名称或规格..."
: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"
: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="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-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>
<el-col :span="8"><el-form-item label="类别"><el-input v-model="form.category" disabled class="is-text-view" /></el-form-item></el-col>
<el-col :span="8"><el-form-item label="类型"><el-input v-model="form.material_type" disabled class="is-text-view" /></el-form-item></el-col>
</el-row>
</div>
</div>
</div>
<div class="form-card inbound-card">
<div class="card-title">
<el-icon class="icon"><House /></el-icon>
<span>2. 入库详情</span>
</div>
<div class="card-content">
<el-row :gutter="24">
<el-col :span="6">
<el-form-item label="编码/SKU" prop="sku">
<el-input v-model="form.sku" placeholder="系统自动生成" disabled />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="入库日期" prop="in_date">
<el-date-picker v-model="form.in_date" type="date" value-format="YYYY-MM-DD" style="width:100%" disabled />
</el-form-item>
</el-col>
<el-col :span="6"><el-form-item label="条码" prop="barcode"><el-input v-model="form.barcode" placeholder="扫描条码" /></el-form-item></el-col>
<el-col :span="6"><el-form-item label="库位" prop="warehouse_location"><el-input v-model="form.warehouse_location" placeholder="例如: B-01-01" /></el-form-item></el-col>
</el-row>
<div class="identity-panel">
<el-row>
<el-col :span="24" style="margin-bottom: 12px;">
<el-radio-group v-model="entryMode" @change="handleEntryModeChange" :disabled="modeLocked" 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>
</el-col>
</el-row>
<el-row :gutter="24">
<el-col :span="12">
<el-form-item label="批号" prop="batch_number">
<el-input
v-model="form.batch_number"
:placeholder="entryMode === 'batch' ? '系统生成...' : '不可用'"
:disabled="entryMode === 'serial'"
clearable
>
<template #prefix><span class="prefix-tag bn">BN</span></template>
</el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="序列号" prop="serial_number">
<el-input
v-model="form.serial_number"
:placeholder="entryMode === 'serial' ? '请扫描 SN...' : '不可用'"
:disabled="entryMode === 'batch'"
clearable
>
<template #prefix><span class="prefix-tag sn">SN</span></template>
</el-input>
</el-form-item>
</el-col>
</el-row>
</div>
<el-row :gutter="24" style="margin-top: 15px;">
<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" />
</el-form-item>
</el-col>
<template v-if="dialogStatus === 'update'">
<el-col :span="6">
<el-form-item label="当前库存" prop="stock_quantity">
<el-input-number v-model="form.stock_quantity" disabled style="width:100%" :controls="false" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="当前可用" prop="available_quantity">
<el-input-number v-model="form.available_quantity" disabled style="width:100%" :controls="false" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="库存状态" prop="status">
<el-select v-model="form.status" style="width:100%">
<el-option label="在库" value="在库" />
<el-option label="出库" value="出库" />
<el-option label="损耗" value="损耗" />
</el-select>
</el-form-item>
</el-col>
</template>
<el-col :span="6">
<el-form-item label="质量状态" prop="quality_status">
<el-select v-model="form.quality_status" style="width:100%">
<el-option label="待检" value="待检"><span style="color:#909399"> 待检</span></el-option>
<el-option label="合格" value="合格"><span style="color:#67C23A">🟢 合格</span></el-option>
<el-option label="不合格" value="不合格"><span style="color:#F56C6C">🔴 不合格</span></el-option>
<el-option label="返修中" value="返修中"><span style="color:#E6A23C">🟠 返修中</span></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12"><el-form-item label="质量报告" prop="quality_report_link"><el-input v-model="form.quality_report_link" placeholder="报告链接 URL" /></el-form-item></el-col>
</el-row>
</div>
</div>
<div class="form-card production-card">
<div class="card-title">
<el-icon class="icon"><Setting /></el-icon>
<span>3. 生产与成本信息</span>
</div>
<div class="card-content">
<div class="divider-text">生产任务信息</div>
<el-row :gutter="24">
<el-col :span="8"><el-form-item label="工单号"><el-input v-model="form.work_order_code" placeholder="WO-xxx" /></el-form-item></el-col>
<el-col :span="8"><el-form-item label="BOM编号"><el-input v-model="form.bom_code" placeholder="BOM-xxx" /></el-form-item></el-col>
<el-col :span="8"><el-form-item label="BOM版本"><el-input v-model="form.bom_version" placeholder="v1.0" /></el-form-item></el-col>
</el-row>
<el-row :gutter="24">
<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
v-model="form.production_time_range"
type="datetimerange"
range-separator=""
start-placeholder="开始时间"
end-placeholder="结束时间"
value-format="YYYY-MM-DD HH:mm:ss"
style="width: 100%"
/>
</el-form-item>
</el-col>
</el-row>
<div class="divider-text">成本核算 (单件)</div>
<el-row :gutter="24">
<el-col :span="8">
<el-form-item label="原材料成本">
<el-input-number v-model="form.raw_material_cost" :precision="2" controls-position="right" style="width:100%">
<template #prefix>¥</template>
</el-input-number>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="手动/工时">
<el-input-number v-model="form.manual_cost" :precision="2" controls-position="right" style="width:100%">
<template #prefix>¥</template>
</el-input-number>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="单件总成本">
<el-input-number v-model="form.unit_total_cost" :precision="2" disabled :controls="false" style="width:100%" class="total-price-input">
<template #prefix>¥</template>
</el-input-number>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="24" style="margin-top:10px">
<el-col :span="24"><el-form-item label="详情链接"><el-input v-model="form.detail_link" placeholder="外部生产系统详情页 http://" /></el-form-item></el-col>
</el-row>
</div>
</div>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="visible = false" size="large">取消</el-button>
<el-button type="primary" :loading="submitting" @click="submitForm" size="large" class="confirm-btn">
{{ dialogStatus === 'create' ? '确认入库并打印' : '保存修改' }}
</el-button>
</div>
</template>
</el-dialog>
<el-dialog
v-model="printVisible"
title="标签打印预览"
width="400px"
destroy-on-close
append-to-body
>
<div style="text-align: center;">
<div v-loading="printLoading" class="preview-box">
<img v-if="previewUrl" :src="previewUrl" alt="Label Preview" style="width: 100%; border: 1px solid #ccc;"/>
<div v-else class="empty-preview">正在生成预览...</div>
</div>
<div style="margin-top: 20px; font-size: 14px; color: #666;">
<p>打印机 IP: 192.168.9.205</p>
<p>尺寸: 40mm x 30mm</p>
</div>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="printVisible = false">取消</el-button>
<el-button type="primary" :loading="printing" @click="confirmPrint">
<el-icon>
<Printer/>
</el-icon>
确认打印
</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, watch, computed } from 'vue'
import { Plus, Setting, Refresh, Search, Lock, Box, House, InfoFilled, Link, Printer } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import dayjs from 'dayjs'
import {
getSemiList,
createSemiInbound,
updateSemiInbound,
deleteSemiInbound,
searchMaterialBase
} from '@/api/inbound/semi'
import { getLabelPreview, executePrint } from '@/api/common/print'
// ------------------------------------
// 状态与变量
// ------------------------------------
const loading = ref(false)
const submitting = ref(false)
const visible = ref(false)
const searchLoading = ref(false)
const dialogStatus = ref<'create' | 'update'>('create')
const tableData = ref([])
const total = ref(0)
const formRef = ref()
const queryParams = reactive({ page: 1, pageSize: 15, keyword: '' })
const materialOptions = ref<any[]>([])
// 打印相关变量
const printVisible = ref(false)
const printLoading = ref(false)
const printing = ref(false)
const previewUrl = ref('')
const currentPrintData = ref<any>({})
const entryMode = ref('batch')
const modeLocked = ref(false)
// 列定义
const baseColumns = [
{ prop: 'material_name', label: '名称' },
{ prop: 'category', label: '类别' },
{ prop: 'material_type', label: '类型' },
{ prop: 'spec_model', label: '规格型号' },
{ prop: 'unit', label: '单位' },
]
// 半成品特有的列定义
const stockColumns = [
{ prop: 'id', label: 'ID', minWidth: '60' },
{ prop: 'base_id', label: 'BaseID', minWidth: '80' },
{ prop: 'sku', label: 'SKU', minWidth: '120' },
{ prop: 'inbound_date', label: '入库日期', minWidth: '120' },
{ prop: 'barcode', label: '条码', minWidth: '120' },
{ prop: 'serial_number', label: '序列号', minWidth: '150' },
{ prop: 'batch_number', label: '批号', minWidth: '150' },
{ prop: 'status', label: '状态', minWidth: '100' },
{ prop: 'quality_status', label: '质量状态', minWidth: '100' }, // 对应 inspection_status
{ prop: 'qty_inbound', label: '入库量', minWidth: '100' },
{ prop: 'qty_stock', label: '库存数', minWidth: '100' },
{ prop: 'qty_available', label: '可用数', minWidth: '100' },
{ prop: 'warehouse_loc', label: '库位', minWidth: '120' },
// 新增字段
{ prop: 'bom_code', label: 'BOM编号', minWidth: '120' },
{ prop: 'bom_version', label: 'BOM版本', minWidth: '90' },
{ prop: 'work_order_code', label: '工单号', minWidth: '120' },
{ prop: 'raw_material_cost', label: '原料成本', minWidth: '100' },
{ prop: 'manual_cost', label: '人工成本', minWidth: '100' },
{ prop: 'production_manager', label: '生产负责人', minWidth: '100' },
{ prop: 'production_start_time', label: '生产开始', minWidth: '160' },
{ prop: 'production_end_time', label: '生产结束', minWidth: '160' },
{ prop: 'quality_report_link', label: '质量报告', minWidth: '100' },
{ prop: 'detail_link', label: '详情链接', minWidth: '100' },
]
const allColumns = [...baseColumns, ...stockColumns]
// 表头持久化
const STORAGE_KEY = 'stock_semi_visible_columns'
const defaultColumns = [
'material_name', 'spec_model', 'unit',
'inbound_date', 'serial_number', 'batch_number', 'status', 'quality_status',
'bom_code', 'work_order_code', 'qty_stock', 'qty_available'
]
const getSavedColumns = () => {
try {
const saved = localStorage.getItem(STORAGE_KEY)
return saved ? JSON.parse(saved) : defaultColumns
} catch (e) {
return defaultColumns
}
}
const visibleColumnProps = ref(getSavedColumns())
watch(visibleColumnProps, (newVal) => {
localStorage.setItem(STORAGE_KEY, JSON.stringify(newVal))
}, { deep: true })
const form = reactive({
id: undefined,
base_id: undefined as number | undefined,
material_name: '', spec_model: '', category: '', unit: '', material_type: '',
sku: '', barcode: '', in_date: '',
serial_number: '', batch_number: '',
status: '在库',
quality_status: '合格', // 原 inspection_status
in_quantity: 1, stock_quantity: 1, available_quantity: 1,
warehouse_location: '',
// 半成品特有字段
bom_code: '',
bom_version: '',
work_order_code: '',
raw_material_cost: 0,
manual_cost: 0,
unit_total_cost: 0, // 计算字段
production_manager: '',
production_time_range: [] as string[], // [start, end]
quality_report_link: '',
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)
// ------------------------------------
// 逻辑校验规则
// ------------------------------------
const validateUnique = (rule: any, value: string, callback: any) => {
if (!value) return callback()
const isDuplicate = tableData.value.some((row: any) => {
if (dialogStatus.value === 'update' && row.id === form.id) return false
if (rule.field === 'serial_number' && row.serial_number === value) return true
if (rule.field === 'batch_number' && row.batch_number === value) return true
return false
})
if (isDuplicate) {
callback(new Error('编号重复'))
} else {
callback()
}
}
const validateIdentity = (rule: any, value: any, callback: any) => {
if (entryMode.value === 'serial' && !form.serial_number && rule.field === 'serial_number') {
callback(new Error('SN必填'))
} else if (entryMode.value === 'batch' && !form.batch_number && rule.field === 'batch_number') {
callback(new Error('批号必填'))
} else {
callback()
}
}
const rules = {
base_id: [{ required: true, message: '请选择物料', trigger: 'change' }],
in_quantity: [{ required: true, message: '请输入数量', trigger: 'blur' }],
serial_number: [{ validator: validateIdentity, trigger: 'blur' }, { validator: validateUnique, trigger: 'blur' }],
batch_number: [{ validator: validateIdentity, trigger: 'blur' }, { validator: validateUnique, trigger: 'blur' }]
}
// ------------------------------------
// 核心逻辑函数
// ------------------------------------
const checkHistoryAndSetMode = async (baseId: number) => {
try {
const res: any = await getSemiList({ page: 1, pageSize: 1000 })
const allItems = res.data.items || []
const historyItems = allItems.filter((item: any) => item.base_id === baseId)
if (historyItems.length > 0) {
modeLocked.value = true
const latest = historyItems.sort((a: any, b: any) => b.id - a.id)[0]
if (latest.serial_number) {
entryMode.value = 'serial'
form.serial_number = ''
form.batch_number = ''
} else {
entryMode.value = 'batch'
form.serial_number = ''
const lastBatch = latest.batch_number || '000000'
form.batch_number = incrementBatchNumber(lastBatch)
}
} else {
modeLocked.value = false
entryMode.value = 'batch'
form.batch_number = '000001'
}
if(formRef.value) {
formRef.value.clearValidate('serial_number')
formRef.value.clearValidate('batch_number')
}
} catch (e) {
console.error(e)
modeLocked.value = false
entryMode.value = 'batch'
form.batch_number = '000001'
}
}
const incrementBatchNumber = (batchStr: string) => {
if (!batchStr || !/^\d+$/.test(batchStr)) return '000001'
const num = parseInt(batchStr, 10)
return (num + 1).toString().padStart(6, '0')
}
const handleEntryModeChange = (val: string) => {
if (val === 'batch') {
form.serial_number = ''
form.batch_number = '000001'
if(formRef.value) formRef.value.clearValidate('serial_number')
} else {
form.batch_number = ''
if(formRef.value) formRef.value.clearValidate('batch_number')
}
}
// ------------------------------------
// 物料搜索逻辑 (优化:支持空查询加载默认值)
// ------------------------------------
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.category = item.category
form.unit = item.unit
form.material_type = item.type
checkHistoryAndSetMode(item.id)
}
}
// 自动计算单件总成本
watch(() => [form.raw_material_cost, form.manual_cost], () => {
form.unit_total_cost = Number((form.raw_material_cost + form.manual_cost).toFixed(2))
})
const fetchData = async () => {
loading.value = true
try {
const res: any = await getSemiList(queryParams)
tableData.value = res.data.items || []
total.value = res.data.total || 0
} finally { loading.value = false }
}
const handleCreate = () => {
dialogStatus.value = 'create'
resetForm()
form.in_date = dayjs().format('YYYY-MM-DD')
modeLocked.value = false
entryMode.value = 'batch'
form.batch_number = ''
visible.value = true
// 每次打开弹窗时,先清空选项,让下拉时触发“历史加载”
materialOptions.value = []
}
const handleUpdate = (row: any) => {
dialogStatus.value = 'update'
resetForm()
modeLocked.value = true
form.id = row.id
form.base_id = row.base_id
form.material_name = row.material_name
form.spec_model = row.spec_model
form.category = row.category
form.unit = row.unit
form.material_type = row.material_type
form.sku = row.sku
form.barcode = row.barcode
form.in_date = row.inbound_date
form.warehouse_location = row.warehouse_loc
if (row.serial_number) {
entryMode.value = 'serial'
form.serial_number = row.serial_number
form.batch_number = ''
} else {
entryMode.value = 'batch'
form.batch_number = row.batch_number
form.serial_number = ''
}
form.status = row.status
form.quality_status = row.quality_status // 质量状态
form.in_quantity = Number(row.qty_inbound) || 0
form.stock_quantity = Number(row.qty_stock) || 0
form.available_quantity = Number(row.qty_available) || 0
// 映射新字段
form.bom_code = row.bom_code
form.bom_version = row.bom_version
form.work_order_code = row.work_order_code
form.raw_material_cost = Number(row.raw_material_cost) || 0
form.manual_cost = Number(row.manual_cost) || 0
form.production_manager = row.production_manager
if (row.production_start_time && row.production_end_time) {
form.production_time_range = [row.production_start_time, row.production_end_time]
} else {
form.production_time_range = []
}
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
}
// ------------------------------------
// 提交逻辑 (新增自动打印逻辑)
// ------------------------------------
const submitForm = async () => {
if (!formRef.value) return
await formRef.value.validate(async (valid: boolean) => {
if (valid) {
submitting.value = true
try {
// 解构时间范围
const payload: any = {
...form,
in_quantity: Number(form.in_quantity),
raw_material_cost: Number(form.raw_material_cost),
manual_cost: Number(form.manual_cost),
production_start_time: form.production_time_range?.[0] || null,
production_end_time: form.production_time_range?.[1] || null
}
delete payload.production_time_range // 后端不需要数组
if (dialogStatus.value === 'create') {
// 1. 创建入库
const res: any = await createSemiInbound(payload)
ElMessage.success('入库成功')
// 2. 自动打印 (使用返回的完整数据)
// res.data 包含了 newly created stock item, with generated ID and SKU
const newItem = res.data
if (newItem) {
ElMessage.info('正在发送打印指令...')
try {
await executePrint(newItem)
ElMessage.success('打印指令已发送')
} catch (printErr: any) {
console.error(printErr)
ElMessage.warning('入库成功,但自动打印失败:' + (printErr.msg || '未知错误'))
}
}
} else {
await updateSemiInbound(form.id!, payload)
ElMessage.success('更新成功')
}
// 保存生产负责人信息到历史记录
saveToHistory(HISTORY_KEYS.PRODUCTION_MANAGER, form.production_manager)
await fetchData()
visible.value = false
} catch (e: any) {
ElMessage.error(e.msg || '操作失败')
} finally {
submitting.value = false
}
}
})
}
const handleDelete = async (row: any) => {
try {
await deleteSemiInbound(row.id)
ElMessage.success('删除成功')
fetchData()
} catch (e) {
ElMessage.error('删除失败')
}
}
// ------------------------------------
// 打印逻辑 (手动 & 预览)
// ------------------------------------
const handlePrint = async (row: any) => {
printVisible.value = true
printLoading.value = true
previewUrl.value = ''
const printData = {
global_print_id: row.global_print_id,
material_name: row.material_name,
spec_model: row.spec_model,
category: row.category,
material_type: row.material_type,
warehouse_loc: row.warehouse_loc,
serial_number: row.serial_number,
batch_number: row.batch_number,
sku: row.sku // 【重要】显式增加 SKU 字段传递给后端
}
currentPrintData.value = printData
try {
const res: any = await getLabelPreview(printData)
previewUrl.value = res.data
} catch (e) {
ElMessage.error('预览生成失败')
} finally {
printLoading.value = false
}
}
const confirmPrint = async () => {
printing.value = true
try {
await executePrint(currentPrintData.value)
ElMessage.success('指令已发送')
printVisible.value = false
} catch (e: any) {
ElMessage.error(e.msg || '打印失败')
} finally {
printing.value = false
}
}
const resetForm = () => {
materialOptions.value = []
Object.assign(form, {
id: undefined, base_id: undefined,
material_name: '', spec_model: '', category: '', unit: '', material_type: '',
sku: '', barcode: '', in_date: '',
serial_number: '', batch_number: '',
status: '在库', quality_status: '合格',
in_quantity: 1, stock_quantity: 1, available_quantity: 1,
warehouse_location: '',
bom_code: '', bom_version: '', work_order_code: '',
raw_material_cost: 0, manual_cost: 0, unit_total_cost: 0,
production_manager: '', production_time_range: [],
quality_report_link: '', detail_link: ''
})
}
const getStatusType = (status: string) => {
const map: any = { '在库': 'success', '出库': 'info', '损耗': 'danger' }
return map[status] || 'warning'
}
const getQualityType = (status: string) => {
const map: any = { '合格': 'success', '不合格': 'danger', '待检': 'info', '返修中': 'warning' }
return map[status] || 'info'
}
const formatMoney = (val: any) => {
const num = Number(val)
return isNaN(num) ? '-' : `¥ ${num.toFixed(2)}`
}
onMounted(() => fetchData())
</script>
<style scoped>
/* 全局布局 */
.semi-module {
background: #f5f7fa;
padding: 20px;
min-height: 100vh;
}
/* 顶部工具栏 */
.header-tools {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
background: #fff;
padding: 15px 20px;
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgba(0,0,0,0.05);
}
.left-tools { flex: 0 0 350px; }
.right-tools { display: flex; gap: 10px; align-items: center; }
.action-btn { font-weight: 500; }
/* 表格美化 */
.modern-table {
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 12px 0 rgba(0,0,0,0.05);
}
:deep(.table-header-gray th) {
background-color: #f8f9fb !important;
color: #606266;
font-weight: 600;
height: 50px;
}
.tag-sn { color: #409EFF; font-weight: bold; font-family: monospace; }
.tag-bn { color: #67C23A; font-weight: bold; font-family: monospace; }
.money-text { font-family: 'Consolas', monospace; color: #303133; }
.stock-num { font-weight: bold; color: #333; font-size: 15px; }
.avail-num { font-weight: bold; color: #67C23A; font-size: 15px; }
.sum-tag { margin-left: 4px; transform: scale(0.9); }
/* 弹窗与表单美化 */
.stylish-form .form-card {
background: #fff;
border-radius: 8px;
border: 1px solid #e4e7ed;
margin-bottom: 20px;
overflow: hidden;
}
.card-title {
background: #fcfcfc;
padding: 12px 20px;
border-bottom: 1px solid #ebeef5;
font-weight: 600;
font-size: 15px;
color: #303133;
display: flex;
align-items: center;
}
.card-title .icon { margin-right: 8px; font-size: 18px; color: #409EFF; }
.card-title .sub-title { font-size: 12px; color: #909399; font-weight: normal; margin-left: 10px; }
.card-content { padding: 20px; }
/* 基础信息卡片 (蓝色调,示读) */
.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;
border-bottom: 1px solid #dcdfe6;
border-radius: 0;
padding-left: 0;
}
.is-text-view :deep(.el-input__inner) { color: #606266; font-weight: 500; }
/* 入库信息卡片 */
.inbound-card { border-left: 4px solid #67C23A; }
/* 生产与成本卡片 (橙色调) */
.production-card { border-left: 4px solid #E6A23C; }
.production-card .card-title .icon { color: #E6A23C; }
/* 身份区域 (SN/BN) */
.identity-panel {
background: #fffbf0;
border: 1px dashed #e6a23c;
border-radius: 6px;
padding: 15px;
margin-bottom: 20px;
}
.custom-radio-group { margin-bottom: 10px; }
.locked-msg { font-size: 12px; color: #e6a23c; margin-left: 15px; }
.prefix-tag { font-weight: bold; font-size: 12px; padding: 0 5px; border-radius: 4px; }
.prefix-tag.bn { color: #67C23A; background: #f0f9eb; }
.prefix-tag.sn { color: #409EFF; background: #ecf5ff; }
/* 分割线 */
.divider-text {
display: flex;
align-items: center;
text-align: center;
margin: 30px 0 20px;
color: #909399;
font-size: 14px;
font-weight: 500;
}
.divider-text::before, .divider-text::after {
content: '';
flex: 1;
border-bottom: 1px solid #ebeef5;
}
.divider-text::before { margin-right: 15px; }
.divider-text::after { margin-left: 15px; }
/* 底部按钮 */
.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%; align-items: center;}
.opt-name { font-weight: bold; }
.opt-spec { color: #8492a6; font-size: 13px; margin-right: 10px; }
.total-price-input :deep(.el-input__inner) { color: #F56C6C; font-weight: bold; }
.preview-box {
min-height: 150px;
display: flex;
justify-content: center;
align-items: center;
background: #f5f7fa;
border-radius: 4px;
}
.empty-preview {
color: #909399;
}
</style>